// DeploymentEditStorage — 存储挂载 Tab(Kuboard 一卷一卡:定义卷 + 多容器挂载)// Logan.Li import { useState, useEffect, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Plus, Trash2, Save, Loader2, HardDrive, Database, Folder, Lock, File, Disc, Server, Container, } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { kubernetesApi } from '@/api'; import { saveDeploymentManifest } from './deployment-manifest-save'; import { ResourceVisualEditor } from './ResourceVisualEditor'; import { VolumeTypeFields, extractResourceNames, type NamespaceResourceLists, } from './deployment-storage-fields'; import { type VolumeForm, type VolumeEntry, type VolumeMountRow, parseVolumeEntries, applyStorageToManifest, formToVolume, volumeEntriesToStorage, emptyVolumeEntry, emptyMountRow, } from './deployment-volume-utils'; import type { JsonData } from '@/types/common'; import i18n from '@/i18n'; interface Props { clusterCode: string; namespace: string; deploymentName: string; manifest: JsonData | null; onSaved: (updated?: JsonData) => void; onCancel: () => void; } const VOLUME_TYPES: { key: VolumeForm['type']; label: string; icon: typeof HardDrive; color: string }[] = [ { key: 'persistentVolumeClaim', label: i18n.t('k8s.nsResource.pvcDesc'), icon: Database, color: 'dg-text-green' }, { key: 'emptyDir', label: i18n.t('k8s.editStorage.emptyDir'), icon: Disc, color: 'dg-text-secondary' }, { key: 'hostPath', label: 'HostPath', icon: HardDrive, color: 'dg-text-yellow' }, { key: 'nfs', label: 'NFS', icon: Server, color: 'dg-text-purple' }, { key: 'configMap', label: i18n.t('k8s.nsResource.configMapDesc'), icon: File, color: 'dg-text-blue' }, { key: 'secret', label: i18n.t('k8s.secret.data'), icon: Lock, color: 'text-rose-400' }, ]; const SELECTABLE_TYPES = new Set(VOLUME_TYPES.map((t) => t.key)); type CreateResourceKind = 'configmaps' | 'secrets' | 'pvcs'; export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) { const { toast } = useToast(); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(!manifest); const [rawManifest, setRawManifest] = useState(manifest || {}); const [entries, setEntries] = useState([]); const [containers, setContainers] = useState([]); const [passthroughVolumes, setPassthroughVolumes] = useState([]); const [resources, setResources] = useState({ configMaps: [], secrets: [], pvcs: [] }); const [resourcesLoading, setResourcesLoading] = useState(false); const [createResource, setCreateResource] = useState(null); const loadResources = useCallback(async () => { if (!clusterCode || !namespace) return; setResourcesLoading(true); try { const [cm, sec, pvc] = await Promise.all([ kubernetesApi.getNamespaceConfigMaps(clusterCode, namespace).catch(() => []), kubernetesApi.getNamespaceSecrets(clusterCode, namespace).catch(() => []), kubernetesApi.getPVCs(clusterCode, namespace).catch(() => []), ]); setResources({ configMaps: extractResourceNames(cm), secrets: extractResourceNames(sec), pvcs: extractResourceNames(pvc), }); } finally { setResourcesLoading(false); } }, [clusterCode, namespace]); useEffect(() => { loadResources(); }, [loadResources]); useEffect(() => { if (!manifest) { setLoading(false); return; } setRawManifest(manifest); const parsed = parseVolumeEntries(manifest); setEntries(parsed.entries); setContainers(parsed.containers); setPassthroughVolumes(parsed.passthroughVolumes); setLoading(false); }, [manifest]); const defaultContainer = containers.find((c) => c !== '(unnamed)') || containers[0] || ''; const updateVolume = (i: number, patch: Partial) => setEntries((prev) => prev.map((e, idx) => (idx === i ? { ...e, volume: { ...e.volume, ...patch } } : e))); const updateMount = (entryIdx: number, mountIdx: number, patch: Partial) => setEntries((prev) => prev.map((e, i) => { if (i !== entryIdx) return e; return { ...e, mounts: e.mounts.map((m, j) => (j === mountIdx ? { ...m, ...patch } : m)), }; })); const addMount = (entryIdx: number) => setEntries((prev) => prev.map((e, i) => { if (i !== entryIdx) return e; return { ...e, mounts: [...e.mounts, emptyMountRow(defaultContainer)] }; })); const removeMount = (entryIdx: number, mountIdx: number) => setEntries((prev) => prev.map((e, i) => { if (i !== entryIdx) return e; const next = e.mounts.filter((_, j) => j !== mountIdx); return { ...e, mounts: next.length > 0 ? next : [emptyMountRow(defaultContainer)] }; })); const addEntry = () => setEntries((prev) => [...prev, emptyVolumeEntry(defaultContainer)]); const removeEntry = (i: number) => setEntries((prev) => prev.filter((_, idx) => idx !== i)); const save = async () => { for (const entry of entries) { if (!entry.volume.name.trim()) { toast({ title: i18n.t('k8s.editStorage.volumeNameRequired'), variant: 'destructive' }); return; } if (!formToVolume(entry.volume)) { const hint = entry.volume.type === 'configMap' ? i18n.t('k8s.editStorage.selectConfigMap') : entry.volume.type === 'secret' ? i18n.t('k8s.editStorage.selectSecret') : entry.volume.type === 'persistentVolumeClaim' ? i18n.t('k8s.editStorage.selectPVC') : i18n.t('k8s.editStorage.typeRequiredHint'); toast({ title: i18n.t('k8s.editStorage.incompleteConfig'), description: i18n.t('k8s.editStorage.incompleteConfigDesc', { name: entry.volume.name, hint }), variant: 'destructive' }); return; } const validMounts = entry.mounts.filter((m) => m.mountPath.trim()); if (validMounts.length === 0) { toast({ title: i18n.t('k8s.editStorage.mountPathRequired'), description: i18n.t('k8s.editStorage.mountPathRequiredDesc', { name: entry.volume.name }), variant: 'destructive' }); return; } } setSaving(true); try { const { volumes, mounts } = volumeEntriesToStorage(entries); const base = applyStorageToManifest(rawManifest, volumes, mounts, passthroughVolumes); await saveDeploymentManifest(clusterCode, namespace, deploymentName, base); toast({ title: i18n.t('k8s.editStorage.saved'), description: i18n.t('k8s.toast.updatedSubmitted', { name: deploymentName }) }); onSaved(base); } catch (e) { toast({ title: i18n.t('kubernetes.msg.saveFailed'), description: (e as Error)?.message || String(e), variant: 'destructive' }); } finally { setSaving(false); } }; if (loading) { return (

