1.30 k8s管理

This commit is contained in:
dengqichen 2025-12-14 13:48:25 +08:00
parent dfbf8e4f0a
commit bc6eb294d9
4 changed files with 276 additions and 292 deletions

View File

@ -42,9 +42,13 @@ const LogViewer: React.FC<LogViewerProps> = ({
className, className,
monacoLayout, monacoLayout,
fontSize = 12, fontSize = 12,
onScrollToTop,
onScrollToBottom,
}) => { }) => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null); const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const isStructuredMode = !!logs; const isStructuredMode = !!logs;
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isLoadingRef = useRef(false);
// 合并默认布局配置和自定义配置 // 合并默认布局配置和自定义配置
const layoutConfig = { ...DEFAULT_MONACO_LAYOUT, ...monacoLayout }; const layoutConfig = { ...DEFAULT_MONACO_LAYOUT, ...monacoLayout };
@ -69,6 +73,45 @@ const LogViewer: React.FC<LogViewerProps> = ({
// Editor挂载回调 // Editor挂载回调
const handleEditorDidMount: OnMount = (editor) => { const handleEditorDidMount: OnMount = (editor) => {
editorRef.current = editor; editorRef.current = editor;
// 监听滚动事件
if (onScrollToTop || onScrollToBottom) {
editor.onDidScrollChange(() => {
// 清除之前的定时器
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
// 防抖500ms后检查滚动位置
scrollTimeoutRef.current = setTimeout(() => {
if (isLoadingRef.current) return;
const scrollTop = editor.getScrollTop();
const scrollHeight = editor.getScrollHeight();
const viewHeight = editor.getLayoutInfo().height;
// 滚动到顶部距离顶部小于50px
if (scrollTop < 50 && onScrollToTop) {
isLoadingRef.current = true;
onScrollToTop();
// 500ms后重置加载状态
setTimeout(() => {
isLoadingRef.current = false;
}, 500);
}
// 滚动到底部距离底部小于50px
if (scrollTop + viewHeight >= scrollHeight - 50 && onScrollToBottom) {
isLoadingRef.current = true;
onScrollToBottom();
// 500ms后重置加载状态
setTimeout(() => {
isLoadingRef.current = false;
}, 500);
}
}, 300);
});
}
}; };
// 自动滚动到底部 // 自动滚动到底部

View File

@ -70,4 +70,10 @@ export interface LogViewerProps {
/** 字体大小默认12 */ /** 字体大小默认12 */
fontSize?: number; fontSize?: number;
/** 滚动到顶部回调(用于加载更早的日志) */
onScrollToTop?: () => void;
/** 滚动到底部回调(用于加载更新的日志) */
onScrollToBottom?: () => void;
} }

View File

@ -11,13 +11,13 @@ import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { FileText, RefreshCw, Download, ChevronUp, ChevronDown } from 'lucide-react'; import { FileText, RefreshCw, Download } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getK8sPodLogs, getK8sPodsByDeployment } from '../service'; import { getK8sPodLogs, getK8sPodsByDeployment } from '../service';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import LogViewer from '@/components/LogViewer'; import LogViewer from '@/components/LogViewer';
import type { K8sPodResponse, LogSelection } from '../types'; import type { K8sPodResponse } from '../types';
import { LOG_QUERY_CONSTANTS as LOG_CONSTANTS } from '../types'; import { LOG_QUERY_CONSTANTS as LOG_CONSTANTS, LOG_COUNT_OPTIONS } from '../types';
interface PodLogDialogProps { interface PodLogDialogProps {
open: boolean; open: boolean;
@ -44,14 +44,130 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
const [refreshInterval, setRefreshInterval] = useState(5); const [refreshInterval, setRefreshInterval] = useState(5);
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null); const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
const [referenceForPrevious, setReferenceForPrevious] = useState<LogSelection | null>(null); const [logCount, setLogCount] = useState<number>(LOG_CONSTANTS.DEFAULT_LOG_COUNT);
const [referenceForNext, setReferenceForNext] = useState<LogSelection | null>(null);
const [lastDisplayedTimestamp, setLastDisplayedTimestamp] = useState<string | null>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null); const timerRef = useRef<NodeJS.Timeout | null>(null);
const referenceForNextRef = useRef<LogSelection | null>(null); // 只保存时间戳字符串
const referenceForPreviousRef = useRef<string | null>(null);
const referenceForNextRef = useRef<string | null>(null);
const lastDisplayedTimestampRef = useRef<string | null>(null); const lastDisplayedTimestampRef = useRef<string | null>(null);
const firstDisplayedTimestampRef = useRef<string | null>(null);
const [loadingTop, setLoadingTop] = useState(false);
const [loadingBottom, setLoadingBottom] = useState(false);
const isLoadingPreviousRef = useRef(false);
const isLoadingNextRef = useRef(false);
// 打开对话框时获取Deployment下的所有Pod const formatLogs = (logs: Array<{ timestamp: string; content: string }>): string => {
return logs.map(log => `${log.timestamp} ${log.content}`).join('\n');
};
/**
*
* URL: referenceTimestamp=${referenceForPrevious}&direction=prev&logCount=500
*/
const fetchPreviousLogs = async () => {
const refTimestamp = referenceForPreviousRef.current;
if (!refTimestamp || !deploymentId || !selectedPod) return;
if (isLoadingPreviousRef.current) return;
isLoadingPreviousRef.current = true;
setLoadingTop(true);
try {
const params = {
container: selectedContainer,
referenceTimestamp: refTimestamp,
direction: 'prev' as const,
logCount: logCount,
};
console.log('[向上加载] 请求参数:', params);
const response = await getK8sPodLogs(deploymentId, selectedPod, params);
console.log('[向上加载] 响应:', {
logsCount: response.logs.length,
referenceForPrevious: response.referenceForPrevious,
});
// 去重:只保留比当前最早日志更早的日志
const uniqueLogs = response.logs.filter(log => {
if (!firstDisplayedTimestampRef.current) return true;
return log.timestamp < firstDisplayedTimestampRef.current;
});
if (uniqueLogs.length > 0) {
setLogContent(prev => prev ? `${formatLogs(uniqueLogs)}\n${prev}` : formatLogs(uniqueLogs));
}
// 更新最早时间戳和引用点用logs[0]作为下次向上加载的引用点)
if (response.logs.length > 0) {
firstDisplayedTimestampRef.current = response.logs[0].timestamp;
referenceForPreviousRef.current = response.logs[0].timestamp;
}
} catch (error: any) {
console.error('加载历史日志失败:', error);
toast({
title: '加载失败',
description: error.message || '无法加载历史日志',
variant: 'destructive',
});
} finally {
isLoadingPreviousRef.current = false;
setLoadingTop(false);
}
};
/**
* /
* URL: referenceTimestamp=${referenceForNext}&direction=next&logCount=100
*/
const fetchNextLogs = async () => {
const refTimestamp = referenceForNextRef.current;
if (!refTimestamp || !deploymentId || !selectedPod) return;
if (isLoadingNextRef.current) return;
isLoadingNextRef.current = true;
setLoadingBottom(true);
try {
const params = {
container: selectedContainer,
referenceTimestamp: refTimestamp,
direction: 'next' as const,
logCount: logCount,
};
console.log('[向下加载/轮询] 请求参数:', params);
const response = await getK8sPodLogs(deploymentId, selectedPod, params);
console.log('[向下加载/轮询] 响应:', {
logsCount: response.logs.length,
referenceForNext: response.referenceForNext,
});
// 去重:只保留比当前最新日志更新的日志
const uniqueLogs = response.logs.filter(log => {
if (!lastDisplayedTimestampRef.current) return true;
return log.timestamp > lastDisplayedTimestampRef.current;
});
if (uniqueLogs.length > 0) {
setLogContent(prev => prev ? `${prev}\n${formatLogs(uniqueLogs)}` : formatLogs(uniqueLogs));
lastDisplayedTimestampRef.current = uniqueLogs[uniqueLogs.length - 1].timestamp;
}
// 更新引用点
referenceForNextRef.current = response.referenceForNext?.referenceTimestamp || null;
setLastRefreshTime(new Date());
} catch (error: any) {
console.error('加载新日志失败:', error);
toast({
title: '加载失败',
description: error.message || '无法加载新日志',
variant: 'destructive',
});
} finally {
isLoadingNextRef.current = false;
setLoadingBottom(false);
}
};
// 打开对话框时获取Pod列表
useEffect(() => { useEffect(() => {
if (open && deploymentId) { if (open && deploymentId) {
const fetchPods = async () => { const fetchPods = async () => {
@ -59,8 +175,6 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
try { try {
const podList = await getK8sPodsByDeployment(deploymentId); const podList = await getK8sPodsByDeployment(deploymentId);
setPods(podList); setPods(podList);
// 设置初始选中的Pod优先使用传入的podName
const initialPod = podList.find(p => p.name === podName) || podList[0]; const initialPod = podList.find(p => p.name === podName) || podList[0];
if (initialPod) { if (initialPod) {
setSelectedPod(initialPod.name); setSelectedPod(initialPod.name);
@ -71,7 +185,6 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
} }
} }
} catch (error: any) { } catch (error: any) {
console.error('获取Pod列表失败:', error);
toast({ toast({
title: '获取Pod列表失败', title: '获取Pod列表失败',
description: error.message || '无法获取Pod信息', description: error.message || '无法获取Pod信息',
@ -85,7 +198,7 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
} }
}, [open, deploymentId, podName]); }, [open, deploymentId, podName]);
// 当选中的Pod变化时更新容器列表 // Pod变化时更新容器列表
useEffect(() => { useEffect(() => {
if (selectedPod && pods.length > 0) { if (selectedPod && pods.length > 0) {
const pod = pods.find(p => p.name === selectedPod); const pod = pods.find(p => p.name === selectedPod);
@ -99,239 +212,106 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
} }
}, [selectedPod, pods]); }, [selectedPod, pods]);
/**
*
* URL: referenceTimestamp=newest&logCount=500
*/
const fetchLogs = async (isManualRefresh: boolean = false) => { const fetchLogs = async (isManualRefresh: boolean = false) => {
if (!deploymentId || !selectedPod) return; if (!deploymentId || !selectedPod) return;
const isInitialOrManual = isManualRefresh || !referenceForNextRef.current;
if (isInitialOrManual) {
setLoading(true); setLoading(true);
}
try { try {
const params: any = { if (isInitialOrManual) {
const params = {
container: selectedContainer, container: selectedContainer,
referenceTimestamp: LOG_CONSTANTS.REFERENCE_NEWEST,
logCount: logCount,
}; };
if (isManualRefresh || !referenceForNextRef.current) {
// 初始加载或手动刷新获取最新100行日志
params.referenceTimestamp = LOG_CONSTANTS.REFERENCE_NEWEST;
params.offsetFrom = LOG_CONSTANTS.INITIAL_OFFSET_FROM;
params.offsetTo = LOG_CONSTANTS.INITIAL_OFFSET_TO;
console.log('[初始加载] 请求参数:', params); console.log('[初始加载] 请求参数:', params);
const response = await getK8sPodLogs(deploymentId, selectedPod, params); const response = await getK8sPodLogs(deploymentId, selectedPod, params);
console.log('[初始加载] 响应:', { console.log('[初始加载] 响应:', {
logsCount: response.logs.length, logsCount: response.logs.length,
referenceForPrevious: response.referenceForPrevious, referenceForPrevious: response.referenceForPrevious,
referenceForNext: response.referenceForNext, referenceForNext: response.referenceForNext,
}); });
// 格式化并显示初始日志 setLogContent(response.logs.length > 0 ? formatLogs(response.logs) : '暂无日志');
const formattedLogs = response.logs
.map(log => `${log.timestamp} ${log.content}`)
.join('\n');
setLogContent(formattedLogs || '暂无日志');
setReferenceForPrevious(response.referenceForPrevious);
setReferenceForNext(response.referenceForNext);
referenceForNextRef.current = response.referenceForNext;
// 记录最后显示的日志时间戳,用于去重
if (response.logs.length > 0) { if (response.logs.length > 0) {
const lastTimestamp = response.logs[response.logs.length - 1].timestamp; firstDisplayedTimestampRef.current = response.logs[0].timestamp;
setLastDisplayedTimestamp(lastTimestamp); lastDisplayedTimestampRef.current = response.logs[response.logs.length - 1].timestamp;
lastDisplayedTimestampRef.current = lastTimestamp;
}
} else {
// 增量轮询使用referenceForNext获取新日志
const currentRef = referenceForNextRef.current;
if (!currentRef) return;
params.referenceTimestamp = currentRef.referenceTimestamp;
params.offsetFrom = currentRef.offsetFrom;
params.offsetTo = currentRef.offsetTo;
console.log('[轮询刷新] 请求参数:', params);
const response = await getK8sPodLogs(deploymentId, selectedPod, params);
console.log('[轮询刷新] 响应:', {
logsCount: response.logs.length,
referenceForNext: response.referenceForNext,
});
// 基于时间戳去重:只追加比最后显示时间戳更新的日志
if (response.logs && response.logs.length > 0) {
const uniqueLogs = lastDisplayedTimestampRef.current
? response.logs.filter(log => log.timestamp > lastDisplayedTimestampRef.current!)
: response.logs;
if (uniqueLogs.length > 0) {
const newLogs = uniqueLogs
.map(log => `${log.timestamp} ${log.content}`)
.join('\n');
setLogContent(prev => prev ? `${prev}\n${newLogs}` : newLogs);
// 更新最后显示的时间戳同时更新state和ref
const lastTimestamp = uniqueLogs[uniqueLogs.length - 1].timestamp;
setLastDisplayedTimestamp(lastTimestamp);
lastDisplayedTimestampRef.current = lastTimestamp;
}
}
// 无论是否有新日志都要更新referenceForNext用于下次轮询
setReferenceForNext(response.referenceForNext);
referenceForNextRef.current = response.referenceForNext;
} }
// 保存引用点时间戳
referenceForPreviousRef.current = response.referenceForPrevious?.referenceTimestamp || null;
referenceForNextRef.current = response.referenceForNext?.referenceTimestamp || null;
setLastRefreshTime(new Date()); setLastRefreshTime(new Date());
} else {
await fetchNextLogs();
}
} catch (error: any) { } catch (error: any) {
console.error('获取Pod日志失败:', error);
toast({ toast({
title: '获取日志失败', title: '获取日志失败',
description: error.message || '无法获取Pod日志', description: error.message || '无法获取Pod日志',
variant: 'destructive', variant: 'destructive',
}); });
if (isManualRefresh || !referenceForNextRef.current) { if (isInitialOrManual) {
setLogContent('获取日志失败'); setLogContent('获取日志失败');
} }
} finally { } finally {
if (isInitialOrManual) {
setLoading(false); setLoading(false);
} }
}
}; };
// 选中Pod或容器变化时加载日志 // Pod/容器变化时重新加载
useEffect(() => { useEffect(() => {
if (open && selectedPod && selectedContainer) { if (open && selectedPod && selectedContainer) {
setLogContent(''); setLogContent('');
setReferenceForPrevious(null); referenceForPreviousRef.current = null;
setReferenceForNext(null);
setLastDisplayedTimestamp(null);
referenceForNextRef.current = null; referenceForNextRef.current = null;
firstDisplayedTimestampRef.current = null;
lastDisplayedTimestampRef.current = null; lastDisplayedTimestampRef.current = null;
fetchLogs(); fetchLogs();
} }
}, [open, selectedPod, selectedContainer, deploymentId]); }, [open, selectedPod, selectedContainer, deploymentId]);
// 自动刷新轮询 // 自动刷新
useEffect(() => { useEffect(() => {
// 清理之前的定时器
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
timerRef.current = null; timerRef.current = null;
} }
// 启动新的定时器
if (autoRefresh && open && selectedPod && selectedContainer) { if (autoRefresh && open && selectedPod && selectedContainer) {
timerRef.current = setInterval(() => { timerRef.current = setInterval(() => {
fetchLogs(); fetchLogs();
}, refreshInterval * 1000); }, refreshInterval * 1000);
} }
// 清理函数
return () => { return () => {
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
timerRef.current = null; timerRef.current = null;
} }
}; };
}, [autoRefresh, refreshInterval, open, selectedPod, selectedContainer]); }, [autoRefresh, refreshInterval, open, selectedPod, selectedContainer, deploymentId]);
// 向前翻页(查看更早的日志) const loadPreviousPage = () => fetchPreviousLogs();
const loadPreviousPage = async () => { const loadNextPage = () => {
if (!referenceForPrevious || !deploymentId || !selectedPod) return; if (autoRefresh) return;
fetchNextLogs();
setLoading(true);
try {
const params: any = {
container: selectedContainer,
referenceTimestamp: referenceForPrevious.referenceTimestamp,
offsetFrom: referenceForPrevious.offsetFrom,
offsetTo: referenceForPrevious.offsetTo,
}; };
console.log('[向前翻页] 请求参数:', params);
const response = await getK8sPodLogs(deploymentId, selectedPod, params);
console.log('[向前翻页] 响应:', {
logsCount: response.logs.length,
referenceForPrevious: response.referenceForPrevious,
});
if (response.logs && response.logs.length > 0) {
const newLogs = response.logs
.map(log => `${log.timestamp} ${log.content}`)
.join('\n');
// 在顶部插入日志
setLogContent(prev => prev ? `${newLogs}\n${prev}` : newLogs);
// 更新referenceForPrevious用于继续向前翻页
setReferenceForPrevious(response.referenceForPrevious);
}
} catch (error: any) {
console.error('加载历史日志失败:', error);
toast({
title: '加载失败',
description: error.message || '无法加载历史日志',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
// 向后翻页(手动获取更新的日志)
const loadNextPage = async () => {
if (!referenceForNext || !deploymentId || !selectedPod) return;
setLoading(true);
try {
const params: any = {
container: selectedContainer,
referenceTimestamp: referenceForNext.referenceTimestamp,
offsetFrom: referenceForNext.offsetFrom,
offsetTo: referenceForNext.offsetTo,
};
console.log('[向后翻页] 请求参数:', params);
const response = await getK8sPodLogs(deploymentId, selectedPod, params);
console.log('[向后翻页] 响应:', {
logsCount: response.logs.length,
referenceForNext: response.referenceForNext,
});
if (response.logs && response.logs.length > 0) {
const newLogs = response.logs
.map(log => `${log.timestamp} ${log.content}`)
.join('\n');
// 追加新日志
setLogContent(prev => prev ? `${prev}\n${newLogs}` : newLogs);
// 更新referenceForNext用于继续向后翻页
setReferenceForNext(response.referenceForNext);
}
setLastRefreshTime(new Date());
} catch (error: any) {
console.error('加载新日志失败:', error);
toast({
title: '加载失败',
description: error.message || '无法加载新日志',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
// 下载日志回调
const handleDownload = () => { const handleDownload = () => {
toast({ toast({ title: '下载成功', description: '日志文件已保存' });
title: '下载成功',
description: '日志文件已保存',
});
}; };
return ( return (
@ -363,86 +343,72 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
showToolbar={true} showToolbar={true}
showLineNumbers={true} showLineNumbers={true}
fontSize={10} fontSize={10}
onScrollToTop={loadPreviousPage}
onScrollToBottom={loadNextPage}
customToolbar={ customToolbar={
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* 左侧容器、Pod选择器和自动刷新控件 */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* 容器选择器 */}
{containers.length > 1 && ( {containers.length > 1 && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span> <span className="text-sm text-muted-foreground">:</span>
<Select <Select value={selectedContainer} onValueChange={setSelectedContainer}>
value={selectedContainer}
onValueChange={setSelectedContainer}
>
<SelectTrigger className="w-48"> <SelectTrigger className="w-48">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{containers.map((container) => ( {containers.map((c) => (
<SelectItem key={container} value={container}> <SelectItem key={c} value={c}>{c}</SelectItem>
{container}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)} )}
{/* Pod选择器 */}
{pods.length > 1 && ( {pods.length > 1 && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Pod:</span> <span className="text-sm text-muted-foreground">Pod:</span>
<Select <Select value={selectedPod} onValueChange={setSelectedPod}>
value={selectedPod}
onValueChange={setSelectedPod}
>
<SelectTrigger className="w-64"> <SelectTrigger className="w-64">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{pods.map((pod) => ( {pods.map((p) => (
<SelectItem key={pod.name} value={pod.name}> <SelectItem key={p.name} value={p.name}>{p.name}</SelectItem>
{pod.name}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)} )}
{/* 自动刷新开关 */} {/* 日志行数选择器 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <span className="text-sm text-muted-foreground">:</span>
id="auto-refresh" <Select value={logCount.toString()} onValueChange={(v) => setLogCount(Number(v))}>
checked={autoRefresh} <SelectTrigger className="w-24 h-8 text-sm">
onCheckedChange={setAutoRefresh} <SelectValue />
/> </SelectTrigger>
<Label htmlFor="auto-refresh" className="text-sm text-muted-foreground cursor-pointer"> <SelectContent>
{LOG_COUNT_OPTIONS.map((count) => (
</Label> <SelectItem key={count} value={count.toString()}>{count}</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
{/* 自动滚动开关 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch id="auto-refresh" checked={autoRefresh} onCheckedChange={setAutoRefresh} />
id="auto-scroll" <Label htmlFor="auto-refresh" className="text-sm text-muted-foreground cursor-pointer"></Label>
checked={autoScrollEnabled} </div>
onCheckedChange={setAutoScrollEnabled}
/> <div className="flex items-center gap-2">
<Label htmlFor="auto-scroll" className="text-sm text-muted-foreground cursor-pointer"> <Switch id="auto-scroll" checked={autoScrollEnabled} onCheckedChange={setAutoScrollEnabled} />
<Label htmlFor="auto-scroll" className="text-sm text-muted-foreground cursor-pointer"></Label>
</Label>
</div> </div>
{/* 刷新间隔选择器 */}
{autoRefresh && ( {autoRefresh && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span> <span className="text-sm text-muted-foreground">:</span>
<Select <Select value={refreshInterval.toString()} onValueChange={(v) => setRefreshInterval(Number(v))}>
value={refreshInterval.toString()}
onValueChange={(value) => setRefreshInterval(Number(value))}
>
<SelectTrigger className="w-20 h-8 text-sm"> <SelectTrigger className="w-20 h-8 text-sm">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@ -456,7 +422,6 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
</div> </div>
)} )}
{/* 最后刷新时间 */}
{lastRefreshTime && ( {lastRefreshTime && (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
: {lastRefreshTime.toLocaleTimeString()} : {lastRefreshTime.toLocaleTimeString()}
@ -464,45 +429,19 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
)} )}
</div> </div>
{/* 右侧:操作按钮 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button {(loadingTop || loadingBottom) && (
variant="outline" <span className="text-xs text-muted-foreground flex items-center gap-1">
size="sm" <RefreshCw className="h-3 w-3 animate-spin" />
onClick={loadPreviousPage} {loadingTop ? '加载更早日志...' : '加载更新日志...'}
disabled={loading || !referenceForPrevious} </span>
title="向前翻页(查看更早的日志)" )}
> <Button variant="outline" size="sm" onClick={handleDownload}
<ChevronUp className="h-4 w-4 mr-1" /> disabled={!logContent || logContent === '暂无日志' || logContent === '获取日志失败'}>
<Download className="h-4 w-4 mr-2" />
</Button> </Button>
<Button <Button variant="outline" size="sm" onClick={() => fetchLogs(true)} disabled={loading}>
variant="outline" <RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
size="sm"
onClick={loadNextPage}
disabled={loading || !referenceForNext}
title="向后翻页(查看更新的日志)"
>
<ChevronDown className="h-4 w-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDownload}
disabled={!logContent || logContent === '暂无日志' || logContent === '获取日志失败'}
>
<Download className="h-4 w-4 mr-2" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => fetchLogs(true)}
disabled={loading}
>
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
</Button> </Button>
</div> </div>
</div> </div>
@ -512,9 +451,7 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
</DialogBody> </DialogBody>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}> <Button variant="outline" onClick={() => onOpenChange(false)}></Button>
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -281,15 +281,16 @@ export interface K8sPodResponse {
// ==================== 日志相关 ==================== // ==================== 日志相关 ====================
/** /**
* - *
*/ */
export interface LogSelection { export type LogDirection = 'prev' | 'next';
/**
*
*/
export interface LogReference {
/** 引用点时间戳 */ /** 引用点时间戳 */
referenceTimestamp: string; referenceTimestamp: string;
/** 相对于引用点的起始偏移量(包含) */
offsetFrom: number;
/** 相对于引用点的结束偏移量(不包含) */
offsetTo: number;
} }
/** /**
@ -311,9 +312,9 @@ export interface PodLogsResponse {
/** 容器名称 */ /** 容器名称 */
containerName: string; containerName: string;
/** 向前翻页的引用点 */ /** 向前翻页的引用点 */
referenceForPrevious: LogSelection; referenceForPrevious: LogReference | null;
/** 向后翻页/轮询的引用点 */ /** 向后翻页/轮询的引用点 */
referenceForNext: LogSelection; referenceForNext: LogReference | null;
/** 日志行数组 */ /** 日志行数组 */
logs: LogLine[]; logs: LogLine[];
/** 是否被截断 */ /** 是否被截断 */
@ -321,17 +322,17 @@ export interface PodLogsResponse {
} }
/** /**
* Pod日志查询参数 - * Pod日志查询参数
*/ */
export interface PodLogsQuery { export interface PodLogsQuery {
/** 容器名称 */ /** 容器名称 */
container?: string; container?: string;
/** 引用点时间戳("newest", "oldest", 或具体时间戳) */ /** 引用点时间戳("newest", "oldest", 或具体时间戳) */
referenceTimestamp?: string; referenceTimestamp?: string;
/** 相对于引用点的起始偏移量 */ /** 方向prev=向上加载历史, next=向下加载新日志 */
offsetFrom?: number; direction?: LogDirection;
/** 相对于引用点的结束偏移量 */ /** 日志行数 */
offsetTo?: number; logCount?: number;
} }
/** /**
@ -342,20 +343,17 @@ export const LOG_QUERY_CONSTANTS = {
REFERENCE_NEWEST: 'newest' as const, REFERENCE_NEWEST: 'newest' as const,
/** 引用点:最早日志 */ /** 引用点:最早日志 */
REFERENCE_OLDEST: 'oldest' as const, REFERENCE_OLDEST: 'oldest' as const,
/** 初始加载offsetFrom */ /** 默认日志行数 */
INITIAL_OFFSET_FROM: -100, DEFAULT_LOG_COUNT: 500,
/** 初始加载offsetTo */ /** 轮询日志行数 */
INITIAL_OFFSET_TO: 0, POLL_LOG_COUNT: 100,
/** 向前翻页offsetFrom */
PREVIOUS_OFFSET_FROM: -100,
/** 向前翻页offsetTo */
PREVIOUS_OFFSET_TO: 0,
/** 向后翻页/轮询offsetFrom */
NEXT_OFFSET_FROM: 1,
/** 向后翻页/轮询offsetTo */
NEXT_OFFSET_TO: 101,
} as const; } as const;
/**
*
*/
export const LOG_COUNT_OPTIONS: number[] = [100, 200, 500, 1000, 2000];
// ==================== 操作相关 ==================== // ==================== 操作相关 ====================
/** /**