feat: 新增日志中心、功能优化与代码重构
- 新增 log_datasource.go / log_proxy.go / user_api_token.go 后端控制器 - 新增 log_datasource.go model - 新增 monitor/log-center.ts API 模块 - 新增 LogCenter 前端组件及页面 - 重构 deployment 相关组件 (HPDA/UpdateImageModal/bundle-yaml 等) - 新增 ResetPasswordDialog / code-snippet-block / copy-button 等 UI 组件 - 重构多个 k8s 相关组件 (TopologyView/ResourceVisualEditor/PodFileBrowser 等) - 更新多个 API 和工具函数 (auth.ts/monitor/index.ts/request.ts 等) - 更新静态资源 (index.html + 新 JS/CSS bundle) - 删除废弃静态资源文件
This commit is contained in:
1 parent
5ca0e6d83a
commit
8d46e8f352
197 files changed
+5014
-815
No files matched your search
@@ -171,16 +171,23 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-xl w-[90vw] max-w-3xl h-[85vh] flex flex-col shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-[var(--dg-border)]">
|
||||
<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)]">{isCreate ? `新建 ${meta.title}` : `${meta.title} 编辑`}</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{isCreate ? namespace : name} · {namespace} · {clusterCode}</p>
|
||||
{/* Header:长名称/域名截断,避免挤压模式切换按钮 // Logan.Li */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-[var(--dg-border)] gap-3 shrink-0">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<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 ? `新建 ${meta.title}` : `${meta.title} 编辑`}
|
||||
</h2>
|
||||
<p
|
||||
className="text-xs text-[var(--dg-text-muted)] truncate"
|
||||
title={isCreate ? `${namespace} · ${clusterCode}` : `${name} · ${namespace} · ${clusterCode}`}
|
||||
>
|
||||
{isCreate ? `${namespace} · ${clusterCode}` : `${name} · ${namespace} · ${clusterCode}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{/* Mode switcher */}
|
||||
<div className="flex rounded-lg border border-[var(--dg-border)] overflow-hidden">
|
||||
<button
|
||||
@@ -198,7 +205,7 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className={`flex-1 min-h-0 p-4 ${mode === 'yaml' ? 'flex flex-col overflow-hidden' : 'overflow-auto'}`}>
|
||||
<div className={`flex-1 min-h-0 p-4 ${mode === 'yaml' ? 'flex flex-col overflow-hidden' : 'flex flex-col overflow-hidden'}`}>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full py-20"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>
|
||||
) : mode === 'yaml' ? (
|
||||
@@ -241,19 +248,21 @@ export function ResourceVisualEditor({ clusterCode, namespace, resource, name, o
|
||||
/>
|
||||
</div>
|
||||
) : manifest ? (
|
||||
<>
|
||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||
{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 === 'ingresses' && <IngressEditor manifest={manifest} clusterCode={clusterCode} 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>
|
||||
<Button variant="outline" size="sm" className="mt-4" onClick={() => setMode('yaml')}>切换到 YAML 编辑</Button>
|
||||
<div className="flex-1 flex items-center justify-center text-center text-[var(--dg-text-muted)]">
|
||||
<div>
|
||||
<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>无法解析资源结构</p>
|
||||
@@ -275,6 +284,7 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
const [entries, setEntries] = useState<{ key: string; value: string }[]>(
|
||||
Object.entries(data).map(([key, value]) => ({ key, value: String(value) }))
|
||||
);
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
|
||||
const update = (i: number, field: 'key' | 'value', val: string) => {
|
||||
const next = [...entries];
|
||||
@@ -294,19 +304,25 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="shrink-0">
|
||||
<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>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
|
||||
<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">配置数据 (键值对)</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRow}><Plus className="h-3 w-3 mr-1" />添加</Button>
|
||||
<div className="flex gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => setExpandAll(!expandAll)}>
|
||||
{expandAll ? '折叠全部' : '展开全部'}
|
||||
</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="space-y-2 max-h-[45vh] overflow-y-auto pr-1">
|
||||
<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">
|
||||
{entries.map((entry, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
||||
<Input
|
||||
value={entry.key}
|
||||
onChange={e => update(i, 'key', e.target.value)}
|
||||
@@ -317,7 +333,8 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
value={entry.value}
|
||||
onChange={e => update(i, 'value', e.target.value)}
|
||||
placeholder="值"
|
||||
className="flex-[2] font-mono text-xs min-h-[36px] max-h-[120px] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
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"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-400 hover:text-red-300 shrink-0" onClick={() => removeRow(i)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
@@ -329,7 +346,8 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-[var(--dg-border)]">
|
||||
|
||||
<div className="shrink-0 flex justify-end gap-2 pt-3 mt-2 border-t border-[var(--dg-border)]">
|
||||
<Button size="sm" className="bg-blue-600 hover:bg-blue-700" onClick={handleSave} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
|
||||
保存配置
|
||||
@@ -345,6 +363,7 @@ function ConfigMapEditor({ manifest, onSave, saving, isCreate }: { manifest: any
|
||||
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 || '');
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
// Decode base64 values for editing
|
||||
const [entries, setEntries] = useState<{ key: string; value: string; encoded: boolean }[]>(
|
||||
Object.entries(rawData).map(([key, value]) => {
|
||||
@@ -380,34 +399,43 @@ function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; o
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-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">类型</Label>
|
||||
<Input value={manifest?.type || 'Opaque'} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-sm">密钥数据 (已自动解码 Base64)</Label>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRow}><Plus className="h-3 w-3 mr-1" />添加</Button>
|
||||
<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">名称</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="space-y-2 max-h-[45vh] overflow-y-auto pr-1">
|
||||
<div>
|
||||
<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">密钥数据 (已自动解码 Base64)</Label>
|
||||
<div className="flex gap-1">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => setExpandAll(!expandAll)}>
|
||||
{expandAll ? '折叠全部' : '展开全部'}
|
||||
</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">
|
||||
{entries.map((entry, i) => (
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
|
||||
<div key={i} className="flex items-start gap-2 p-2 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
||||
<Input
|
||||
value={entry.key}
|
||||
onChange={e => update(i, 'key', e.target.value)}
|
||||
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)]"
|
||||
/>
|
||||
<Input
|
||||
<Textarea
|
||||
value={entry.value}
|
||||
onChange={e => update(i, 'value', e.target.value)}
|
||||
placeholder="明文值"
|
||||
className="flex-[2] font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
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"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-400 hover:text-red-300 shrink-0" onClick={() => removeRow(i)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
@@ -419,7 +447,8 @@ function SecretEditor({ manifest, onSave, saving, isCreate }: { manifest: any; o
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-[var(--dg-border)]">
|
||||
|
||||
<div className="shrink-0 flex justify-end gap-2 pt-3 mt-2 border-t border-[var(--dg-border)]">
|
||||
<Button size="sm" className="bg-blue-600 hover:bg-blue-700" onClick={handleSave} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
|
||||
保存密钥
|
||||
@@ -491,12 +520,12 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="shrink-0">
|
||||
<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>
|
||||
<div className="shrink-0 mt-3">
|
||||
<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) => (
|
||||
@@ -512,15 +541,17 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ports */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<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">
|
||||
{/* 端口 + 选择器:统一滚动,避免条目过多时底部被裁切 // Logan.Li */}
|
||||
<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">端口映射</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="grid grid-cols-[1fr_80px_80px_80px_80px_32px] gap-2 items-center">
|
||||
<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="名称" 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)]" />
|
||||
@@ -533,31 +564,33 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
) : <div />}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-400 hover:text-red-300" onClick={() => removePort(i)}><Trash2 className="h-3 w-3" /></Button>
|
||||
</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">选择器 (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">
|
||||
<Input value={s.key} onChange={e => updateSelector(i, 'key', e.target.value)} placeholder="键 (如 app)" className="flex-1 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">=</span>
|
||||
<Input value={s.value} onChange={e => updateSelector(i, 'value', e.target.value)} placeholder="值 (如 nginx)" className="flex-1 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 text-red-400 hover:text-red-300" 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">暂无选择器</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">选择器 (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="键 (如 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="值 (如 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 text-red-400 hover:text-red-300" 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">暂无选择器</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-[var(--dg-border)]">
|
||||
<div className="shrink-0 flex justify-end gap-2 pt-3 mt-2 border-t border-[var(--dg-border)]">
|
||||
<Button size="sm" className="bg-blue-600 hover:bg-blue-700" onClick={handleSave} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
|
||||
保存 Service
|
||||
@@ -570,10 +603,29 @@ function ServiceEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
// ══════════════════════════════════════════════
|
||||
// Ingress 编辑器 — Host/Path 路由规则
|
||||
// ══════════════════════════════════════════════
|
||||
function IngressEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSave: (m: any) => void; saving: boolean; isCreate?: boolean }) {
|
||||
function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: { manifest: any; clusterCode: string; 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');
|
||||
// IngressClass 列表(关联集群中的 IngressController)
|
||||
const [ingressClasses, setIngressClasses] = useState<Array<{ name: string; controller: string }>>([]);
|
||||
const [loadingClasses, setLoadingClasses] = useState(false);
|
||||
|
||||
// 加载集群 IngressClass 列表
|
||||
useEffect(() => {
|
||||
if (!clusterCode) return;
|
||||
setLoadingClasses(true);
|
||||
kubernetesApi.getIngressClasses(clusterCode).then((res: any) => {
|
||||
const list = Array.isArray(res) ? res : (res?.list || res || []);
|
||||
setIngressClasses(list);
|
||||
// 新建时如果还没指定且有列表,尝试默认选第一个
|
||||
if (isCreate && (!spec.ingressClassName || spec.ingressClassName === 'nginx') && list.length > 0) {
|
||||
// 优先选 nginx 或第一个
|
||||
const prefer = list.find((c: any) => c.name?.toLowerCase().includes('nginx')) || list[0];
|
||||
if (prefer?.name) setIngressClass(prefer.name);
|
||||
}
|
||||
}).catch(() => {}).finally(() => setLoadingClasses(false));
|
||||
}, [clusterCode]);
|
||||
const [rules, setRules] = useState<{ host: string; paths: { path: string; pathType: string; serviceName: string; servicePort: string }[] }[]>(
|
||||
(spec.rules || []).map(( r: any) => ({
|
||||
host: r.host || '',
|
||||
@@ -632,44 +684,103 @@ function IngressEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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">名称</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 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)]">(可选)</span>
|
||||
</Label>
|
||||
{/* IngressClass 下拉 + 手工输入支持 // Logan.Li */}
|
||||
<div className="space-y-1 mt-1">
|
||||
<Select
|
||||
value={ingressClass || ''}
|
||||
onValueChange={(val) => {
|
||||
if (val && val !== '__manual__') {
|
||||
setIngressClass(val);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue placeholder={loadingClasses ? '加载中...' : '选择或输入 IngressClass'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ingressClasses.length > 0 ? (
|
||||
ingressClasses.map((ic) => (
|
||||
<SelectItem key={ic.name} value={ic.name}>
|
||||
{ic.name} {ic.controller ? <span className="text-[var(--dg-text-muted)] text-[10px]">({ic.controller})</span> : ''}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
!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__">—— 继续手动输入 ——</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 总是显示输入框支持手工输入或修改 */}
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] flex items-center gap-1">
|
||||
手工输入:
|
||||
<Input
|
||||
value={ingressClass}
|
||||
onChange={(e) => setIngressClass(e.target.value)}
|
||||
placeholder="alb / nginx / my-ingress-class"
|
||||
className="flex-1 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 帮助提示 */}
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] leading-tight">
|
||||
您的集群中可能有多个 IngressController,您可在此指定当前 Ingress 对应哪个 IngressController。<br />
|
||||
从 IngressClass 列表选择,或手工输入名称。
|
||||
</div>
|
||||
{/* 常见示例 */}
|
||||
{(ingressClasses.length === 0 || ingressClass) && (
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)]">
|
||||
常见:<span className="font-mono">alb</span> → ingress.k8s.alibabacloud/alb <span className="font-mono">nginx</span> → k8s.io/ingress-nginx
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<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">路由规则</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="space-y-3 max-h-[50vh] overflow-y-auto pr-1">
|
||||
<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) => (
|
||||
<div key={ri} className="p-3 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div key={ri} className="p-3 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] space-y-2 min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Input
|
||||
value={rule.host}
|
||||
onChange={e => updateRule(ri, e.target.value)}
|
||||
placeholder="域名 (如 api.example.com)"
|
||||
className="flex-1 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
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}
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-400 hover:text-red-300" onClick={() => removeRule(ri)}><Trash2 className="h-3 w-3" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0 text-red-400 hover:text-red-300" onClick={() => removeRule(ri)}><Trash2 className="h-3 w-3" /></Button>
|
||||
</div>
|
||||
{rule.paths.map((p, pi) => (
|
||||
<div key={pi} className="grid grid-cols-[1fr_100px_1fr_60px_28px] gap-1.5 items-center pl-4">
|
||||
<Input value={p.path} onChange={e => updatePath(ri, pi, 'path', 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)]" />
|
||||
<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 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 text-red-400 hover:text-red-300" onClick={() => removePath(ri, pi)}><Trash2 className="h-3 w-3" /></Button>
|
||||
<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="路径 /" 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="端口" 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 text-red-400 hover:text-red-300" 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" />添加路径</Button>
|
||||
@@ -678,7 +789,7 @@ function IngressEditor({ manifest, onSave, saving, isCreate }: { manifest: any;
|
||||
{rules.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">暂无路由规则,点击「添加 Host」</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-[var(--dg-border)]">
|
||||
<div className="shrink-0 flex justify-end gap-2 pt-3 mt-2 border-t border-[var(--dg-border)]">
|
||||
<Button size="sm" className="bg-blue-600 hover:bg-blue-700" onClick={handleSave} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
|
||||
保存 Ingress
|
||||
@@ -713,12 +824,12 @@ function PvcEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSa
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="shrink-0">
|
||||
<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">
|
||||
<div className="grid grid-cols-2 gap-4 mt-3 shrink-0">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">存储大小</Label>
|
||||
<Input
|
||||
@@ -743,7 +854,7 @@ function PvcEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSa
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="shrink-0 mt-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">StorageClass</Label>
|
||||
<Input
|
||||
value={storageClass}
|
||||
@@ -752,7 +863,10 @@ function PvcEditor({ manifest, onSave, saving, isCreate }: { manifest: any; onSa
|
||||
className="mt-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-[var(--dg-border)]">
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<div className="shrink-0 flex justify-end gap-2 pt-3 mt-2 border-t border-[var(--dg-border)]">
|
||||
<Button size="sm" className="bg-blue-600 hover:bg-blue-700" onClick={handleSave} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
|
||||
保存 PVC
|
||||
|
||||
Reference in new issue
Block a user