1.30 k8s管理
This commit is contained in:
parent
dfbf8e4f0a
commit
bc6eb294d9
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动到底部
|
||||||
|
|||||||
@ -70,4 +70,10 @@ export interface LogViewerProps {
|
|||||||
|
|
||||||
/** 字体大小(默认12) */
|
/** 字体大小(默认12) */
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
|
|
||||||
|
/** 滚动到顶部回调(用于加载更早的日志) */
|
||||||
|
onScrollToTop?: () => void;
|
||||||
|
|
||||||
|
/** 滚动到底部回调(用于加载更新的日志) */
|
||||||
|
onScrollToBottom?: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
setLoading(true);
|
const isInitialOrManual = isManualRefresh || !referenceForNextRef.current;
|
||||||
try {
|
|
||||||
const params: any = {
|
|
||||||
container: selectedContainer,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isManualRefresh || !referenceForNextRef.current) {
|
if (isInitialOrManual) {
|
||||||
// 初始加载或手动刷新:获取最新100行日志
|
setLoading(true);
|
||||||
params.referenceTimestamp = LOG_CONSTANTS.REFERENCE_NEWEST;
|
}
|
||||||
params.offsetFrom = LOG_CONSTANTS.INITIAL_OFFSET_FROM;
|
|
||||||
params.offsetTo = LOG_CONSTANTS.INITIAL_OFFSET_TO;
|
try {
|
||||||
|
if (isInitialOrManual) {
|
||||||
|
const params = {
|
||||||
|
container: selectedContainer,
|
||||||
|
referenceTimestamp: LOG_CONSTANTS.REFERENCE_NEWEST,
|
||||||
|
logCount: logCount,
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存引用点时间戳
|
||||||
|
referenceForPreviousRef.current = response.referenceForPrevious?.referenceTimestamp || null;
|
||||||
|
referenceForNextRef.current = response.referenceForNext?.referenceTimestamp || null;
|
||||||
|
setLastRefreshTime(new Date());
|
||||||
} else {
|
} else {
|
||||||
// 增量轮询:使用referenceForNext获取新日志
|
await fetchNextLogs();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLastRefreshTime(new Date());
|
|
||||||
} 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 {
|
||||||
setLoading(false);
|
if (isInitialOrManual) {
|
||||||
|
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>
|
||||||
|
|||||||
@ -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];
|
||||||
|
|
||||||
// ==================== 操作相关 ====================
|
// ==================== 操作相关 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user