- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
101 lines
4.9 KiB
TypeScript
101 lines
4.9 KiB
TypeScript
// PodLogsModal.tsx — Pod 日志弹窗(带容器切换 + 行数 + 实时流)
|
|
// 作者: Logan.Li
|
|
import { useState, useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { FileText, RefreshCw, Loader2, X } from 'lucide-react';
|
|
import { kubernetesApi } from '@/api';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { useUserStore } from '@/store';
|
|
import type { JsonData } from '@/types/common';
|
|
|
|
export interface PodLogsModalProps {
|
|
clusterCode: string;
|
|
namespace: string;
|
|
podName: string;
|
|
containers: string[];
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function PodLogsModal({ clusterCode, namespace, podName, containers, onClose }: PodLogsModalProps) {
|
|
const { toast } = useToast();
|
|
const [logs, setLogs] = useState('');
|
|
const [tail, setTail] = useState(200);
|
|
const [container, setContainer] = useState(containers[0] || '');
|
|
const [loading, setLoading] = useState(false);
|
|
const [streaming, setStreaming] = useState(false);
|
|
|
|
const loadLogs = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res: JsonData = await kubernetesApi.getPodLogs(clusterCode, namespace, podName, tail, container || undefined);
|
|
setLogs(res?.data?.logs || res?.logs || res || '无日志');
|
|
} catch {
|
|
setLogs('获取日志失败');
|
|
} finally { setLoading(false); }
|
|
};
|
|
|
|
useEffect(() => { loadLogs(); }, [container, tail]);
|
|
|
|
const startStream = async () => {
|
|
setStreaming(true);
|
|
const token = useUserStore.getState().token || '';
|
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || '';
|
|
const cParam = container ? `&container=${encodeURIComponent(container)}` : '';
|
|
const url = `${baseUrl}/api/v1/kubernetes/${clusterCode}/namespace/${namespace}/pods/${podName}/logs?tail=${tail}&stream=true${cParam}`;
|
|
try {
|
|
const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
const reader = resp.body?.getReader();
|
|
if (!reader) return;
|
|
const decoder = new TextDecoder();
|
|
let buf = '';
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buf += decoder.decode(value, { stream: true });
|
|
setLogs(buf);
|
|
}
|
|
} catch {
|
|
toast({ title: '流式日志连接中断', variant: 'destructive' });
|
|
} finally { setStreaming(false); }
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
|
|
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-xl w-[90vw] max-w-4xl max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between p-3 border-b border-[var(--dg-border)] gap-3 shrink-0">
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<FileText className="h-4 w-4 shrink-0 dg-text-blue" />
|
|
<span className="text-sm font-semibold text-[var(--dg-text-primary)] truncate" title={podName}>{podName}</span>
|
|
{container && <Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary text-[10px] shrink-0">{container}</Badge>}
|
|
</div>
|
|
<div className="flex items-center gap-2 shrink-0 flex-wrap justify-end">
|
|
{containers.length > 0 && (
|
|
<select value={container} onChange={e => setContainer(e.target.value)}
|
|
className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-2 py-1">
|
|
{containers.map(c => <option key={c} value={c}>{c}</option>)}
|
|
</select>
|
|
)}
|
|
<span className="text-[11px] dg-text-muted">最近</span>
|
|
<Input type="number" value={tail} onChange={e => setTail(Number(e.target.value) || 200)}
|
|
className="w-16 h-7 dg-input text-xs" />
|
|
<span className="text-[11px] dg-text-muted">行</span>
|
|
<Button size="sm" variant="outline" className="h-7 border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs" onClick={loadLogs} disabled={loading}>
|
|
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
|
|
</Button>
|
|
<Button size="sm" variant="outline" className="h-7 border-[var(--dg-border)] dg-text-green text-xs" onClick={startStream} disabled={streaming}>
|
|
{streaming ? '流中...' : '实时流'}
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-secondary" onClick={onClose}><X className="h-4 w-4" /></Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 overflow-auto p-3">
|
|
<pre className="bg-[var(--dg-bg-primary)] rounded p-3 text-xs text-[var(--dg-text-secondary)] font-mono whitespace-pre-wrap max-h-[60vh] overflow-auto">
|
|
{logs || '无日志内容'}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |