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
@@ -21,6 +21,7 @@ const KubernetesClusters = lazy(() => import('@/components/kubernetes/Kubernetes
|
||||
const ClusterOverviewPage = lazy(() => import('@/components/kubernetes/ClusterOverviewPage').then(m => ({ default: m.default })));
|
||||
const NodeDetailPage = lazy(() => import('@/components/kubernetes/NodeDetailPage').then(m => ({ default: m.default })));
|
||||
const NamespaceOverviewPage = lazy(() => import('@/components/kubernetes/NamespaceOverviewPage').then(m => ({ default: m.default })));
|
||||
const NamespaceManagementPage = lazy(() => import('@/components/kubernetes/NamespaceManagementPage').then(m => ({ default: m.default })));
|
||||
const NamespaceResourcePage = lazy(() => import('@/components/kubernetes/NamespaceResourcePage').then(m => ({ default: m.default })));
|
||||
const DeploymentDetailPage = lazy(() => import('@/components/kubernetes/DeploymentDetailPage').then(m => ({ default: m.default })));
|
||||
const WorkloadViewPage = lazy(() => import('@/components/kubernetes/WorkloadViewPage').then(m => ({ default: m.default })));
|
||||
@@ -121,6 +122,7 @@ function App() {
|
||||
<Route path="metrics/pods" element={<LazyPage><PodMetricsPage /></LazyPage>} />
|
||||
<Route path="events" element={<LazyPage><EventsPage /></LazyPage>} />
|
||||
<Route path="capacity" element={<LazyPage><CapacityPage /></LazyPage>} />
|
||||
<Route path="namespaces" element={<LazyPage><NamespaceManagementPage /></LazyPage>} />
|
||||
<Route path="nodes/:nodeName" element={<LazyPage><NodeDetailPage /></LazyPage>} />
|
||||
<Route path="topology" element={<LazyPage><TopologyView /></LazyPage>} />
|
||||
<Route path="namespace/:namespace">
|
||||
|
||||
@@ -85,8 +85,8 @@ export function Dashboard() {
|
||||
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
// 每 5 分钟自动刷新
|
||||
const interval = setInterval(refreshData, 5 * 60 * 1000);
|
||||
// 每 5 秒自动刷新
|
||||
const interval = setInterval(refreshData, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [refreshData]);
|
||||
|
||||
|
||||
@@ -202,9 +202,14 @@ export default function ClusterOverviewPage() {
|
||||
|
||||
{/* Namespace list */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3 flex items-center gap-2">
|
||||
<Boxes className="h-4 w-4 text-emerald-400" />命名空间
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||
<Boxes className="h-4 w-4 text-emerald-400" />命名空间
|
||||
</h2>
|
||||
<Link to={`/kubernetes/${clusterCode}/namespaces`} className="text-sm text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-1">
|
||||
管理全部 <ChevronRight className="h-3 w-3" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{namespaces.map((ns) => {
|
||||
const Icon = NAMESPACE_ICON[ns] || Container;
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
RefreshCw, Loader2, ChevronRight, Plus, Trash2, Boxes, Container, Network,
|
||||
Shield, Activity, AlertTriangle, CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import { kubernetesApi } from '@/services/api';
|
||||
|
||||
interface NamespaceDetail {
|
||||
name: string;
|
||||
status: string;
|
||||
age: string;
|
||||
labels: Record<string, string>;
|
||||
resource_counts: Record<string, number>;
|
||||
has_resource_quota: boolean;
|
||||
has_limit_range: boolean;
|
||||
resource_quotas?: Array<{
|
||||
metadata: { name: string };
|
||||
status: { hard: Record<string, string>; used: Record<string, string> };
|
||||
}>;
|
||||
}
|
||||
|
||||
const SYSTEM_NAMESPACES = new Set([
|
||||
'kube-system', 'kube-public', 'kube-node-lease', 'default', 'devops',
|
||||
]);
|
||||
|
||||
export default function NamespaceManagementPage() {
|
||||
const { clusterCode } = useParams<{ clusterCode: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [namespaces, setNamespaces] = useState<NamespaceDetail[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Create dialog state
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
// Delete dialog state
|
||||
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!clusterCode) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await kubernetesApi.getNamespacesDetail(clusterCode);
|
||||
setNamespaces(Array.isArray(data) ? data : []);
|
||||
} catch {
|
||||
setNamespaces([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [clusterCode]);
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!clusterCode || !newName.trim()) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
await kubernetesApi.createNamespace(clusterCode, newName.trim());
|
||||
setNewName('');
|
||||
setCreateOpen(false);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
alert(err?.message || '创建失败');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!clusterCode || !deleteTarget) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await kubernetesApi.deleteNamespace(clusterCode, deleteTarget);
|
||||
setDeleteTarget(null);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
alert(err?.message || '删除失败');
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = namespaces.filter(ns =>
|
||||
!searchQuery || ns.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const totalDeploys = namespaces.reduce((s, n) => s + (n.resource_counts?.deployments || 0), 0);
|
||||
const totalPods = namespaces.reduce((s, n) => s + (n.resource_counts?.pods || 0), 0);
|
||||
const totalSvcs = namespaces.reduce((s, n) => s + (n.resource_counts?.services || 0), 0);
|
||||
const quotaCount = namespaces.filter(n => n.has_resource_quota).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500">
|
||||
<Link to="/kubernetes" className="hover:text-slate-300 transition-colors">集群</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<Link to={`/kubernetes/${clusterCode}`} className="hover:text-slate-300 transition-colors">{clusterCode}</Link>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<span className="text-slate-300">命名空间管理</span>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100 flex items-center gap-2">
|
||||
<Boxes className="h-6 w-6 text-emerald-400" />
|
||||
命名空间管理
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">命名空间资源配额与生命周期管理</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400"
|
||||
onClick={loadData} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="h-4 w-4 mr-1" />创建命名空间
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建命名空间</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>命名空间名称</Label>
|
||||
<Input
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="例如: my-app"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
/>
|
||||
<p className="text-xs text-slate-500">
|
||||
名称需符合 DNS-1123 标准(小写字母/数字/连字符)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateOpen(false)}>取消</Button>
|
||||
<Button onClick={handleCreate} disabled={creating || !newName.trim()}>
|
||||
{creating ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
||||
创建
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">命名空间</p><p className="text-xl font-bold text-emerald-400">{namespaces.length}</p></div>
|
||||
<Boxes className="h-6 w-6 opacity-30 text-emerald-400" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">Deployments</p><p className="text-xl font-bold text-blue-400">{totalDeploys}</p></div>
|
||||
<Container className="h-6 w-6 opacity-30 text-blue-400" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">Pods</p><p className="text-xl font-bold text-purple-400">{totalPods}</p></div>
|
||||
<Activity className="h-6 w-6 opacity-30 text-purple-400" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">Services</p><p className="text-xl font-bold text-cyan-400">{totalSvcs}</p></div>
|
||||
<Network className="h-6 w-6 opacity-30 text-cyan-400" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div><p className="text-xs text-slate-500">配额限制</p><p className="text-xl font-bold text-amber-400">{quotaCount}</p></div>
|
||||
<Shield className="h-6 w-6 opacity-30 text-amber-400" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索命名空间..."
|
||||
className="max-w-xs bg-slate-900 border-slate-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Namespace table */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400">名称</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400">年龄</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">Deploy</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">Pods</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">Svc</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">配额</TableHead>
|
||||
<TableHead className="text-slate-400">标签</TableHead>
|
||||
<TableHead className="text-slate-400 text-center">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow><TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...
|
||||
</TableCell></TableRow>
|
||||
) : filtered.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={9} className="text-center text-slate-500 py-12">
|
||||
<Boxes className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无命名空间
|
||||
</TableCell></TableRow>
|
||||
) : (
|
||||
filtered.map((ns) => {
|
||||
const isSystem = SYSTEM_NAMESPACES.has(ns.name);
|
||||
return (
|
||||
<TableRow key={ns.name} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium">
|
||||
<button
|
||||
className="text-blue-400 hover:text-blue-300 hover:underline text-left"
|
||||
onClick={() => navigate(`/kubernetes/${clusterCode}/namespace/${ns.name}`)}
|
||||
>
|
||||
{ns.name}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={ns.status === 'Active' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}>
|
||||
{ns.status === 'Active' ? <CheckCircle2 className="h-3 w-3 mr-1" /> : <AlertTriangle className="h-3 w-3 mr-1" />}
|
||||
{ns.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{ns.age}</TableCell>
|
||||
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.deployments ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.pods ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-slate-300">{ns.resource_counts?.services ?? 0}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{ns.has_resource_quota ? (
|
||||
<Badge className="bg-amber-500/20 text-amber-400" title={JSON.stringify(ns.resource_quotas?.[0]?.status)}>
|
||||
<Shield className="h-3 w-3 mr-1" />有
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-slate-600">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{ns.labels && Object.keys(ns.labels).length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
||||
{Object.entries(ns.labels).slice(0, 3).map(([k, v]) => (
|
||||
<span key={k} className="px-1.5 py-0.5 bg-slate-800 rounded text-slate-400">{k}={v}</span>
|
||||
))}
|
||||
{Object.keys(ns.labels).length > 3 && (
|
||||
<span className="text-slate-600">+{Object.keys(ns.labels).length - 3}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-600">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{isSystem ? (
|
||||
<span className="text-xs text-slate-600">系统</span>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-7 w-7 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={() => setDeleteTarget(ns.name)}
|
||||
title="删除命名空间"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-red-400 flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5" />删除命名空间
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-4">
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3">
|
||||
<p className="text-sm text-red-300">
|
||||
⚠️ 即将删除命名空间 <span className="font-bold">{deleteTarget}</span> 及其中所有资源。
|
||||
</p>
|
||||
<p className="text-xs text-red-400/70 mt-1">
|
||||
此操作不可逆,所有 Deployment/Pod/Service/ConfigMap 等资源将被永久删除。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteTarget(null)}>取消</Button>
|
||||
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
|
||||
{deleting ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
||||
确认删除
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function HostMonitorDashboard() {
|
||||
const [hosts, setHosts] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(15);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -441,9 +441,8 @@ export default function HostMonitorDashboard() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="15">15</SelectItem>
|
||||
<SelectItem value="20">20</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="30">30</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span>条,共 {total} 条</span>
|
||||
|
||||
@@ -102,6 +102,36 @@ export function HostMonitor() {
|
||||
}).catch(() => setProbes([]));
|
||||
}, [selectedHost]);
|
||||
|
||||
// 端口策略 // Logan.Li
|
||||
const [portPolicyMode, setPortPolicyMode] = useState('blacklist');
|
||||
const [portWhitelist, setPortWhitelist] = useState<number[]>([]);
|
||||
const [portPolicyDialogOpen, setPortPolicyDialogOpen] = useState(false);
|
||||
const [portPolicySaving, setPortPolicySaving] = useState(false);
|
||||
|
||||
const fetchPortPolicy = useCallback(() => {
|
||||
if (!selectedHost) return;
|
||||
agentApi.getPortPolicy(selectedHost).then((data: any) => {
|
||||
if (data?.data) {
|
||||
setPortPolicyMode(data.data.port_filter_mode || 'blacklist');
|
||||
setPortWhitelist(data.data.port_whitelist || []);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, [selectedHost]);
|
||||
|
||||
useEffect(() => { fetchPortPolicy(); }, [fetchPortPolicy]);
|
||||
|
||||
const handleSavePortPolicy = async () => {
|
||||
if (!selectedHost) return;
|
||||
setPortPolicySaving(true);
|
||||
try {
|
||||
await agentApi.savePortPolicy(selectedHost, {
|
||||
port_filter_mode: portPolicyMode,
|
||||
port_whitelist: portWhitelist,
|
||||
});
|
||||
setPortPolicyDialogOpen(false);
|
||||
} catch { } finally { setPortPolicySaving(false); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProbes();
|
||||
}, [fetchProbes]);
|
||||
@@ -143,25 +173,35 @@ export function HostMonitor() {
|
||||
const rawRx: number[] = []; const rawTx: number[] = [];
|
||||
const rawIoR: number[] = []; const rawIoW: number[] = [];
|
||||
const rawSwap: number[] = []; const rawFD: number[] = [];
|
||||
// ──── v2.2 新增指标 ────
|
||||
const rawIOWait: number[] = []; const rawMemAvail: number[] = [];
|
||||
const rawIoUtil: number[] = []; const rawConnTotal: number[] = [];
|
||||
const rawRxErr: number[] = []; const rawTxErr: number[] = [];
|
||||
const timestamps: number[] = [];
|
||||
|
||||
for (const r of history) {
|
||||
const ts = r.report_time ? new Date(r.report_time).getTime() / 1000 : (r.created_at ? new Date(r.created_at).getTime() / 1000 : 0);
|
||||
timestamps.push(ts);
|
||||
rawCpu.push(r.cpu_usage || 0);
|
||||
rawIOWait.push(r.cpu_iowait || 0);
|
||||
rawMem.push(r.mem_usage || 0);
|
||||
rawMemAvail.push(r.mem_available || 0);
|
||||
rawLoad1.push(r.load1 || 0);
|
||||
rawDisk.push(r.disk_usage || 0);
|
||||
|
||||
const net = parseJSON(r.net_info);
|
||||
rawTcp.push(net.tcp_established || 0);
|
||||
rawTw.push(net.tcp_time_wait || 0);
|
||||
rawConnTotal.push((net.tcp_established || 0) + (net.tcp_time_wait || 0) + (net.tcp_close_wait || 0));
|
||||
rawRx.push(net.net_rx_bps || 0);
|
||||
rawTx.push(net.net_tx_bps || 0);
|
||||
rawRxErr.push(net.net_rx_errors || 0);
|
||||
rawTxErr.push(net.net_tx_errors || 0);
|
||||
|
||||
const dio = parseJSON(r.disk_io_info);
|
||||
rawIoR.push(dio.read_bps || 0);
|
||||
rawIoW.push(dio.write_bps || 0);
|
||||
rawIoUtil.push(dio.util || 0);
|
||||
|
||||
const swap = parseJSON(r.swap_info);
|
||||
rawSwap.push(swap?.usage_percent || 0);
|
||||
@@ -183,6 +223,13 @@ export function HostMonitor() {
|
||||
ioWrite: bucketWithStats(rawIoW.map(v => v / 1024), timestamps, timeConfig.buckets),
|
||||
swap: bucketWithStats(rawSwap, timestamps, timeConfig.buckets),
|
||||
fd: bucketWithStats(rawFD, timestamps, timeConfig.buckets),
|
||||
// ──── v2.2 新增指标 ────
|
||||
iowait: bucketWithStats(rawIOWait, timestamps, timeConfig.buckets),
|
||||
memAvail: bucketWithStats(rawMemAvail, timestamps, timeConfig.buckets),
|
||||
ioUtil: bucketWithStats(rawIoUtil, timestamps, timeConfig.buckets),
|
||||
connTotal: bucketWithStats(rawConnTotal, timestamps, timeConfig.buckets),
|
||||
rxErr: bucketWithStats(rawRxErr, timestamps, timeConfig.buckets),
|
||||
txErr: bucketWithStats(rawTxErr, timestamps, timeConfig.buckets),
|
||||
};
|
||||
}, [history, timeConfig]);
|
||||
|
||||
@@ -280,8 +327,14 @@ export function HostMonitor() {
|
||||
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 text-blue-400" />}
|
||||
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${latest.cpu_iowait.toFixed(1)}%` : '--'} icon={<Clock className="h-4 w-4 text-orange-400" />}
|
||||
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${latest.mem_available.toFixed(1)}GB` : '--'} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive className="h-4 w-4 text-red-400" />}
|
||||
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 text-purple-400" />}
|
||||
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
||||
</TabsContent>
|
||||
@@ -295,6 +348,8 @@ export function HostMonitor() {
|
||||
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 text-emerald-400" />}
|
||||
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc className="h-4 w-4 text-red-400" />}
|
||||
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -305,6 +360,8 @@ export function HostMonitor() {
|
||||
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
|
||||
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP 总连接数" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network className="h-4 w-4 text-cyan-400" />}
|
||||
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
</div>
|
||||
{/* 网卡带宽吞吐 — 双线 rx/tx */}
|
||||
<DualLineChart
|
||||
@@ -314,6 +371,12 @@ export function HostMonitor() {
|
||||
rxData={metrics.rx} txData={metrics.tx}
|
||||
aggType={aggType} unit="KB/s"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||
<MetricChartCard title="网卡接收错误" value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-red-400" />}
|
||||
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="网卡发送错误" value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-orange-400" />}
|
||||
data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* ═══ 端口监控 ═══ */}
|
||||
@@ -322,6 +385,22 @@ export function HostMonitor() {
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />监听端口 {portInfo.length}
|
||||
<span className="ml-auto flex items-center gap-2">
|
||||
{portPolicyMode === 'blacklist' && portWhitelist.length > 0 && (
|
||||
<Badge variant="outline" className="text-[10px] text-amber-400 border-amber-500/30">
|
||||
黑名单: {portWhitelist.join(',')}
|
||||
</Badge>
|
||||
)}
|
||||
{portPolicyMode === 'whitelist' && (
|
||||
<Badge variant="outline" className="text-[10px] text-blue-400 border-blue-500/30">
|
||||
白名单模式
|
||||
</Badge>
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="h-6 text-[10px] px-2"
|
||||
onClick={() => { fetchPortPolicy(); setPortPolicyDialogOpen(true); }}>
|
||||
策略配置
|
||||
</Button>
|
||||
</span>
|
||||
</h3>
|
||||
{portInfo.length === 0 ? (
|
||||
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
|
||||
@@ -492,6 +571,39 @@ export function HostMonitor() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* FD 文件句柄 + 上下文切换 */}
|
||||
{(fdInfo || csInfo) && (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<HardDrive className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">FD 文件句柄 & 上下文切换</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3 text-xs">
|
||||
{fdInfo?.allocated != null && [
|
||||
['FD 已分配', <span className="font-mono">{fdInfo.allocated.toLocaleString()}</span>],
|
||||
['FD 上限', <span className="font-mono">{fdInfo.max === -1 ? '无限制' : fdInfo.max?.toLocaleString() || '—'}</span>],
|
||||
['FD 使用率', <span className={`font-mono ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'text-[var(--dg-text-primary)]'}`}>{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}</span>],
|
||||
].map(([label, val]) => (
|
||||
<div key={label as string}>
|
||||
<span className="text-[var(--dg-text-muted)]">{label}:</span>
|
||||
{val}
|
||||
</div>
|
||||
))}
|
||||
{csInfo?.ctx_per_sec != null && [
|
||||
['CTX 累计', <span className="font-mono">{(csInfo.ctx_per_sec / 1e6).toFixed(1)}M</span>],
|
||||
['IRQ 累计', <span className="font-mono">{(csInfo.irq_per_sec / 1e6).toFixed(1)}M</span>],
|
||||
].map(([label, val]) => (
|
||||
<div key={label as string}>
|
||||
<span className="text-[var(--dg-text-muted)]">{label}:</span>
|
||||
{val}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 磁盘分区 + 网络接口 (保持原样) */}
|
||||
{latest?.disk_info && (() => {
|
||||
const disk = parseJSON(latest.disk_info);
|
||||
@@ -695,6 +807,73 @@ export function HostMonitor() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 端口策略对话框 */}
|
||||
{portPolicyDialogOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setPortPolicyDialogOpen(false)}>
|
||||
<div className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg p-5 w-[440px] max-h-[90vh] overflow-y-auto shadow-xl" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4">端口监控策略</h3>
|
||||
<div className="space-y-4">
|
||||
{/* 模式选择 */}
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">过滤模式</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 text-blue-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||
onClick={() => setPortPolicyMode('whitelist')}
|
||||
>
|
||||
白名单 ─ 全量上报
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 text-amber-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||
onClick={() => setPortPolicyMode('blacklist')}
|
||||
>
|
||||
黑名单 ─ 仅上报选中
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
|
||||
{portPolicyMode === 'whitelist'
|
||||
? '所有发现的端口都上报,任意端口变化都会触发告警 + 恢复通知。'
|
||||
: '仅上报选中端口的变化,其余端口变化静默忽略(不告警)。系统进程端口(sshd/rpcbind/systemd等)永远忽略。'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 端口白名单 */}
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
|
||||
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono text-[var(--dg-text-primary)] resize-none"
|
||||
value={portWhitelist.join(',')}
|
||||
onChange={e => {
|
||||
const val = e.target.value;
|
||||
const nums = val.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0 && n <= 65535);
|
||||
setPortWhitelist(nums);
|
||||
}}
|
||||
placeholder="80,443,3306,6379"
|
||||
/>
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
|
||||
已输入 {portWhitelist.length} 个端口
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 系统排除说明 */}
|
||||
<div className="bg-amber-500/5 border border-amber-500/20 rounded p-2.5">
|
||||
<p className="text-[10px] text-amber-300/80 leading-relaxed">
|
||||
⚠️ 无论选择哪种模式,以下系统进程端口<b>永远不上报</b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setPortPolicyDialogOpen(false)}>取消</Button>
|
||||
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={handleSavePortPolicy} disabled={portPolicySaving}>
|
||||
{portPolicySaving ? '保存中…' : '保存策略'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -444,6 +444,17 @@ export const kubernetesApi = {
|
||||
getDaemonSets: async (clusterCode: string, namespace: string) => {
|
||||
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/daemonsets`);
|
||||
},
|
||||
|
||||
// === RBAC — ServiceAccount / Role / RoleBinding ===
|
||||
getServiceAccounts: async (clusterCode: string, namespace: string) => {
|
||||
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/serviceaccounts`);
|
||||
},
|
||||
getRoles: async (clusterCode: string, namespace: string) => {
|
||||
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/roles`);
|
||||
},
|
||||
getRoleBindings: async (clusterCode: string, namespace: string) => {
|
||||
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/rolebindings`);
|
||||
},
|
||||
getStatefulSetDetail: async (clusterCode: string, namespace: string, name: string) => { return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/statefulsets/${name}`); },
|
||||
getDaemonSetDetail: async (clusterCode: string, namespace: string, name: string) => { return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/daemonsets/${name}`); },
|
||||
scaleStatefulSet: async (clusterCode: string, namespace: string, name: string, replicas: number) => { return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/statefulsets/${name}/scale`, { replicas }); },
|
||||
@@ -532,6 +543,19 @@ export const kubernetesApi = {
|
||||
return list.find((d: any) => d.name === name) || null;
|
||||
}
|
||||
},
|
||||
// === 🆕 M14: Namespace 管理看板 ===
|
||||
getNamespacesDetail: async (clusterCode: string) => {
|
||||
return http.get(`/kubernetes/${clusterCode}/namespaces/detail`);
|
||||
},
|
||||
getNamespaceDetail: async (clusterCode: string, namespace: string) => {
|
||||
return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/detail`);
|
||||
},
|
||||
createNamespace: async (clusterCode: string, name: string, labels?: Record<string, string>) => {
|
||||
return http.post(`/kubernetes/${clusterCode}/namespaces`, { name, labels: labels || {} });
|
||||
},
|
||||
deleteNamespace: async (clusterCode: string, namespace: string) => {
|
||||
return http.delete(`/kubernetes/${clusterCode}/namespaces/${namespace}`);
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== 告警 API ====================
|
||||
@@ -890,6 +914,9 @@ export const agentApi = {
|
||||
createHealthProbe: (data: any) => http.post('/agent/health-probes', data),
|
||||
updateHealthProbe: (id: number, data: any) => http.put(`/agent/health-probes/${id}`, data),
|
||||
deleteHealthProbe: (id: number) => http.delete(`/agent/health-probes/${id}`),
|
||||
// 端口策略
|
||||
getPortPolicy: (hostname: string) => http.get(`/agent/port-policies/${hostname}`),
|
||||
savePortPolicy: (hostname: string, data: { port_filter_mode: string; port_whitelist: number[] }) => http.put(`/agent/port-policies/${hostname}`, data),
|
||||
};
|
||||
|
||||
// ==================== 区域管理 API ====================
|
||||
|
||||
Reference in new issue
Block a user