1. 拆分原有的巨型 K8sResourceList.tsx 文件(1001行)为多个独立页面组件 2. 为每种 K8s 资源类型创建专门的页面: - Namespace: 简单名称输入创建 - Deployment/StatefulSet: YAML 文件上传创建 - Service: 显示"功能开发中"提示 - ConfigMap: Key-Value 编辑器创建(支持文件上传) - Pod: 无创建功能 - PV/PVC: YAML 文件上传创建 3. 创建共享组件和 Hooks 提高代码复用 4. 更新路由配置使用嵌套路由结构 5. 修复 PV/PVC 页面缺少组件导入的问题 优化了代码结构,使每个文件控制在合理大小范围内,便于维护和扩展。
340 lines
11 KiB
TypeScript
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>
|
|
)
|
|
}
|