使用 i18n.t() 全局方式(避免 hook 定位问题): - 288 目标文件中 135 个成功转换 - 2228 处中文替换为 i18n.t() 调用 - 1006 个新翻译 key 添加到 zh-CN.json + en-US.json - 词典总量: 3554 keys (22 namespaces) - tsc 零错误 + Vite 构建成功 - 6 个含代码示例/数据字符串的文件安全回退
301 lines
14 KiB
TypeScript
301 lines
14 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
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, Plus, Trash2, Boxes,
|
|
Shield, AlertTriangle, CheckCircle2,
|
|
} from 'lucide-react';
|
|
import { kubernetesApi } from '@/api';
|
|
import { toast } from 'sonner';
|
|
import { PageShell } from '@/components/layout/PageShell';
|
|
import { ContentPanel } from '@/components/layout/ContentPanel';
|
|
import { StatStrip } from '@/components/ui/stat-strip';
|
|
import i18n from '@/i18n';
|
|
|
|
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) {
|
|
toast.error(err?.message || i18n.t('monitor.dashboard.custom.toastCreateFailed'));
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!clusterCode || !deleteTarget) return;
|
|
setDeleting(true);
|
|
try {
|
|
await kubernetesApi.deleteNamespace(clusterCode, deleteTarget);
|
|
setDeleteTarget(null);
|
|
await loadData();
|
|
} catch (err) {
|
|
toast.error(err?.message || i18n.t('kubernetes.msg.deleteFailed2'));
|
|
} 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;
|
|
|
|
const searchToolbar = (
|
|
<Input
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder={i18n.t('kubernetes.namespacemanage.se91247a0')}
|
|
className="max-w-xs dg-input"
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<PageShell
|
|
variant="list"
|
|
title={i18n.t('kubernetes.namespacemanage.se935a33d')}
|
|
subtitle={`${clusterCode} · 资源配额与生命周期管理`}
|
|
icon={<Boxes className="h-5 w-5" />}
|
|
metrics={!loading ? (
|
|
<StatStrip
|
|
items={[
|
|
{ label: i18n.t('kubernetes.namespace.title'), value: namespaces.length, color: 'green' },
|
|
{ label: 'Deployments', value: totalDeploys, color: 'blue' },
|
|
{ label: 'Pods', value: totalPods, color: 'purple' },
|
|
{ label: 'Services', value: totalSvcs, color: 'cyan' },
|
|
{ label: i18n.t('kubernetes.namespacemanage.s47c3aee9'), value: quotaCount, color: 'amber' },
|
|
]}
|
|
/>
|
|
) : undefined}
|
|
actions={(
|
|
<>
|
|
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={loadData} disabled={loading}>
|
|
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
|
刷新
|
|
</Button>
|
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm" className="dg-btn-primary">
|
|
<Plus className="h-4 w-4 mr-1" />创建命名空间
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle> {i18n.t('kubernetes.namespacemanage.s737f41e9')} </DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label> {i18n.t('kubernetes.namespace.namespaceName')} </Label>
|
|
<Input
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
placeholder={i18n.t('kubernetes.namespacemanage.scba00862')}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
|
/>
|
|
<p className="text-xs text-[var(--dg-text-muted)]">
|
|
名称需符合 DNS-1123 标准(小写字母/数字/连字符)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setCreateOpen(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
|
<Button onClick={handleCreate} disabled={creating || !newName.trim()}>
|
|
{creating ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
|
创建
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
)}
|
|
>
|
|
<ContentPanel toolbar={searchToolbar}>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
|
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('kubernetes.secret.secretName')} </TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('system.terminalStatus')} </TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('kubernetes.namespacemanage.sa01a9963')} </TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)] text-center">Deploy</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)] text-center">Pods</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)] text-center">Svc</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)] text-center"> {i18n.t('common.quota')} </TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]"> {i18n.t('kubernetes.namespace.labels')} </TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)] text-center"> {i18n.t('system.auditAction')} </TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow><TableCell colSpan={9} className="text-center text-[var(--dg-text-muted)] 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-[var(--dg-text-muted)] 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-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
|
|
<TableCell className="font-medium">
|
|
<button
|
|
className="text-[var(--dg-blue)] hover:text-[var(--dg-blue)] hover:underline text-left"
|
|
onClick={() => navigate(`/kubernetes/${clusterCode}/namespace/${ns.name}`)}
|
|
>
|
|
{ns.name}
|
|
</button>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge className={ns.status === 'Active' ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]'}>
|
|
{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-[var(--dg-text-secondary)]">{ns.age}</TableCell>
|
|
<TableCell className="text-center text-sm text-[var(--dg-text-secondary)]">{ns.resource_counts?.deployments ?? 0}</TableCell>
|
|
<TableCell className="text-center text-sm text-[var(--dg-text-secondary)]">{ns.resource_counts?.pods ?? 0}</TableCell>
|
|
<TableCell className="text-center text-sm text-[var(--dg-text-secondary)]">{ns.resource_counts?.services ?? 0}</TableCell>
|
|
<TableCell className="text-center">
|
|
{ns.has_resource_quota ? (
|
|
<Badge className="bg-[var(--dg-orange)]/20 text-[var(--dg-orange)]" title={JSON.stringify(ns.resource_quotas?.[0]?.status)}>
|
|
<Shield className="h-3 w-3 mr-1" />有
|
|
</Badge>
|
|
) : (
|
|
<span className="text-xs text-[var(--dg-text-muted)]">-</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-[var(--dg-bg-tertiary)] rounded text-[var(--dg-text-secondary)]">{k}={v}</span>
|
|
))}
|
|
{Object.keys(ns.labels).length > 3 && (
|
|
<span className="text-[var(--dg-text-muted)]">+{Object.keys(ns.labels).length - 3}</span>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<span className="text-[var(--dg-text-muted)]">-</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
{isSystem ? (
|
|
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('common.system3')} </span>
|
|
) : (
|
|
<Button
|
|
variant="ghost" size="icon"
|
|
className="h-7 w-7 text-[var(--dg-red)] hover:text-[var(--dg-red)] hover:bg-[var(--dg-red)]/10"
|
|
onClick={() => setDeleteTarget(ns.name)}
|
|
title={i18n.t('kubernetes.namespace.deleteNamespace')}
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</ContentPanel>
|
|
|
|
{/* Delete confirmation dialog */}
|
|
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle className="text-[var(--dg-red)] flex items-center gap-2">
|
|
<AlertTriangle className="h-5 w-5" />删除命名空间
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-3 py-4">
|
|
<div className="bg-[var(--dg-red)]/10 border border-[var(--dg-red)]/30 rounded-lg p-3">
|
|
<p className="text-sm text-[var(--dg-red)]">
|
|
⚠️ 即将删除命名空间 <span className="font-bold">{deleteTarget}</span> 及其中所有资源。
|
|
</p>
|
|
<p className="text-xs text-[var(--dg-red)]/70 mt-1">
|
|
此操作不可逆,所有 Deployment/Pod/Service/ConfigMap 等资源将被永久删除。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setDeleteTarget(null)}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
|
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
|
|
{deleting ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
|
确认删除
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</PageShell>
|
|
);
|
|
}
|