Files
aiops/frontend/src/components/kubernetes/deployment-storage-fields.tsx
T
Logan.Li d66772481f feat(k8s): 存储挂载 ConfigMap/Secret/PVC 下拉关联命名空间资源
- 从集群拉取同命名空间资源列表,下拉选择而非手填
- 支持刷新列表;无资源时提示并弹窗新建(ResourceVisualEditor)
- 已引用但不在集群中的资源保留为「未在集群中找到」选项
2026-06-26 15:16:00 +08:00

176 lines
7.8 KiB
TypeScript

// 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;
}