feat(k8s): 事件页简化、可视化编辑 P0、Deployment 五 Tab 修复
- 事件中心:去彩虹图表,改专业排名列表 + 主题变量 - 网络/配置/存储:ResourceVisualEditor 双向 YAML 同步、导入 YAML、新建 - vite manualChunks 减小 embed 主包体积 - run.sh:多副本强制 MODE=release + MySQL/Redis - Deployment 编辑:共享 manifest、统一 saveDeploymentManifest - 修复存储挂载 initContainers 混入 bug、补 Tolerations UI - 服务 Tab:applyYAML 创建 + ResourceVisualEditor 编辑 - 基本信息:Pod 模板标签 + Selector 同步
This commit is contained in:
1 parent
339b24a579
commit
d0cf71b05a
17 files changed
+814
-497
No files matched your search
@@ -1,7 +1,4 @@
|
||||
// ResourceVisualEditor.tsx — Kubernetes 资源可视化编辑器
|
||||
// 支持:ConfigMap / Secret / Service / Ingress / PVC
|
||||
// 替代纯 YAML 编辑,提供表单式交互体验
|
||||
// 作者: Logan <admin@attacker.club>
|
||||
// ResourceVisualEditor — ConfigMap/Secret/Service/Ingress/PVC 可视化编辑 // Logan.Li
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Save, Loader2, Plus, Trash2, FileText, Network, Shield, Database, ChevronRight} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -15,6 +12,8 @@ import CodeMirror from '@uiw/react-codemirror';
|
||||
import { yaml } from '@codemirror/lang-yaml';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { cleanYAML } from './k8s-yaml-utils';
|
||||
import { createEmptyManifest } from './resource-create-templates';
|
||||
import { parseManifest, dumpManifest, prepareManifestForSave } from './k8s-manifest-utils';
|
||||
|
||||
interface ResourceVisualEditorProps {
|
||||
clusterCode: string;
|
||||
@@ -23,69 +22,80 @@ interface ResourceVisualEditorProps {
|
||||
name: string;
|
||||
onClose: () => void;
|
||||
onSaved?: () => void;
|
||||
/** 新建模式:不拉取远端 YAML,使用空模板 // Logan.Li */
|
||||
isCreate?: boolean;
|
||||
}
|
||||
|
||||
// Simple YAML→JS parser (avoids js-yaml dependency — K8s API returns JSON-compatible YAML)
|
||||
function tryParseYAML(text: string): any {
|
||||
try { return JSON.parse(text); } catch {}
|
||||
// Fallback: try trimming and re-parsing as JSON (K8s API mostly returns JSON-serialized YAML)
|
||||
try {
|
||||
const lines = text.split('\n').filter(l => !l.trim().startsWith('#'));
|
||||
return JSON.parse(lines.join('\n'));
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export function ResourceVisualEditor({ clusterCode, namespace, resource, name, onClose, onSaved }: ResourceVisualEditorProps) {
|
||||
export function ResourceVisualEditor({ clusterCode, namespace, resource, name, onClose, onSaved, isCreate = false }: ResourceVisualEditorProps) {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [rawYaml, setRawYaml] = useState('');
|
||||
const [manifest, setManifest] = useState<any>(null);
|
||||
// Track which tab: 'visual' | 'yaml'
|
||||
const [mode, setMode] = useState<'visual' | 'yaml'>('visual');
|
||||
// YAML editor state (for fallback yaml mode)
|
||||
const [editYaml, setEditYaml] = useState('');
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isCreate) {
|
||||
const empty = createEmptyManifest(resource, namespace, name || '');
|
||||
setManifest(empty);
|
||||
setEditYaml(JSON.stringify(empty, null, 2));
|
||||
setRawYaml('');
|
||||
return;
|
||||
}
|
||||
const text = await kubernetesApi.getResourceYaml(clusterCode, namespace, resource, name);
|
||||
const cleaned = cleanYAML(text);
|
||||
setRawYaml(text);
|
||||
setEditYaml(cleaned);
|
||||
const parsed = tryParseYAML(cleaned);
|
||||
const parsed = await parseManifest(cleaned);
|
||||
setManifest(parsed);
|
||||
if (!parsed) {
|
||||
// Can't parse — fall back to YAML mode
|
||||
setMode('yaml');
|
||||
toast({ title: '可视化编辑不可用', description: '无法解析资源结构,已切换到 YAML 编辑模式' });
|
||||
}
|
||||
} catch (e) {
|
||||
toast({ title: '加载失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
||||
} finally { setLoading(false); }
|
||||
}, [clusterCode, namespace, resource, name]);
|
||||
}, [clusterCode, namespace, resource, name, isCreate]);
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
const handleSave = async ( updatedManifest: any) => {
|
||||
const handleSave = async (updatedManifest: any) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Serialize manifest back to YAML
|
||||
const yamlStr = manifestToYaml(updatedManifest);
|
||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
|
||||
toast({ title: '保存成功', description: `${resource}/${name} 已更新` });
|
||||
const yamlStr = await prepareManifestForSave(updatedManifest);
|
||||
const resName = updatedManifest?.metadata?.name || name;
|
||||
if (isCreate) {
|
||||
if (!resName?.trim()) {
|
||||
toast({ title: '请填写名称', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
await kubernetesApi.applyYAML(clusterCode, yamlStr);
|
||||
toast({ title: '创建成功', description: `${resource}/${resName}` });
|
||||
} else {
|
||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
|
||||
toast({ title: '保存成功', description: `${resource}/${name} 已更新` });
|
||||
}
|
||||
onSaved?.();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast({ title: '保存失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
||||
toast({ title: isCreate ? '创建失败' : '保存失败', description: (e as Record<string, any>).message, variant: 'destructive' });
|
||||
} finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const handleSaveYaml = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, editYaml);
|
||||
toast({ title: 'YAML 已应用', description: `${resource}/${name} 更新成功` });
|
||||
const yamlStr = cleanYAML(editYaml);
|
||||
if (isCreate) {
|
||||
await kubernetesApi.applyYAML(clusterCode, yamlStr);
|
||||
toast({ title: '创建成功', description: 'YAML 已应用' });
|
||||
} else {
|
||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
|
||||
toast({ title: 'YAML 已应用', description: `${resource}/${name} 更新成功` });
|
||||
}
|
||||
onSaved?.();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
@@ -93,13 +103,24 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
} finally { setSaving(false); }
|
||||
};
|
||||
|
||||
// Manifest → YAML string (JSON serialization works as YAML superset for K8s API)
|
||||
function manifestToYaml( m: any): string {
|
||||
// Use 2-space indented JSON — K8s API accepts JSON as YAML
|
||||
return JSON.stringify(m, null, 2);
|
||||
}
|
||||
// 可视化 ↔ YAML 双向同步 // Logan.Li
|
||||
const switchToVisual = async () => {
|
||||
const parsed = await parseManifest(editYaml);
|
||||
if (!parsed) {
|
||||
toast({ title: '无法解析 YAML', description: '请检查语法后继续用 YAML 模式', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
setManifest(parsed);
|
||||
setMode('visual');
|
||||
};
|
||||
|
||||
// ─── Resource type config ───
|
||||
const switchToYaml = async () => {
|
||||
if (manifest) {
|
||||
const dumped = await dumpManifest(manifest);
|
||||
setEditYaml(cleanYAML(dumped));
|
||||
}
|
||||
setMode('yaml');
|
||||
};
|
||||
const resourceMeta: Record<string, { title: string; icon: any; color: string }> = {
|
||||
configmaps: { title: 'ConfigMap', icon: FileText, color: 'text-blue-400' },
|
||||
secrets: { title: 'Secret', icon: Shield, color: 'text-amber-400' },
|
||||
@@ -118,8 +139,8 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
<div className="flex items-center gap-2">
|
||||
<MetaIcon className={`h-5 w-5 ${meta.color}`} />
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-[var(--dg-text-primary)]">{meta.title} 编辑器</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{name} · {namespace} · {clusterCode}</p>
|
||||
<h2 className="text-lg font-bold text-[var(--dg-text-primary)]">{isCreate ? `新建 ${meta.title}` : `${meta.title} 编辑`}</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{isCreate ? namespace : name} · {namespace} · {clusterCode}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -127,12 +148,12 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
<div className="flex rounded-lg border border-[var(--dg-border)] overflow-hidden">
|
||||
<button
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'visual' ? 'bg-blue-600 text-white' : 'bg-transparent text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}
|
||||
onClick={() => manifest && setMode('visual')}
|
||||
disabled={!manifest}
|
||||
onClick={() => { if (mode === 'yaml') switchToVisual(); else setMode('visual'); }}
|
||||
disabled={!manifest && mode !== 'yaml'}
|
||||
>可视化编辑</button>
|
||||
<button
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'yaml' ? 'bg-blue-600 text-white' : 'bg-transparent text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}
|
||||
onClick={() => setMode('yaml')}
|
||||
onClick={() => { if (mode === 'visual') switchToYaml(); else setMode('yaml'); }}
|
||||
>YAML 编辑</button>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)]" onClick={onClose}><X className="h-4 w-4" /></Button>
|
||||
@@ -165,11 +186,11 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
</div>
|
||||
) : manifest ? (
|
||||
<>
|
||||
{resource === 'configmaps' && <ConfigMapEditor manifest={manifest} onSave={handleSave} saving={saving} />}
|
||||
{resource === 'secrets' && <SecretEditor manifest={manifest} onSave={handleSave} saving={saving} />}
|
||||
{resource === 'services' && <ServiceEditor manifest={manifest} onSave={handleSave} saving={saving} />}
|
||||
{resource === 'ingresses' && <IngressEditor manifest={manifest} onSave={handleSave} saving={saving} />}
|
||||
{resource === 'pvcs' && <PvcEditor manifest={manifest} onSave={handleSave} saving={saving} />}
|
||||
{resource === 'configmaps' && <ConfigMapEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
|
||||
{resource === 'secrets' && <SecretEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
|
||||
{resource === 'services' && <ServiceEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
|
||||
{resource === 'ingresses' && <IngressEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
|
||||
{resource === 'pvcs' && <PvcEditor manifest={manifest} onSave={handleSave} saving={saving} isCreate={isCreate} />}
|
||||
{!['configmaps', 'secrets', 'services', 'ingresses', 'pvcs'].includes(resource) && (
|
||||
<div className="text-center py-12 text-[var(--dg-text-muted)]">
|
||||
<p>该资源类型暂不支持可视化编辑</p>
|
||||
@@ -192,8 +213,9 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
// ══════════════════════════════════════════════
|
||||
// ConfigMap 编辑器 — 键值对表格
|
||||
// ══════════════════════════════════════════════
|
||||
function ConfigMapEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m: any) => void; saving: boolean }) {
|
||||
function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const data = manifest?.data || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [entries, setEntries] = useState<{ key: string; value: string }[]>(
|
||||
Object.entries(data).map(([key, value]) => ({ key, value: String(value) }))
|
||||
);
|
||||
@@ -212,14 +234,14 @@ function ConfigMapEditor({ manifest, onSave, saving }: { manifest: any; onSave:
|
||||
entries.forEach(e => {
|
||||
if (e.key.trim()) newData[e.key.trim()] = e.value;
|
||||
});
|
||||
onSave({ ...manifest, data: newData });
|
||||
onSave({ ...manifest, metadata: { ...manifest.metadata, name: resName.trim() }, data: newData });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">名称</Label>
|
||||
<Input value={manifest?.metadata?.name || ''} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
<Input value={resName} onChange={(e) => setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
@@ -264,8 +286,9 @@ function ConfigMapEditor({ manifest, onSave, saving }: { manifest: any; onSave:
|
||||
// ══════════════════════════════════════════════
|
||||
// Secret 编辑器 — 键值对表格 (Base64 编码/解码)
|
||||
// ══════════════════════════════════════════════
|
||||
function SecretEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m: any) => void; saving: boolean }) {
|
||||
function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const rawData = manifest?.data || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
// Decode base64 values for editing
|
||||
const [entries, setEntries] = useState<{ key: string; value: string; encoded: boolean }[]>(
|
||||
Object.entries(rawData).map(([key, value]) => {
|
||||
@@ -297,14 +320,14 @@ function SecretEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m
|
||||
newData[e.key.trim()] = e.encoded ? btoa(e.value) : e.value;
|
||||
}
|
||||
});
|
||||
onSave({ ...manifest, data: newData });
|
||||
onSave({ ...manifest, metadata: { ...manifest.metadata, name: resName.trim() }, data: newData });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">名称</Label>
|
||||
<Input value={manifest?.metadata?.name || ''} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
<Input value={resName} onChange={(e) => setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">类型</Label>
|
||||
@@ -353,8 +376,9 @@ function SecretEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m
|
||||
// ══════════════════════════════════════════════
|
||||
// Service 编辑器 — 端口配置 + 类型 + 选择器
|
||||
// ══════════════════════════════════════════════
|
||||
function ServiceEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m: any) => void; saving: boolean }) {
|
||||
function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const spec = manifest?.spec || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [svcType, setSvcType] = useState(spec.type || 'ClusterIP');
|
||||
const [ports, setPorts] = useState<any[]>(
|
||||
(spec.ports || []).map(( p: any) => ({
|
||||
@@ -401,28 +425,34 @@ function ServiceEditor({ manifest, onSave, saving }: { manifest: any; onSave: (
|
||||
const selObj: Record<string, string> = {};
|
||||
selector.forEach(s => { if (s.key.trim()) selObj[s.key.trim()] = s.value; });
|
||||
newSpec.selector = selObj;
|
||||
onSave({ ...manifest, spec: newSpec });
|
||||
onSave({ ...manifest, metadata: { ...manifest.metadata, name: resName.trim() }, spec: newSpec });
|
||||
};
|
||||
|
||||
const svcTypes = [
|
||||
{ value: 'ClusterIP', label: 'ClusterIP' },
|
||||
{ value: 'NodePort', label: 'NodePort' },
|
||||
{ value: 'LoadBalancer', label: 'LoadBalancer' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">名称</Label>
|
||||
<Input value={manifest?.metadata?.name || ''} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">Service 类型</Label>
|
||||
<Select value={svcType} onValueChange={setSvcType}>
|
||||
<SelectTrigger className="mt-1 h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ClusterIP">ClusterIP (集群内部)</SelectItem>
|
||||
<SelectItem value="NodePort">NodePort (节点端口)</SelectItem>
|
||||
<SelectItem value="LoadBalancer">LoadBalancer (负载均衡)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">服务名称</Label>
|
||||
<Input value={resName} onChange={(e) => setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs mb-2 block">服务类型</Label>
|
||||
<div className="flex rounded-lg border border-[var(--dg-border)] overflow-hidden w-fit">
|
||||
{svcTypes.map((t) => (
|
||||
<button
|
||||
key={t.value}
|
||||
type="button"
|
||||
className={`px-3 py-1.5 text-xs transition-colors ${svcType === t.value ? 'bg-blue-600 text-white' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}
|
||||
onClick={() => setSvcType(t.value)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -484,8 +514,10 @@ function ServiceEditor({ manifest, onSave, saving }: { manifest: any; onSave: (
|
||||
// ══════════════════════════════════════════════
|
||||
// Ingress 编辑器 — Host/Path 路由规则
|
||||
// ══════════════════════════════════════════════
|
||||
function IngressEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m: any) => void; saving: boolean }) {
|
||||
function IngressEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const spec = manifest?.spec || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [ingressClass, setIngressClass] = useState(spec.ingressClassName || 'nginx');
|
||||
const [rules, setRules] = useState<{ host: string; paths: { path: string; pathType: string; serviceName: string; servicePort: string }[] }[]>(
|
||||
(spec.rules || []).map(( r: any) => ({
|
||||
host: r.host || '',
|
||||
@@ -537,14 +569,23 @@ function IngressEditor({ manifest, onSave, saving }: { manifest: any; onSave: (
|
||||
})),
|
||||
},
|
||||
}));
|
||||
onSave({ ...manifest, spec: { ...spec, rules: newRules } });
|
||||
const newSpec: Record<string, any> = { ...spec, rules: newRules };
|
||||
if (ingressClass.trim()) newSpec.ingressClassName = ingressClass.trim();
|
||||
else delete newSpec.ingressClassName;
|
||||
onSave({ ...manifest, metadata: { ...manifest.metadata, name: resName.trim() }, spec: newSpec });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">名称</Label>
|
||||
<Input value={manifest?.metadata?.name || ''} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">名称</Label>
|
||||
<Input value={resName} onChange={(e) => setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">Ingress Class</Label>
|
||||
<Input value={ingressClass} onChange={(e) => setIngressClass(e.target.value)} placeholder="nginx / alb" className="mt-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
@@ -594,9 +635,10 @@ function IngressEditor({ manifest, onSave, saving }: { manifest: any; onSave: (
|
||||
// ══════════════════════════════════════════════
|
||||
// PVC 编辑器 — 存储大小/访问模式/StorageClass
|
||||
// ══════════════════════════════════════════════
|
||||
function PvcEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m: any) => void; saving: boolean }) {
|
||||
function PvcEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const spec = manifest?.spec || {};
|
||||
const resources = spec.resources?.requests || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [size, setSize] = useState(resources.storage || '1Gi');
|
||||
const [accessMode, setAccessMode] = useState(spec.accessModes?.[0] || 'ReadWriteOnce');
|
||||
const [storageClass, setStorageClass] = useState(spec.storageClassName || '');
|
||||
@@ -604,6 +646,7 @@ function PvcEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m: a
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
...manifest,
|
||||
metadata: { ...manifest.metadata, name: resName.trim() },
|
||||
spec: {
|
||||
...spec,
|
||||
accessModes: [accessMode],
|
||||
@@ -617,7 +660,7 @@ function PvcEditor({ manifest, onSave, saving }: { manifest: any; onSave: ( m: a
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">名称</Label>
|
||||
<Input value={manifest?.metadata?.name || ''} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
<Input value={resName} onChange={(e) => setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
|
||||
Reference in new issue
Block a user