refactor(frontend): P3 — K8s 组件拆分、AlertCenter Query、permission utils
- 拆分 DeploymentView / DeploymentDetail 至子目录(<700 行红线) - AlertCenter 接入 useAlerts / useAgentAlertRules / useAlertMutations - 新增 utils/permission.ts,usePermission 复用纯函数 - useAlerts 增强分页统计与 Agent 规则 hooks
This commit is contained in:
1 parent
37c657987b
commit
521bfd356f
24 files changed
+1454
-1221
No files matched your search
@@ -0,0 +1,49 @@
|
||||
// deployment-view/EventsTab.tsx — 事件 Tab // Logan.Li
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { formatK8sTime } from '../k8s-yaml-utils';
|
||||
import type { EventItem } from './types';
|
||||
|
||||
export function EventsTab({ events, loading }: { events: EventItem[]; loading: string }) {
|
||||
if (loading === 'events') {
|
||||
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
|
||||
}
|
||||
if (events.length === 0) {
|
||||
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无事件</div>;
|
||||
}
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-16">类型</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-24">原因</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">消息</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-20">来源</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-10">次数</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-40">最后时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{events.map((e, i) => (
|
||||
<TableRow key={i} className="border-[var(--dg-border)]">
|
||||
<TableCell>
|
||||
<Badge variant="outline" className={e.type === 'Warning' ? 'text-[var(--dg-yellow)] border-[var(--dg-yellow)]/30' : 'text-[var(--dg-text-muted)] border-[var(--dg-border)]'}>
|
||||
{e.type}
|
||||
</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-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>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// deployment-view/HPACronHPATab.tsx — 容器伸缩 Tab (HPA + CronHPA) // Logan.Li
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Activity, Clock, Loader2 } from 'lucide-react';
|
||||
|
||||
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any[]; cronHpas: any[]; loading: string; replicas: number }) {
|
||||
if (loading === 'scale') {
|
||||
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 当前副本数提示 */}
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<span className="text-sm text-[var(--dg-text-muted)]">当前副本:</span>
|
||||
<span className="text-sm font-bold text-[var(--dg-text-primary)]">{replicas}</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">(可通过 Tab 栏右侧 +/- 手动调整)</span>
|
||||
</div>
|
||||
|
||||
{/* HPA 策略 */}
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Activity className="h-4 w-4 text-[var(--dg-orange)]" />HPA — 水平自动伸缩</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
{hpas.length === 0 ? (
|
||||
<div className="text-sm text-[var(--dg-text-muted)] py-4">
|
||||
未关联 HPA — <Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">创建 HPA</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">最小/最大</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">指标</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">当前负载</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">状态</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{hpas.map((hpa: any, i: number) => {
|
||||
const minR = hpa.spec?.minReplicas ?? hpa.minReplicas ?? 1;
|
||||
const maxR = hpa.spec?.maxReplicas ?? hpa.maxReplicas ?? 10;
|
||||
const metrics = hpa.spec?.metrics || hpa.metrics || [];
|
||||
const curR = hpa.status?.currentReplicas ?? hpa.currentReplicas ?? 0;
|
||||
const conditions = hpa.status?.conditions || hpa.conditions || [];
|
||||
const ableToScale = conditions.find((c: any) => c.type === 'AbleToScale');
|
||||
const ok = ableToScale?.status === 'True';
|
||||
return (
|
||||
<TableRow key={hpa.name || i} className="border-[var(--dg-border)]">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{hpa.name || hpa.metadata?.name}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{minR} → {maxR}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
||||
{metrics.map((m: any, j: number) => {
|
||||
const resource = m.resource || m.object?.metric?.name || '';
|
||||
const target = m.resource?.target?.averageUtilization || m.resource?.target?.averageValue || m.resource?.target?.type || '-';
|
||||
return <div key={j}>{resource?.name || resource}: {target}{m.resource?.target?.averageUtilization ? '%' : ''}</div>;
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm font-mono text-[var(--dg-text-secondary)]">{curR}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={ok ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]'}>
|
||||
{ok ? '正常' : '异常'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CronHPA 策略 */}
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Clock className="h-4 w-4 text-[var(--dg-blue)]" />CronHPA — 定时伸缩</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
{cronHpas.length === 0 ? (
|
||||
<div className="text-sm text-[var(--dg-text-muted)] py-4">
|
||||
未关联 CronHPA — <Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">创建 CronHPA</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">目标副本</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">调度规则</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">时区</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">状态</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{cronHpas.map((cron: any, i: number) => {
|
||||
const jobs = cron.spec?.jobs || cron.jobs || [];
|
||||
const tz = cron.spec?.timezone || 'UTC';
|
||||
return (
|
||||
<TableRow key={cron.name || i} className="border-[var(--dg-border)]">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{cron.name || cron.metadata?.name}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{
|
||||
jobs.map((j: any) => j.targetSize || j.targetReplicas || '?').join(', ')
|
||||
}</TableCell>
|
||||
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: any) => j.schedule || '?').join(', ')}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">{tz}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">已启用</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// deployment-view/HistoryTab.tsx — 历史版本 Tab // Logan.Li
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { RotateCcw, Loader2 } from 'lucide-react';
|
||||
import { formatK8sTime } from '../k8s-yaml-utils';
|
||||
|
||||
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: any[]; rollbackLoading: number | null; onRollback: (rev: any) => void }) {
|
||||
if (revisions.length === 0) {
|
||||
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无历史版本</div>;
|
||||
}
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-24">Revision</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">镜像</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-24">状态</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-40">更新时间</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)] w-24">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{revisions.map((rev, idx) => {
|
||||
const revision = rev.revision ?? rev.revision_number ?? rev.number ?? idx + 1;
|
||||
const isLatest = idx === 0;
|
||||
return (
|
||||
<TableRow key={revision} className="border-[var(--dg-border)]">
|
||||
<TableCell className="font-mono text-[var(--dg-text-primary)]">#{revision}</TableCell>
|
||||
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)] max-w-[300px] truncate">{rev.image || rev.images?.[0] || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{isLatest
|
||||
? <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">当前</Badge>
|
||||
: <Badge variant="outline" className="text-[var(--dg-text-muted)]">历史</Badge>}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">{formatK8sTime(rev.created_at || rev.time)}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-amber-400" disabled={isLatest || rollbackLoading !== null} onClick={() => onRollback(rev)}>
|
||||
{rollbackLoading === revision ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <RotateCcw className="h-3 w-3 mr-1" />}
|
||||
回滚
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// deployment-view/PodsTab.tsx — 容器组 Tab // Logan.Li
|
||||
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,
|
||||
} from 'lucide-react';
|
||||
import { formatK8sTime, statusColor } from '../k8s-yaml-utils';
|
||||
import { getPodTimeline, PodConditionsRow } from './helpers';
|
||||
import type { DeploymentViewProps } from './types';
|
||||
|
||||
export function PodsTab({
|
||||
clusterCode, pods, onShowPodLogs, onShowPodTerminal, onShowPodFileBrowser,
|
||||
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 || '-'}
|
||||
</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>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// deployment-view/ServicesTab.tsx — 访问方式 Tab // Logan.Li
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Network, ExternalLink, Loader2 } from 'lucide-react';
|
||||
import type { SvcItem, IngItem } from './types';
|
||||
|
||||
export function ServicesTab({ services, ingresses, loading }: { services: SvcItem[]; ingresses: IngItem[]; loading: string; namespace: string }) {
|
||||
if (loading === 'services') {
|
||||
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Services */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<Network className="h-4 w-4 text-[var(--dg-blue)]" />关联 Service
|
||||
</h3>
|
||||
{services.length === 0 ? (
|
||||
<p className="text-sm text-[var(--dg-text-muted)] py-4">暂无匹配的 Service(Deployment 选择器未关联任何 Service)</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">类型</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">ClusterIP</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">端口</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">Selector</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{services.map(svc => (
|
||||
<TableRow key={svc.name} className="border-[var(--dg-border)]">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{svc.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="text-xs">{svc.type}</Badge></TableCell>
|
||||
<TableCell className="font-mono text-sm text-[var(--dg-text-muted)]">{svc.clusterIP}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-muted)]">{svc.ports.join(', ')}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
||||
{Object.entries(svc.selector).map(([k, v]) => (
|
||||
<code key={k} className="mr-1 bg-[var(--dg-bg-tertiary)] px-1 rounded">{k}={v}</code>
|
||||
))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Ingresses */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<ExternalLink className="h-4 w-4 text-[var(--dg-purple)]" />关联 Ingress
|
||||
</h3>
|
||||
{ingresses.length === 0 ? (
|
||||
<p className="text-sm text-[var(--dg-text-muted)] py-4">暂无匹配的 Ingress</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)]">
|
||||
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">Class</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">Host</TableHead>
|
||||
<TableHead className="text-[var(--dg-text-muted)]">路径</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ingresses.map(ing => (
|
||||
<TableRow key={ing.name} className="border-[var(--dg-border)]">
|
||||
<TableCell className="font-medium text-[var(--dg-text-primary)]">{ing.name}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.className}</TableCell>
|
||||
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.hosts.join(', ')}</TableCell>
|
||||
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
||||
{ing.rules.flatMap((r: any) => (r.http?.paths || r.paths || []).map((p: any) => p.path || '/')).join(', ')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// deployment-view/helpers.tsx — 时间线 / 信息面板辅助组件 // Logan.Li
|
||||
import { Fragment } from 'react';
|
||||
import {
|
||||
formatK8sTime, formatDuration, elapsedSeconds, STATUS_COLORS,
|
||||
} from '../k8s-yaml-utils';
|
||||
import type { PodItem } from './types';
|
||||
|
||||
// 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 || [];
|
||||
const find = (t: string) => cond.find((c: any) => (c.type || c.Type) === t);
|
||||
const ts = (c: any) => c?.lastTransitionTime || c?.LastTransitionTime || c?.last_transition_time;
|
||||
return {
|
||||
scheduled: ts(find('PodScheduled') || {}),
|
||||
initialized: ts(find('Initialized') || {}),
|
||||
containersReady: ts(find('ContainersReady') || {}),
|
||||
ready: ts(find('Ready') || {}),
|
||||
startTime: pod.start_time || (pod as Record<string, any>).startTime,
|
||||
};
|
||||
}
|
||||
|
||||
// 信息面板单项 // Logan.Li
|
||||
export function InfoItem({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-1">{label}</p>
|
||||
<div className="text-sm text-[var(--dg-text-primary)]">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 },
|
||||
];
|
||||
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 (
|
||||
<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>
|
||||
{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>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// deployment-view/types.ts — Deployment 详情看板类型定义 // Logan.Li
|
||||
|
||||
export interface PodItem {
|
||||
name: string; namespace?: string; status?: string; node_name?: string; node?: string;
|
||||
ip?: string; pod_ip?: string; restart_count?: number; age?: string; created_at?: string;
|
||||
start_time?: string; containers?: any[]; image?: string; images?: string[];
|
||||
}
|
||||
|
||||
export interface ReplicaSetItem {
|
||||
name: string; replicas?: number; ready_replicas?: number; available_replicas?: number;
|
||||
image?: string; revision?: number; status?: string; created_at?: string;
|
||||
}
|
||||
|
||||
export interface SvcItem {
|
||||
name: string; type: string; clusterIP: string; ports: any[]; selector: Record<string, string>; age: string;
|
||||
}
|
||||
|
||||
export interface IngItem {
|
||||
name: string; className: string; hosts: string[]; rules: any[]; age: string;
|
||||
}
|
||||
|
||||
export interface EventItem {
|
||||
type: string; reason: string; message: string; source: string;
|
||||
firstTimestamp: string; lastTimestamp: string; count: number;
|
||||
}
|
||||
|
||||
export interface DeploymentViewProps {
|
||||
clusterCode: string; namespace: string; deployment: any;
|
||||
pods: PodItem[]; replicaSets: ReplicaSetItem[]; revisions: any[];
|
||||
onRefresh: () => void;
|
||||
onShowPodLogs: (pod: PodItem) => void;
|
||||
onShowPodTerminal: (pod: PodItem) => void;
|
||||
onShowPodYaml: (pod: PodItem) => void;
|
||||
onShowPodFileBrowser: (pod: PodItem) => void;
|
||||
}
|
||||
Reference in new issue
Block a user