feat(k8s): M14 Namespace 管理看板(资源配额 + 命名空间 CRUD)
- 后端: ListNamespacesDetailed/CreateNamespace/DeleteNamespace/ListResourceQuotas/ListLimitRanges - 后端: GetNamespacesDetail handler (6 API calls aggregate, not N×3) - 后端: CreateNamespaceHandler + DeleteNamespaceHandler (system NS protection) - 前端: NamespaceManagementPage.tsx (~280行) — 表格+概览卡片+创建/删除弹框 - 路由: GET/POST/DELETE /:clusterCode/namespaces + GET /namespace/:ns/detail - 验证: 9/9 通过 (urllib), TS 零错误, Vite 构建成功
This commit is contained in:
1 parent
c7b61968bc
commit
8240a88a84
33 files changed
+2029
-71
No files matched your search
@@ -24,7 +24,8 @@ import { useToast } from '@/hooks/use-toast';
|
||||
type ResourceType =
|
||||
| 'deployments' | 'pods' | 'services' | 'ingresses'
|
||||
| 'configmaps' | 'secrets' | 'hpas' | 'cronjobs'
|
||||
| 'pvcs' | 'statefulsets' | 'daemonsets';
|
||||
| 'pvcs' | 'statefulsets' | 'daemonsets'
|
||||
| 'serviceaccounts' | 'roles' | 'rolebindings';
|
||||
|
||||
const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
title: string;
|
||||
@@ -228,6 +229,88 @@ const RESOURCE_CONFIG: Record<ResourceType, {
|
||||
</TableRow>
|
||||
),
|
||||
},
|
||||
serviceaccounts: {
|
||||
title: 'ServiceAccounts', subtitle: '服务账号(RBAC 身份标识)', icon: Shield,
|
||||
columns: ['名称', '命名空间', 'Secret数', '镜像拉取凭证', '创建时间', '操作'],
|
||||
renderRow: (sa, opts) => (
|
||||
<TableRow key={sa.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{sa.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{sa.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{sa.secret_count ?? 0}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate">
|
||||
{sa.image_pull_secrets && sa.image_pull_secrets.length > 0 ? sa.image_pull_secrets.join(', ') : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{sa.age || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||
onClick={() => opts.onYaml?.(sa.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
},
|
||||
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 (
|
||||
<TableRow key={r.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{r.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{r.namespace}</Badge></TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{r.rule_count ?? 0}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[280px]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-cyan-400 truncate" title={verbs.join(', ')}>verbs: {verbsStr || '-'}</span>
|
||||
<span className="text-amber-400 truncate" title={resources.join(', ')}>resources: {resStr || '-'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{r.age || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||
onClick={() => opts.onYaml?.(r.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
|
||||
onClick={() => opts.onEdit?.(r.name)}><Pencil className="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
},
|
||||
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 (
|
||||
<TableRow key={rb.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{rb.name}</TableCell>
|
||||
<TableCell><Badge variant="outline" className="border-slate-700 text-slate-400">{rb.namespace}</Badge></TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-purple-500/20 text-purple-400 border-purple-500/30">
|
||||
{rb.role_ref_kind || '-'}/{rb.role_ref_name || '-'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500 max-w-[200px] truncate" title={subjects.join(', ')}>
|
||||
{subjStr || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{rb.age || '-'}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-400" title="查看YAML"
|
||||
onClick={() => opts.onYaml?.(rb.name)}><FileCode className="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-emerald-400 hover:text-emerald-300" title="编辑"
|
||||
onClick={() => opts.onEdit?.(rb.name)}><Pencil className="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function NamespaceResourcePage() {
|
||||
@@ -295,6 +378,9 @@ export default function NamespaceResourcePage() {
|
||||
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); }
|
||||
@@ -332,6 +418,9 @@ export default function NamespaceResourcePage() {
|
||||
{ type: 'secrets' as ResourceType, label: 'Secrets', icon: Shield },
|
||||
{ type: 'hpas' as ResourceType, label: 'HPAs', icon: Activity },
|
||||
{ type: 'pvcs' as ResourceType, label: '存储卷', icon: Boxes },
|
||||
{ type: 'serviceaccounts' as ResourceType, label: 'ServiceAccounts', icon: Shield },
|
||||
{ type: 'roles' as ResourceType, label: 'Roles', icon: Shield },
|
||||
{ type: 'rolebindings' as ResourceType, label: 'RoleBindings', icon: Shield },
|
||||
], []);
|
||||
|
||||
if (!config) return <div className="text-slate-400 p-8 text-center">未知资源类型: {type}</div>;
|
||||
|
||||
Reference in new issue
Block a user