/* eslint-disable @typescript-eslint/no-explicit-any */ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useParams, useNavigate, useLocation, Link } from 'react-router-dom'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { RefreshCw, Search, Loader2, Boxes, Container, Network, FileText, Shield, Activity, Clock, ChevronRight, Download, FileCode, Pencil, Terminal, HardDrive, Plus, Upload, } from 'lucide-react'; import { YamlViewer } from './YamlViewer'; import { ResourceVisualEditor } from './ResourceVisualEditor'; import { YamlImportDialog } from './YamlImportDialog'; import { PodExecTerminal } from './PodExecTerminal'; import { PodFileBrowser } from './PodFileBrowser'; import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; type ResourceType = | 'deployments' | 'pods' | 'services' | 'ingresses' | 'configmaps' | 'secrets' | 'hpas' | 'cronjobs' | 'pvcs' | 'statefulsets' | 'daemonsets' | 'serviceaccounts' | 'roles' | 'rolebindings'; // 支持可视化编辑/新建的资源类型 // Logan.Li const VISUAL_RESOURCE_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs']; /** Deployment 列表:新建 + YAML 导入,不做批量导出 // Logan.Li */ const DEPLOYMENT_LIST_TYPES: ResourceType[] = ['deployments']; /** 支持命名空间 YAML 批量导出(对齐集群导出)// Logan.Li */ const EXPORT_YAML_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets']; /** 列表页保留 CSV/JSON 导出的配置类资源 // Logan.Li */ const BULK_EXPORT_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs']; const VisualActions = ({ name, opts }: { name: string; opts: { onYaml?: (n: string) => void; onEdit?: (n: string) => void } }) => (
); const RESOURCE_CONFIG: Record; columns: string[]; renderRow: ( item: any, opts: { clusterCode: string; namespace: string; onDetail?: ( item: any) => void; onYaml?: (name: string) => void; onEdit?: (name: string) => void; onTerminal?: ( item: any) => void; onFileBrowser?: ( item: any) => void }) => React.ReactNode; }> = { deployments: { title: 'Deployments', subtitle: 'Deployment 工作负载运行状态', icon: Boxes, columns: ['名称', '命名空间', '副本', '镜像'], renderRow: (d, opts) => ( opts.onDetail?.(d)}> {d.name} {d.namespace} {(() => { const ready = d.ready_replicas || 0; const total = d.replicas || 0; const pct = total > 0 ? (ready / total) * 100 : 0; const done = ready === total; return (
{ready}/{total}
); })()} {d.image || '-'} ), }, pods: { title: 'Pods', subtitle: 'Pod 实例运行状态', icon: Container, columns: ['名称', '命名空间', '状态', '节点', 'CPU', '内存', '重启', '镜像', '操作'], renderRow: (p, opts) => { const statusMap: Record = { Running: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', Pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', Failed: 'bg-red-500/20 text-red-400 border-red-500/30' }; return ( {p.name} {p.namespace} {p.status || 'Unknown'} {p.node_name || '-'} {p.cpu_usage_m || '-'} {p.mem_usage_mi || '-'} 0 ? 'text-yellow-400' : 'text-slate-500')}>{p.restart_count || 0} {p.image || '-'}
); }, }, services: { title: 'Services', subtitle: 'Service 网络服务', icon: Network, columns: ['名称', '命名空间', '类型', 'Cluster IP', '端口', '操作'], renderRow: (s, opts) => ( {s.name} {s.namespace} {s.type || '-'} {s.cluster_ip || '-'} {s.ports || '-'} e.stopPropagation()}> ), }, ingresses: { title: 'Ingresses', subtitle: 'Ingress 路由规则', icon: Network, columns: ['名称', '命名空间', 'Hosts', 'TLS', '操作'], renderRow: (i, opts) => { const hosts = i.hosts || []; return ( {i.name} {i.namespace} {hosts.length > 0 ? hosts.join(', ') : '-'} {i.tls ? 'HTTPS' : 'HTTP'} e.stopPropagation()}> ); }, }, configmaps: { title: 'ConfigMaps', subtitle: '配置字典', icon: FileText, columns: ['名称', '命名空间', '键数量', '操作'], renderRow: (c, opts) => ( {c.name} {c.namespace} {c.key_count || '-'} e.stopPropagation()}> ), }, secrets: { title: 'Secrets', subtitle: '密钥管理(仅名称与类型)', icon: Shield, columns: ['名称', '命名空间', '类型', '操作'], renderRow: (s, opts) => ( {s.name} {s.namespace} {s.type || '-'} e.stopPropagation()}> ), }, hpas: { title: 'HPAs', subtitle: '水平自动伸缩', icon: Activity, columns: ['名称', '命名空间', '目标', '最小/最大', 'CPU%', 'Mem%'], renderRow: (h, _opts) => ( {h.name} {h.namespace} {h.target_kind}/{h.target_name} {h.min_replicas}/{h.max_replicas} {h.cpu_percent ?? '-'}% {h.mem_percent ?? '-'}% ), }, cronjobs: { title: 'CronJobs', subtitle: '定时任务', icon: Clock, columns: ['名称', '命名空间', '调度', '状态', '上次执行'], renderRow: (cj) => ( {cj.name} {cj.namespace} {cj.schedule || '-'} {cj.suspend ? '已暂停' : '运行中'} {cj.last_schedule || '-'} ), }, pvcs: { title: '存储卷声明', subtitle: 'PersistentVolumeClaim', icon: Boxes, columns: ['名称', '命名空间', '状态', '大小', 'StorageClass', '操作'], renderRow: (p, opts) => ( {p.name} {p.namespace} {p.phase || '-'} {p.size || '-'} {p.storage_class || '-'} e.stopPropagation()}> ), }, statefulsets: { title: 'StatefulSets', subtitle: '有状态应用', icon: Boxes, columns: ['名称', '命名空间', '副本', '就绪', '镜像'], renderRow: (s, opts) => ( opts.onDetail?.(s)}> {s.name} {s.namespace} {s.replicas || 0} {s.ready || 0} {s.image || '-'} ), }, daemonsets: { title: 'DaemonSets', subtitle: '守护进程集', icon: Boxes, columns: ['名称', '命名空间', '期望', '当前', '就绪', '镜像'], renderRow: (d, opts) => ( opts.onDetail?.(d)}> {d.name} {d.namespace} {d.desired || 0} {d.current || 0} {d.ready || 0} {d.image || '-'} ), }, serviceaccounts: { title: 'ServiceAccounts', subtitle: '服务账号(RBAC 身份标识)', icon: Shield, columns: ['名称', '命名空间', 'Secret数', '镜像拉取凭证', '创建时间', '操作'], renderRow: (sa, opts) => ( {sa.name} {sa.namespace} {sa.secret_count ?? 0} {sa.image_pull_secrets && sa.image_pull_secrets.length > 0 ? sa.image_pull_secrets.join(', ') : '-'} {sa.age || '-'} e.stopPropagation()}> ), }, roles: { title: 'Roles', subtitle: '命名空间级角色权限(RBAC)', icon: Shield, columns: ['名称', '命名空间', '规则数', '权限范围', '创建时间', '操作'], renderRow: (r, opts) => { const verbs = r.verbs || []; const resources = r.resources || []; const verbsStr = verbs.length > 4 ? verbs.slice(0, 4).join(', ') + ` +${verbs.length - 4}` : verbs.join(', '); const resStr = resources.length > 4 ? resources.slice(0, 4).join(', ') + ` +${resources.length - 4}` : resources.join(', '); return ( {r.name} {r.namespace} {r.rule_count ?? 0}
verbs: {verbsStr || '-'} resources: {resStr || '-'}
{r.age || '-'} e.stopPropagation()}>
); }, }, rolebindings: { title: 'RoleBindings', subtitle: '角色绑定关系(RBAC 授权)', icon: Shield, columns: ['名称', '命名空间', '角色引用', '绑定主体', '创建时间', '操作'], renderRow: (rb, opts) => { const subjects = rb.subjects || []; const subjStr = subjects.length > 3 ? subjects.slice(0, 3).join(', ') + ` +${subjects.length - 3}` : subjects.join(', '); return ( {rb.name} {rb.namespace} {rb.role_ref_kind || '-'}/{rb.role_ref_name || '-'} {subjStr || '-'} {rb.age || '-'} e.stopPropagation()}>
); }, }, }; export default function NamespaceResourcePage() { const { clusterCode, namespace } = useParams<{ clusterCode: string; namespace: string }>(); const location = useLocation(); const navigate = useNavigate(); const { toast } = useToast(); const type = useMemo(() => { const segments = location.pathname.split('/'); const last = segments[segments.length - 1]; if (last && RESOURCE_CONFIG[last as ResourceType]) return last as ResourceType; if (segments.length >= 2) { const secondLast = segments[segments.length - 2]; if (secondLast && RESOURCE_CONFIG[secondLast as ResourceType]) return secondLast as ResourceType; } return 'deployments'; }, [location.pathname]); const config = RESOURCE_CONFIG[type]; const [clusters, setClusters] = useState([]); const [namespaces, setNamespaces] = useState([]); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [search, setSearch] = useState(''); const [yamlItem, setYamlItem] = useState<{resource: string; name: string; editMode?: boolean} | null>(null); const [visualItem, setVisualItem] = useState<{ resource: string; name: string; isCreate?: boolean } | null>(null); const [showYamlImport, setShowYamlImport] = useState(false); const [terminalPod, setTerminalPod] = useState(null); const [fileBrowserPod, setFileBrowserPod] = useState(null); useEffect(() => { kubernetesApi.getClusters().then(( res: any) => { const list = res?.list || []; setClusters(list); if (!clusterCode && list.length > 0) { const firstCode = list[0].code || String(list[0].id); navigate(`/kubernetes/${firstCode}/namespace/${namespace || 'default'}/${type}`, { replace: true }); } }).catch(() => setClusters([])); }, []); useEffect(() => { if (!clusterCode) return; kubernetesApi.getNamespaces(clusterCode).then(( res: any) => { const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []); setNamespaces(nsList); if (!namespace && nsList.length > 0) { navigate(`/kubernetes/${clusterCode}/namespace/${nsList[0]}/${type}`, { replace: true }); } }).catch(() => setNamespaces([])); }, [clusterCode]); const loadData = useCallback(async () => { if (!clusterCode || !namespace) return; setLoading(true); try { let res: Record = []; switch (type) { case 'deployments': res = await kubernetesApi.getNamespaceDeployments(clusterCode, namespace).catch(() => []); break; case 'pods': res = await kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []); break; case 'services': res = await kubernetesApi.getNamespaceServices(clusterCode, namespace).catch(() => []); break; case 'ingresses': res = await kubernetesApi.getNamespaceIngresses(clusterCode, namespace).catch(() => []); break; case 'configmaps': res = await kubernetesApi.getNamespaceConfigMaps(clusterCode, namespace).catch(() => []); break; case 'secrets': res = await kubernetesApi.getNamespaceSecrets(clusterCode, namespace).catch(() => []); break; case 'hpas': res = await kubernetesApi.getHPAs(clusterCode, namespace).catch(() => []); break; case 'cronjobs': res = await kubernetesApi.getCronJobs(clusterCode, namespace).catch(() => []); break; case 'pvcs': res = await kubernetesApi.getPVCs(clusterCode, namespace).catch(() => []); break; case 'statefulsets': res = await kubernetesApi.getStatefulSets(clusterCode, namespace).catch(() => []); break; case 'daemonsets': res = await kubernetesApi.getDaemonSets(clusterCode, namespace).catch(() => []); break; case 'serviceaccounts': res = await kubernetesApi.getServiceAccounts(clusterCode, namespace).catch(() => []); break; case 'roles': res = await kubernetesApi.getRoles(clusterCode, namespace).catch(() => []); break; case 'rolebindings': res = await kubernetesApi.getRoleBindings(clusterCode, namespace).catch(() => []); break; } setData(Array.isArray(res) ? res : (res?.list || res?.data || [])); } catch { setData([]); } finally { setLoading(false); } }, [clusterCode, namespace, type]); useEffect(() => { loadData(); }, [loadData]); const filtered = data.filter(( d: any) => (d.name || '').toLowerCase().includes(search.toLowerCase())); const handleDetail = ( item: any) => { if (type === 'deployments') { // Kuboard 风格双模式路由(默认 view 模式) navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${item.name}`); } else if (type === 'statefulsets') { navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/StatefulSet/${item.name}`); } else if (type === 'daemonsets') { navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/DaemonSet/${item.name}`); } }; const handleYaml = (name: string) => setYamlItem({ resource: type, name }); // 网络/配置/存储走可视化编辑器,YAML 作为高级入口 // Logan.Li const handleEdit = (name: string) => { if (VISUAL_RESOURCE_TYPES.includes(type)) { setVisualItem({ resource: type, name }); } else { setYamlItem({ resource: type, name, editMode: true }); } }; const handleCreate = () => { if (type === 'deployments') { navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/edit/Deployment/_create`); return; } setVisualItem({ resource: type, name: '', isCreate: true }); }; const handleExportCSV = () => kubernetesApi.exportAsCSV(`${type}-${clusterCode}-${namespace}.csv`, filtered); const handleExportJSON = () => kubernetesApi.exportAsJSON(`${type}-${clusterCode}-${namespace}.json`, filtered); const handleExportYaml = async () => { if (!clusterCode || !namespace) return; try { await kubernetesApi.exportNamespaceResources(clusterCode, namespace); toast({ title: '导出完成', description: `${namespace} 资源 YAML 已开始下载` }); } catch (e) { toast({ title: '导出失败', description: (e as Error)?.message || String(e), variant: 'destructive' }); } }; if (!config) return
未知资源类型: {type}
; return (
{yamlItem && ( setYamlItem(null)} onSaved={loadData} autoEdit={yamlItem.editMode === true} /> )} {showYamlImport && ( setShowYamlImport(false)} onApplied={loadData} /> )} {visualItem && ( setVisualItem(null)} onSaved={loadData} /> )}
集群 {clusterCode && <>{clusterCode}} {namespace && <>{namespace}} {config.title}

