diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md
index 6242e39..3ba434b 100644
--- a/PROJECT_EVALUATION.md
+++ b/PROJECT_EVALUATION.md
@@ -129,7 +129,7 @@
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
-| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6+Q7+Q8 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%),累计提取 14 个独立组件文件** |
+| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6+Q7+Q8+Q9 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%),累计提取 16 个独立组件文件** |
### 3.2 🟡 中优先级
diff --git a/ROADMAP.md b/ROADMAP.md
index 5a5b019..922790b 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -480,6 +480,7 @@ services/api.ts (1324L)
| Q6 | 前端巨型组件拆分(ResourceVisualEditor 882L→282L,提取 5 个独立编辑器) | P0 | **done** |
| Q7 | 前端巨型组件拆分(AgentCLI 814L→521L,提取工具栏+常量+空状态+滚动按钮) | P0 | **done** |
| Q8 | 前端巨型组件拆分(SystemSettingsTab 754L→173L,提取通用/安全/LDAP 3 个子 Tab) | P0 | **done** |
+| Q9 | 前端巨型组件拆分(NamespaceResourcePage 719L→300L,提取 RESOURCE_CONFIG + PodDetailDialog) | P0 | **done** |
---
@@ -710,3 +711,34 @@ pending → in_progress → done / blocked
- [x] 17/17 集成回归测试通过(零回归)
- [x] Docker 部署 healthy
- [x] 系统设置页 3 个 Tab 功能不变(通用/安全/LDAP 全部可切换)
+
+---
+
+## Q9: 前端巨型组件拆分 — NamespaceResourcePage [done]
+
+**问题**: `NamespaceResourcePage.tsx`(719 行)是 Q8 拆分后项目最大的业务组件之一,包含命名空间资源列表页面的全部逻辑:13 种资源类型的 `RESOURCE_CONFIG` 声明式行渲染器(Deployments/Pods/Services/Ingresses/ConfigMaps/Secrets/HPAs/CronJobs/PVCs/StatefulSets/DaemonSets/ServiceAccounts/Roles/RoleBindings)、VisualActions 组件、类型常量数组、以及 Pod 详情弹窗(基本信息网格 + Conditions 表格)。单文件过大导致维护困难、代码导航低效。
+
+**已完成 (2026-07-07)**:
+
+将 RESOURCE_CONFIG 配置和 Pod 详情弹窗提取为独立文件:
+
+| 新文件 | 行数 | 职责 |
+|--------|------|------|
+| `namespace-resource-config.tsx` | 321 | 13 种资源类型的列定义 + 行渲染器 + VisualActions + 类型常量 + ResourceRowOpts 接口 |
+| `PodDetailDialog.tsx` | 144 | Pod 详情弹窗(基本信息 2 列网格 + Labels/Annotations Badge + Conditions 现状表格) |
+
+主文件 `NamespaceResourcePage.tsx` 从 **719 行降至 300 行**(-58%),仅保留:状态管理(clusters/namespaces/data/yamlItem/visualItem/podDetail)、数据加载(loadData switch 13 分支)、事件处理(handleDetail/handleYaml/handleEdit/handleCreate/handleExportCSV/JSON/Yaml/handleDeletePod)、PageShell 布局(工具栏 + 搜索 + Table + Dialog 集成)。
+
+**提取原则**: config 文件为纯声明式数据(`RESOURCE_CONFIG` 对象 + `VisualActions` 函数组件 + 类型/常量导出),主组件通过 import 使用;PodDetailDialog 为纯展示组件(接收 `pod` + `onClose` props)。零逻辑变更。
+
+**验收标准**:
+- [x] NamespaceResourcePage.tsx 从 719 行降至 **300 行**(-58%)
+- [x] 2 个提取的文件全部 < 325 行(namespace-resource-config 321 行 + PodDetailDialog 144 行)
+- [x] `tsc -b` 零错误
+- [x] `npm run build` 零错误(✓ built in 15.88s)
+- [x] ESLint errors 保持 **0**(清理 4 个 unused import: Table/TableBody/TableHead/TableHeader)
+- [x] ESLint warnings 保持 **59**(5 个 react-refresh/only-export-components 符合既有模式)
+- [x] `go build` 零错误
+- [x] 17/17 集成回归测试通过(零回归)
+- [x] Docker 部署 healthy
+- [x] 命名空间资源列表页 13 种资源类型功能不变(全部可正常渲染/操作)
diff --git a/frontend/src/components/kubernetes/NamespaceResourcePage.tsx b/frontend/src/components/kubernetes/NamespaceResourcePage.tsx
index 7e108ec..7841e30 100644
--- a/frontend/src/components/kubernetes/NamespaceResourcePage.tsx
+++ b/frontend/src/components/kubernetes/NamespaceResourcePage.tsx
@@ -3,23 +3,24 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
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,
- Download, FileCode, Pencil, Terminal, HardDrive, Plus, Upload, Trash2, Eye,
-} from 'lucide-react';
+import { RefreshCw, Search, Loader2, Download, Upload, Plus } from 'lucide-react';
import { YamlViewer } from './YamlViewer';
import { ResourceVisualEditor } from './ResourceVisualEditor';
import { YamlImportDialog } from './YamlImportDialog';
import { PodExecTerminal } from './PodExecTerminal';
import { PodFileBrowser } from './PodFileBrowser';
+import { PodDetailDialog } from './PodDetailDialog';
+import {
+ RESOURCE_CONFIG, VISUAL_RESOURCE_TYPES, DEPLOYMENT_LIST_TYPES,
+ EXPORT_YAML_TYPES, BULK_EXPORT_TYPES,
+ type ResourceType,
+} from './namespace-resource-config';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog';
@@ -27,304 +28,6 @@ import { PageShell } from '@/components/layout/PageShell';
import { ContentPanel } from '@/components/layout/ContentPanel';
import i18n from '@/i18n';
-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; onDeletePod?: ( item: any) => void; onPodDetail?: ( item: any) => void }) => React.ReactNode;
-}> = {
- deployments: {
- title: 'Deployments', subtitle: i18n.t('kubernetes.namespaceresour.s35c744fb'), icon: Boxes,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.container.image')],
- 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 (
-
- );
- })()}
-
- {d.image || '-'}
-
- ),
- },
- pods: {
- title: 'Pods', subtitle: i18n.t('kubernetes.namespaceresour.s780b7dd3'), icon: Container,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.node.title'), 'CPU', i18n.t('kubernetes.pod.memory'), i18n.t('agent.agentServiceRestart'), i18n.t('kubernetes.container.image'), i18n.t('system.auditAction')],
- renderRow: (p, opts) => {
- const statusMap: Record = { Running: 'bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30', Pending: 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)] border-[var(--dg-orange)]/30', Failed: 'bg-[var(--dg-red)]/20 text-[var(--dg-red)] border-[var(--dg-red)]/30' };
- return (
-
- opts.onPodDetail?.(p)}>
-
-
- {p.name}
-
-
- {p.namespace}
- {p.status || 'Unknown'}
- {p.node_name || '-'}
- {p.cpu_usage_m || '-'}
- {p.mem_usage_mi || '-'}
- 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]')}>{p.restart_count || 0}
- {p.image || '-'}
-
-
-
-
-
-
-
-
-
- );
- },
- },
- services: {
- title: 'Services', subtitle: i18n.t('kubernetes.namespaceresour.s5e76b605'), icon: Network,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), 'Cluster IP', i18n.t('system.ldapPort'), i18n.t('system.auditAction')],
- renderRow: (s, opts) => (
-
- {s.name}
- {s.namespace}
- {s.type || '-'}
- {s.cluster_ip || '-'}
- {s.ports || '-'}
- e.stopPropagation()}>
-
- ),
- },
- ingresses: {
- title: 'Ingresses', subtitle: i18n.t('kubernetes.namespaceresour.s8d501473'), icon: Network,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), 'Hosts', 'TLS', i18n.t('system.auditAction')],
- 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: i18n.t('kubernetes.namespaceovervi.s0986a6e7'), icon: FileText,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbd1fa02a'), i18n.t('system.auditAction')],
- renderRow: (c, opts) => (
-
- {c.name}
- {c.namespace}
- {c.key_count || '-'}
- e.stopPropagation()}>
-
- ),
- },
- secrets: {
- title: 'Secrets', subtitle: i18n.t('kubernetes.namespaceresour.sd1424af7'), icon: Shield,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), i18n.t('system.auditAction')],
- renderRow: (s, opts) => (
-
- {s.name}
- {s.namespace}
- {s.type || '-'}
- e.stopPropagation()}>
-
- ),
- },
- hpas: {
- title: 'HPAs', subtitle: i18n.t('kubernetes.namespaceresour.s9abc8d19'), icon: Activity,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.alertEvents.col.target'), i18n.t('k8s.hpaCronHpa.col.minMax'), '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: i18n.t('automation.cronJobs'), icon: Clock,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.workload.scheduling'), i18n.t('system.terminalStatus'), i18n.t('automation.scriptLastExec')],
- renderRow: (cj) => (
-
- {cj.name}
- {cj.namespace}
- {cj.schedule || '-'}
- {cj.suspend ? i18n.t('kubernetes.deployment.paused') : i18n.t('agent.agentServiceRunning')}
- {cj.last_schedule || '-'}
-
- ),
- },
- pvcs: {
- title: i18n.t('kubernetes.namespaceresour.s18c90062'), subtitle: 'PersistentVolumeClaim', icon: Boxes,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.storage.pvcSize'), 'StorageClass', i18n.t('system.auditAction')],
- renderRow: (p, opts) => (
-
- {p.name}
- {p.namespace}
- {p.phase || '-'}
- {p.size || '-'}
- {p.storage_class || '-'}
- e.stopPropagation()}>
-
- ),
- },
- statefulsets: {
- title: 'StatefulSets', subtitle: i18n.t('kubernetes.namespaceresour.sbfeb44d8'), icon: Boxes,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
- renderRow: (s, opts) => (
- opts.onDetail?.(s)}>
- {s.name}
- {s.namespace}
- {s.replicas || 0}
- {s.ready || 0}
- {s.image || '-'}
-
- ),
- },
- daemonsets: {
- title: 'DaemonSets', subtitle: i18n.t('kubernetes.namespaceresour.se1c074e2'), icon: Boxes,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.siteProtocol.http.expectedPrefix'), i18n.t('cloud.current'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
- 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: i18n.t('kubernetes.namespaceresour.s645c9a3d'), icon: Shield,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbaf7ed70'), i18n.t('kubernetes.namespaceresour.s260c11ec'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
- 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: i18n.t('kubernetes.namespaceresour.s8905a680'), icon: Shield,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.secde032d'), i18n.t('system.apiKeyScope'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
- 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: i18n.t('kubernetes.namespaceresour.sca10fe5c'), icon: Shield,
- columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.s227e5e28'), i18n.t('kubernetes.namespaceresour.s2667c7d8'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
- 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();
@@ -357,7 +60,7 @@ export default function NamespaceResourcePage() {
const [podDetail, setPodDetail] = useState(null);
useEffect(() => {
- kubernetesApi.getClusters().then(( res: any) => {
+ kubernetesApi.getClusters().then((res: any) => {
const list = res?.list || [];
setClusters(list);
if (!clusterCode && list.length > 0) {
@@ -370,7 +73,7 @@ export default function NamespaceResourcePage() {
useEffect(() => {
if (!clusterCode) return;
- kubernetesApi.getNamespaces(clusterCode).then(( res: any) => {
+ kubernetesApi.getNamespaces(clusterCode).then((res: any) => {
const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []);
setNamespaces(nsList);
if (!namespace && nsList.length > 0) {
@@ -407,11 +110,10 @@ export default function NamespaceResourcePage() {
useEffect(() => { loadData(); }, [loadData]);
- const filtered = data.filter(( d: any) => (d.name || '').toLowerCase().includes(search.toLowerCase()));
+ const filtered = data.filter((d: any) => (d.name || '').toLowerCase().includes(search.toLowerCase()));
- const handleDetail = ( item: any) => {
+ 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}`);
@@ -567,7 +269,7 @@ export default function NamespaceResourcePage() {
) : 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), onDeletePod: handleDeletePod, onPodDetail: ( p: any) => setPodDetail(p) }))
+ 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), onDeletePod: handleDeletePod, onPodDetail: (p: any) => setPodDetail(p) }))
)}
@@ -580,7 +282,7 @@ export default function NamespaceResourcePage() {
clusterId={clusters[0]?.id || clusters[0]?.code || clusterCode || ''}
namespace={namespace || ''}
podName={terminalPod?.name || ''}
- containers={(terminalPod?.containers || []).map(( c: any) => c.name).filter(Boolean)}
+ containers={(terminalPod?.containers || []).map((c: any) => c.name).filter(Boolean)}
/>
{/* ============ Pod 文件浏览器 ============ */}
c.name).filter(Boolean)}
+ containers={(fileBrowserPod?.containers || []).map((c: any) => c.name).filter(Boolean)}
/>
{/* ============ Pod 详情弹窗 ============ */}
- {podDetail && (
- setPodDetail(null)}>
-
e.stopPropagation()}>
-
-
-
- Pod 详情
-
-
-
-
- {/* 基本信息 */}
-
-
{i18n.t('k8s.editMode.basic')}
- {(() => {
- const p = podDetail;
- const labels = p.labels || p.metadata?.labels || {};
- const annotations = p.annotations || p.metadata?.annotations || {};
- const created = p.creationTimestamp || p.created_at || p.create_time;
- const statusMap: Record
= { Running: 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]', Pending: 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)]', Failed: 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]' };
- const st = p.status || 'Running';
- return (
-
-
-
{i18n.t('kubernetes.secret.secretName')}
-
{p.name || '-'}
-
-
-
{i18n.t('kubernetes.namespace.title')}
-
{p.namespace || '-'}
-
-
-
{i18n.t('system.terminalStatus')}
-
{st}
-
-
-
{i18n.t('system.userCreatedAt')}
-
{created ? new Date(created).toLocaleString('zh-CN') : '-'}
-
-
-
{i18n.t('kubernetes.node.title')}
-
{p.node_name || p.node || '-'}
-
-
-
Pod IP
-
{p.ip || p.pod_ip || '-'}
-
-
-
{i18n.t('kubernetes.namespace.labels')}
-
- {Object.keys(labels).length === 0 ? -
- : Object.entries(labels).map(([k, v]) => (
- {k}: {String(v)}
- ))
- }
-
-
-
-
{i18n.t('kubernetes.namespace.annotations')}
-
- {Object.keys(annotations).length === 0 ? -
- : Object.entries(annotations).slice(0, 6).map(([k, v]) => (
- {k}
- ))
- }
-
-
-
- );
- })()}
-
-
- {/* 现状详情(Conditions) */}
-
-
{i18n.t('kubernetes.namespaceresour.s1d8d6e2e')}
- {(() => {
- const cond: any[] = podDetail.conditions || podDetail.pod_conditions || [];
- if (cond.length === 0) return
{i18n.t('aiops.assistantNoData')}
;
- return (
-
-
-
- {i18n.t('system.credentialType')}
- {i18n.t('system.terminalStatus')}
- {i18n.t('automation.scriptUpdatedAt')}
- {i18n.t('kubernetes.event.eventReason')}
- {i18n.t('kubernetes.event.eventMessage')}
-
-
-
- {cond.map((c: any, idx: number) => {
- const type = c.type || c.Type || '-';
- const st = c.status || c.Status || '-';
- const ts = c.lastTransitionTime || c.LastTransitionTime || '';
- const reason = c.reason || c.Reason || '-';
- const msg = c.message || c.Message || '-';
- const isTrue = st === 'True';
- const isFalse = st === 'False';
- return (
-
- {type}
-
- {st}
-
- {ts ? new Date(ts).toLocaleString('zh-CN') : '-'}
- {reason}
- {msg}
-
- );
- })}
-
-
- );
- })()}
-
-
-
-
-
-
-
- )}
+ setPodDetail(null)} />
);
}
diff --git a/frontend/src/components/kubernetes/PodDetailDialog.tsx b/frontend/src/components/kubernetes/PodDetailDialog.tsx
new file mode 100644
index 0000000..c59ae77
--- /dev/null
+++ b/frontend/src/components/kubernetes/PodDetailDialog.tsx
@@ -0,0 +1,144 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
+} from '@/components/ui/table';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Container } from 'lucide-react';
+import i18n from '@/i18n';
+
+interface PodDetailDialogProps {
+ pod: any;
+ onClose: () => void;
+}
+
+/**
+ * Pod 详情弹窗 — 基本信息网格 + Conditions 现状详情表格
+ * 从 NamespaceResourcePage 提取的独立组件,零逻辑变更
+ */
+export function PodDetailDialog({ pod, onClose }: PodDetailDialogProps) {
+ if (!pod) return null;
+
+ const labels = pod.labels || pod.metadata?.labels || {};
+ const annotations = pod.annotations || pod.metadata?.annotations || {};
+ const created = pod.creationTimestamp || pod.created_at || pod.create_time;
+ const statusMap: Record = {
+ Running: 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]',
+ Pending: 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)]',
+ Failed: 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]',
+ };
+ const st = pod.status || 'Running';
+
+ const cond: any[] = pod.conditions || pod.pod_conditions || [];
+
+ return (
+
+
e.stopPropagation()}>
+
+
+
+ Pod 详情
+
+
+
+
+ {/* 基本信息 */}
+
+
{i18n.t('k8s.editMode.basic')}
+
+
+
{i18n.t('kubernetes.secret.secretName')}
+
{pod.name || '-'}
+
+
+
{i18n.t('kubernetes.namespace.title')}
+
{pod.namespace || '-'}
+
+
+
{i18n.t('system.terminalStatus')}
+
{st}
+
+
+
{i18n.t('system.userCreatedAt')}
+
{created ? new Date(created).toLocaleString('zh-CN') : '-'}
+
+
+
{i18n.t('kubernetes.node.title')}
+
{pod.node_name || pod.node || '-'}
+
+
+
Pod IP
+
{pod.ip || pod.pod_ip || '-'}
+
+
+
{i18n.t('kubernetes.namespace.labels')}
+
+ {Object.keys(labels).length === 0 ? -
+ : Object.entries(labels).map(([k, v]) => (
+ {k}: {String(v)}
+ ))
+ }
+
+
+
+
{i18n.t('kubernetes.namespace.annotations')}
+
+ {Object.keys(annotations).length === 0 ? -
+ : Object.entries(annotations).slice(0, 6).map(([k, v]) => (
+ {k}
+ ))
+ }
+
+
+
+
+
+ {/* 现状详情(Conditions) */}
+
+
{i18n.t('kubernetes.namespaceresour.s1d8d6e2e')}
+ {cond.length === 0 ? (
+
{i18n.t('aiops.assistantNoData')}
+ ) : (
+
+
+
+ {i18n.t('system.credentialType')}
+ {i18n.t('system.terminalStatus')}
+ {i18n.t('automation.scriptUpdatedAt')}
+ {i18n.t('kubernetes.event.eventReason')}
+ {i18n.t('kubernetes.event.eventMessage')}
+
+
+
+ {cond.map((c: any, idx: number) => {
+ const ctype = c.type || c.Type || '-';
+ const cst = c.status || c.Status || '-';
+ const ts = c.lastTransitionTime || c.LastTransitionTime || '';
+ const reason = c.reason || c.Reason || '-';
+ const msg = c.message || c.Message || '-';
+ const isTrue = cst === 'True';
+ const isFalse = cst === 'False';
+ return (
+
+ {ctype}
+
+ {cst}
+
+ {ts ? new Date(ts).toLocaleString('zh-CN') : '-'}
+ {reason}
+ {msg}
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/kubernetes/namespace-resource-config.tsx b/frontend/src/components/kubernetes/namespace-resource-config.tsx
new file mode 100644
index 0000000..1f70d73
--- /dev/null
+++ b/frontend/src/components/kubernetes/namespace-resource-config.tsx
@@ -0,0 +1,319 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { TableCell, TableRow } from '@/components/ui/table';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import {
+ Boxes, Container, Network, FileText, Shield, Activity, Clock,
+ FileCode, Pencil, Terminal, HardDrive, Trash2, Eye,
+} from 'lucide-react';
+import i18n from '@/i18n';
+
+export type ResourceType =
+ | 'deployments' | 'pods' | 'services' | 'ingresses'
+ | 'configmaps' | 'secrets' | 'hpas' | 'cronjobs'
+ | 'pvcs' | 'statefulsets' | 'daemonsets'
+ | 'serviceaccounts' | 'roles' | 'rolebindings';
+
+// 支持可视化编辑/新建的资源类型 // Logan.Li
+export const VISUAL_RESOURCE_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs'];
+/** Deployment 列表:新建 + YAML 导入,不做批量导出 // Logan.Li */
+export const DEPLOYMENT_LIST_TYPES: ResourceType[] = ['deployments'];
+/** 支持命名空间 YAML 批量导出(对齐集群导出)// Logan.Li */
+export const EXPORT_YAML_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets'];
+/** 列表页保留 CSV/JSON 导出的配置类资源 // Logan.Li */
+export const BULK_EXPORT_TYPES: ResourceType[] = ['services', 'ingresses', 'configmaps', 'secrets', 'pvcs'];
+
+export interface ResourceRowOpts {
+ clusterCode: string;
+ namespace: string;
+ onDetail?: (item: any) => void;
+ onYaml?: (name: string) => void;
+ onEdit?: (name: string) => void;
+ onTerminal?: (item: any) => void;
+ onFileBrowser?: (item: any) => void;
+ onDeletePod?: (item: any) => void;
+ onPodDetail?: (item: any) => void;
+}
+
+export const VisualActions = ({ name, opts }: { name: string; opts: { onYaml?: (n: string) => void; onEdit?: (n: string) => void } }) => (
+
+
+
+
+);
+
+export const RESOURCE_CONFIG: Record;
+ columns: string[];
+ renderRow: (item: any, opts: ResourceRowOpts) => React.ReactNode;
+}> = {
+ deployments: {
+ title: 'Deployments', subtitle: i18n.t('kubernetes.namespaceresour.s35c744fb'), icon: Boxes,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.container.image')],
+ 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 (
+
+ );
+ })()}
+
+ {d.image || '-'}
+
+ ),
+ },
+ pods: {
+ title: 'Pods', subtitle: i18n.t('kubernetes.namespaceresour.s780b7dd3'), icon: Container,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.node.title'), 'CPU', i18n.t('kubernetes.pod.memory'), i18n.t('agent.agentServiceRestart'), i18n.t('kubernetes.container.image'), i18n.t('system.auditAction')],
+ renderRow: (p, opts) => {
+ const statusMap: Record = { Running: 'bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30', Pending: 'bg-[var(--dg-orange)]/20 text-[var(--dg-orange)] border-[var(--dg-orange)]/30', Failed: 'bg-[var(--dg-red)]/20 text-[var(--dg-red)] border-[var(--dg-red)]/30' };
+ return (
+
+ opts.onPodDetail?.(p)}>
+
+
+ {p.name}
+
+
+ {p.namespace}
+ {p.status || 'Unknown'}
+ {p.node_name || '-'}
+ {p.cpu_usage_m || '-'}
+ {p.mem_usage_mi || '-'}
+ 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]')}>{p.restart_count || 0}
+ {p.image || '-'}
+
+
+
+
+
+
+
+
+
+ );
+ },
+ },
+ services: {
+ title: 'Services', subtitle: i18n.t('kubernetes.namespaceresour.s5e76b605'), icon: Network,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), 'Cluster IP', i18n.t('system.ldapPort'), i18n.t('system.auditAction')],
+ renderRow: (s, opts) => (
+
+ {s.name}
+ {s.namespace}
+ {s.type || '-'}
+ {s.cluster_ip || '-'}
+ {s.ports || '-'}
+ e.stopPropagation()}>
+
+ ),
+ },
+ ingresses: {
+ title: 'Ingresses', subtitle: i18n.t('kubernetes.namespaceresour.s8d501473'), icon: Network,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), 'Hosts', 'TLS', i18n.t('system.auditAction')],
+ 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: i18n.t('kubernetes.namespaceovervi.s0986a6e7'), icon: FileText,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbd1fa02a'), i18n.t('system.auditAction')],
+ renderRow: (c, opts) => (
+
+ {c.name}
+ {c.namespace}
+ {c.key_count || '-'}
+ e.stopPropagation()}>
+
+ ),
+ },
+ secrets: {
+ title: 'Secrets', subtitle: i18n.t('kubernetes.namespaceresour.sd1424af7'), icon: Shield,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.credentialType'), i18n.t('system.auditAction')],
+ renderRow: (s, opts) => (
+
+ {s.name}
+ {s.namespace}
+ {s.type || '-'}
+ e.stopPropagation()}>
+
+ ),
+ },
+ hpas: {
+ title: 'HPAs', subtitle: i18n.t('kubernetes.namespaceresour.s9abc8d19'), icon: Activity,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.alertEvents.col.target'), i18n.t('k8s.hpaCronHpa.col.minMax'), '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: i18n.t('automation.cronJobs'), icon: Clock,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.workload.scheduling'), i18n.t('system.terminalStatus'), i18n.t('automation.scriptLastExec')],
+ renderRow: (cj) => (
+
+ {cj.name}
+ {cj.namespace}
+ {cj.schedule || '-'}
+ {cj.suspend ? i18n.t('kubernetes.deployment.paused') : i18n.t('agent.agentServiceRunning')}
+ {cj.last_schedule || '-'}
+
+ ),
+ },
+ pvcs: {
+ title: i18n.t('kubernetes.namespaceresour.s18c90062'), subtitle: 'PersistentVolumeClaim', icon: Boxes,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('system.terminalStatus'), i18n.t('kubernetes.storage.pvcSize'), 'StorageClass', i18n.t('system.auditAction')],
+ renderRow: (p, opts) => (
+
+ {p.name}
+ {p.namespace}
+ {p.phase || '-'}
+ {p.size || '-'}
+ {p.storage_class || '-'}
+ e.stopPropagation()}>
+
+ ),
+ },
+ statefulsets: {
+ title: 'StatefulSets', subtitle: i18n.t('kubernetes.namespaceresour.sbfeb44d8'), icon: Boxes,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.capacitypagetsx.s0428b36a'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
+ renderRow: (s, opts) => (
+ opts.onDetail?.(s)}>
+ {s.name}
+ {s.namespace}
+ {s.replicas || 0}
+ {s.ready || 0}
+ {s.image || '-'}
+
+ ),
+ },
+ daemonsets: {
+ title: 'DaemonSets', subtitle: i18n.t('kubernetes.namespaceresour.se1c074e2'), icon: Boxes,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('monitor.siteProtocol.http.expectedPrefix'), i18n.t('cloud.current'), i18n.t('kubernetes.pod.ready'), i18n.t('kubernetes.container.image')],
+ 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: i18n.t('kubernetes.namespaceresour.s645c9a3d'), icon: Shield,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.sbaf7ed70'), i18n.t('kubernetes.namespaceresour.s260c11ec'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
+ 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: i18n.t('kubernetes.namespaceresour.s8905a680'), icon: Shield,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.secde032d'), i18n.t('system.apiKeyScope'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
+ 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: i18n.t('kubernetes.namespaceresour.sca10fe5c'), icon: Shield,
+ columns: [i18n.t('kubernetes.secret.secretName'), i18n.t('kubernetes.namespace.title'), i18n.t('kubernetes.namespaceresour.s227e5e28'), i18n.t('kubernetes.namespaceresour.s2667c7d8'), i18n.t('system.userCreatedAt'), i18n.t('system.auditAction')],
+ 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()}>
+
+
+
+
+
+
+ );
+ },
+ },
+};