重构 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 页面缺少组件导入的问题 优化了代码结构,使每个文件控制在合理大小范围内,便于维护和扩展。
This commit is contained in:
339
frontend/src/pages/k8s/StatefulSetPage.tsx
Normal file
339
frontend/src/pages/k8s/StatefulSetPage.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user