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
@@ -3,89 +3,166 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
ChevronDown, ChevronRight, Terminal, FileText, HardDrive, FileCode,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
ChevronDown, ChevronRight, Terminal, HardDrive, MoreVertical, FileText, FileCode, Trash2,
|
||||
} from 'lucide-react';
|
||||
import { formatK8sTime, statusColor } from '../k8s-yaml-utils';
|
||||
import { getPodTimeline, PodConditionsRow } from './helpers';
|
||||
import { statusColor } from '../k8s-yaml-utils';
|
||||
import { PodConditionsRow } from './helpers';
|
||||
import type { DeploymentViewProps } from './types';
|
||||
|
||||
export function PodsTab({
|
||||
clusterCode, pods, onShowPodLogs, onShowPodTerminal, onShowPodYaml, onShowPodFileBrowser,
|
||||
onDeletePod,
|
||||
expandedPod, setExpandedPod,
|
||||
}: DeploymentViewProps & { expandedPod: string | null; setExpandedPod: (v: string | null) => void }) {
|
||||
if (pods.length === 0) {
|
||||
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无容器组</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{pods.map(pod => {
|
||||
const tl = getPodTimeline(pod);
|
||||
const expanded = expandedPod === pod.name;
|
||||
const nodeName = pod.node_name || pod.node;
|
||||
return (
|
||||
<Card key={pod.name} className="dg-card">
|
||||
<CardContent className="p-4">
|
||||
{/* Pod header row */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 text-[var(--dg-text-muted)]" onClick={() => setExpandedPod(expanded ? null : pod.name)}>
|
||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
<span className="font-mono text-sm text-[var(--dg-text-primary)]">{pod.name}</span>
|
||||
<Badge style={{ background: `${statusColor(pod.status)}22`, color: statusColor(pod.status), border: `1px solid ${statusColor(pod.status)}44` }}>
|
||||
{pod.status || 'Unknown'}
|
||||
</Badge>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">
|
||||
<button className="text-[var(--dg-blue)] hover:underline mr-2" onClick={() => window.open(`/kubernetes/${clusterCode}/nodes/${nodeName}`, '_blank')}>
|
||||
{nodeName || '-'}
|
||||
<Card className="dg-card overflow-hidden">
|
||||
<CardContent className="p-0 overflow-x-auto">
|
||||
{/* 表头:操作列固定宽度,避免更多按钮溢出卡片 // Logan.Li */}
|
||||
<div className="grid grid-cols-[28px_minmax(140px,1fr)_72px_100px_96px_48px_56px_88px] gap-2 min-w-[740px] px-4 py-2.5 border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/40 text-[11px] font-medium text-[var(--dg-text-muted)] uppercase tracking-wide">
|
||||
<span />
|
||||
<span>名称</span>
|
||||
<span>状态</span>
|
||||
<span>节点</span>
|
||||
<span>IP</span>
|
||||
<span className="text-center">重启</span>
|
||||
<span>Age</span>
|
||||
<span className="text-right">操作</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-[var(--dg-border)]">
|
||||
{pods.map(pod => {
|
||||
const expanded = expandedPod === pod.name;
|
||||
const nodeName = pod.node_name || pod.node;
|
||||
const sc = statusColor(pod.status);
|
||||
const containers = (pod as Record<string, any>).containers as any[] | undefined;
|
||||
|
||||
return (
|
||||
<div key={pod.name}>
|
||||
{/* 列表行:紧凑信息,不重复展示完整时间线 // Logan.Li */}
|
||||
<div
|
||||
className={`grid grid-cols-[28px_minmax(140px,1fr)_72px_100px_96px_48px_56px_88px] gap-2 min-w-[740px] items-center px-4 py-3 transition-colors ${
|
||||
expanded ? 'bg-[var(--dg-bg-tertiary)]/30' : 'hover:bg-[var(--dg-bg-tertiary)]/20'
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-[var(--dg-text-muted)]"
|
||||
onClick={() => setExpandedPod(expanded ? null : pod.name)}
|
||||
>
|
||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
<span className="font-mono text-sm text-[var(--dg-text-primary)] truncate" title={pod.name}>
|
||||
{pod.name}
|
||||
</span>
|
||||
|
||||
<Badge
|
||||
className="w-fit text-[11px] font-normal"
|
||||
style={{ background: `${sc}18`, color: sc, border: `1px solid ${sc}33` }}
|
||||
>
|
||||
{pod.status || 'Unknown'}
|
||||
</Badge>
|
||||
|
||||
<button
|
||||
className="text-xs text-[var(--dg-blue)] hover:underline truncate text-left"
|
||||
title={nodeName}
|
||||
onClick={() => nodeName && window.open(`/kubernetes/${clusterCode}/nodes/${nodeName}`, '_blank')}
|
||||
>
|
||||
{nodeName || '—'}
|
||||
</button>
|
||||
</span>
|
||||
<span className="text-xs font-mono text-[var(--dg-text-muted)]">{pod.ip || pod.pod_ip || '-'}</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">重启 {pod.restart_count || 0} 次</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{formatK8sTime(tl.startTime || pod.created_at)}</span>
|
||||
<div className="flex-1" />
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-[var(--dg-blue)]" onClick={() => onShowPodLogs(pod)}>
|
||||
<FileText className="h-3.5 w-3.5 mr-1" />日志
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-emerald-400" onClick={() => onShowPodTerminal(pod)}>
|
||||
<Terminal className="h-3.5 w-3.5 mr-1" />终端
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-amber-400" onClick={() => onShowPodFileBrowser(pod)}>
|
||||
<HardDrive className="h-3.5 w-3.5 mr-1" />文件
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-slate-400 hover:text-blue-400" onClick={() => onShowPodYaml(pod)}>
|
||||
<FileCode className="h-3.5 w-3.5 mr-1" />YAML
|
||||
</Button>
|
||||
</div>
|
||||
{/* Expanded: timeline */}
|
||||
{expanded && (
|
||||
<div className="mt-3 pt-3 border-t border-[var(--dg-border)]">
|
||||
<PodConditionsRow pod={pod} />
|
||||
{/* Container details */}
|
||||
{(pod as Record<string, any>).containers && ((pod as Record<string, any>).containers as any[]).length > 0 && (
|
||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{((pod as Record<string, any>).containers as any[]).map((c: any, idx: number) => (
|
||||
<div key={idx} className="p-2 rounded bg-[var(--dg-bg-tertiary)] text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-[var(--dg-text-primary)]">{c.name}</span>
|
||||
<span className={c.ready ? 'text-emerald-400' : 'text-[var(--dg-text-muted)]'}>{c.ready ? 'Ready' : 'Pending'}</span>
|
||||
</div>
|
||||
<div className="text-[var(--dg-text-muted)] mt-1">{c.image}</div>
|
||||
{c.resources && (
|
||||
<div className="mt-1 flex gap-3">
|
||||
<span>CPU: {c.resources?.requests?.cpu || '0'}/{c.resources?.limits?.cpu || '-'} m</span>
|
||||
<span>内存: {c.resources?.requests?.memory || '0'}/{c.resources?.limits?.memory || '-'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="text-xs font-mono text-[var(--dg-text-muted)] truncate">
|
||||
{pod.ip || pod.pod_ip || '—'}
|
||||
</span>
|
||||
|
||||
<span className={`text-xs text-center font-mono ${(pod.restart_count || 0) > 0 ? 'text-amber-400' : 'text-[var(--dg-text-muted)]'}`}>
|
||||
{pod.restart_count || 0}
|
||||
</span>
|
||||
|
||||
<span className="text-xs text-[var(--dg-text-muted)] whitespace-nowrap">
|
||||
{pod.age || '—'}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center justify-end gap-0.5 shrink-0 w-[88px]">
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-emerald-400" title="终端" onClick={() => onShowPodTerminal(pod)}>
|
||||
<Terminal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 text-amber-400" title="文件" onClick={() => onShowPodFileBrowser(pod)}>
|
||||
<HardDrive className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 shrink-0 text-[var(--dg-text-muted)] hover:text-[var(--dg-blue)]"
|
||||
title="更多"
|
||||
aria-label="更多"
|
||||
>
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DropdownMenuItem onClick={() => onShowPodLogs(pod)} className="text-[var(--dg-text-primary)]">
|
||||
<FileText className="h-3.5 w-3.5 mr-2" /> 日志
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onShowPodYaml(pod)} className="text-[var(--dg-text-primary)]">
|
||||
<FileCode className="h-3.5 w-3.5 mr-2" /> YAML
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onDeletePod?.(pod)} className="text-red-400 focus:text-red-400">
|
||||
<Trash2 className="h-3.5 w-3.5 mr-2" /> 删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 展开详情:时间线与容器信息仅在此处展示 // Logan.Li */}
|
||||
{expanded && (
|
||||
<div className="px-4 pb-4 pt-1 space-y-3 bg-[var(--dg-bg-tertiary)]/20">
|
||||
<PodConditionsRow pod={pod} />
|
||||
{containers && containers.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-2">容器</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{containers.map((c: any, idx: number) => (
|
||||
<div key={idx} className="rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-3 py-2.5 text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium text-[var(--dg-text-primary)]">{c.name}</span>
|
||||
<span className={c.ready ? 'text-emerald-400' : 'text-[var(--dg-text-muted)]'}>
|
||||
{c.ready ? 'Ready' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[var(--dg-text-muted)] mt-1 truncate" title={c.image}>{c.image}</p>
|
||||
{c.resources && (
|
||||
<div className="mt-1.5 flex gap-4 text-[var(--dg-text-muted)]">
|
||||
<span>CPU {c.resources?.requests?.cpu || '0'} / {c.resources?.limits?.cpu || '—'}</span>
|
||||
<span>内存 {c.resources?.requests?.memory || '0'} / {c.resources?.limits?.memory || '—'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,12 @@ import {
|
||||
} from '../k8s-yaml-utils';
|
||||
import type { PodItem } from './types';
|
||||
|
||||
export interface PodStage {
|
||||
label: string;
|
||||
ts?: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// Pod 条件时间线提取 // Logan.Li
|
||||
export function getPodTimeline(pod: PodItem) {
|
||||
const cond: Record<string, any>[] = (pod as Record<string, any>).conditions || (pod as Record<string, any>).pod_conditions || [];
|
||||
@@ -19,6 +25,17 @@ export function getPodTimeline(pod: PodItem) {
|
||||
};
|
||||
}
|
||||
|
||||
// 启动阶段列表(列表/详情共用) // Logan.Li
|
||||
export function getPodStages(pod: PodItem): PodStage[] {
|
||||
const tl = getPodTimeline(pod);
|
||||
return [
|
||||
{ label: '已调度', ts: tl.scheduled, color: STATUS_COLORS.blue },
|
||||
{ label: '已初始化', ts: tl.initialized, color: STATUS_COLORS.orange },
|
||||
{ label: '容器已就绪', ts: tl.containersReady, color: STATUS_COLORS.green },
|
||||
{ label: '容器组已就绪', ts: tl.ready, color: STATUS_COLORS.green },
|
||||
];
|
||||
}
|
||||
|
||||
// 信息面板单项 // Logan.Li
|
||||
export function InfoItem({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
@@ -29,36 +46,99 @@ export function InfoItem({ label, value }: { label: string; value: React.ReactNo
|
||||
);
|
||||
}
|
||||
|
||||
// Pod 条件进度条 // Logan.Li
|
||||
export function PodConditionsRow({ pod }: { pod: PodItem }) {
|
||||
const tl = getPodTimeline(pod);
|
||||
const stages = [
|
||||
{ label: '已调度', ts: tl.scheduled, color: STATUS_COLORS.blue },
|
||||
{ label: '已初始化', ts: tl.initialized, color: STATUS_COLORS.orange },
|
||||
{ label: '容器已就绪', ts: tl.containersReady, color: STATUS_COLORS.green },
|
||||
{ label: '容器组已就绪', ts: tl.ready, color: STATUS_COLORS.green },
|
||||
];
|
||||
// 列表行紧凑就绪指示(无时间戳,避免视觉疲劳) // Logan.Li
|
||||
export function PodStatusCompact({ pod }: { pod: PodItem }) {
|
||||
const stages = getPodStages(pod);
|
||||
const done = stages.filter(s => s.ts).length;
|
||||
const allReady = done === stages.length;
|
||||
const pending = stages.find(s => !s.ts);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 overflow-x-auto">
|
||||
{stages.map((s, i) => {
|
||||
const prev = stages[i - 1];
|
||||
const dur = elapsedSeconds(prev?.ts, s.ts);
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[88px]">
|
||||
<div className="flex items-center gap-0.5" aria-label="启动进度">
|
||||
{stages.map((s, i) => (
|
||||
<Fragment key={s.label}>
|
||||
<div className="flex flex-col items-center min-w-[80px]">
|
||||
<div className="h-2 w-2 rounded-full" style={{ background: s.ts ? s.color : '#334155' }} />
|
||||
<span className="text-[10px] mt-1 font-medium" style={{ color: s.ts ? s.color : '#475569' }}>{s.label}</span>
|
||||
<span className="text-[9px] text-[var(--dg-text-muted)]">{s.ts ? formatK8sTime(s.ts) : '-'}</span>
|
||||
</div>
|
||||
<span
|
||||
className="inline-block h-1.5 w-1.5 rounded-full shrink-0"
|
||||
style={{ background: s.ts ? s.color : 'var(--dg-border)' }}
|
||||
title={`${s.label}${s.ts ? ` · ${formatK8sTime(s.ts)}` : ''}`}
|
||||
/>
|
||||
{i < stages.length - 1 && (
|
||||
<div className="flex flex-col items-center mx-0.5 min-w-[30px]">
|
||||
<div className="h-0.5 w-8 bg-[var(--dg-border)]" />
|
||||
<span className="text-[9px] text-[var(--dg-text-muted)] mt-0.5">{dur != null ? formatDuration(dur) : ''}</span>
|
||||
</div>
|
||||
<span
|
||||
className="inline-block h-px w-2 shrink-0"
|
||||
style={{ background: s.ts && stages[i + 1]?.ts ? s.color : 'var(--dg-border)' }}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
<span
|
||||
className="text-[11px] whitespace-nowrap"
|
||||
style={{ color: allReady ? STATUS_COLORS.green : 'var(--dg-text-muted)' }}
|
||||
>
|
||||
{allReady ? '就绪' : pending?.label || `${done}/${stages.length}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 详情区完整启动时间线(仅展开或概览面板使用) // Logan.Li
|
||||
export function PodConditionsRow({ pod }: { pod: PodItem }) {
|
||||
const stages = getPodStages(pod);
|
||||
const totalBoot = elapsedSeconds(stages[0]?.ts, stages[stages.length - 1]?.ts);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg bg-[var(--dg-bg-tertiary)]/60 px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">启动时间线</span>
|
||||
{totalBoot != null && (
|
||||
<span className="text-[11px] text-[var(--dg-text-muted)]">
|
||||
总耗时 <span className="font-mono text-[var(--dg-text-secondary)]">{formatDuration(totalBoot)}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-start gap-0 overflow-x-auto">
|
||||
{stages.map((s, i) => {
|
||||
const prev = stages[i - 1];
|
||||
const dur = elapsedSeconds(prev?.ts, s.ts);
|
||||
const active = !!s.ts;
|
||||
return (
|
||||
<Fragment key={s.label}>
|
||||
<div className="flex flex-col min-w-[108px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold"
|
||||
style={{
|
||||
background: active ? `${s.color}22` : 'var(--dg-bg-secondary)',
|
||||
color: active ? s.color : 'var(--dg-text-muted)',
|
||||
border: `1px solid ${active ? `${s.color}55` : 'var(--dg-border)'}`,
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="text-xs font-medium" style={{ color: active ? s.color : 'var(--dg-text-muted)' }}>
|
||||
{s.label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="mt-1.5 pl-7 text-[11px] font-mono text-[var(--dg-text-muted)]">
|
||||
{s.ts ? formatK8sTime(s.ts) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
{i < stages.length - 1 && (
|
||||
<div className="flex flex-col items-center justify-center px-1 pt-2.5 min-w-[48px]">
|
||||
<div
|
||||
className="h-px w-full"
|
||||
style={{ background: active && stages[i + 1]?.ts ? s.color : 'var(--dg-border)' }}
|
||||
/>
|
||||
<span className="mt-1 text-[10px] text-[var(--dg-text-muted)]">
|
||||
{dur != null ? formatDuration(dur) : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -29,4 +29,5 @@ export interface DeploymentViewProps {
|
||||
onShowPodTerminal: (pod: PodItem) => void;
|
||||
onShowPodYaml: (pod: PodItem) => void;
|
||||
onShowPodFileBrowser: (pod: PodItem) => void;
|
||||
onDeletePod?: (pod: PodItem) => void;
|
||||
}
|
||||
Reference in new issue
Block a user