feat: add pod logs and delete operations

- Add pod logs button with SSE streaming (WIP: SSE connection issues with HTTP/2)
- Add pod delete button with confirmation dialog
- Use existing resp.SSE package for log streaming
- Force HTTP/1.1 for k8s client to avoid stream closing issues
- Update frontend to handle pod actions and dialogs

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
loveuer
2025-11-12 23:05:32 +08:00
parent db28bc0425
commit 54ed79cea3
4 changed files with 275 additions and 1 deletions

View File

@@ -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<HTMLInputElement>(null)
const [logsDialogOpen, setLogsDialogOpen] = useState(false)
const [logs, setLogs] = useState<string[]>([])
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<HTMLDivElement>(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() {
<TableCell>{status.podIP || '-'}</TableCell>
<TableCell>{spec.nodeName || '-'}</TableCell>
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
<TableCell>
<IconButton
size="small"
onClick={() => handleViewLogs(metadata.name, metadata.namespace)}
title="查看日志"
>
<VisibilityIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => openDeleteDialog(metadata.name, metadata.namespace)}
title="删除"
>
<DeleteIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
)
case 'pv':
@@ -469,6 +549,65 @@ export default function K8sResourceList() {
</DialogActions>
</Dialog>
<Dialog
open={logsDialogOpen}
onClose={() => setLogsDialogOpen(false)}
maxWidth="lg"
fullWidth
>
<DialogTitle>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">
Pod : {selectedPod?.name} ({selectedPod?.namespace})
</Typography>
<IconButton onClick={() => setLogsDialogOpen(false)}>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<Paper
sx={{
p: 2,
bgcolor: '#1e1e1e',
color: '#d4d4d4',
fontFamily: 'monospace',
fontSize: '0.875rem',
maxHeight: '60vh',
overflow: 'auto',
}}
>
{logs.length === 0 && <Typography>...</Typography>}
{logs.map((log, index) => (
<Box key={index} sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{log}
</Box>
))}
<div ref={logsEndRef} />
</Paper>
</DialogContent>
</Dialog>
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle></DialogTitle>
<DialogContent>
<Typography>
Pod <strong>{deleteTarget?.name}</strong> (namespace: {deleteTarget?.namespace})
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}></Button>
<Button
variant="contained"
color="error"
onClick={handleDeletePod}
disabled={deleting}
>
{deleting ? <CircularProgress size={24} /> : '删除'}
</Button>
</DialogActions>
</Dialog>
<Snackbar
open={snackbar.open}
autoHideDuration={6000}