@@ -140,12 +330,143 @@ export const DeploymentRow: React.FC = ({
))
)}
>
)}
+
+ {/* 扩缩容对话框 */}
+
+
+ {/* 删除确认对话框 */}
+
+
+ {/* YAML查看器 */}
+
+
+ {/* 标签查看对话框 */}
+
>
);
};
diff --git a/frontend/src/pages/Resource/K8s/List/components/DeploymentTable.tsx b/frontend/src/pages/Resource/K8s/List/components/DeploymentTable.tsx
index ee7caa52..64ffef3b 100644
--- a/frontend/src/pages/Resource/K8s/List/components/DeploymentTable.tsx
+++ b/frontend/src/pages/Resource/K8s/List/components/DeploymentTable.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { DeploymentRow } from './DeploymentRow';
import type { K8sDeploymentResponse, K8sPodResponse } from '../types';
@@ -7,7 +7,7 @@ interface DeploymentTableProps {
deployments: K8sDeploymentResponse[];
expandedIds: Set;
onToggleExpand: (id: number) => void;
- onViewLogs: (pod: K8sPodResponse) => void;
+ onViewLogs: (pod: K8sPodResponse, deploymentId: number) => void;
onRefresh?: () => void;
}
@@ -27,8 +27,8 @@ export const DeploymentTable: React.FC = ({
}
return (
-
-
+
+
名称
@@ -37,7 +37,7 @@ export const DeploymentTable: React.FC = ({
镜像
重启
更新时间
- 操作
+ 操作
@@ -52,7 +52,7 @@ export const DeploymentTable: React.FC = ({
/>
))}
-
+
);
};
diff --git a/frontend/src/pages/Resource/K8s/List/components/PodLogDialog.tsx b/frontend/src/pages/Resource/K8s/List/components/PodLogDialog.tsx
new file mode 100644
index 00000000..149f6300
--- /dev/null
+++ b/frontend/src/pages/Resource/K8s/List/components/PodLogDialog.tsx
@@ -0,0 +1,206 @@
+import React, { useEffect, useState } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogBody,
+ DialogFooter,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { FileText, RefreshCw, Download } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { getK8sPodLogs, getK8sPodDetail } from '../service';
+import { useToast } from '@/components/ui/use-toast';
+import LogViewer from '@/components/LogViewer';
+
+interface PodLogDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ deploymentId: number;
+ podName: string;
+}
+
+const PodLogDialog: React.FC = ({
+ open,
+ onOpenChange,
+ deploymentId,
+ podName,
+}) => {
+ const { toast } = useToast();
+ const [loading, setLoading] = useState(false);
+ const [loadingPod, setLoadingPod] = useState(false);
+ const [logContent, setLogContent] = useState('');
+ const [selectedContainer, setSelectedContainer] = useState('');
+ const [containers, setContainers] = useState([]);
+
+ // 打开对话框时获取Pod详情,获取最新的容器列表
+ useEffect(() => {
+ if (open && deploymentId && podName) {
+ const fetchPodDetail = async () => {
+ setLoadingPod(true);
+ try {
+ const pod = await getK8sPodDetail(deploymentId, podName);
+ const containerNames = pod.containers.map(c => c.name);
+ setContainers(containerNames);
+ if (containerNames.length > 0) {
+ setSelectedContainer(containerNames[0]);
+ }
+ } catch (error: any) {
+ console.error('获取Pod详情失败:', error);
+ toast({
+ title: '获取Pod详情失败',
+ description: error.message || '无法获取Pod容器信息',
+ variant: 'destructive',
+ });
+ } finally {
+ setLoadingPod(false);
+ }
+ };
+ fetchPodDetail();
+ }
+ }, [open, deploymentId, podName]);
+
+ const fetchLogs = async () => {
+ if (!deploymentId || !podName) return;
+
+ setLoading(true);
+ try {
+ // 如果有选中的容器,传递容器名;否则不传,让后端使用默认容器
+ const params: any = {
+ tail: 1000, // 获取最后1000行
+ };
+ if (selectedContainer) {
+ params.container = selectedContainer;
+ }
+
+ const logs = await getK8sPodLogs(deploymentId, podName, params);
+ setLogContent(logs || '暂无日志');
+ } catch (error: any) {
+ console.error('获取Pod日志失败:', error);
+ toast({
+ title: '获取日志失败',
+ description: error.message || '无法获取Pod日志',
+ variant: 'destructive',
+ });
+ setLogContent('获取日志失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 打开对话框或切换容器时加载日志
+ useEffect(() => {
+ if (open && selectedContainer) {
+ setLogContent('');
+ fetchLogs();
+ }
+ }, [open, selectedContainer, deploymentId, podName]);
+
+ // 下载日志回调
+ const handleDownload = () => {
+ toast({
+ title: '下载成功',
+ description: '日志文件已保存',
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default PodLogDialog;
diff --git a/frontend/src/pages/Resource/K8s/List/components/PodRow.tsx b/frontend/src/pages/Resource/K8s/List/components/PodRow.tsx
index 1f13bb5f..f6f06ba5 100644
--- a/frontend/src/pages/Resource/K8s/List/components/PodRow.tsx
+++ b/frontend/src/pages/Resource/K8s/List/components/PodRow.tsx
@@ -1,105 +1,273 @@
-import React from 'react';
+import React, { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
-import { FileText, Copy, Check, Box } from 'lucide-react';
+import { FileText, Box, FileCode, Tag, Copy, Check } from 'lucide-react';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useToast } from '@/components/ui/use-toast';
+import { YamlViewerDialog } from './YamlViewerDialog';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogBody,
+} from '@/components/ui/dialog';
import type { K8sPodResponse } from '../types';
import { PodPhaseLabels } from '../types';
interface PodRowProps {
pod: K8sPodResponse;
- onViewLogs: (pod: K8sPodResponse) => void;
+ deploymentId: number;
+ onViewLogs: (pod: K8sPodResponse, deploymentId: number) => void;
}
-export const PodRow: React.FC = ({ pod, onViewLogs }) => {
+export const PodRow: React.FC = ({ pod, deploymentId, onViewLogs }) => {
const { toast } = useToast();
- const [copiedField, setCopiedField] = React.useState(null);
- const phaseLabel = PodPhaseLabels[pod.phase];
+ const [yamlDialogOpen, setYamlDialogOpen] = useState(false);
+ const [labelDialogOpen, setLabelDialogOpen] = useState(false);
+ const [copiedImage, setCopiedImage] = useState(false);
+
+ const phaseLabel = PodPhaseLabels[pod.phase] || PodPhaseLabels['UNKNOWN'] || {
+ label: '未知',
+ variant: 'secondary' as const,
+ color: 'bg-gray-600 hover:bg-gray-700'
+ };
- const handleCopy = (text: string, field: string) => {
- navigator.clipboard.writeText(text).then(() => {
- setCopiedField(field);
- toast({
- title: '已复制',
- description: `已复制${field}到剪贴板`,
+ // 获取主容器信息
+ const mainContainer = pod.containers[0];
+
+ // 复制镜像名称
+ const handleCopyImage = () => {
+ if (mainContainer?.image) {
+ navigator.clipboard.writeText(mainContainer.image).then(() => {
+ setCopiedImage(true);
+ toast({
+ title: '已复制',
+ description: '镜像名称已复制到剪贴板',
+ });
+ setTimeout(() => setCopiedImage(false), 2000);
});
- setTimeout(() => setCopiedField(null), 2000);
- });
+ }
};
return (
-
-
-
- {/* Pod名称 */}
-
-
- {pod.name}
-
+ <>
+
+ {/* Pod信息列(占6列) */}
+
+
+ {/* 第一行:Pod名称 + 状态 */}
+
+ {/* Pod名称 - 固定宽度 */}
+
+
+
+ {pod.name}
+
+
- {/* 状态 */}
-
-
- {phaseLabel.label}
-
-
-
- {/* 就绪状态 */}
-
-
- {pod.ready ? '就绪' : '未就绪'}
-
-
-
- {/* 重启次数 */}
-
- {pod.restartCount > 0 ? (
-
- {pod.restartCount}次
+ {/* 状态徽章 */}
+
+ {phaseLabel.label}
- ) : (
- -
- )}
-
- {/* Pod IP */}
-
- {pod.podIP || '-'}
- {pod.podIP && (
-
+
+ {/* 第二行:详细信息 */}
+
+ {/* 节点 - 固定宽度 */}
+
+ 节点:
+ {pod.nodeName || '-'}
+
+
+ |
+
+ {/* IP - 固定宽度 */}
+
+ IP:
+ {pod.podIP || '-'}
+
+
+ {mainContainer && (
+ <>
+ |
+
+ {/* 镜像 - 固定宽度 */}
+
+ 镜像:
+
+ {mainContainer.image}
+
+
+
+
+ {/* 资源 - 固定宽度 */}
+
+ 资源:
+
+ CPU {mainContainer.cpuLimit || '-'} / 内存 {mainContainer.memoryLimit || '-'}
+
+
+ >
+ )}
+
+
+ |
+
+ {/* 操作列(占1列,固定右侧) */}
+
+
+
+
+
+
+
+ 查看日志
+
+
+
+
+
+
+ 查看YAML
+
+
+
+
+
+
+ 查看标签
+
+
+
+ |
+
+
+ {/* YAML查看器 */}
+
+
+ {/* 标签和注解对话框 - 使用Tab切换 */}
+
-
- {/* 节点 */}
-
- {pod.nodeName || '-'}
-
-
- {/* 日志按钮 */}
-
-
-
-
- |
-
+
+
+
+ {pod.annotations && Object.keys(pod.annotations).length > 0 ? (
+
+ {Object.entries(pod.annotations).map(([key, value]) => (
+
+
+
+ {key}
+
+ :
+ {value}
+
+
+ ))}
+
+ ) : (
+ 暂无注解
+ )}
+
+
+
+
+
+ >
);
};
diff --git a/frontend/src/pages/Resource/K8s/List/components/PodRowSkeleton.tsx b/frontend/src/pages/Resource/K8s/List/components/PodRowSkeleton.tsx
new file mode 100644
index 00000000..096d8bdd
--- /dev/null
+++ b/frontend/src/pages/Resource/K8s/List/components/PodRowSkeleton.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { Skeleton } from '@/components/ui/skeleton';
+
+export const PodRowSkeleton: React.FC = () => {
+ return (
+
+ {/* Pod信息列(占6列) */}
+
+
+ {/* 第一行:Pod名称 + 状态 */}
+
+ {/* Pod名称 */}
+
+
+
+
+
+ {/* 状态徽章 */}
+
+
+
+ {/* 第二行:详细信息 */}
+
+ {/* 节点 */}
+
+
+
+
+
+
+
+ {/* IP */}
+
+
+
+
+
+
+
+ {/* 镜像 */}
+
+
+
+
+
+
+
+ {/* 资源 */}
+
+
+
+
+
+
+ |
+
+ {/* 操作列(占1列,固定右侧) */}
+
+
+
+
+
+
+ |
+
+ );
+};
diff --git a/frontend/src/pages/Resource/K8s/List/index.tsx b/frontend/src/pages/Resource/K8s/List/index.tsx
index 76bae2e0..04c36d6a 100644
--- a/frontend/src/pages/Resource/K8s/List/index.tsx
+++ b/frontend/src/pages/Resource/K8s/List/index.tsx
@@ -6,11 +6,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Input } from '@/components/ui/input';
import { RefreshCw, Loader2, Cloud, History as HistoryIcon, Database, Search } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
+import { useAppDispatch, useAppSelector } from '@/store/hooks';
+import { setSelectedClusterId, setSelectedNamespaceId } from '@/store/k8sSlice';
import { StatsCards } from './components/StatsCards';
import { FilterTabs } from './components/FilterTabs';
import { DeploymentTable } from './components/DeploymentTable';
import { DeploymentTableSkeleton } from './components/DeploymentTableSkeleton';
import { SyncHistoryTab } from './components/SyncHistoryTab';
+import PodLogDialog from './components/PodLogDialog';
import { getExternalSystemList } from '@/pages/Resource/External/List/service';
import { syncK8sNamespace, syncK8sDeployment, getK8sNamespaceList, getK8sDeploymentByNamespace } from './service';
import type { ExternalSystemResponse } from '@/pages/Resource/External/List/types';
@@ -23,10 +26,12 @@ import dayjs from 'dayjs';
const K8sManagement: React.FC = () => {
const { toast } = useToast();
+ const dispatch = useAppDispatch();
+ const selectedClusterId = useAppSelector((state) => state.k8s.selectedClusterId);
+ const selectedNamespaceId = useAppSelector((state) => state.k8s.selectedNamespaceId);
+
const [k8sClusters, setK8sClusters] = useState([]);
- const [selectedClusterId, setSelectedClusterId] = useState(null);
const [namespaces, setNamespaces] = useState([]);
- const [selectedNamespaceId, setSelectedNamespaceId] = useState(null);
const [deployments, setDeployments] = useState([]);
const [syncingNamespace, setSyncingNamespace] = useState(false);
const [syncingDeployment, setSyncingDeployment] = useState(false);
@@ -35,6 +40,8 @@ const K8sManagement: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [activeFilter, setActiveFilter] = useState('all');
const [lastUpdateTime, setLastUpdateTime] = useState(new Date());
+ const [logDialogOpen, setLogDialogOpen] = useState(false);
+ const [selectedPod, setSelectedPod] = useState<(K8sPodResponse & { deploymentId: number }) | null>(null);
const { expandedIds, toggleExpand } = useDeploymentExpand();
@@ -46,9 +53,14 @@ const K8sManagement: React.FC = () => {
const enabledClusters = (clusters || []).filter(c => c.enabled);
setK8sClusters(enabledClusters);
- // 如果有集群,默认选择第一个
+ // 如果有集群且store中没有选中的集群,默认选择第一个
if (enabledClusters.length > 0 && !selectedClusterId) {
- setSelectedClusterId(enabledClusters[0].id);
+ dispatch(setSelectedClusterId(enabledClusters[0].id));
+ }
+ // 如果store中有选中的集群,但该集群已被禁用或删除,清除选择
+ if (selectedClusterId && !enabledClusters.find(c => c.id === selectedClusterId)) {
+ dispatch(setSelectedClusterId(null));
+ dispatch(setSelectedNamespaceId(null));
}
} catch (error) {
console.error('加载K8S集群失败:', error);
@@ -69,9 +81,13 @@ const K8sManagement: React.FC = () => {
try {
const data = await getK8sNamespaceList(selectedClusterId);
setNamespaces(data || []);
- // 默认选择第一个命名空间
+ // 如果有命名空间且store中没有选中的命名空间,默认选择第一个
if (data && data.length > 0 && !selectedNamespaceId) {
- setSelectedNamespaceId(data[0].id);
+ dispatch(setSelectedNamespaceId(data[0].id));
+ }
+ // 如果store中有选中的命名空间,但该命名空间已被删除,清除选择
+ if (selectedNamespaceId && data && !data.find(ns => ns.id === selectedNamespaceId)) {
+ dispatch(setSelectedNamespaceId(null));
}
} catch (error) {
console.error('加载命名空间失败:', error);
@@ -232,9 +248,9 @@ const K8sManagement: React.FC = () => {
}, [deployments]);
// 处理日志查看
- const handleViewLogs = (pod: K8sPodResponse) => {
- // TODO: 打开日志抽屉
- console.log('查看日志:', pod.name);
+ const handleViewLogs = (pod: K8sPodResponse, deploymentId: number) => {
+ setSelectedPod({ ...pod, deploymentId });
+ setLogDialogOpen(true);
};
if (initialLoading) {
@@ -292,7 +308,12 @@ const K8sManagement: React.FC = () => {
|