1.30 k8s管理
This commit is contained in:
parent
e54d6834c3
commit
4e87fc1b98
@ -8,6 +8,29 @@ export const calculateLineNumberWidth = (totalLines: number): number => {
|
|||||||
return Math.max(4, String(totalLines).length + 1);
|
return Math.max(4, String(totalLines).length + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除文本中的ANSI转义序列(颜色控制字符)
|
||||||
|
*
|
||||||
|
* ANSI转义序列包括:
|
||||||
|
* - CSI序列:\u001b[<参数>m(如 \u001b[39m 重置前景色)
|
||||||
|
* - OSC序列:\u001b]<参数>;<文本>BEL/ST
|
||||||
|
* - 其他控制序列:光标移动、字符集选择等
|
||||||
|
*
|
||||||
|
* 使用业界验证的通用正则表达式,匹配所有标准ANSI转义序列
|
||||||
|
*
|
||||||
|
* @param text - 包含ANSI转义码的原始文本
|
||||||
|
* @returns 清理后的纯文本
|
||||||
|
*/
|
||||||
|
export const stripAnsiCodes = (text: string): string => {
|
||||||
|
if (!text) return '';
|
||||||
|
// 匹配所有ANSI转义序列的通用正则表达式
|
||||||
|
// \u001b\u009b - ESC字符(两种编码)
|
||||||
|
// [[()#;?]* - 可选的前缀字符
|
||||||
|
// (?:[0-9]{1,4}(?:;[0-9]{0,4})*)? - 参数(数字和分号)
|
||||||
|
// [0-9A-ORZcf-nqry=><] - 命令字符
|
||||||
|
return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化结构化日志为Monaco显示文本
|
* 格式化结构化日志为Monaco显示文本
|
||||||
* 格式:行号 | 时间戳 | 级别 | 消息
|
* 格式:行号 | 时间戳 | 级别 | 消息
|
||||||
@ -30,10 +53,10 @@ export const formatStructuredLogs = (logs: StructuredLog[]): string => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化纯文本日志
|
* 格式化纯文本日志
|
||||||
* 保持原样,不做额外处理
|
* 自动清理ANSI转义码(终端颜色控制字符)
|
||||||
*/
|
*/
|
||||||
export const formatPlainText = (content: string): string => {
|
export const formatPlainText = (content: string): string => {
|
||||||
return content || '';
|
return stripAnsiCodes(content || '');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -41,6 +41,7 @@ const LogViewer: React.FC<LogViewerProps> = ({
|
|||||||
autoScroll = true,
|
autoScroll = true,
|
||||||
className,
|
className,
|
||||||
monacoLayout,
|
monacoLayout,
|
||||||
|
fontSize = 12,
|
||||||
}) => {
|
}) => {
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||||
const isStructuredMode = !!logs;
|
const isStructuredMode = !!logs;
|
||||||
@ -109,7 +110,7 @@ const LogViewer: React.FC<LogViewerProps> = ({
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
fontSize: 12,
|
fontSize,
|
||||||
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
|
||||||
lineNumbers: showLineNumbers ? 'on' : 'off',
|
lineNumbers: showLineNumbers ? 'on' : 'off',
|
||||||
glyphMargin: layoutConfig.glyphMargin,
|
glyphMargin: layoutConfig.glyphMargin,
|
||||||
|
|||||||
@ -67,4 +67,7 @@ export interface LogViewerProps {
|
|||||||
|
|
||||||
/** Monaco Editor布局配置 */
|
/** Monaco Editor布局配置 */
|
||||||
monacoLayout?: MonacoLayoutConfig;
|
monacoLayout?: MonacoLayoutConfig;
|
||||||
|
|
||||||
|
/** 字体大小(默认12) */
|
||||||
|
fontSize?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -9,11 +9,15 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { FileText, RefreshCw, Download } from 'lucide-react';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { FileText, RefreshCw, Download, ChevronUp, ChevronDown } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getK8sPodLogs, getK8sPodDetail } 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 { LOG_QUERY_CONSTANTS as LOG_CONSTANTS } from '../types';
|
||||||
|
|
||||||
interface PodLogDialogProps {
|
interface PodLogDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -30,53 +34,156 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingPod, setLoadingPod] = useState(false);
|
const [loadingPods, setLoadingPods] = useState(false);
|
||||||
const [logContent, setLogContent] = useState<string>('');
|
const [logContent, setLogContent] = useState<string>('');
|
||||||
|
const [pods, setPods] = useState<K8sPodResponse[]>([]);
|
||||||
|
const [selectedPod, setSelectedPod] = useState<string>('');
|
||||||
const [selectedContainer, setSelectedContainer] = useState<string>('');
|
const [selectedContainer, setSelectedContainer] = useState<string>('');
|
||||||
const [containers, setContainers] = useState<string[]>([]);
|
const [containers, setContainers] = useState<string[]>([]);
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||||
|
const [refreshInterval, setRefreshInterval] = useState(5);
|
||||||
|
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
|
||||||
|
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
||||||
|
const [referenceForPrevious, setReferenceForPrevious] = useState<LogSelection | null>(null);
|
||||||
|
const [referenceForNext, setReferenceForNext] = useState<LogSelection | null>(null);
|
||||||
|
const [lastDisplayedTimestamp, setLastDisplayedTimestamp] = useState<string | null>(null);
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const referenceForNextRef = useRef<LogSelection | null>(null);
|
||||||
|
const lastDisplayedTimestampRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// 打开对话框时获取Pod详情,获取最新的容器列表
|
// 打开对话框时获取Deployment下的所有Pod
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && deploymentId && podName) {
|
if (open && deploymentId) {
|
||||||
const fetchPodDetail = async () => {
|
const fetchPods = async () => {
|
||||||
setLoadingPod(true);
|
setLoadingPods(true);
|
||||||
try {
|
try {
|
||||||
const pod = await getK8sPodDetail(deploymentId, podName);
|
const podList = await getK8sPodsByDeployment(deploymentId);
|
||||||
const containerNames = pod.containers.map(c => c.name);
|
setPods(podList);
|
||||||
setContainers(containerNames);
|
|
||||||
if (containerNames.length > 0) {
|
// 设置初始选中的Pod(优先使用传入的podName)
|
||||||
setSelectedContainer(containerNames[0]);
|
const initialPod = podList.find(p => p.name === podName) || podList[0];
|
||||||
|
if (initialPod) {
|
||||||
|
setSelectedPod(initialPod.name);
|
||||||
|
const containerNames = initialPod.containers.map(c => c.name);
|
||||||
|
setContainers(containerNames);
|
||||||
|
if (containerNames.length > 0) {
|
||||||
|
setSelectedContainer(containerNames[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('获取Pod详情失败:', error);
|
console.error('获取Pod列表失败:', error);
|
||||||
toast({
|
toast({
|
||||||
title: '获取Pod详情失败',
|
title: '获取Pod列表失败',
|
||||||
description: error.message || '无法获取Pod容器信息',
|
description: error.message || '无法获取Pod信息',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingPod(false);
|
setLoadingPods(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchPodDetail();
|
fetchPods();
|
||||||
}
|
}
|
||||||
}, [open, deploymentId, podName]);
|
}, [open, deploymentId, podName]);
|
||||||
|
|
||||||
|
// 当选中的Pod变化时,更新容器列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPod && pods.length > 0) {
|
||||||
|
const pod = pods.find(p => p.name === selectedPod);
|
||||||
|
if (pod) {
|
||||||
|
const containerNames = pod.containers.map(c => c.name);
|
||||||
|
setContainers(containerNames);
|
||||||
|
if (containerNames.length > 0) {
|
||||||
|
setSelectedContainer(containerNames[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedPod, pods]);
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async (isManualRefresh: boolean = false) => {
|
||||||
if (!deploymentId || !podName) return;
|
if (!deploymentId || !selectedPod) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 如果有选中的容器,传递容器名;否则不传,让后端使用默认容器
|
|
||||||
const params: any = {
|
const params: any = {
|
||||||
tail: 1000, // 获取最后1000行
|
container: selectedContainer,
|
||||||
};
|
};
|
||||||
if (selectedContainer) {
|
|
||||||
params.container = selectedContainer;
|
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);
|
||||||
|
|
||||||
|
const response = await getK8sPodLogs(deploymentId, selectedPod, params);
|
||||||
|
|
||||||
|
console.log('[初始加载] 响应:', {
|
||||||
|
logsCount: response.logs.length,
|
||||||
|
referenceForPrevious: response.referenceForPrevious,
|
||||||
|
referenceForNext: response.referenceForNext,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化并显示初始日志
|
||||||
|
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) {
|
||||||
|
const lastTimestamp = response.logs[response.logs.length - 1].timestamp;
|
||||||
|
setLastDisplayedTimestamp(lastTimestamp);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = await getK8sPodLogs(deploymentId, podName, params);
|
setLastRefreshTime(new Date());
|
||||||
setLogContent(logs || '暂无日志');
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('获取Pod日志失败:', error);
|
console.error('获取Pod日志失败:', error);
|
||||||
toast({
|
toast({
|
||||||
@ -84,19 +191,140 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
|
|||||||
description: error.message || '无法获取Pod日志',
|
description: error.message || '无法获取Pod日志',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
setLogContent('获取日志失败');
|
if (isManualRefresh || !referenceForNextRef.current) {
|
||||||
|
setLogContent('获取日志失败');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开对话框或切换容器时加载日志
|
// 选中Pod或容器变化时加载日志
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && selectedContainer) {
|
if (open && selectedPod && selectedContainer) {
|
||||||
setLogContent('');
|
setLogContent('');
|
||||||
|
setReferenceForPrevious(null);
|
||||||
|
setReferenceForNext(null);
|
||||||
|
setLastDisplayedTimestamp(null);
|
||||||
|
referenceForNextRef.current = null;
|
||||||
|
lastDisplayedTimestampRef.current = null;
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
}
|
}
|
||||||
}, [open, selectedContainer, deploymentId, podName]);
|
}, [open, selectedPod, selectedContainer, deploymentId]);
|
||||||
|
|
||||||
|
// 自动刷新轮询
|
||||||
|
useEffect(() => {
|
||||||
|
// 清理之前的定时器
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动新的定时器
|
||||||
|
if (autoRefresh && open && selectedPod && selectedContainer) {
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
fetchLogs();
|
||||||
|
}, refreshInterval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [autoRefresh, refreshInterval, open, selectedPod, selectedContainer]);
|
||||||
|
|
||||||
|
// 向前翻页(查看更早的日志)
|
||||||
|
const loadPreviousPage = async () => {
|
||||||
|
if (!referenceForPrevious || !deploymentId || !selectedPod) return;
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
@ -112,12 +340,12 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<FileText className="h-5 w-5 text-primary" />
|
<FileText className="h-5 w-5 text-primary" />
|
||||||
Pod日志 - {podName}
|
容器: {selectedContainer || '加载中...'}日志
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogBody className="flex-1 flex flex-col min-h-0">
|
<DialogBody className="flex-1 flex flex-col min-h-0">
|
||||||
{loadingPod ? (
|
{loadingPods ? (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent mx-auto mb-2" />
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent mx-auto mb-2" />
|
||||||
@ -128,17 +356,19 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
|
|||||||
<LogViewer
|
<LogViewer
|
||||||
content={logContent}
|
content={logContent}
|
||||||
loading={loading && !logContent}
|
loading={loading && !logContent}
|
||||||
onRefresh={fetchLogs}
|
onRefresh={() => fetchLogs()}
|
||||||
onDownload={handleDownload}
|
onDownload={handleDownload}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
autoScroll={true}
|
autoScroll={autoScrollEnabled}
|
||||||
showToolbar={true}
|
showToolbar={true}
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
|
fontSize={10}
|
||||||
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
|
||||||
@ -157,17 +387,105 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
) : containers.length === 1 ? (
|
)}
|
||||||
<div className="flex items-center h-9">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
{/* Pod选择器 */}
|
||||||
容器: <span className="font-medium text-foreground">{containers[0]}</span>
|
{pods.length > 1 && (
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Pod:</span>
|
||||||
|
<Select
|
||||||
|
value={selectedPod}
|
||||||
|
onValueChange={setSelectedPod}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-64">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pods.map((pod) => (
|
||||||
|
<SelectItem key={pod.name} value={pod.name}>
|
||||||
|
{pod.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
|
{/* 自动刷新开关 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="auto-refresh"
|
||||||
|
checked={autoRefresh}
|
||||||
|
onCheckedChange={setAutoRefresh}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="auto-refresh" className="text-sm text-muted-foreground cursor-pointer">
|
||||||
|
自动刷新
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 自动滚动开关 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="auto-scroll"
|
||||||
|
checked={autoScrollEnabled}
|
||||||
|
onCheckedChange={setAutoScrollEnabled}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="auto-scroll" className="text-sm text-muted-foreground cursor-pointer">
|
||||||
|
自动滚动
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 刷新间隔选择器 */}
|
||||||
|
{autoRefresh && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">间隔:</span>
|
||||||
|
<Select
|
||||||
|
value={refreshInterval.toString()}
|
||||||
|
onValueChange={(value) => setRefreshInterval(Number(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20 h-8 text-sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="3">3秒</SelectItem>
|
||||||
|
<SelectItem value="5">5秒</SelectItem>
|
||||||
|
<SelectItem value="10">10秒</SelectItem>
|
||||||
|
<SelectItem value="30">30秒</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 最后刷新时间 */}
|
||||||
|
{lastRefreshTime && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
最后刷新: {lastRefreshTime.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:操作按钮 */}
|
{/* 右侧:操作按钮 */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadPreviousPage}
|
||||||
|
disabled={loading || !referenceForPrevious}
|
||||||
|
title="向前翻页(查看更早的日志)"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4 mr-1" />
|
||||||
|
向前
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadNextPage}
|
||||||
|
disabled={loading || !referenceForNext}
|
||||||
|
title="向后翻页(查看更新的日志)"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4 mr-1" />
|
||||||
|
向后
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -180,7 +498,7 @@ const PodLogDialog: React.FC<PodLogDialogProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={fetchLogs}
|
onClick={() => fetchLogs(true)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
|
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import type {
|
|||||||
K8sSyncHistoryResponse,
|
K8sSyncHistoryResponse,
|
||||||
K8sSyncHistoryQuery,
|
K8sSyncHistoryQuery,
|
||||||
K8sPodResponse,
|
K8sPodResponse,
|
||||||
|
PodLogsQuery,
|
||||||
|
PodLogsResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// ==================== K8S命名空间接口 ====================
|
// ==================== K8S命名空间接口 ====================
|
||||||
@ -168,18 +170,13 @@ export const getK8sPodDetail = async (
|
|||||||
// ==================== K8S日志接口 ====================
|
// ==================== K8S日志接口 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取Pod日志
|
* 获取Pod日志 - 基于引用点系统
|
||||||
*/
|
*/
|
||||||
export const getK8sPodLogs = async (
|
export const getK8sPodLogs = async (
|
||||||
deploymentId: number,
|
deploymentId: number,
|
||||||
podName: string,
|
podName: string,
|
||||||
params?: {
|
params?: PodLogsQuery
|
||||||
container?: string;
|
): Promise<PodLogsResponse> => {
|
||||||
tail?: number;
|
|
||||||
since?: string;
|
|
||||||
follow?: boolean;
|
|
||||||
}
|
|
||||||
): Promise<string> => {
|
|
||||||
return request.get(`/api/v1/k8s-deployment/${deploymentId}/pods/${podName}/logs`, { params });
|
return request.get(`/api/v1/k8s-deployment/${deploymentId}/pods/${podName}/logs`, { params });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -281,19 +281,81 @@ export interface K8sPodResponse {
|
|||||||
// ==================== 日志相关 ====================
|
// ==================== 日志相关 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pod日志查询参数
|
* 日志选择器 - 基于引用点系统
|
||||||
|
*/
|
||||||
|
export interface LogSelection {
|
||||||
|
/** 引用点时间戳 */
|
||||||
|
referenceTimestamp: string;
|
||||||
|
/** 相对于引用点的起始偏移量(包含) */
|
||||||
|
offsetFrom: number;
|
||||||
|
/** 相对于引用点的结束偏移量(不包含) */
|
||||||
|
offsetTo: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志行
|
||||||
|
*/
|
||||||
|
export interface LogLine {
|
||||||
|
/** 日志时间戳(RFC3339格式) */
|
||||||
|
timestamp: string;
|
||||||
|
/** 日志内容 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pod日志响应
|
||||||
|
*/
|
||||||
|
export interface PodLogsResponse {
|
||||||
|
/** Pod名称 */
|
||||||
|
podName: string;
|
||||||
|
/** 容器名称 */
|
||||||
|
containerName: string;
|
||||||
|
/** 向前翻页的引用点 */
|
||||||
|
referenceForPrevious: LogSelection;
|
||||||
|
/** 向后翻页/轮询的引用点 */
|
||||||
|
referenceForNext: LogSelection;
|
||||||
|
/** 日志行数组 */
|
||||||
|
logs: LogLine[];
|
||||||
|
/** 是否被截断 */
|
||||||
|
truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pod日志查询参数 - 基于引用点系统
|
||||||
*/
|
*/
|
||||||
export interface PodLogsQuery {
|
export interface PodLogsQuery {
|
||||||
/** 容器名称 */
|
/** 容器名称 */
|
||||||
container?: string;
|
container?: string;
|
||||||
/** 显示最后N行 */
|
/** 引用点时间戳("newest", "oldest", 或具体时间戳) */
|
||||||
tail?: number;
|
referenceTimestamp?: string;
|
||||||
/** 时间戳,显示此时间之后的日志 */
|
/** 相对于引用点的起始偏移量 */
|
||||||
since?: string;
|
offsetFrom?: number;
|
||||||
/** 是否实时跟踪 */
|
/** 相对于引用点的结束偏移量 */
|
||||||
follow?: boolean;
|
offsetTo?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志查询常量
|
||||||
|
*/
|
||||||
|
export const LOG_QUERY_CONSTANTS = {
|
||||||
|
/** 引用点:最新日志 */
|
||||||
|
REFERENCE_NEWEST: 'newest' as const,
|
||||||
|
/** 引用点:最早日志 */
|
||||||
|
REFERENCE_OLDEST: 'oldest' as const,
|
||||||
|
/** 初始加载:offsetFrom */
|
||||||
|
INITIAL_OFFSET_FROM: -100,
|
||||||
|
/** 初始加载:offsetTo */
|
||||||
|
INITIAL_OFFSET_TO: 0,
|
||||||
|
/** 向前翻页:offsetFrom */
|
||||||
|
PREVIOUS_OFFSET_FROM: -100,
|
||||||
|
/** 向前翻页:offsetTo */
|
||||||
|
PREVIOUS_OFFSET_TO: 0,
|
||||||
|
/** 向后翻页/轮询:offsetFrom */
|
||||||
|
NEXT_OFFSET_FROM: 1,
|
||||||
|
/** 向后翻页/轮询:offsetTo */
|
||||||
|
NEXT_OFFSET_TO: 101,
|
||||||
|
} as const;
|
||||||
|
|
||||||
// ==================== 操作相关 ====================
|
// ==================== 操作相关 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user