feat(k8s): 存储挂载 ConfigMap/Secret/PVC 下拉关联命名空间资源
- 从集群拉取同命名空间资源列表,下拉选择而非手填 - 支持刷新列表;无资源时提示并弹窗新建(ResourceVisualEditor) - 已引用但不在集群中的资源保留为「未在集群中找到」选项
This commit is contained in:
1 parent
b2939470f4
commit
d66772481f
2 files changed
+250
-85
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
// DeploymentEditStorage — 存储挂载 Tab(Kuboard 一卷一卡:定义卷 + 多容器挂载)// Logan.Li
|
// 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -8,7 +8,14 @@ import {
|
|||||||
Plus, Trash2, Save, Loader2, HardDrive, Database, Folder, Lock, File, Disc, Server, Container,
|
Plus, Trash2, Save, Loader2, HardDrive, Database, Folder, Lock, File, Disc, Server, Container,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { kubernetesApi } from '@/api';
|
||||||
import { saveDeploymentManifest } from './deployment-manifest-save';
|
import { saveDeploymentManifest } from './deployment-manifest-save';
|
||||||
|
import { ResourceVisualEditor } from './ResourceVisualEditor';
|
||||||
|
import {
|
||||||
|
VolumeTypeFields,
|
||||||
|
extractResourceNames,
|
||||||
|
type NamespaceResourceLists,
|
||||||
|
} from './deployment-storage-fields';
|
||||||
import {
|
import {
|
||||||
type VolumeForm,
|
type VolumeForm,
|
||||||
type VolumeEntry,
|
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));
|
const SELECTABLE_TYPES = new Set(VOLUME_TYPES.map((t) => t.key));
|
||||||
|
|
||||||
function VolumeTypeFields({ vol, onChange }: { vol: VolumeForm; onChange: (p: Partial<VolumeForm>) => void }) {
|
type CreateResourceKind = 'configmaps' | 'secrets' | 'pvcs';
|
||||||
if (vol.type === 'persistentVolumeClaim') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="text-[10px] text-slate-500 w-20 shrink-0">PVC 名称</span>
|
|
||||||
<Input value={vol.pvcName || ''} onChange={(e) => 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" />
|
|
||||||
<label className="flex items-center gap-1.5 text-xs text-slate-400 cursor-pointer whitespace-nowrap">
|
|
||||||
<input type="checkbox" checked={!!vol.pvcReadOnly} onChange={(e) => onChange({ pvcReadOnly: e.target.checked })} className="accent-blue-500" />
|
|
||||||
卷只读
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vol.type === 'emptyDir') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="text-[10px] text-slate-500 w-20 shrink-0">介质</span>
|
|
||||||
<select value={vol.medium || ''} onChange={(e) => onChange({ medium: e.target.value })}
|
|
||||||
className="h-7 w-28 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
|
||||||
<option value="">默认磁盘</option>
|
|
||||||
<option value="Memory">Memory</option>
|
|
||||||
</select>
|
|
||||||
<Input value={vol.sizeLimit || ''} onChange={(e) => 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" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vol.type === 'hostPath') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="text-[10px] text-slate-500 w-20 shrink-0">主机路径</span>
|
|
||||||
<Input value={vol.path || ''} onChange={(e) => 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" />
|
|
||||||
<select value={vol.pathType || 'Directory'} onChange={(e) => onChange({ pathType: e.target.value })}
|
|
||||||
className="h-7 w-28 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
|
||||||
<option value="Directory">Directory</option>
|
|
||||||
<option value="File">File</option>
|
|
||||||
<option value="">默认</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vol.type === 'nfs') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="text-[10px] text-slate-500 w-20 shrink-0">NFS</span>
|
|
||||||
<Input value={vol.nfsServer || ''} onChange={(e) => 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" />
|
|
||||||
<Input value={vol.nfsPath || ''} onChange={(e) => 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" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vol.type === 'configMap') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="text-[10px] text-slate-500 w-20 shrink-0">ConfigMap</span>
|
|
||||||
<Input value={vol.configMapName || ''} onChange={(e) => 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" />
|
|
||||||
<Input value={vol.cmItems || ''} onChange={(e) => 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" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vol.type === 'secret') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="text-[10px] text-slate-500 w-20 shrink-0">Secret</span>
|
|
||||||
<Input value={vol.secretName || ''} onChange={(e) => 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" />
|
|
||||||
<Input value={vol.secItems || ''} onChange={(e) => 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" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (vol.type === 'custom') {
|
|
||||||
return <p className="text-[11px] text-amber-400/80">未识别卷类型,保存时将保留原始 YAML 结构</p>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) {
|
export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -131,6 +58,30 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
|||||||
const [entries, setEntries] = useState<VolumeEntry[]>([]);
|
const [entries, setEntries] = useState<VolumeEntry[]>([]);
|
||||||
const [containers, setContainers] = useState<string[]>([]);
|
const [containers, setContainers] = useState<string[]>([]);
|
||||||
const [passthroughVolumes, setPassthroughVolumes] = useState<Record<string, any>[]>([]);
|
const [passthroughVolumes, setPassthroughVolumes] = useState<Record<string, any>[]>([]);
|
||||||
|
const [resources, setResources] = useState<NamespaceResourceLists>({ configMaps: [], secrets: [], pvcs: [] });
|
||||||
|
const [resourcesLoading, setResourcesLoading] = useState(false);
|
||||||
|
const [createResource, setCreateResource] = useState<CreateResourceKind | null>(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(() => {
|
useEffect(() => {
|
||||||
if (!manifest) { setLoading(false); return; }
|
if (!manifest) { setLoading(false); return; }
|
||||||
@@ -183,7 +134,11 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!formToVolume(entry.volume)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const validMounts = entry.mounts.filter((m) => m.mountPath.trim());
|
const validMounts = entry.mounts.filter((m) => m.mountPath.trim());
|
||||||
@@ -225,6 +180,17 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{createResource && (
|
||||||
|
<ResourceVisualEditor
|
||||||
|
clusterCode={clusterCode}
|
||||||
|
namespace={namespace}
|
||||||
|
resource={createResource}
|
||||||
|
name=""
|
||||||
|
isCreate
|
||||||
|
onClose={() => setCreateResource(null)}
|
||||||
|
onSaved={() => { loadResources(); setCreateResource(null); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Card className="bg-slate-900 border-slate-800">
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -275,7 +241,14 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
|||||||
{/* 原生 select,避免 Radix Select 在 overflow 容器内无法下拉 // Logan.Li */}
|
{/* 原生 select,避免 Radix Select 在 overflow 容器内无法下拉 // Logan.Li */}
|
||||||
<select
|
<select
|
||||||
value={selectValue}
|
value={selectValue}
|
||||||
onChange={(e) => updateVolume(i, { type: e.target.value as VolumeForm['type'] })}
|
onChange={(e) => {
|
||||||
|
const type = e.target.value as VolumeForm['type'];
|
||||||
|
const patch: Partial<VolumeForm> = { type };
|
||||||
|
if (type === 'configMap' && resources.configMaps.length === 1) patch.configMapName = resources.configMaps[0];
|
||||||
|
if (type === 'secret' && resources.secrets.length === 1) patch.secretName = resources.secrets[0];
|
||||||
|
if (type === 'persistentVolumeClaim' && resources.pvcs.length === 1) patch.pvcName = resources.pvcs[0];
|
||||||
|
updateVolume(i, patch);
|
||||||
|
}}
|
||||||
className="h-8 min-w-[168px] bg-slate-800 border border-slate-700 rounded-md text-xs text-slate-300 px-2"
|
className="h-8 min-w-[168px] bg-slate-800 border border-slate-700 rounded-md text-xs text-slate-300 px-2"
|
||||||
>
|
>
|
||||||
{VOLUME_TYPES.map((t) => (
|
{VOLUME_TYPES.map((t) => (
|
||||||
@@ -286,7 +259,23 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
|||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<VolumeTypeFields vol={vol} onChange={(p) => updateVolume(i, p)} />
|
<VolumeTypeFields
|
||||||
|
vol={vol}
|
||||||
|
resources={resources}
|
||||||
|
resourcesLoading={resourcesLoading}
|
||||||
|
onChange={(p) => {
|
||||||
|
const vol = entries[i]?.volume;
|
||||||
|
const extra: Partial<VolumeForm> = {};
|
||||||
|
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}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 挂载到容器(支持一卷多容器,Kuboard 同卡) */}
|
{/* 挂载到容器(支持一卷多容器,Kuboard 同卡) */}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// deployment-storage-fields — 存储卷类型字段 + 命名空间资源下拉关联 // Logan.Li
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { RefreshCw, Plus, Loader2 } from 'lucide-react';
|
||||||
|
import type { VolumeForm } from './deployment-volume-utils';
|
||||||
|
|
||||||
|
export interface NamespaceResourceLists {
|
||||||
|
configMaps: string[];
|
||||||
|
secrets: string[];
|
||||||
|
pvcs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从列表 API 响应提取资源名称 // Logan.Li */
|
||||||
|
export function extractResourceNames(res: unknown): string[] {
|
||||||
|
const arr = Array.isArray(res) ? res : ((res as Record<string, unknown>)?.list || (res as Record<string, unknown>)?.data || []);
|
||||||
|
if (!Array.isArray(arr)) return [];
|
||||||
|
return arr.map((x) => String((x as Record<string, unknown>)?.name || '')).filter(Boolean).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourcePickerProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
options: string[];
|
||||||
|
loading?: boolean;
|
||||||
|
onChange: (name: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 命名空间资源下拉:关联已有 / 刷新 / 新建 // Logan.Li */
|
||||||
|
export function NamespaceResourcePicker({
|
||||||
|
label, value, options, loading, onChange, onRefresh, onCreate,
|
||||||
|
}: ResourcePickerProps) {
|
||||||
|
const orphan = value && !options.includes(value);
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap flex-1 min-w-0">
|
||||||
|
<span className="text-[10px] text-slate-500 w-20 shrink-0">{label}</span>
|
||||||
|
<select
|
||||||
|
value={value || ''}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
>
|
||||||
|
<option value="" disabled>{options.length === 0 ? '暂无资源' : '请选择'}</option>
|
||||||
|
{orphan && <option value={value}>{value}(未在集群中找到)</option>}
|
||||||
|
{options.map((n) => <option key={n} value={n}>{n}</option>)}
|
||||||
|
</select>
|
||||||
|
<Button type="button" size="sm" variant="outline" className="h-7 px-2 border-slate-700 text-slate-400 text-xs" onClick={onRefresh} disabled={loading}>
|
||||||
|
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" variant="outline" className="h-7 px-2 border-slate-700 text-blue-400 text-xs" onClick={onCreate}>
|
||||||
|
<Plus className="h-3 w-3 mr-0.5" />新建
|
||||||
|
</Button>
|
||||||
|
{options.length === 0 && !orphan && (
|
||||||
|
<span className="text-[10px] text-amber-400/90">命名空间内暂无{label},请先新建</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolumeTypeFieldsProps {
|
||||||
|
vol: VolumeForm;
|
||||||
|
resources: NamespaceResourceLists;
|
||||||
|
resourcesLoading: boolean;
|
||||||
|
onChange: (p: Partial<VolumeForm>) => void;
|
||||||
|
onRefreshResources: () => void;
|
||||||
|
onCreateResource: (kind: 'configmaps' | 'secrets' | 'pvcs') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VolumeTypeFields({
|
||||||
|
vol, resources, resourcesLoading, onChange, onRefreshResources, onCreateResource,
|
||||||
|
}: VolumeTypeFieldsProps) {
|
||||||
|
if (vol.type === 'persistentVolumeClaim') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<NamespaceResourcePicker
|
||||||
|
label="PVC 关联"
|
||||||
|
value={vol.pvcName || ''}
|
||||||
|
options={resources.pvcs}
|
||||||
|
loading={resourcesLoading}
|
||||||
|
onChange={(name) => onChange({ pvcName: name })}
|
||||||
|
onRefresh={onRefreshResources}
|
||||||
|
onCreate={() => onCreateResource('pvcs')}
|
||||||
|
/>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs text-slate-400 cursor-pointer whitespace-nowrap pl-[88px]">
|
||||||
|
<input type="checkbox" checked={!!vol.pvcReadOnly} onChange={(e) => onChange({ pvcReadOnly: e.target.checked })} className="accent-blue-500" />
|
||||||
|
卷只读
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (vol.type === 'emptyDir') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-[10px] text-slate-500 w-20 shrink-0">介质</span>
|
||||||
|
<select value={vol.medium || ''} onChange={(e) => onChange({ medium: e.target.value })}
|
||||||
|
className="h-7 w-28 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
||||||
|
<option value="">默认磁盘</option>
|
||||||
|
<option value="Memory">Memory</option>
|
||||||
|
</select>
|
||||||
|
<Input value={vol.sizeLimit || ''} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (vol.type === 'hostPath') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-[10px] text-slate-500 w-20 shrink-0">主机路径</span>
|
||||||
|
<Input value={vol.path || ''} onChange={(e) => 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" />
|
||||||
|
<select value={vol.pathType || 'Directory'} onChange={(e) => onChange({ pathType: e.target.value })}
|
||||||
|
className="h-7 w-28 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
||||||
|
<option value="Directory">Directory</option>
|
||||||
|
<option value="File">File</option>
|
||||||
|
<option value="">默认</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (vol.type === 'nfs') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-[10px] text-slate-500 w-20 shrink-0">NFS</span>
|
||||||
|
<Input value={vol.nfsServer || ''} onChange={(e) => 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" />
|
||||||
|
<Input value={vol.nfsPath || ''} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (vol.type === 'configMap') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<NamespaceResourcePicker
|
||||||
|
label="ConfigMap"
|
||||||
|
value={vol.configMapName || ''}
|
||||||
|
options={resources.configMaps}
|
||||||
|
loading={resourcesLoading}
|
||||||
|
onChange={(name) => onChange({ configMapName: name })}
|
||||||
|
onRefresh={onRefreshResources}
|
||||||
|
onCreate={() => onCreateResource('configmaps')}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2 pl-[88px]">
|
||||||
|
<span className="text-[10px] text-slate-500 shrink-0">键映射</span>
|
||||||
|
<Input value={vol.cmItems || ''} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (vol.type === 'secret') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<NamespaceResourcePicker
|
||||||
|
label="Secret"
|
||||||
|
value={vol.secretName || ''}
|
||||||
|
options={resources.secrets}
|
||||||
|
loading={resourcesLoading}
|
||||||
|
onChange={(name) => onChange({ secretName: name })}
|
||||||
|
onRefresh={onRefreshResources}
|
||||||
|
onCreate={() => onCreateResource('secrets')}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2 pl-[88px]">
|
||||||
|
<span className="text-[10px] text-slate-500 shrink-0">键映射</span>
|
||||||
|
<Input value={vol.secItems || ''} onChange={(e) => 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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (vol.type === 'custom') {
|
||||||
|
return <p className="text-[11px] text-amber-400/80">未识别卷类型,保存时将保留原始 YAML 结构</p>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user