diff --git a/frontend/src/pages/K8sResourceList.tsx b/frontend/src/pages/K8sResourceList.tsx index 2e91c0d..8f556db 100644 --- a/frontend/src/pages/K8sResourceList.tsx +++ b/frontend/src/pages/K8sResourceList.tsx @@ -31,6 +31,8 @@ import SettingsIcon from '@mui/icons-material/Settings' import CloseIcon from '@mui/icons-material/Close' import AddIcon from '@mui/icons-material/Add' import UploadFileIcon from '@mui/icons-material/UploadFile' +import VisibilityIcon from '@mui/icons-material/Visibility' +import DeleteIcon from '@mui/icons-material/Delete' const KINDS = [ { key: 'namespace', label: 'Namespace', endpoint: '/api/v1/k8s/namespace/list' }, @@ -63,6 +65,13 @@ export default function K8sResourceList() { severity: 'success', }) const fileInputRef = useRef(null) + const [logsDialogOpen, setLogsDialogOpen] = useState(false) + const [logs, setLogs] = useState([]) + const [selectedPod, setSelectedPod] = useState<{ name: string; namespace: string } | null>(null) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [deleteTarget, setDeleteTarget] = useState<{ name: string; namespace: string } | null>(null) + const [deleting, setDeleting] = useState(false) + const logsEndRef = useRef(null) useEffect(() => { fetchKubeconfig() @@ -146,6 +155,60 @@ export default function K8sResourceList() { } } + const handleViewLogs = (podName: string, podNamespace: string) => { + setSelectedPod({ name: podName, namespace: podNamespace }) + setLogs([]) + setLogsDialogOpen(true) + + const eventSource = new EventSource( + `/api/v1/k8s/pod/logs?name=${encodeURIComponent(podName)}&namespace=${encodeURIComponent(podNamespace)}&tail=1000&follow=true` + ) + + eventSource.onmessage = (event) => { + setLogs((prev) => [...prev, event.data]) + setTimeout(() => logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100) + } + + eventSource.onerror = () => { + eventSource.close() + } + + return () => eventSource.close() + } + + const handleDeletePod = async () => { + if (!deleteTarget) return + + setDeleting(true) + try { + const res = await fetch('/api/v1/k8s/pod/delete', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: deleteTarget.name, namespace: deleteTarget.namespace }), + }) + + const result = await res.json() + + if (!res.ok) { + throw new Error(result.err || 'Failed to delete pod') + } + + setSnackbar({ open: true, message: 'Pod 删除成功', severity: 'success' }) + setDeleteDialogOpen(false) + setDeleteTarget(null) + fetchResources() + } catch (e: any) { + setSnackbar({ open: true, message: `删除失败: ${e.message}`, severity: 'error' }) + } finally { + setDeleting(false) + } + } + + const openDeleteDialog = (podName: string, podNamespace: string) => { + setDeleteTarget({ name: podName, namespace: podNamespace }) + setDeleteDialogOpen(true) + } + const fetchResources = async () => { if (!kubeconfig) { setKubeconfigError(true) @@ -183,7 +246,7 @@ export default function K8sResourceList() { case 'configmap': return ['Name', 'Namespace', 'Data Keys', 'Age'] case 'pod': - return ['Name', 'Namespace', 'Status', 'IP', 'Node', 'Age'] + return ['Name', 'Namespace', 'Status', 'IP', 'Node', 'Age', 'Actions'] case 'pv': return ['Name', 'Capacity', 'Access Modes', 'Status', 'Claim', 'Age'] case 'pvc': @@ -256,6 +319,23 @@ export default function K8sResourceList() { {status.podIP || '-'} {spec.nodeName || '-'} {getAge(metadata.creationTimestamp)} + + handleViewLogs(metadata.name, metadata.namespace)} + title="查看日志" + > + + + openDeleteDialog(metadata.name, metadata.namespace)} + title="删除" + > + + + ) case 'pv': @@ -469,6 +549,65 @@ export default function K8sResourceList() { + setLogsDialogOpen(false)} + maxWidth="lg" + fullWidth + > + + + + Pod 日志: {selectedPod?.name} ({selectedPod?.namespace}) + + setLogsDialogOpen(false)}> + + + + + + + {logs.length === 0 && 等待日志...} + {logs.map((log, index) => ( + + {log} + + ))} +
+ + +
+ + setDeleteDialogOpen(false)}> + 确认删除 + + + 确定要删除 Pod {deleteTarget?.name} (namespace: {deleteTarget?.namespace}) 吗? + + + + + + + +