feat: add resource edit functionality for k8s resources
- Add edit button for Deployment, StatefulSet, Service, and ConfigMap resources - Implement resource fetch API to get YAML representation - Implement resource update API to apply edited YAML - Add edit dialog with YAML editor and apply/cancel buttons - Add tooltip icons for better UX - Restore K8sResourceApply function with HTTP/1.1 enforcement - Support for fetching and updating the following resource kinds: - Deployment - StatefulSet - Service - ConfigMap 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -28,6 +28,9 @@ import {
|
||||
Snackbar,
|
||||
MenuItem,
|
||||
Select,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Tooltip,
|
||||
} from '@mui/material'
|
||||
import SettingsIcon from '@mui/icons-material/Settings'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
@@ -35,6 +38,7 @@ 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'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
|
||||
const KINDS = [
|
||||
{ key: 'namespace', label: 'Namespace', endpoint: '/api/v1/k8s/namespace/list' },
|
||||
@@ -75,6 +79,10 @@ export default function K8sResourceList() {
|
||||
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; kind: string; yaml: string } | null>(null)
|
||||
const [editYaml, setEditYaml] = useState('')
|
||||
const [editing, setEditing] = useState(false)
|
||||
const logsEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -211,6 +219,52 @@ export default function K8sResourceList() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditResource = async (name: string, namespace: string, kind: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/k8s/resource/get?name=${encodeURIComponent(name)}&namespace=${encodeURIComponent(namespace)}&kind=${encodeURIComponent(kind)}`)
|
||||
const result = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result.err || 'Failed to get resource')
|
||||
}
|
||||
|
||||
setEditResource({ name, namespace, kind, yaml: result.data.yaml })
|
||||
setEditYaml(result.data.yaml)
|
||||
setEditDialogOpen(true)
|
||||
} catch (e: any) {
|
||||
setSnackbar({ open: true, message: `获取资源失败: ${e.message}`, severity: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleApplyEdit = async () => {
|
||||
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: editYaml }),
|
||||
})
|
||||
|
||||
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)
|
||||
setEditYaml('')
|
||||
fetchResources()
|
||||
} catch (e: any) {
|
||||
setSnackbar({ open: true, message: `更新失败: ${e.message}`, severity: 'error' })
|
||||
} finally {
|
||||
setEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openDeleteDialog = (podName: string, podNamespace: string) => {
|
||||
setDeleteTarget({ name: podName, namespace: podNamespace })
|
||||
setDeleteDialogOpen(true)
|
||||
@@ -264,9 +318,9 @@ export default function K8sResourceList() {
|
||||
case 'statefulset':
|
||||
return ['Name', 'Namespace', 'Replicas', 'Age']
|
||||
case 'service':
|
||||
return ['Name', 'Namespace', 'Type', 'Cluster IP', 'External IP', 'Ports', 'Age']
|
||||
return ['Name', 'Namespace', 'Type', 'Cluster IP', 'External IP', 'Ports', 'Age', 'Actions']
|
||||
case 'configmap':
|
||||
return ['Name', 'Namespace', 'Data Keys', 'Age']
|
||||
return ['Name', 'Namespace', 'Data Keys', 'Age', 'Actions']
|
||||
case 'pod':
|
||||
return ['Name', 'Namespace', 'Status', 'IP', 'Node', 'Age', 'Actions']
|
||||
case 'pv':
|
||||
@@ -537,6 +591,46 @@ export default function K8sResourceList() {
|
||||
</Box>
|
||||
</Drawer>
|
||||
|
||||
<Dialog
|
||||
open={editDialogOpen}
|
||||
onClose={() => setEditDialogOpen(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">
|
||||
编辑 {editResource?.kind}: {editResource?.name} ({editResource?.namespace})
|
||||
</Typography>
|
||||
<IconButton onClick={() => setEditDialogOpen(false)}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
multiline
|
||||
rows={20}
|
||||
value={editYaml}
|
||||
onChange={(e) => setEditYaml(e.target.value)}
|
||||
placeholder="YAML 内容..."
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setEditDialogOpen(false)}>取消</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleApplyEdit}
|
||||
disabled={editing}
|
||||
>
|
||||
{editing ? <CircularProgress size={24} /> : '应用'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={createDialogOpen}
|
||||
onClose={() => setCreateDialogOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user