优化 K8s 页面布局与 local 模式部署收尾

P0-P1:
- Dockerfile.embed 启用 CGO 支持 SQLite+redka
- K8s scale>1 时警告 local 模式数据不共享

K8s UI:
- 移除侧边栏资源用量/容量分析及资源页横向 Tab 栏
- 修复事件路由(/workloads/events)与 Deployment 事件过滤
- 网络/配置/存储统一 YamlViewer 编辑,加大编辑弹窗
This commit is contained in:
Logan.Li committed 2026-06-26 13:36:27 +08:00
1 parent 859db427a4
commit 339b24a579
12 files changed
+87 -90

No files matched your search

@@ -104,17 +104,20 @@ export function DeploymentView(props: DeploymentViewProps) {
setTabLoading('events');
kubernetesApi.getEvents(clusterCode, namespace, undefined).then((res: any) => {
const allEvents = Array.isArray(res) ? res : (res?.list || res?.items || res?.data || []);
const depName = deployment?.name || '';
const depEvents = allEvents.filter((e: any) => {
const involved = e.involvedObject || e.involved_object || {};
const objName = involved.name || '';
return objName === deployment?.name || objName.startsWith(deployment?.name + '-');
const obj = String(e.object || '');
if (!depName || !obj) return false;
if (obj.endsWith(`/${depName}`)) return true;
const name = obj.split('/').pop() || '';
return name === depName || name.startsWith(`${depName}-`);
}).map((e: any) => ({
type: e.type || 'Normal',
reason: e.reason || '',
message: e.message || '',
source: (e.source?.component || e.sourceComponent || '-'),
firstTimestamp: e.firstTimestamp || e.first_timestamp || '',
lastTimestamp: e.lastTimestamp || e.last_timestamp || '',
source: e.object || e.source?.component || e.sourceComponent || '-',
firstTimestamp: e.first_time || e.firstTimestamp || e.first_timestamp || '',
lastTimestamp: e.last_time || e.lastTimestamp || e.last_timestamp || '',
count: e.count || 1,
}));
setEvents(depEvents);
@@ -85,6 +85,14 @@ export default function EventsPage() {
// Filter events by selected reason/kind from aggregation click
const displayEvents = useMemo(() => {
let filtered = events;
const hours = parseInt(timeWindow, 10);
if (hours > 0) {
const since = Date.now() - hours * 3600 * 1000;
filtered = filtered.filter((e) => {
const ts = e.last_time || e.lastTime || e.last_timestamp;
return !ts || new Date(ts).getTime() >= since;
});
}
if (selectedReason) {
filtered = filtered.filter(e => e.reason === selectedReason);
}
@@ -92,7 +100,7 @@ export default function EventsPage() {
filtered = filtered.filter(e => (e.object || '').startsWith(selectedKind + '/'));
}
return filtered;
}, [events, selectedReason, selectedKind]);
}, [events, selectedReason, selectedKind, timeWindow]);
const warningCount = useMemo(() => events.filter(e => e.type === 'Warning').length, [events]);
const normalCount = useMemo(() => events.filter(e => e.type === 'Normal').length, [events]);
@@ -0,0 +1,35 @@
// K8sShortcutRedirect — 侧边栏快捷入口跳转到首个集群 // Logan.Li
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { kubernetesApi } from '@/api';
interface Props {
/** 集群下路径,如 events、namespace/default/deployments */
target: string;
}
export default function K8sShortcutRedirect({ target }: Props) {
const navigate = useNavigate();
useEffect(() => {
kubernetesApi.getClusters()
.then((res: any) => {
const list = res?.list || [];
if (list.length > 0) {
const code = list[0].code || String(list[0].id);
navigate(`/kubernetes/${code}/${target}`, { replace: true });
} else {
navigate('/kubernetes', { replace: true });
}
})
.catch(() => navigate('/kubernetes', { replace: true }));
}, [navigate, target]);
return (
<div className="flex flex-col items-center justify-center py-24 text-slate-500 gap-2">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="text-sm">正在跳转...</span>
</div>
);
}
@@ -16,7 +16,6 @@ import {
Download, FileCode, Pencil, Terminal, HardDrive,
} from 'lucide-react';
import { YamlViewer } from './YamlViewer';
import { ResourceVisualEditor } from './ResourceVisualEditor';
import { PodExecTerminal } from './PodExecTerminal';
import { PodFileBrowser } from './PodFileBrowser';
import { kubernetesApi } from '@/api';
@@ -106,10 +105,8 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell className="text-sm text-slate-400">{s.cluster_ip || '-'}</TableCell>
<TableCell className="text-sm text-slate-400">{s.ports || '-'}</TableCell>
<TableCell>
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML" onClick={() => opts.onYaml?.(s.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑" onClick={() => opts.onEdit?.(s.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-blue-400 hover:text-blue-300" title="YAML 编辑"
onClick={() => opts.onEdit?.(s.name)}><FileCode className="h-3.5 w-3.5 mr-1" />YAML</Button>
</TableCell>
</TableRow>
),
@@ -126,10 +123,8 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell className="text-sm text-slate-400 max-w-[250px] truncate">{hosts.length > 0 ? hosts.join(', ') : '-'}</TableCell>
<TableCell><Badge className={i.tls ? 'bg-emerald-500/20 text-emerald-400' : 'bg-slate-500/20 text-slate-400'}>{i.tls ? 'HTTPS' : 'HTTP'}</Badge></TableCell>
<TableCell>
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML" onClick={() => opts.onYaml?.(i.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑" onClick={() => opts.onEdit?.(i.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-blue-400 hover:text-blue-300" title="YAML 编辑"
onClick={() => opts.onEdit?.(i.name)}><FileCode className="h-3.5 w-3.5 mr-1" />YAML</Button>
</TableCell>
</TableRow>
);
@@ -144,12 +139,8 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{c.namespace}</Badge></TableCell>
<TableCell className="text-sm text-slate-400">{c.key_count || '-'}</TableCell>
<TableCell>
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
onClick={() => opts.onYaml?.(c.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
onClick={() => opts.onEdit?.(c.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-blue-400 hover:text-blue-300" title="YAML 编辑"
onClick={() => opts.onEdit?.(c.name)}><FileCode className="h-3.5 w-3.5 mr-1" />YAML</Button>
</TableCell>
</TableRow>
),
@@ -163,12 +154,8 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{s.namespace}</Badge></TableCell>
<TableCell className="text-sm text-slate-400">{s.type || '-'}</TableCell>
<TableCell>
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
onClick={() => opts.onYaml?.(s.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
onClick={() => opts.onEdit?.(s.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-blue-400 hover:text-blue-300" title="YAML 编辑"
onClick={() => opts.onEdit?.(s.name)}><FileCode className="h-3.5 w-3.5 mr-1" />YAML</Button>
</TableCell>
</TableRow>
),
@@ -211,10 +198,8 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell className="text-sm text-slate-400">{p.size || '-'}</TableCell>
<TableCell className="text-sm text-slate-400">{p.storage_class || '-'}</TableCell>
<TableCell>
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML" onClick={() => opts.onYaml?.(p.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑" onClick={() => opts.onEdit?.(p.name)}><Pencil className="h-3.5 w-3.5" /></Button>
</div>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-blue-400 hover:text-blue-300" title="YAML 编辑"
onClick={() => opts.onEdit?.(p.name)}><FileCode className="h-3.5 w-3.5 mr-1" />YAML</Button>
</TableCell>
</TableRow>
),
@@ -353,7 +338,7 @@ export default function NamespaceResourcePage() {
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
const [yamlItem, setYamlItem] = useState<{resource: string; name: string; editMode?: boolean} | null>(null);
const [visualEditItem, setVisualEditItem] = useState<{resource: string; name: string} | null>(null);
const [terminalPod, setTerminalPod] = useState<any>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<any>(null);
@@ -420,34 +405,11 @@ export default function NamespaceResourcePage() {
};
const handleYaml = (name: string) => setYamlItem({ resource: type, name });
const handleEdit = (name: string) => {
// 使用可视化编辑器的资源类型
if (['configmaps', 'secrets', 'services', 'ingresses', 'pvcs'].includes(type)) {
setVisualEditItem({ resource: type, name });
} else {
setYamlItem({ resource: type, name, editMode: true });
}
};
// 网络/配置/存储统一走 YamlViewer 编辑(清理只读字段 + CodeMirror)// Logan.Li
const handleEdit = (name: string) => setYamlItem({ resource: type, name, editMode: true });
const handleExportCSV = () => kubernetesApi.exportAsCSV(`${type}-${clusterCode}-${namespace}.csv`, filtered);
const handleExportJSON = () => kubernetesApi.exportAsJSON(`${type}-${clusterCode}-${namespace}.json`, filtered);
const sidebarResources = useMemo(() => [
{ type: 'deployments' as ResourceType, label: 'Deployments', icon: Boxes },
{ type: 'statefulsets' as ResourceType, label: 'StatefulSets', icon: Boxes },
{ type: 'daemonsets' as ResourceType, label: 'DaemonSets', icon: Boxes },
{ type: 'pods' as ResourceType, label: 'Pods', icon: Container },
{ type: 'cronjobs' as ResourceType, label: 'CronJobs', icon: Clock },
{ type: 'services' as ResourceType, label: 'Services', icon: Network },
{ type: 'ingresses' as ResourceType, label: 'Ingresses', icon: Network },
{ type: 'configmaps' as ResourceType, label: 'ConfigMaps', icon: FileText },
{ type: 'secrets' as ResourceType, label: 'Secrets', icon: Shield },
{ type: 'hpas' as ResourceType, label: 'HPAs', icon: Activity },
{ type: 'pvcs' as ResourceType, label: '存储卷', icon: Boxes },
{ type: 'serviceaccounts' as ResourceType, label: 'ServiceAccounts', icon: Shield },
{ type: 'roles' as ResourceType, label: 'Roles', icon: Shield },
{ type: 'rolebindings' as ResourceType, label: 'RoleBindings', icon: Shield },
], []);
if (!config) return <div className="text-slate-400 p-8 text-center">未知资源类型: {type}</div>;
return (
@@ -464,16 +426,7 @@ export default function NamespaceResourcePage() {
autoEdit={yamlItem.editMode === true}
/>
)}
{visualEditItem && (
<ResourceVisualEditor
clusterCode={clusterCode || ''}
namespace={namespace || ''}
resource={visualEditItem.resource}
name={visualEditItem.name}
onClose={() => setVisualEditItem(null)}
onSaved={loadData}
/>
)}
<div className="flex items-center gap-2 text-sm text-slate-500">
<Link to="/kubernetes" className="hover:text-slate-300 transition-colors">集群</Link>
{clusterCode && <><ChevronRight className="h-3 w-3" /><Link to={`/kubernetes/${clusterCode}`} className="hover:text-slate-300 transition-colors">{clusterCode}</Link></>}
@@ -505,18 +458,12 @@ export default function NamespaceResourcePage() {
</Button>
</div>
</div>
<div className="flex items-center gap-1 bg-slate-900/50 border border-slate-800 rounded-lg p-1 overflow-x-auto">
{sidebarResources.map((r) => (
<Link key={r.type} to={`/kubernetes/${clusterCode}/namespace/${namespace}/${r.type}`}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors whitespace-nowrap ${r.type === type ? 'bg-blue-600/20 text-blue-400' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'}`}>
<r.icon className="h-3 w-3" />{r.label}
</Link>
))}
</div>
<div className="relative w-56">
<div className="flex items-center gap-3 flex-wrap">
<div className="relative w-56 flex-1 min-w-[200px] max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input placeholder="搜索..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
</div>
</div>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<Table>
@@ -183,12 +183,12 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
// 弹窗模式(原有行为)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
<div className="bg-slate-900 border border-slate-700 rounded-xl w-[90vw] max-w-4xl max-h-[85vh] flex flex-col shadow-2xl" onClick={e => e.stopPropagation()}>
<div className="bg-slate-900 border border-slate-700 rounded-xl w-[92vw] max-w-5xl h-[88vh] flex flex-col shadow-2xl" onClick={e => e.stopPropagation()}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-700">
<div>
<h2 className="text-lg font-bold text-slate-100">{resource}/{name}</h2>
<p className="text-xs text-slate-500">{namespace} · {clusterCode}</p>
<h2 className="text-lg font-bold text-slate-100">{editing ? 'YAML 编辑' : 'YAML 查看'} — {resource}/{name}</h2>
<p className="text-xs text-slate-500">{namespace} · {clusterCode}{editing ? ' · 已清理只读字段' : ''}</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300" onClick={copyYaml}>
@@ -229,13 +229,13 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
</div>
</div>
{/* Body */}
<div className="flex-1 overflow-auto p-4">
<div className="flex-1 min-h-0 overflow-hidden p-4">
{loading ? (
<div className="flex items-center justify-center h-full"><Loader2 className="h-6 w-6 animate-spin text-slate-500" /></div>
) : (
<CodeMirror
value={display}
height="600px"
height="100%"
theme={oneDark}
extensions={extensions}
onChange={(val) => editing && setEditYaml(val)}
@@ -249,7 +249,7 @@ export function YamlViewer({ clusterCode, namespace, resource, name, onClose, on
closeBrackets: true,
indentOnInput: true,
}}
style={{ fontSize: '12px' }}
style={{ fontSize: '13px', height: '100%' }}
/>
)}
</div>
@@ -36,7 +36,7 @@ export function EventsTab({ events, loading }: { events: EventItem[]; loading: s
</Badge>
</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{e.reason}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-primary)] max-w-md truncate" title={e.message}>{e.message}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-primary)] max-w-md whitespace-normal break-words">{e.message}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">{e.source}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-muted)]">{e.count}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">{formatK8sTime(e.lastTimestamp)}</TableCell>
@@ -27,9 +27,8 @@ const ROUTE_NAME_MAP: Record<string, string> = {
'secrets': 'Secrets',
'hpas': 'HPA',
'pvcs': '存储',
'metrics': '资源用量',
'events': '事件',
'capacity': '容量分析',
'topology': '架构拓扑',
'namespace': '命名空间',
'workload': '工作负载',
@@ -98,9 +98,7 @@ const menuItems: MenuItem[] = [
],
},
{ id: 'k8s-storage', name: '存储', icon: Database, path: '/workloads/pvcs', permission: 'k8s:workloads' },
{ id: 'k8s-metrics', name: '资源用量', icon: Activity, path: '/workloads/metrics', permission: 'k8s:workloads' },
{ id: 'k8s-events', name: '事件', icon: Bell, path: '/workloads/events', permission: 'k8s:workloads' },
{ id: 'k8s-capacity', name: '容量分析', icon: BarChart3, path: '/workloads/capacity', permission: 'k8s:workloads' },
{ id: 'k8s-topology', name: '架构拓扑', icon: Network, path: '/topology', permission: 'k8s:topology' },
],
},