fix(i18n): 回退到稳定的 32% 覆盖率状态 (tsc + Vite 构建通过)
批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换: - 翻译词典: 22 namespaces, 3600+ keys - 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy) - 语言切换器在 Header 中可用 (🇨🇳/🇺🇸) - tsc 零错误, Vite 构建成功
This commit is contained in:
1 parent
ecec73989b
commit
72bfb0e12d
191 files changed
+5057
-7592
No files matched your search
@@ -17,7 +17,6 @@ import { YamlEditorToolbar } from './YamlEditorToolbar';
|
||||
import { YamlCodeMirror } from './YamlCodeMirror';
|
||||
import { getYamlResourceConfig, yamlDownloadFilename } from './k8s-yaml-resource-config';
|
||||
import { downloadYamlFile } from '@/utils/export';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ResourceVisualEditorProps {
|
||||
clusterCode: string;
|
||||
@@ -31,7 +30,6 @@ interface ResourceVisualEditorProps {
|
||||
}
|
||||
|
||||
export function ResourceVisualEditor({ clusterCode, namespace, resource, name, onClose, onSaved, isCreate = false }: ResourceVisualEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -68,10 +66,10 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
setManifest(parsed);
|
||||
if (!parsed) {
|
||||
setMode('yaml');
|
||||
toast({ title: t('kubernetes.resourcevisualeditor.s80ca4f'), description: t('kubernetes.resourcevisualeditor.scc94aa') });
|
||||
toast({ title: '可视化编辑不可用', description: '无法解析资源结构,已切换到 YAML 编辑模式' });
|
||||
}
|
||||
} catch (e) {
|
||||
toast({ title: t('common.loadFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
|
||||
toast({ title: '加载失败', description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
|
||||
} finally { setLoading(false); }
|
||||
}, [clusterCode, namespace, resource, name, isCreate]);
|
||||
|
||||
@@ -84,19 +82,19 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
const resName = updatedManifest?.metadata?.name || name;
|
||||
if (isCreate) {
|
||||
if (!resName?.trim()) {
|
||||
toast({ title: t('deploy.form.nameRequired'), variant: 'destructive' });
|
||||
toast({ title: '请填写名称', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
await kubernetesApi.applyYAML(clusterCode, yamlStr);
|
||||
toast({ title: t('monitor.dashboard.custom.toastCreateSuccess'), description: `${resource}/${resName}` });
|
||||
toast({ title: '创建成功', description: `${resource}/${resName}` });
|
||||
} else {
|
||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
|
||||
toast({ title: t('common.savedSuccessfully'), description: t('kubernetes.resourcevisualeditor.se7e1ac_0', { resource: resource, name: name }) });
|
||||
toast({ title: '保存成功', description: `${resource}/${name} 已更新` });
|
||||
}
|
||||
onSaved?.();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast({ title: isCreate ? t('monitor.quickSetupFailed') : t('common.saveFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
|
||||
toast({ title: isCreate ? '创建失败' : '保存失败', description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
|
||||
} finally { setSaving(false); }
|
||||
};
|
||||
|
||||
@@ -106,10 +104,10 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
const yamlStr = await prepareYamlForSave(editYaml);
|
||||
if (isCreate) {
|
||||
await kubernetesApi.applyYAML(clusterCode, yamlStr);
|
||||
toast({ title: t('monitor.dashboard.custom.toastCreateSuccess'), description: t('kubernetes.resourcevisualeditor.se5ff4b') });
|
||||
toast({ title: '创建成功', description: 'YAML 已应用' });
|
||||
} else {
|
||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, resource, name, yamlStr);
|
||||
toast({ title: t('common.savedSuccessfully'), description: t('kubernetes.resourcevisualeditor.se7e1ac', { resource: resource, name: name }) });
|
||||
toast({ title: '保存成功', description: `${resource}/${name} 已更新` });
|
||||
}
|
||||
setYamlEditing(false);
|
||||
setDisplayYaml(yamlStr);
|
||||
@@ -118,7 +116,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
onSaved?.();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
toast({ title: t('common.saveFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
|
||||
toast({ title: '保存失败', description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
|
||||
} finally { setSaving(false); }
|
||||
};
|
||||
|
||||
@@ -126,7 +124,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
const switchToVisual = async () => {
|
||||
const parsed = await parseManifest(editYaml);
|
||||
if (!parsed) {
|
||||
toast({ title: t('kubernetes.resourcevisualeditor.sf53bdf'), description: t('kubernetes.resourcevisualeditor.sf2219a'), variant: 'destructive' });
|
||||
toast({ title: '无法解析 YAML', description: '请检查语法后继续用 YAML 模式', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
setManifest(parsed);
|
||||
@@ -155,7 +153,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
const downloadYamlContent = () => {
|
||||
const fname = yamlDownloadFilename(resource, namespace, name || manifest?.metadata?.name || 'resource');
|
||||
downloadYamlFile(fname, currentYamlText());
|
||||
toast({ title: t('kubernetes.resourcevisualeditor.s9719fe'), description: fname });
|
||||
toast({ title: '已下载', description: fname });
|
||||
};
|
||||
|
||||
const resCfg = getYamlResourceConfig(resource);
|
||||
@@ -181,7 +179,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
<MetaIcon className={`h-5 w-5 shrink-0 ${meta.color}`} />
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-bold text-[var(--dg-text-primary)] truncate">
|
||||
{isCreate ? t('kubernetes.resourcevisualeditor.s1876b4', { meta.title: meta.title }) : t('kubernetes.resourcevisualeditor.sc05c9e', { meta.title: meta.title })}
|
||||
{isCreate ? `新建 ${meta.title}` : `${meta.title} 编辑`}
|
||||
</h2>
|
||||
<p
|
||||
className="text-xs text-[var(--dg-text-muted)] truncate"
|
||||
@@ -198,11 +196,11 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
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={() => { if (mode === 'yaml') switchToVisual(); else setMode('visual'); }}
|
||||
disabled={!manifest && mode !== 'yaml'}
|
||||
>{t('k8s.resource.visualEditor')}</button>
|
||||
>可视化编辑</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={() => { if (mode === 'visual') switchToYaml(); else setMode('yaml'); }}
|
||||
>{t('k8s.resource.yamlEditor')}</button>
|
||||
>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>
|
||||
</div>
|
||||
@@ -261,16 +259,16 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
{!['configmaps', 'secrets', 'services', 'ingresses', 'pvcs'].includes(resource) && (
|
||||
<div className="flex-1 flex items-center justify-center text-center text-[var(--dg-text-muted)]">
|
||||
<div>
|
||||
<p>{t('kubernetes.resourcevisualeditor.sfeb34f')}</p>
|
||||
<Button variant="outline" size="sm" className="mt-4" onClick={() => setMode('yaml')}>{t('kubernetes.resourcevisualeditor.s54c78d')}</Button>
|
||||
<p>该资源类型暂不支持可视化编辑</p>
|
||||
<Button variant="outline" size="sm" className="mt-4" onClick={() => setMode('yaml')}>切换到 YAML 编辑</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-[var(--dg-text-muted)]">
|
||||
<p>{t('kubernetes.resourcevisualeditor.s8fe0ae')}</p>
|
||||
<Button variant="outline" size="sm" className="mt-4" onClick={() => setMode('yaml')}>{t('kubernetes.resourcevisualeditor.s54c78d')}</Button>
|
||||
<p>无法解析资源结构</p>
|
||||
<Button variant="outline" size="sm" className="mt-4" onClick={() => setMode('yaml')}>切换到 YAML 编辑</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -283,7 +281,6 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
// ConfigMap 编辑器 — 键值对表格
|
||||
// ══════════════════════════════════════════════
|
||||
function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const data = manifest?.data || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [entries, setEntries] = useState<{ key: string; value: string }[]>(
|
||||
@@ -311,18 +308,18 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="shrink-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.name')}</Label>
|
||||
<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 className="flex-1 min-h-0 flex flex-col mt-3">
|
||||
<div className="flex items-center justify-between mb-2 shrink-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">{t('kubernetes.resourcevisualeditor.s6bd91c')}</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">配置数据 (键值对)</Label>
|
||||
<div className="flex gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => setExpandAll(!expandAll)}>
|
||||
{expandAll ? t('kubernetes.resourcevisualeditor.s880e84') : t('common.expandAll')}
|
||||
{expandAll ? '折叠全部' : '展开全部'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRow}><Plus className="h-3 w-3 mr-1" />{t('common.add')}</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRow}><Plus className="h-3 w-3 mr-1" />添加</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto pr-1 space-y-2 border border-[var(--dg-border)] rounded-lg bg-[var(--dg-bg-secondary)] p-2">
|
||||
@@ -331,13 +328,13 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
<Input
|
||||
value={entry.key}
|
||||
onChange={e => update(i, 'key', e.target.value)}
|
||||
placeholder=t('kubernetes.resourcevisualeditor.s65017d')
|
||||
placeholder="键名 (如 LOG_LEVEL)"
|
||||
className="flex-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
/>
|
||||
<Textarea
|
||||
value={entry.value}
|
||||
onChange={e => update(i, 'value', e.target.value)}
|
||||
placeholder=t('k8s.resource.value')
|
||||
placeholder="值"
|
||||
rows={expandAll || entry.value.length > 300 ? 10 : (entry.value.length > 100 ? 6 : 3)}
|
||||
className="flex-[2] font-mono text-xs min-h-[70px] max-h-[420px] resize-y bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] leading-relaxed"
|
||||
/>
|
||||
@@ -347,7 +344,7 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
</div>
|
||||
))}
|
||||
{entries.length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)] text-xs">{t('kubernetes.resourcevisualeditor.saf7696')}</div>
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)] text-xs">暂无配置数据,点击「添加」创建</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,7 +363,6 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
// Secret 编辑器 — 键值对表格 (Base64 编码/解码)
|
||||
// ══════════════════════════════════════════════
|
||||
function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const rawData = manifest?.data || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
@@ -408,23 +404,23 @@ function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; o
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="shrink-0 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.name')}</Label>
|
||||
<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">{t('common.type')}</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">类型</Label>
|
||||
<Input value={manifest?.type || 'Opaque'} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 flex flex-col mt-3">
|
||||
<div className="flex items-center justify-between mb-2 shrink-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">{t('kubernetes.resourcevisualeditor.se19370')}</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">密钥数据 (已自动解码 Base64)</Label>
|
||||
<div className="flex gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => setExpandAll(!expandAll)}>
|
||||
{expandAll ? t('kubernetes.resourcevisualeditor.s880e84') : t('common.expandAll')}
|
||||
{expandAll ? '折叠全部' : '展开全部'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRow}><Plus className="h-3 w-3 mr-1" />{t('common.add')}</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRow}><Plus className="h-3 w-3 mr-1" />添加</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto pr-1 space-y-2 border border-[var(--dg-border)] rounded-lg bg-[var(--dg-bg-secondary)] p-2">
|
||||
@@ -433,13 +429,13 @@ function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; o
|
||||
<Input
|
||||
value={entry.key}
|
||||
onChange={e => update(i, 'key', e.target.value)}
|
||||
placeholder=t('kubernetes.resourcevisualeditor.scdecc4')
|
||||
placeholder="键名 (如 password)"
|
||||
className="flex-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
/>
|
||||
<Textarea
|
||||
value={entry.value}
|
||||
onChange={e => update(i, 'value', e.target.value)}
|
||||
placeholder=t('kubernetes.resourcevisualeditor.s7f123d')
|
||||
placeholder="明文值"
|
||||
rows={expandAll || entry.value.length > 300 ? 10 : (entry.value.length > 100 ? 6 : 3)}
|
||||
className="flex-[2] font-mono text-xs min-h-[70px] max-h-[420px] resize-y bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] leading-relaxed"
|
||||
/>
|
||||
@@ -449,7 +445,7 @@ function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; o
|
||||
</div>
|
||||
))}
|
||||
{entries.length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)] text-xs">{t('kubernetes.resourcevisualeditor.s47ff69')}</div>
|
||||
<div className="text-center py-8 text-[var(--dg-text-muted)] text-xs">暂无密钥数据,点击「添加」创建</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -468,7 +464,6 @@ function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; o
|
||||
// Service 编辑器 — 端口配置 + 类型 + 选择器
|
||||
// ══════════════════════════════════════════════
|
||||
function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const spec = manifest?.spec || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [svcType, setSvcType] = useState(spec.type || 'ClusterIP');
|
||||
@@ -529,11 +524,11 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="shrink-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('agent.agentServiceName')}</Label>
|
||||
<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 className="shrink-0 mt-3">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs mb-2 block">{t('k8s.resource.serviceType')}</Label>
|
||||
<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
|
||||
@@ -552,16 +547,16 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
<div className="flex-1 min-h-0 overflow-y-auto mt-3 pr-1 space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">{t('kubernetes.resourcevisualeditor.s736788')}</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addPort}><Plus className="h-3 w-3 mr-1" />{t('agent.addPort')}</Button>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">端口映射</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addPort}><Plus className="h-3 w-3 mr-1" />添加端口</Button>
|
||||
</div>
|
||||
<div className="space-y-2 border border-[var(--dg-border)] rounded-lg p-2 bg-[var(--dg-bg-secondary)]">
|
||||
{ports.map((p, i) => (
|
||||
<div key={i} className="overflow-x-auto">
|
||||
<div className="grid grid-cols-[minmax(80px,1fr)_80px_80px_80px_80px_32px] gap-2 items-center min-w-[480px]">
|
||||
<Input value={p.name} onChange={e => updatePort(i, 'name', e.target.value)} placeholder=t('common.name') className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.port} onChange={e => updatePort(i, 'port', e.target.value)} placeholder=t('server.csv.port') type="number" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.targetPort} onChange={e => updatePort(i, 'targetPort', e.target.value)} placeholder=t('common.target') type="number" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.name} onChange={e => updatePort(i, 'name', e.target.value)} placeholder="名称" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.port} onChange={e => updatePort(i, 'port', e.target.value)} placeholder="端口" type="number" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.targetPort} onChange={e => updatePort(i, 'targetPort', e.target.value)} placeholder="目标" type="number" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Select value={p.protocol} onValueChange={v => updatePort(i, 'protocol', v)}>
|
||||
<SelectTrigger className="h-7 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="TCP">TCP</SelectItem><SelectItem value="UDP">UDP</SelectItem></SelectContent>
|
||||
@@ -573,26 +568,26 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{ports.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">{t('kubernetes.resourcevisualeditor.sf3ba5e')}</div>}
|
||||
{ports.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">暂无端口,点击「添加端口」</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selector */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">{t('kubernetes.resourcevisualeditor.scecdd7')}</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addSelector}><Plus className="h-3 w-3 mr-1" />{t('kubernetes.resourcevisualeditor.s736eaa')}</Button>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">选择器 (Selector)</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addSelector}><Plus className="h-3 w-3 mr-1" />添加标签</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{selector.map((s, i) => (
|
||||
<div key={i} className="flex items-center gap-2 min-w-0">
|
||||
<Input value={s.key} onChange={e => updateSelector(i, 'key', e.target.value)} placeholder=t('kubernetes.resourcevisualeditor.s450c99') className="flex-1 min-w-0 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={s.key} onChange={e => updateSelector(i, 'key', e.target.value)} placeholder="键 (如 app)" className="flex-1 min-w-0 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<span className="text-[var(--dg-text-muted)] text-xs shrink-0">=</span>
|
||||
<Input value={s.value} onChange={e => updateSelector(i, 'value', e.target.value)} placeholder=t('kubernetes.resourcevisualeditor.sb873e4') className="flex-1 min-w-0 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={s.value} onChange={e => updateSelector(i, 'value', e.target.value)} placeholder="值 (如 nginx)" className="flex-1 min-w-0 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0 dg-text-red hover:dg-text-red" onClick={() => removeSelector(i)}><Trash2 className="h-3 w-3" /></Button>
|
||||
</div>
|
||||
))}
|
||||
{selector.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">{t('k8s.resource.noSelectors')}</div>}
|
||||
{selector.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">暂无选择器</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -611,7 +606,6 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
// Ingress 编辑器 — Host/Path 路由规则
|
||||
// ══════════════════════════════════════════════
|
||||
function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { manifest: any; clusterCode: string; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const spec = manifest?.spec || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
const [ingressClass, setIngressClass] = useState(spec.ingressClassName || 'nginx');
|
||||
@@ -695,13 +689,13 @@ function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { ma
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="shrink-0 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="min-w-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.name')}</Label>
|
||||
<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 className="min-w-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs flex items-center gap-1">
|
||||
Ingress Class
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">{t('kubernetes.resourcevisualeditor.s6c9a78')}</span>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">(可选)</span>
|
||||
</Label>
|
||||
{/* IngressClass 下拉 + 手工输入支持 // Logan.Li */}
|
||||
<div className="space-y-1 mt-1">
|
||||
@@ -714,7 +708,7 @@ function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { ma
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder={loadingClasses ? t('common.loading') : t('kubernetes.resourcevisualeditor.s25a87d')} />
|
||||
<SelectValue placeholder={loadingClasses ? '加载中...' : '选择或输入 IngressClass'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ingressClasses.length > 0 ? (
|
||||
@@ -724,13 +718,13 @@ function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { ma
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
!loadingClasses && <div className="px-2 py-1.5 text-xs text-[var(--dg-text-muted)]">{t('kubernetes.resourcevisualeditor.s619a36')}</div>
|
||||
!loadingClasses && <div className="px-2 py-1.5 text-xs text-[var(--dg-text-muted)]">未发现 IngressClass,请手动输入</div>
|
||||
)}
|
||||
{/* 当前自定义值作为选项,确保选中态 */}
|
||||
{ingressClass && !ingressClasses.some((ic) => ic.name === ingressClass) && (
|
||||
<SelectItem value={ingressClass}>{ingressClass} (手动输入)</SelectItem>
|
||||
)}
|
||||
<SelectItem value="__manual__">{t('kubernetes.resourcevisualeditor.se83eb1')}</SelectItem>
|
||||
<SelectItem value="__manual__">—— 继续手动输入 ——</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -761,8 +755,8 @@ function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { ma
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 flex flex-col mt-3">
|
||||
<div className="flex items-center justify-between mb-2 shrink-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">{t('k8s.resource.routeRules')}</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRule}><Plus className="h-3 w-3 mr-1" />{t('kubernetes.resourcevisualeditor.s4f0a38')}</Button>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">路由规则</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRule}><Plus className="h-3 w-3 mr-1" />添加 Host</Button>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto pr-1 space-y-3 border border-[var(--dg-border)] rounded-lg p-2 bg-[var(--dg-bg-secondary)]">
|
||||
{rules.map((rule, ri) => (
|
||||
@@ -771,7 +765,7 @@ function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { ma
|
||||
<Input
|
||||
value={rule.host}
|
||||
onChange={e => updateRule(ri, e.target.value)}
|
||||
placeholder=t('kubernetes.resourcevisualeditor.s33cde9')
|
||||
placeholder="域名 (如 api.example.com)"
|
||||
className="flex-1 min-w-0 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
title={rule.host}
|
||||
/>
|
||||
@@ -780,21 +774,21 @@ function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { ma
|
||||
{rule.paths.map((p, pi) => (
|
||||
<div key={pi} className="overflow-x-auto pl-4">
|
||||
<div className="grid grid-cols-[minmax(80px,1fr)_100px_minmax(80px,1fr)_60px_28px] gap-1.5 items-center min-w-[420px]">
|
||||
<Input value={p.path} onChange={e => updatePath(ri, pi, 'path', e.target.value)} placeholder=t('kubernetes.resourcevisualeditor.sdfd5eb') className="font-mono text-xs h-7 min-w-0 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.path} onChange={e => updatePath(ri, pi, 'path', e.target.value)} placeholder="路径 /" className="font-mono text-xs h-7 min-w-0 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Select value={p.pathType} onValueChange={v => updatePath(ri, pi, 'pathType', v)}>
|
||||
<SelectTrigger className="h-7 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="Prefix">Prefix</SelectItem><SelectItem value="Exact">Exact</SelectItem></SelectContent>
|
||||
</Select>
|
||||
<Input value={p.serviceName} onChange={e => updatePath(ri, pi, 'serviceName', e.target.value)} placeholder="Service" className="font-mono text-xs h-7 min-w-0 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.servicePort} onChange={e => updatePath(ri, pi, 'servicePort', e.target.value)} placeholder=t('server.csv.port') type="number" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Input value={p.servicePort} onChange={e => updatePath(ri, pi, 'servicePort', e.target.value)} placeholder="端口" type="number" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-red hover:dg-text-red" onClick={() => removePath(ri, pi)}><Trash2 className="h-3 w-3" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="ghost" size="sm" className="h-6 text-xs text-[var(--dg-text-muted)]" onClick={() => addPath(ri)}><Plus className="h-3 w-3 mr-1" />{t('kubernetes.ingress.addPath')}</Button>
|
||||
<Button variant="ghost" size="sm" className="h-6 text-xs text-[var(--dg-text-muted)]" onClick={() => addPath(ri)}><Plus className="h-3 w-3 mr-1" />添加路径</Button>
|
||||
</div>
|
||||
))}
|
||||
{rules.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">{t('kubernetes.resourcevisualeditor.s0b0794')}</div>}
|
||||
{rules.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">暂无路由规则,点击「添加 Host」</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 flex justify-end gap-2 pt-3 mt-2 border-t border-[var(--dg-border)]">
|
||||
@@ -811,7 +805,6 @@ function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { ma
|
||||
// PVC 编辑器 — 存储大小/访问模式/StorageClass
|
||||
// ══════════════════════════════════════════════
|
||||
function PvcEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const spec = manifest?.spec || {};
|
||||
const resources = spec.resources?.requests || {};
|
||||
const [resName, setResName] = useState(manifest?.metadata?.name || '');
|
||||
@@ -835,30 +828,30 @@ function PvcEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSa
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="shrink-0">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.name')}</Label>
|
||||
<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 className="grid grid-cols-2 gap-4 mt-3 shrink-0">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('kubernetes.resourcevisualeditor.s61aa82')}</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">存储大小</Label>
|
||||
<Input
|
||||
value={size}
|
||||
onChange={e => setSize(e.target.value)}
|
||||
placeholder=t('kubernetes.resourcevisualeditor.s1b915a')
|
||||
placeholder="如 10Gi"
|
||||
className="mt-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
/>
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">{t('kubernetes.resourcevisualeditor.s6a1c7a')}</p>
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">单位:Gi (GiB), Mi (MiB), Ti (TiB)</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('k8s.resource.accessMode')}</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">访问模式</Label>
|
||||
<Select value={accessMode} onValueChange={setAccessMode}>
|
||||
<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="ReadWriteOnce">{t('kubernetes.resourcevisualeditor.se1444f')}</SelectItem>
|
||||
<SelectItem value="ReadOnlyMany">{t('kubernetes.resourcevisualeditor.s9d4ea0')}</SelectItem>
|
||||
<SelectItem value="ReadWriteMany">{t('kubernetes.resourcevisualeditor.s093227')}</SelectItem>
|
||||
<SelectItem value="ReadWriteOnce">ReadWriteOnce (单节点读写)</SelectItem>
|
||||
<SelectItem value="ReadOnlyMany">ReadOnlyMany (多节点只读)</SelectItem>
|
||||
<SelectItem value="ReadWriteMany">ReadWriteMany (多节点读写)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -868,7 +861,7 @@ function PvcEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSa
|
||||
<Input
|
||||
value={storageClass}
|
||||
onChange={e => setStorageClass(e.target.value)}
|
||||
placeholder=i18n.t('kubernetes.resourcevisualeditor.s06195f')
|
||||
placeholder="留空使用默认 StorageClass"
|
||||
className="mt-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user