{config.title}

{config.subtitle}

{BULK_EXPORT_TYPES.includes(type) && ( <> )} {EXPORT_YAML_TYPES.includes(type) && ( )} {(VISUAL_RESOURCE_TYPES.includes(type) || DEPLOYMENT_LIST_TYPES.includes(type)) && ( <> )}
setSearch(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
{config.columns.map((col) => {col})} {loading ? ( 加载中... ) : filtered.length === 0 ? ( 暂无 {config.title} ) : ( filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml, onEdit: handleEdit, onTerminal: ( p: any) => setTerminalPod(p), onFileBrowser: ( p: any) => setFileBrowserPod(p) })) )}
{/* ============ Pod 终端 ============ */} { if (!open) setTerminalPod(null); }} clusterId={clusters[0]?.id || clusters[0]?.code || clusterCode || ''} namespace={namespace || ''} podName={terminalPod?.name || ''} containers={(terminalPod?.containers || []).map(( c: any) => c.name).filter(Boolean)} /> {/* ============ Pod 文件浏览器 ============ */} { if (!open) setFileBrowserPod(null); }} clusterCode={clusterCode || ''} namespace={namespace || ''} podName={fileBrowserPod?.name || ''} containers={(fileBrowserPod?.containers || []).map(( c: any) => c.name).filter(Boolean)} />
); }