refactor(frontend): P1-P2 续 — API 子模块化、大组件拆分、Query 铺开
- api/server/ 与 api/cloud/ 子目录拆分,保持 @/api 导入不变 - hooks/api 新增 useCredentials、usePromDatasources,增强 useServers - 拆分 CredentialManager、DeploymentWorkload、PrometheusDatasourceManager - ServerManager、CredentialManager、PromDs 接入 TanStack Query
This commit is contained in:
1 parent
f45e203e21
commit
480e677042
32 files changed
+2280
-1940
No files matched your search
@@ -0,0 +1,100 @@
|
||||
// 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';
|
||||
|
||||
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: Record<string, any> = 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-slate-900 border border-slate-700 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-slate-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm font-semibold text-slate-200">{podName}</span>
|
||||
{container && <Badge variant="outline" className="border-slate-700 text-slate-400 text-[10px]">{container}</Badge>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{containers.length > 0 && (
|
||||
<select value={container} onChange={e => setContainer(e.target.value)}
|
||||
className="bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-2 py-1">
|
||||
{containers.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
)}
|
||||
<span className="text-[11px] text-slate-500">最近</span>
|
||||
<Input type="number" value={tail} onChange={e => setTail(Number(e.target.value) || 200)}
|
||||
className="w-16 h-7 bg-slate-800 border-slate-700 text-slate-200 text-xs" />
|
||||
<span className="text-[11px] text-slate-500">行</span>
|
||||
<Button size="sm" variant="outline" className="h-7 border-slate-700 text-slate-300 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-slate-700 text-emerald-400 text-xs" onClick={startStream} disabled={streaming}>
|
||||
{streaming ? '流中...' : '实时流'}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400" onClick={onClose}><X className="h-4 w-4" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-3">
|
||||
<pre className="bg-slate-950 rounded p-3 text-xs text-slate-300 font-mono whitespace-pre-wrap max-h-[60vh] overflow-auto">
|
||||
{logs || '无日志内容'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user