feat(log-center): 日志中心 v4 全量组件 — 统一查询面板/时间选择器/统计图表/聚类/数据源管理

This commit is contained in:
attacker committed 2026-06-27 11:41:50 +08:00
1 parent 880cf050fe
commit bdf3c2daf4
30 files changed
+4276 -136

No files matched your search

@@ -14,7 +14,7 @@ import {
import {
RefreshCw, Search, Loader2, Boxes, Container,
Network, FileText, Shield, Activity, Clock, ChevronRight,
Download, FileCode, Pencil, Terminal, HardDrive, Plus, Upload,
Download, FileCode, Pencil, Terminal, HardDrive, Plus, Upload, Trash2, Eye,
} from 'lucide-react';
import { YamlViewer } from './YamlViewer';
import { ResourceVisualEditor } from './ResourceVisualEditor';
@@ -23,6 +23,7 @@ import { PodExecTerminal } from './PodExecTerminal';
import { PodFileBrowser } from './PodFileBrowser';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog';
type ResourceType =
| 'deployments' | 'pods' | 'services' | 'ingresses'
@@ -53,7 +54,7 @@ const RESOURCE_CONFIG: Record<ResourceType, {
subtitle: string;
icon: React.ComponentType<any>;
columns: string[];
renderRow: ( item: any, opts: { clusterCode: string; namespace: string; onDetail?: ( item: any) => void; onYaml?: (name: string) => void; onEdit?: (name: string) => void; onTerminal?: ( item: any) => void; onFileBrowser?: ( item: any) => void }) => React.ReactNode;
renderRow: ( item: any, opts: { clusterCode: string; namespace: string; onDetail?: ( item: any) => void; onYaml?: (name: string) => void; onEdit?: (name: string) => void; onTerminal?: ( item: any) => void; onFileBrowser?: ( item: any) => void; onDeletePod?: ( item: any) => void; onPodDetail?: ( item: any) => void }) => React.ReactNode;
}> = {
deployments: {
title: 'Deployments', subtitle: 'Deployment 工作负载运行状态', icon: Boxes,
@@ -87,7 +88,12 @@ const RESOURCE_CONFIG: Record<ResourceType, {
const statusMap: Record<string, string> = { Running: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', Pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', Failed: 'bg-red-500/20 text-red-400 border-red-500/30' };
return (
<TableRow key={p.name} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{p.name}</TableCell>
<TableCell className="font-medium text-slate-200 cursor-pointer hover:text-[var(--dg-blue)]" onClick={() => opts.onPodDetail?.(p)}>
<div className="flex items-center gap-1.5">
<Eye className="h-3.5 w-3.5 text-[var(--dg-text-muted)] shrink-0" />
<span className="truncate">{p.name}</span>
</div>
</TableCell>
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{p.namespace}</Badge></TableCell>
<TableCell><Badge className={statusMap[p.status] || 'bg-slate-500/20 text-slate-400'}>{p.status || 'Unknown'}</Badge></TableCell>
<TableCell className="text-sm text-slate-400">{p.node_name || '-'}</TableCell>
@@ -103,6 +109,8 @@ const RESOURCE_CONFIG: Record<ResourceType, {
onClick={() => (opts as Record<string, any>).onFileBrowser?.(p)}><HardDrive className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-blue-400" title="YAML"
onClick={() => opts.onYaml?.(p.name)}><FileCode className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-red-400 hover:text-red-300" title="删除"
onClick={() => opts.onDeletePod?.(p)}><Trash2 className="h-3.5 w-3.5" /></Button>
</div>
</TableCell>
</TableRow>
@@ -320,6 +328,7 @@ export default function NamespaceResourcePage() {
const location = useLocation();
const navigate = useNavigate();
const { toast } = useToast();
const confirm = useConfirm();
const type = useMemo<ResourceType>(() => {
const segments = location.pathname.split('/');
const last = segments[segments.length - 1];
@@ -343,6 +352,7 @@ export default function NamespaceResourcePage() {
const [terminalPod, setTerminalPod] = useState<any>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<any>(null);
const [podDetail, setPodDetail] = useState<any>(null);
useEffect(() => {
kubernetesApi.getClusters().then(( res: any) => {
@@ -434,6 +444,23 @@ export default function NamespaceResourcePage() {
}
};
// Pod 删除 // Logan.Li
const handleDeletePod = async (p: any) => {
const ok = await confirm({
title: '删除 Pod',
description: '确认删除 Pod "' + (p.name || '') + '"?操作不可恢复。',
confirmText: '删除',
});
if (!ok) return;
try {
await kubernetesApi.deletePod(clusterCode || '', namespace || '', p.name);
toast({ title: 'Pod 已删除' });
loadData();
} catch (e) {
toast({ title: '删除失败', description: (e as any)?.message || String(e), variant: 'destructive' });
}
};
if (!config) return <div className="text-slate-400 p-8 text-center">未知资源类型: {type}</div>;
return (
@@ -534,7 +561,7 @@ export default function NamespaceResourcePage() {
) : filtered.length === 0 ? (
<TableRow><TableCell colSpan={config.columns.length} className="text-center text-slate-500 py-12"><config.icon className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无 {config.title}</TableCell></TableRow>
) : (
filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml, onEdit: handleEdit, onTerminal: ( p: any) => setTerminalPod(p), onFileBrowser: ( p: any) => setFileBrowserPod(p) }))
filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml, onEdit: handleEdit, onTerminal: ( p: any) => setTerminalPod(p), onFileBrowser: ( p: any) => setFileBrowserPod(p), onDeletePod: handleDeletePod, onPodDetail: ( p: any) => setPodDetail(p) }))
)}
</TableBody>
</Table>
@@ -559,6 +586,129 @@ export default function NamespaceResourcePage() {
podName={fileBrowserPod?.name || ''}
containers={(fileBrowserPod?.containers || []).map(( c: any) => c.name).filter(Boolean)}
/>
{/* ============ Pod 详情弹窗 ============ */}
{podDetail && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setPodDetail(null)}>
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg w-[680px] max-h-[85vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--dg-border)]">
<h3 className="text-base font-semibold text-[var(--dg-text-primary)] flex items-center gap-2">
<Container className="h-4 w-4 text-[var(--dg-blue)]" />
Pod 详情
</h3>
<button className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]" onClick={() => setPodDetail(null)}>✕</button>
</div>
<div className="p-5 space-y-5">
{/* 基本信息 */}
<div>
<h4 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3">基本信息</h4>
{(() => {
const p = podDetail;
const labels = p.labels || p.metadata?.labels || {};
const annotations = p.annotations || p.metadata?.annotations || {};
const created = p.creationTimestamp || p.created_at || p.create_time;
const statusMap: Record<string, string> = { Running: 'bg-emerald-500/20 text-emerald-400', Pending: 'bg-yellow-500/20 text-yellow-400', Failed: 'bg-red-500/20 text-red-400' };
const st = p.status || 'Running';
return (
<div className="grid grid-cols-2 gap-x-6 gap-y-2.5">
<div>
<span className="text-xs text-[var(--dg-text-muted)]">名称</span>
<p className="text-sm text-[var(--dg-text-primary)] font-mono">{p.name || '-'}</p>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]">命名空间</span>
<p className="text-sm text-[var(--dg-text-primary)]">{p.namespace || '-'}</p>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]">状态</span>
<div><Badge className={statusMap[st] || 'bg-slate-500/20 text-slate-400'}>{st}</Badge></div>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]">创建时间</span>
<p className="text-sm text-[var(--dg-text-primary)]">{created ? new Date(created).toLocaleString('zh-CN') : '-'}</p>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]">节点</span>
<p className="text-sm text-[var(--dg-text-primary)]">{p.node_name || p.node || '-'}</p>
</div>
<div>
<span className="text-xs text-[var(--dg-text-muted)]">Pod IP</span>
<p className="text-sm text-[var(--dg-text-primary)] font-mono">{p.ip || p.pod_ip || '-'}</p>
</div>
<div className="col-span-2">
<span className="text-xs text-[var(--dg-text-muted)]">标签</span>
<div className="flex flex-wrap gap-1 mt-0.5">
{Object.keys(labels).length === 0 ? <span className="text-xs text-[var(--dg-text-muted)]">-</span>
: Object.entries(labels).map(([k, v]) => (
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px] font-mono">{k}: {String(v)}</Badge>
))
}
</div>
</div>
<div className="col-span-2">
<span className="text-xs text-[var(--dg-text-muted)]">注解</span>
<div className="flex flex-wrap gap-1 mt-0.5">
{Object.keys(annotations).length === 0 ? <span className="text-xs text-[var(--dg-text-muted)]">-</span>
: Object.entries(annotations).slice(0, 6).map(([k, v]) => (
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px] font-mono max-w-[300px] truncate" title={String(v)}>{k}</Badge>
))
}
</div>
</div>
</div>
);
})()}
</div>
{/* 现状详情(Conditions) */}
<div>
<h4 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3">现状详情</h4>
{(() => {
const cond: any[] = podDetail.conditions || podDetail.pod_conditions || [];
if (cond.length === 0) return <p className="text-xs text-[var(--dg-text-muted)] py-2">暂无数据</p>;
return (
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-muted)] text-xs">类型</TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs">状态</TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs">更新时间</TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs">原因</TableHead>
<TableHead className="text-[var(--dg-text-muted)] text-xs">消息</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{cond.map((c: any, idx: number) => {
const type = c.type || c.Type || '-';
const st = c.status || c.Status || '-';
const ts = c.lastTransitionTime || c.LastTransitionTime || '';
const reason = c.reason || c.Reason || '-';
const msg = c.message || c.Message || '-';
const isTrue = st === 'True';
const isFalse = st === 'False';
return (
<TableRow key={idx} className="border-[var(--dg-border)]">
<TableCell className="text-xs text-[var(--dg-text-primary)] font-medium">{type}</TableCell>
<TableCell>
<Badge className={isTrue ? 'bg-emerald-500/15 text-emerald-400 border-emerald-500/25' : isFalse ? 'bg-red-500/15 text-red-400 border-red-500/25' : 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]'}>{st}</Badge>
</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)] whitespace-nowrap">{ts ? new Date(ts).toLocaleString('zh-CN') : '-'}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-primary)]">{reason}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[200px] truncate" title={msg}>{msg}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
})()}
</div>
</div>
<div className="flex justify-end px-5 py-3 border-t border-[var(--dg-border)]">
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setPodDetail(null)}>关闭</Button>
</div>
</div>
</div>
)}
</div>
);
}
@@ -18,8 +18,8 @@ import { statusColor } from '../k8s-yaml-utils';
import { PodConditionsRow } from './helpers';
import type { DeploymentViewProps } from './types';
const GRID_COLS = '28px minmax(140px,1fr) 72px 100px 96px 48px 56px 88px';
const MIN_WIDTH = '740px';
const GRID_COLS = '28px minmax(120px,3fr) 76px minmax(80px,1fr) minmax(70px,1fr) 56px 56px 88px';
const MIN_WIDTH = '700px';
const ROW_HEIGHT = 56;
export function PodsTab({