diff --git a/frontend/src/components/kubernetes/DeploymentEditStorage.tsx b/frontend/src/components/kubernetes/DeploymentEditStorage.tsx index fd12aba..ff26d96 100644 --- a/frontend/src/components/kubernetes/DeploymentEditStorage.tsx +++ b/frontend/src/components/kubernetes/DeploymentEditStorage.tsx @@ -1,5 +1,5 @@ // DeploymentEditStorage — 存储挂载 Tab(Kuboard 一卷一卡:定义卷 + 多容器挂载)// Logan.Li -import { useState, useEffect } from 'react'; +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'; @@ -8,7 +8,14 @@ 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, @@ -41,87 +48,7 @@ const VOLUME_TYPES: { key: VolumeForm['type']; label: string; icon: typeof HardD const SELECTABLE_TYPES = new Set(VOLUME_TYPES.map((t) => t.key)); -function VolumeTypeFields({ vol, onChange }: { vol: VolumeForm; onChange: (p: Partial) => void }) { - if (vol.type === 'persistentVolumeClaim') { - return ( -
- PVC 名称 - onChange({ pvcName: e.target.value })} - placeholder="如 data-pvc" className="h-7 flex-1 min-w-[160px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> - -
- ); - } - if (vol.type === 'emptyDir') { - return ( -
- 介质 - - onChange({ sizeLimit: e.target.value })} - placeholder="sizeLimit 如 512Mi" className="h-7 flex-1 min-w-[120px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> -
- ); - } - if (vol.type === 'hostPath') { - return ( -
- 主机路径 - onChange({ path: e.target.value })} - placeholder="/data/app" className="h-7 flex-1 min-w-[160px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> - -
- ); - } - if (vol.type === 'nfs') { - return ( -
- NFS - onChange({ nfsServer: e.target.value })} - placeholder="Server IP" className="h-7 w-36 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> - onChange({ nfsPath: e.target.value })} - placeholder="/export/path" className="h-7 flex-1 min-w-[120px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> -
- ); - } - if (vol.type === 'configMap') { - return ( -
- ConfigMap - onChange({ configMapName: e.target.value })} - placeholder="ConfigMap 名称" className="h-7 flex-1 min-w-[120px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> - onChange({ cmItems: e.target.value })} - placeholder="key:path(可选)" className="h-7 w-40 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> -
- ); - } - if (vol.type === 'secret') { - return ( -
- Secret - onChange({ secretName: e.target.value })} - placeholder="Secret 名称" className="h-7 flex-1 min-w-[120px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> - onChange({ secItems: e.target.value })} - placeholder="key:path(可选)" className="h-7 w-40 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> -
- ); - } - if (vol.type === 'custom') { - return

未识别卷类型,保存时将保留原始 YAML 结构

; - } - return null; -} +type CreateResourceKind = 'configmaps' | 'secrets' | 'pvcs'; export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) { const { toast } = useToast(); @@ -131,6 +58,30 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, 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; } @@ -183,7 +134,11 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, return; } if (!formToVolume(entry.volume)) { - toast({ title: '数据卷配置不完整', description: `请补全「${entry.volume.name}」的类型必填项`, variant: 'destructive' }); + const hint = entry.volume.type === 'configMap' ? '请选择 ConfigMap' + : entry.volume.type === 'secret' ? '请选择 Secret' + : entry.volume.type === 'persistentVolumeClaim' ? '请选择 PVC' + : '的类型必填项'; + toast({ title: '数据卷配置不完整', description: `请为「${entry.volume.name}」${hint}`, variant: 'destructive' }); return; } const validMounts = entry.mounts.filter((m) => m.mountPath.trim()); @@ -225,6 +180,17 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, return (
+ {createResource && ( + setCreateResource(null)} + onSaved={() => { loadResources(); setCreateResource(null); }} + /> + )}
@@ -275,7 +241,14 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, {/* 原生 select,避免 Radix Select 在 overflow 容器内无法下拉 // Logan.Li */} onChange(e.target.value)} + disabled={loading} + className="h-7 flex-1 min-w-[160px] max-w-xs bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-2 font-mono" + > + + {orphan && } + {options.map((n) => )} + + + + {options.length === 0 && !orphan && ( + 命名空间内暂无{label},请先新建 + )} +
+ ); +} + +interface VolumeTypeFieldsProps { + vol: VolumeForm; + resources: NamespaceResourceLists; + resourcesLoading: boolean; + onChange: (p: Partial) => void; + onRefreshResources: () => void; + onCreateResource: (kind: 'configmaps' | 'secrets' | 'pvcs') => void; +} + +export function VolumeTypeFields({ + vol, resources, resourcesLoading, onChange, onRefreshResources, onCreateResource, +}: VolumeTypeFieldsProps) { + if (vol.type === 'persistentVolumeClaim') { + return ( +
+ onChange({ pvcName: name })} + onRefresh={onRefreshResources} + onCreate={() => onCreateResource('pvcs')} + /> + +
+ ); + } + if (vol.type === 'emptyDir') { + return ( +
+ 介质 + + onChange({ sizeLimit: e.target.value })} + placeholder="sizeLimit 如 512Mi" className="h-7 flex-1 min-w-[120px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> +
+ ); + } + if (vol.type === 'hostPath') { + return ( +
+ 主机路径 + onChange({ path: e.target.value })} + placeholder="/data/app" className="h-7 flex-1 min-w-[160px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> + +
+ ); + } + if (vol.type === 'nfs') { + return ( +
+ NFS + onChange({ nfsServer: e.target.value })} + placeholder="Server IP" className="h-7 w-36 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> + onChange({ nfsPath: e.target.value })} + placeholder="/export/path" className="h-7 flex-1 min-w-[120px] bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> +
+ ); + } + if (vol.type === 'configMap') { + return ( +
+ onChange({ configMapName: name })} + onRefresh={onRefreshResources} + onCreate={() => onCreateResource('configmaps')} + /> +
+ 键映射 + onChange({ cmItems: e.target.value })} + placeholder="key:path(可选,挂载子项)" className="h-7 flex-1 max-w-md bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> +
+
+ ); + } + if (vol.type === 'secret') { + return ( +
+ onChange({ secretName: name })} + onRefresh={onRefreshResources} + onCreate={() => onCreateResource('secrets')} + /> +
+ 键映射 + onChange({ secItems: e.target.value })} + placeholder="key:path(可选,挂载子项)" className="h-7 flex-1 max-w-md bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> +
+
+ ); + } + if (vol.type === 'custom') { + return

未识别卷类型,保存时将保留原始 YAML 结构

; + } + return null; +} \ No newline at end of file