{i18n.t('monitor.logPanel.logResults.loading')}

); } if (!manifest) { return ( {i18n.t('kubernetes.storage.noManifest')} ); } return (
{createResource && ( setCreateResource(null)} onSaved={() => { loadResources(); setCreateResource(null); }} /> )}
数据卷 {entries.length}

{i18n.t('kubernetes.storage.volumeDesc')}

{entries.length === 0 ? (
未配置数据卷,点击「添加数据卷」开始
) : entries.map((entry, i) => { const vol = entry.volume; const vtype = VOLUME_TYPES.find((t) => t.key === vol.type); const VIcon = vtype?.icon || Folder; const selectValue = SELECTABLE_TYPES.has(vol.type) ? vol.type : 'emptyDir'; return (
{/* 卷定义 */}
{i18n.t('kubernetes.deploymentEditService.volumeName')} { const name = e.target.value; const patch: Partial = { name }; if (vol.type === 'configMap' && !vol.configMapName) patch.configMapName = name; if (vol.type === 'secret' && !vol.secretName) patch.secretName = name; if (vol.type === 'persistentVolumeClaim' && !vol.pvcName) patch.pvcName = name; updateVolume(i, patch); }} placeholder={i18n.t('k8s.editStorage.namePlaceholder')} className="h-8 w-44 dg-input text-sm font-mono" /> {/* 原生 select,避免 Radix Select 在 overflow 容器内无法下拉 // Logan.Li */}
{ const vol = entries[i]?.volume; const extra: Partial = {}; if (vol && !vol.name.trim()) { if (p.configMapName) extra.name = p.configMapName; if (p.secretName) extra.name = p.secretName; if (p.pvcName) extra.name = p.pvcName; } updateVolume(i, { ...p, ...extra }); }} onRefreshResources={loadResources} onCreateResource={setCreateResource} />
{/* 挂载到容器(支持一卷多容器,Kuboard 同卡) */}

将上述数据卷挂载到容器

{entry.mounts.map((mount, mi) => (
updateMount(i, mi, { mountPath: e.target.value })} placeholder={i18n.t('k8s.editStorage.mountPathPlaceholder')} className="h-8 flex-1 min-w-[200px] dg-input text-sm font-mono" /> {entry.mounts.length > 1 && ( )}
{i18n.t('kubernetes.deploymentEditService.subPath')} updateMount(i, mi, { subPath: e.target.value })} placeholder={i18n.t('k8s.editStorage.mountPathPlaceholder')} className="h-7 flex-1 max-w-md dg-input text-[var(--dg-text-secondary)] text-xs font-mono" />
))}
); })}
); }