Files
cluster/frontend/src/pages/k8s/StatefulSetPage.tsx
loveuer c22845f83d 重构 K8s 资源管理页面
1. 拆分原有的巨型 K8sResourceList.tsx 文件(1001行)为多个独立页面组件
2. 为每种 K8s 资源类型创建专门的页面:
   - Namespace: 简单名称输入创建
   - Deployment/StatefulSet: YAML 文件上传创建
   - Service: 显示"功能开发中"提示
   - ConfigMap: Key-Value 编辑器创建(支持文件上传)
   - Pod: 无创建功能
   - PV/PVC: YAML 文件上传创建
3. 创建共享组件和 Hooks 提高代码复用
4. 更新路由配置使用嵌套路由结构
5. 修复 PV/PVC 页面缺少组件导入的问题

优化了代码结构,使每个文件控制在合理大小范围内,便于维护和扩展。
2025-12-08 19:06:40 +08:00

340 lines
11 KiB
TypeScript

import { useState, useEffect } from 'react'
import {
Box,
Typography,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
CircularProgress,
Alert,
IconButton,
TextField,
MenuItem,
Tooltip,
Snackbar,
} from '@mui/material'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import AddIcon from '@mui/icons-material/Add'
import { useNamespaces } from '../../hooks/useNamespaces'
import { getAge } from '../../utils/k8sHelpers'
import CreateYamlDialog from '../../components/k8s/CreateYamlDialog'
import DeleteConfirmDialog from '../../components/k8s/DeleteConfirmDialog'
import EditResourceDialog from '../../components/k8s/EditResourceDialog'
export default function StatefulSetPage() {
const [resources, setResources] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [namespace, setNamespace] = useState('')
const [nameFilter, setNameFilter] = useState('')
const [createDialogOpen, setCreateDialogOpen] = useState(false)
const [yamlContent, setYamlContent] = useState('')
const [applyLoading, setApplyLoading] = useState(false)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<{ name: string; namespace: string } | null>(null)
const [deleting, setDeleting] = useState(false)
const [editDialogOpen, setEditDialogOpen] = useState(false)
const [editResource, setEditResource] = useState<{ name: string; namespace: string; yaml: string } | null>(null)
const [editYaml, setEditYaml] = useState('')
const [editing, setEditing] = useState(false)
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
open: false,
message: '',
severity: 'success',
})
const { namespaces } = useNamespaces()
useEffect(() => {
fetchResources()
}, [namespace, nameFilter])
const fetchResources = async () => {
setLoading(true)
setError(null)
try {
const params = new URLSearchParams()
if (namespace) params.append('namespace', namespace)
if (nameFilter) params.append('name', nameFilter)
const res = await fetch(`/api/v1/k8s/statefulset/list?${params}`)
const result = await res.json()
if (!res.ok) {
throw new Error(result.err || 'Failed to fetch resources')
}
setResources(result.data?.items || [])
} catch (e: any) {
setError(e.message)
} finally {
setLoading(false)
}
}
const handleApplyYaml = async (yaml: string) => {
if (!yaml.trim()) {
setSnackbar({ open: true, message: 'YAML 内容不能为空', severity: 'error' })
return
}
setApplyLoading(true)
try {
const res = await fetch('/api/v1/k8s/resource/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ yaml }),
})
const result = await res.json()
if (!res.ok) {
throw new Error(result.err || 'Failed to apply resource')
}
setSnackbar({ open: true, message: 'StatefulSet 应用成功', severity: 'success' })
setCreateDialogOpen(false)
setYamlContent('')
fetchResources()
} catch (e: any) {
setSnackbar({ open: true, message: `应用失败: ${e.message}`, severity: 'error' })
} finally {
setApplyLoading(false)
}
}
const handleDeleteResource = async () => {
if (!deleteTarget) return
setDeleting(true)
try {
const res = await fetch('/api/v1/k8s/statefulset/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 StatefulSet')
}
setSnackbar({ open: true, message: 'StatefulSet 删除成功', severity: 'success' })
setDeleteDialogOpen(false)
setDeleteTarget(null)
fetchResources()
} catch (e: any) {
setSnackbar({ open: true, message: `删除失败: ${e.message}`, severity: 'error' })
} finally {
setDeleting(false)
}
}
const handleEditResource = async (name: string, ns: string) => {
try {
const res = await fetch(`/api/v1/k8s/resource/get?kind=StatefulSet&name=${name}&namespace=${ns}`)
const result = await res.json()
if (!res.ok) {
throw new Error(result.err || 'Failed to fetch resource')
}
setEditResource({ name, namespace: ns, yaml: result.data?.yaml || '' })
setEditYaml(result.data?.yaml || '')
setEditDialogOpen(true)
} catch (e: any) {
setSnackbar({ open: true, message: `获取资源失败: ${e.message}`, severity: 'error' })
}
}
const handleSaveEdit = async (yaml: string) => {
if (!editResource) return
setEditing(true)
try {
const res = await fetch('/api/v1/k8s/resource/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ yaml }),
})
const result = await res.json()
if (!res.ok) {
throw new Error(result.err || 'Failed to update resource')
}
setSnackbar({ open: true, message: '更新成功', severity: 'success' })
setEditDialogOpen(false)
setEditResource(null)
fetchResources()
} catch (e: any) {
setSnackbar({ open: true, message: `更新失败: ${e.message}`, severity: 'error' })
} finally {
setEditing(false)
}
}
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5">StatefulSet</Typography>
<IconButton onClick={() => setCreateDialogOpen(true)}>
<AddIcon />
</IconButton>
</Box>
<Box sx={{ mb: 2, display: 'flex', gap: 2 }}>
<TextField
select
label="Namespace"
value={namespace}
onChange={(e) => setNamespace(e.target.value)}
size="small"
sx={{ width: 200 }}
SelectProps={{
displayEmpty: true,
}}
InputLabelProps={{ shrink: true }}
>
<MenuItem value="">
<em></em>
</MenuItem>
{namespaces.map((ns) => (
<MenuItem key={ns} value={ns}>
{ns}
</MenuItem>
))}
</TextField>
<TextField
label="名称过滤"
placeholder="按名称过滤"
value={nameFilter}
onChange={(e) => setNameFilter(e.target.value)}
size="small"
sx={{ width: 200 }}
/>
</Box>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress />
</Box>
)}
{!loading && !error && (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Namespace</TableCell>
<TableCell>Replicas</TableCell>
<TableCell>Age</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{resources.length === 0 && (
<TableRow>
<TableCell colSpan={5} align="center">
</TableCell>
</TableRow>
)}
{resources.map((resource) => {
const metadata = resource.metadata || {}
const spec = resource.spec || {}
const status = resource.status || {}
return (
<TableRow key={metadata.uid}>
<TableCell>{metadata.name || '-'}</TableCell>
<TableCell>{metadata.namespace || '-'}</TableCell>
<TableCell>{`${status.readyReplicas || 0}/${spec.replicas || 0}`}</TableCell>
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
<TableCell>
<Tooltip title="编辑">
<IconButton
size="small"
onClick={() => handleEditResource(metadata.name, metadata.namespace)}
>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="删除">
<IconButton
size="small"
color="error"
onClick={() => {
setDeleteTarget({ name: metadata.name, namespace: metadata.namespace })
setDeleteDialogOpen(true)
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
)}
<CreateYamlDialog
open={createDialogOpen}
onClose={() => setCreateDialogOpen(false)}
onApply={handleApplyYaml}
yamlContent={yamlContent}
onYamlChange={setYamlContent}
loading={applyLoading}
/>
<DeleteConfirmDialog
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
onConfirm={handleDeleteResource}
resourceType="StatefulSet"
resourceName={deleteTarget?.name || ''}
namespace={deleteTarget?.namespace}
deleting={deleting}
/>
<EditResourceDialog
open={editDialogOpen}
onClose={() => setEditDialogOpen(false)}
onSave={handleSaveEdit}
resourceType="StatefulSet"
resourceName={editResource?.name || ''}
namespace={editResource?.namespace || ''}
yaml={editYaml}
onYamlChange={setEditYaml}
saving={editing}
/>
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert severity={snackbar.severity} onClose={() => setSnackbar({ ...snackbar, open: false })}>
{snackbar.message}
</Alert>
</Snackbar>
</Box>
)
}