feat(i18n): 安全批量转换 135 文件 — 覆盖率 34%→62%

使用 i18n.t() 全局方式(避免 hook 定位问题):
- 288 目标文件中 135 个成功转换
- 2228 处中文替换为 i18n.t() 调用
- 1006 个新翻译 key 添加到 zh-CN.json + en-US.json
- 词典总量: 3554 keys (22 namespaces)
- tsc 零错误 + Vite 构建成功
- 6 个含代码示例/数据字符串的文件安全回退
This commit is contained in:
attacker committed 2026-07-03 02:14:20 +08:00
1 parent 72bfb0e12d
commit 55f216cfbb
131 files changed
+4049 -1908

No files matched your search

@@ -14,6 +14,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { buildNodeApi, serverApi, type BuildNode } from '@/api';
import i18n from '@/i18n';
interface ServerItem {
id: number;
@@ -58,7 +59,7 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
const srvList = (srvRes as { list?: ServerItem[] })?.list || [];
setServers(srvList);
} catch {
toast.error('加载构建节点失败');
toast.error(i18n.t('pipeline.buildnodesmanag.sb9faa2a3'));
} finally {
setLoading(false);
}
@@ -90,7 +91,7 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
const handleSubmit = async () => {
if (!name.trim() || !serverId) {
toast.error('请填写名称并选择服务器');
toast.error(i18n.t('pipeline.buildnodesmanag.sda51a736'));
return;
}
setSubmitting(true);
@@ -105,29 +106,29 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
};
if (editNode) {
await buildNodeApi.update(editNode.id, payload);
toast.success('已更新');
toast.success(i18n.t('deploy.toast.updated'));
} else {
await buildNodeApi.create(payload);
toast.success('已创建');
toast.success(i18n.t('kubernetes.event.created'));
}
setDialogOpen(false);
load();
} catch {
toast.error('操作失败');
toast.error(i18n.t('cloud.operationFailed'));
} finally {
setSubmitting(false);
}
};
const handleDelete = async (node: BuildNode) => {
const ok = await confirm({ title: '删除节点', description: `确定删除构建节点「${node.name}」?` });
const ok = await confirm({ title: i18n.t('kubernetes.topology.deleteNode'), description: `确定删除构建节点「${node.name}」?` });
if (!ok) return;
try {
await buildNodeApi.delete(node.id);
toast.success('已删除');
toast.success(i18n.t('monitor.silenceRules.deleted'));
load();
} catch {
toast.error('删除失败,请确认无流水线绑定');
toast.error(i18n.t('pipeline.buildnodesmanag.s577b185d'));
}
};
@@ -138,10 +139,10 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
if (res?.success) {
toast.success(`连通成功: ${res.message || ''}`);
} else {
toast.error(res?.message || '连接失败');
toast.error(res?.message || i18n.t('aiops.llmTestFailed'));
}
} catch {
toast.error('测试失败');
toast.error(i18n.t('monitor.alertChannels.testFailed'));
} finally {
setTestingId(null);
}
@@ -152,14 +153,14 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
await buildNodeApi.update(node.id, { enabled: !node.enabled });
load();
} catch {
toast.error('更新失败');
toast.error(i18n.t('kubernetes.deployment.imageUpdateFailed'));
}
};
const handleSaveMaster = async () => {
const n = Number(masterMax);
if (!n || n < 1) {
toast.error('并发数至少为 1');
toast.error(i18n.t('pipeline.buildnodesmanag.s5f25f49a'));
return;
}
setSavingMaster(true);
@@ -171,9 +172,9 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
max_executors: m?.max_executors ?? n,
running_count: m?.running_count ?? prev.running_count,
}));
toast.success('Master 并发已更新');
toast.success(i18n.t('pipeline.buildnodesmanag.scb1ab6ab'));
} catch {
toast.error('更新失败');
toast.error(i18n.t('kubernetes.deployment.imageUpdateFailed'));
} finally {
setSavingMaster(false);
}
@@ -184,7 +185,7 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
<ContentPanel
toolbar={(
<div className="flex items-center justify-between w-full">
<span className="text-sm font-medium text-[var(--dg-text-primary)]">构建节点</span>
<span className="text-sm font-medium text-[var(--dg-text-primary)]"> {i18n.t('pipeline.buildNodes')} </span>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={load} disabled={loading}>
<RefreshCw className={`h-3.5 w-3.5 mr-1${loading ? ' animate-spin' : ''}`} />刷新
@@ -201,13 +202,13 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-[var(--dg-text-muted)]" />
<span className="text-sm font-medium text-[var(--dg-text-primary)]">Master(内置)</span>
<Badge variant="outline" className="text-[10px]">默认调度</Badge>
<Badge variant="outline" className="text-[10px]"> {i18n.t('pipeline.buildnodesmanag.sa419fe03')} </Badge>
<span className="text-xs text-[var(--dg-text-muted)]">
占用 {master.running_count}/{master.max_executors}
</span>
</div>
<div className="flex items-center gap-2">
<Label className="text-xs text-[var(--dg-text-muted)] shrink-0">最大并发</Label>
<Label className="text-xs text-[var(--dg-text-muted)] shrink-0"> {i18n.t('pipeline.buildnodesmanag.s8e408e28')} </Label>
<Input
type="number"
min={1}
@@ -217,7 +218,7 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
className="h-7 w-16 text-xs dg-input"
/>
<Button size="sm" className="h-7 text-xs" onClick={handleSaveMaster} disabled={savingMaster}>
{savingMaster ? '保存中' : '保存'}
{savingMaster ? i18n.t('pipeline.buildnodesmanag.s7cbf2d09') : i18n.t('task.script.save')}
</Button>
</div>
</div>
@@ -228,9 +229,9 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
{nodes.length === 0 ? (
<PageEmpty
title="暂无远程构建节点"
description="关联服务器资产添加 SSH 节点,将重型构建任务调度到高性能机器"
action={<Button size="sm" onClick={openCreate}><Plus className="h-3.5 w-3.5 mr-1" />添加节点</Button>}
title={i18n.t('pipeline.buildnodesmanag.s2795bd4a')}
description={i18n.t('pipeline.buildnodesmanag.s90fb5e18')}
action={<Button size="sm" onClick={openCreate}><Plus className="h-3.5 w-3.5 mr-1" /> {i18n.t('kubernetes.topology.addNode')} </Button>}
/>
) : (
<div className="space-y-2">
@@ -243,7 +244,7 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-sm text-[var(--dg-text-primary)]">{node.name}</span>
<Badge variant={node.enabled ? 'default' : 'secondary'} className="text-[10px]">
{node.enabled ? '启用' : '禁用'}
{node.enabled ? i18n.t('system.userActive') : i18n.t('system.userInactive')}
</Badge>
<span className="text-[10px] text-[var(--dg-text-muted)] font-mono">
{node.hostname || node.ip || `server#${node.server_id}`}
@@ -257,10 +258,10 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
</div>
<div className="flex items-center gap-1 shrink-0">
<Button variant="ghost" size="sm" className="h-7 text-xs" disabled={testingId === node.id} onClick={() => handleTest(node)}>
{testingId === node.id ? '测试中' : '测试'}
{testingId === node.id ? i18n.t('pipeline.buildnodesmanag.sf85549cd') : i18n.t('pipeline.testTool')}
</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleToggle(node)}>
{node.enabled ? '禁用' : '启用'}
{node.enabled ? i18n.t('system.userInactive') : i18n.t('system.userActive')}
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => openEdit(node)}>
<Pencil className="h-3.5 w-3.5" />
@@ -281,22 +282,22 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-md bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle>{editNode ? '编辑节点' : '添加节点'}</DialogTitle>
<DialogTitle>{editNode ? i18n.t('kubernetes.topology.editNode') : i18n.t('kubernetes.topology.addNode')}</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
基于服务器 SSH 凭据注册构建 Agent,流水线可绑定或自动调度
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs">节点名称</Label>
<Label className="text-xs"> {i18n.t('kubernetes.node.nodeName')} </Label>
<Input value={name} onChange={(e) => setName(e.target.value)} className="h-8 text-xs dg-input" placeholder="build-node-01" />
</div>
<div className="space-y-1">
<Label className="text-xs">关联服务器</Label>
<Label className="text-xs"> {i18n.t('pipeline.buildnodesmanag.sdfdaf1c5')} </Label>
<Select value={serverId || '__none'} onValueChange={(v) => setServerId(v === '__none' ? '' : v)}>
<SelectTrigger className="h-8 text-xs dg-input"><SelectValue placeholder="选择服务器" /></SelectTrigger>
<SelectTrigger className="h-8 text-xs dg-input"><SelectValue placeholder={i18n.t('server.webshelltsx.s030235d1')} /></SelectTrigger>
<SelectContent>
<SelectItem value="__none">请选择</SelectItem>
<SelectItem value="__none"> {i18n.t('k8s.storage.pleaseSelect')} </SelectItem>
{servers.map((s) => (
<SelectItem key={s.id} value={String(s.id)}>
{s.hostname} ({s.ip})
@@ -307,25 +308,25 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">最大并发</Label>
<Label className="text-xs"> {i18n.t('pipeline.buildnodesmanag.s8e408e28')} </Label>
<Input value={maxExecutors} onChange={(e) => setMaxExecutors(e.target.value)} type="number" min={1} className="h-8 text-xs dg-input" />
</div>
<div className="space-y-1">
<Label className="text-xs">工作区路径</Label>
<Label className="text-xs"> {i18n.t('pipeline.buildnodesmanag.sbcfcd088')} </Label>
<Input value={workspacePath} onChange={(e) => setWorkspacePath(e.target.value)} className="h-8 text-xs font-mono dg-input" />
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">标签 <span className="text-[var(--dg-text-muted)]">(可选)</span></Label>
<Label className="text-xs"> {i18n.t('kubernetes.namespace.labels')} <span className="text-[var(--dg-text-muted)]">(可选)</span></Label>
<Input value={labels} onChange={(e) => setLabels(e.target.value)} className="h-8 text-xs dg-input" placeholder="linux,docker,high-mem" />
</div>
<div className="space-y-1">
<Label className="text-xs">描述</Label>
<Label className="text-xs"> {i18n.t('aiops.llmModelDesc')} </Label>
<Input value={description} onChange={(e) => setDescription(e.target.value)} className="h-8 text-xs dg-input" />
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" onClick={() => setDialogOpen(false)}>取消</Button>
<Button onClick={handleSubmit} disabled={submitting}>{submitting ? '保存中' : '保存'}</Button>
<Button variant="outline" onClick={() => setDialogOpen(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
<Button onClick={handleSubmit} disabled={submitting}>{submitting ? i18n.t('pipeline.buildnodesmanag.s7cbf2d09') : i18n.t('task.script.save')}</Button>
</div>
</div>
</DialogContent>
@@ -342,7 +343,7 @@ export default function BuildNodesManager({ embedded = false }: { embedded?: boo
}
return (
<PageShell title="构建节点" subtitle="Master 与远程 SSH 构建节点">
<PageShell title={i18n.t('pipeline.buildNodes')} subtitle={i18n.t('pipeline.buildnodesmanag.sb2b825e4')}>
{panel}
{dialog}
</PageShell>
+42 -41
View File
@@ -17,6 +17,7 @@ import UserGroupsManager from './UserGroupsManager';
import { JobRoleCreateDialog } from './deploy-rbac/JobRoleCreateDialog';
import { matchPipelines, pipelineDisplayLabel, type PipelineMatchItem } from './deploy-rbac/matchPipeline';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface ItemRole {
id: number;
@@ -37,9 +38,9 @@ interface AssignedUser {
}
const ROLE_LABEL: Record<string, string> = {
owner: '拥有者',
editor: '编辑者',
viewer: '查看者',
owner: i18n.t('pipeline.owner'),
editor: i18n.t('pipeline.editor'),
viewer: i18n.t('pipeline.viewer'),
};
const RBAC_TABS = ['roles', 'users', 'preview', 'groups'] as const;
@@ -110,7 +111,7 @@ export default function DeployRBAC() {
const handleCreate = async () => {
if (!newName.trim() || !newCode.trim() || !newPattern.trim()) {
toast.error('请填写名称、标识与正则'); return;
toast.error(i18n.t('monitor.logrbactsx.sf6e80bf0')); return;
}
setSaving(true);
try {
@@ -121,84 +122,84 @@ export default function DeployRBAC() {
role: newRole,
description: newDesc,
});
toast.success('已创建');
toast.success(i18n.t('kubernetes.event.created'));
setCreateOpen(false);
setNewName(''); setNewCode(''); setNewPattern(''); setNewRole('editor'); setNewDesc('');
loadData();
} catch (e: unknown) {
toast.error((e as Error)?.message || '创建失败');
toast.error((e as Error)?.message || i18n.t('monitor.dashboard.custom.toastCreateFailed'));
} finally { setSaving(false); }
};
const handleUpdate = async (id: number) => {
if (!editPattern.trim()) { toast.error('正则不能为空'); return; }
if (!editPattern.trim()) { toast.error(i18n.t('pipeline.regexEmpty')); return; }
setSaving(true);
try {
await deployApi.updateItemRole(id, { pattern: editPattern, role: editRole });
toast.success('已更新');
toast.success(i18n.t('deploy.toast.updated'));
setEditing(null);
loadData();
} catch (e: unknown) {
toast.error((e as Error)?.message || '更新失败');
toast.error((e as Error)?.message || i18n.t('kubernetes.deployment.imageUpdateFailed'));
} finally { setSaving(false); }
};
const handleDelete = async (role: ItemRole) => {
const ok = await _confirm({
title: '删除权限',
title: i18n.t('monitor.logrbactsx.s5d79e436'),
description: `确定删除「${role.name}」?将影响 ${matchCount[role.id]?.length || 0} 个发布任务`,
variant: 'destructive',
});
if (!ok) return;
try { await deployApi.deleteItemRole(role.id); toast.success('已删除'); loadData(); }
catch { toast.error('删除失败'); }
try { await deployApi.deleteItemRole(role.id); toast.success(i18n.t('monitor.silenceRules.deleted')); loadData(); }
catch { toast.error(i18n.t('kubernetes.msg.deleteFailed2')); }
};
const handleToggleStatus = async (role: ItemRole) => {
try {
await deployApi.updateItemRole(role.id, { status: role.status ? 0 : 1 });
toast.success(role.status ? '已禁用' : '已启用');
toast.success(role.status ? i18n.t('aiops.llmDisabled') : i18n.t('aiops.llmEnabled'));
loadData();
} catch { toast.error('操作失败'); }
} catch { toast.error(i18n.t('cloud.operationFailed')); }
};
const handleAssign = async () => {
if (!selectedUserId || !assignRoleId) { toast.error('请选择用户和权限'); return; }
if (!selectedUserId || !assignRoleId) { toast.error(i18n.t('monitor.logrbactsx.s0fc53985')); return; }
setAssignLoading(true);
try {
await deployApi.assignItemRole(Number(selectedUserId), {
user_id: Number(selectedUserId),
item_role_id: Number(assignRoleId),
});
toast.success('已分配');
toast.success(i18n.t('deploy.toast.assigned'));
setAssignRoleId('');
loadAssignments();
} catch (e: unknown) {
toast.error((e as Error)?.message || '分配失败');
toast.error((e as Error)?.message || i18n.t('deploy.toast.assignFailed'));
} finally { setAssignLoading(false); }
};
const handleRevoke = async (id: number) => {
try {
await deployApi.removeItemRole(Number(selectedUserId), id);
toast.success('已撤销');
toast.success(i18n.t('deploy.toast.revoked'));
loadAssignments();
} catch { toast.error('撤销失败'); }
} catch { toast.error(i18n.t('deploy.toast.revokeFailed')); }
};
const selectedUser = users.find((u) => String(u.id) === selectedUserId);
const pageTabs = [
{ id: 'roles', label: '权限策略', badge: roles.length || undefined },
{ id: 'users', label: '用户授权' },
{ id: 'groups', label: '用户组' },
{ id: 'preview', label: '命中预览' },
{ id: 'roles', label: i18n.t('monitor.logrbactsx.s93ef07cd'), badge: roles.length || undefined },
{ id: 'users', label: i18n.t('monitor.logrbactsx.s2de89571') },
{ id: 'groups', label: i18n.t('pipeline.groupLabel') },
{ id: 'preview', label: i18n.t('pipeline.matchPreview') },
];
return (
<PageShell
variant="list"
title="Job 权限"
subtitle="按流水线 code / Job 名称正则分配发布权限"
title={i18n.t('deploy.deployNav.jobPermission')}
subtitle={i18n.t('pipeline.deployrbactsx.sa596d659')}
icon={<Shield className="h-5 w-5" />}
loading={loading}
tabs={pageTabs}
@@ -215,9 +216,9 @@ export default function DeployRBAC() {
roles.length === 0 ? (
<PageEmpty
icon={<Shield className="h-10 w-10" />}
title="暂无 Job 权限策略"
description="创建策略后,用户将按正则自动获得匹配流水线的访问权限"
action={<Button onClick={() => setCreateOpen(true)}><Plus className="h-4 w-4 mr-1" />新建权限</Button>}
title={i18n.t('pipeline.deployrbactsx.sca578c70')}
description={i18n.t('pipeline.deployrbactsx.s5ac38262')}
action={<Button onClick={() => setCreateOpen(true)}><Plus className="h-4 w-4 mr-1" /> {i18n.t('monitor.logrbactsx.s6058845c')} </Button>}
/>
) : (
<div className="space-y-2">
@@ -231,7 +232,7 @@ export default function DeployRBAC() {
<Badge variant="outline" className="text-[10px] border-[var(--dg-border)]">
{ROLE_LABEL[role.role] || role.role}
</Badge>
{role.status === 0 && <Badge variant="outline" className="text-[10px]">已禁用</Badge>}
{role.status === 0 && <Badge variant="outline" className="text-[10px]"> {i18n.t('aiops.llmDisabled')} </Badge>}
<Badge
variant="outline"
className="text-[10px] cursor-pointer border-[var(--dg-border)]"
@@ -252,9 +253,9 @@ export default function DeployRBAC() {
<Select value={editRole} onValueChange={setEditRole}>
<SelectTrigger className="h-7 w-20 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="owner">拥有者</SelectItem>
<SelectItem value="editor">编辑者</SelectItem>
<SelectItem value="viewer">查看者</SelectItem>
<SelectItem value="owner"> {i18n.t('pipeline.owner')} </SelectItem>
<SelectItem value="editor"> {i18n.t('pipeline.editor')} </SelectItem>
<SelectItem value="viewer"> {i18n.t('pipeline.viewer')} </SelectItem>
</SelectContent>
</Select>
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => handleUpdate(role.id)}><Check className="w-3.5 h-3.5" /></Button>
@@ -262,8 +263,8 @@ export default function DeployRBAC() {
</>
) : (
<>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => { setEditing(role.id); setEditPattern(role.pattern); setEditRole(role.role); }}>编辑</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleToggleStatus(role)}>{role.status ? '禁用' : '启用'}</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => { setEditing(role.id); setEditPattern(role.pattern); setEditRole(role.role); }}> {i18n.t('common.edit')} </Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleToggleStatus(role)}>{role.status ? i18n.t('system.userInactive') : i18n.t('system.userActive')}</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-red)]" onClick={() => handleDelete(role)}><Trash2 className="w-3.5 h-3.5" /></Button>
</>
)}
@@ -281,9 +282,9 @@ export default function DeployRBAC() {
<CardContent className="p-4 space-y-4">
<div className="flex flex-wrap gap-3 items-end">
<div className="space-y-1 flex-1 min-w-[200px]">
<Label className="text-xs">用户</Label>
<Label className="text-xs"> {i18n.t('system.terminalUser')} </Label>
<Select value={selectedUserId} onValueChange={setSelectedUserId}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder="选择用户" /></SelectTrigger>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder={i18n.t('deploy.rbac.selectUser')} /></SelectTrigger>
<SelectContent className="max-h-60">
{users.map((u) => (
<SelectItem key={u.id as number} value={String(u.id)}>
@@ -294,9 +295,9 @@ export default function DeployRBAC() {
</Select>
</div>
<div className="space-y-1 w-48">
<Label className="text-xs">权限策略</Label>
<Label className="text-xs"> {i18n.t('monitor.logrbactsx.s93ef07cd')} </Label>
<Select value={assignRoleId} onValueChange={setAssignRoleId}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder="选择策略" /></SelectTrigger>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder={i18n.t('kubernetes.hpa.selectPolicy')} /></SelectTrigger>
<SelectContent>
{roles.filter((r) => r.status === 1).map((r) => (
<SelectItem key={r.id} value={String(r.id)}>{r.name} ({ROLE_LABEL[r.role]})</SelectItem>
@@ -311,7 +312,7 @@ export default function DeployRBAC() {
</div>
{selectedUser ? (
assignments.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)]">该用户尚未分配 Job 权限</p>
<p className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('pipeline.deployrbactsx.sef9548ee')} </p>
) : (
<div className="space-y-1.5">
{assignments.map((a) => {
@@ -332,7 +333,7 @@ export default function DeployRBAC() {
</div>
)
) : (
<p className="text-xs text-[var(--dg-text-muted)] flex items-center gap-1"><Users className="w-3.5 h-3.5" />选择用户后查看已分配权限</p>
<p className="text-xs text-[var(--dg-text-muted)] flex items-center gap-1"><Users className="w-3.5 h-3.5" /> {i18n.t('monitor.logrbactsx.s9be00c95')} </p>
)}
</CardContent>
</Card>
@@ -367,7 +368,7 @@ export default function DeployRBAC() {
</Badge>
))}
{previewRoleId !== null && previewMatches.length === 0 && (
<span className="text-xs text-[var(--dg-text-muted)]">无匹配 — 请调整正则</span>
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.logrbactsx.s64096f4d')} </span>
)}
</div>
</CardContent>
@@ -13,6 +13,7 @@ import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { deployApi } from '@/api';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface Group {
id: number; name: string; description: string;
@@ -41,43 +42,43 @@ export default function UserGroupsManager({ embedded = false }: { embedded?: boo
const loadGroups = async () => {
setLoading(true);
try { const res = await deployApi.getGroups(); setGroups((res as JsonData)?.list || []); }
catch { toast.error('加载失败'); }
catch { toast.error(i18n.t('kubernetes.msg.loadFailed')); }
finally { setLoading(false); }
};
useEffect(() => { loadGroups(); }, []);
const handleSave = async () => {
if (!formName.trim()) { toast.error('请输入组名'); return; }
if (!formName.trim()) { toast.error(i18n.t('deploy.rbac.groupPlaceholder')); return; }
setSaving(true);
try {
if (editId) await deployApi.updateGroup(editId, { name: formName, description: formDesc });
else await deployApi.createGroup({ name: formName, description: formDesc });
toast.success(editId ? '已更新' : '已创建');
toast.success(editId ? i18n.t('deploy.toast.updated') : i18n.t('kubernetes.event.created'));
setFormOpen(false); loadGroups();
} catch { toast.error('操作失败'); }
} catch { toast.error(i18n.t('cloud.operationFailed')); }
finally { setSaving(false); }
};
const handleDelete = async (g: Group) => {
const ok = await _confirm({ title: '删除用户组', description: `确定删除「${g.name}」?所有成员和流水线授权将同时移除。`, variant: 'destructive' });
const ok = await _confirm({ title: i18n.t('deploy.rbac.deleteGroup'), description: `确定删除「${g.name}」?所有成员和流水线授权将同时移除。`, variant: 'destructive' });
if (!ok) return;
try { await deployApi.deleteGroup(g.id); toast.success('已删除'); loadGroups(); }
catch { toast.error('删除失败'); }
try { await deployApi.deleteGroup(g.id); toast.success(i18n.t('monitor.silenceRules.deleted')); loadGroups(); }
catch { toast.error(i18n.t('kubernetes.msg.deleteFailed2')); }
};
const handleAddMember = async (groupId: number) => {
const username = (memberUsers[groupId] || '').trim();
if (!username) { toast.error('输入用户名'); return; }
try { await deployApi.addGroupMember(groupId, { username }); toast.success('已添加'); loadGroups(); setMemberUsers(prev => ({ ...prev, [groupId]: '' })); }
catch { toast.error('添加失败'); }
if (!username) { toast.error(i18n.t('pipeline.usernamePlaceholder')); return; }
try { await deployApi.addGroupMember(groupId, { username }); toast.success(i18n.t('deploy.toast.added')); loadGroups(); setMemberUsers(prev => ({ ...prev, [groupId]: '' })); }
catch { toast.error(i18n.t('monitor.dashboard.custom.toastPanelFailed')); }
};
const handleRemoveMember = async (groupId: number, memberId: number, username: string) => {
const ok = await _confirm({ title: '移除成员', description: `确定从组中移除「${username}」?` });
const ok = await _confirm({ title: i18n.t('pipeline.removeMember'), description: `确定从组中移除「${username}」?` });
if (!ok) return;
try { await deployApi.removeGroupMember(groupId, memberId); toast.success('已移除'); loadGroups(); }
catch { toast.error('移除失败'); }
try { await deployApi.removeGroupMember(groupId, memberId); toast.success(i18n.t('deploy.toast.removed')); loadGroups(); }
catch { toast.error(i18n.t('deploy.toast.removeFailed')); }
};
const toggleExpand = (gid: number) => setExpanded(p => ({ ...p, [gid]: !p[gid] }));
@@ -98,8 +99,8 @@ export default function UserGroupsManager({ embedded = false }: { embedded?: boo
const listBody = !loading && groups.length === 0 ? (
<PageEmpty
icon={<Users className="h-10 w-10" />}
title="暂无用户组"
description="点击「新建用户组」创建第一个用户组"
title={i18n.t('pipeline.usergroupsmanag.sd1edd832')}
description={i18n.t('pipeline.usergroupsmanag.scf98655a')}
action={(
<Button size="sm" onClick={() => { setEditId(null); setFormName(''); setFormDesc(''); setFormOpen(true); }}>
<Plus className="w-4 h-4 mr-1" />新建用户组
@@ -115,7 +116,7 @@ export default function UserGroupsManager({ embedded = false }: { embedded?: boo
<Shield className="w-5 h-5 dg-text-blue" />
<div>
<div className="font-medium text-[var(--dg-text-primary)]">{g.name}</div>
<div className="text-xs text-[var(--dg-text-secondary)]">{g.description || '暂无描述'} · {(g.members || []).length} 位成员</div>
<div className="text-xs text-[var(--dg-text-secondary)]">{g.description || i18n.t('system.roleDescFallback')} · {(g.members || []).length} 位成员</div>
</div>
</div>
<div className="flex items-center gap-2">
@@ -132,14 +133,14 @@ export default function UserGroupsManager({ embedded = false }: { embedded?: boo
<div className="border-t border-[var(--dg-border)] px-4 py-3 space-y-3">
{/* Add member */}
<div className="flex items-end gap-2">
<div className="flex-1 space-y-1"><Label className="text-xs">添加用户</Label>
<Input value={memberUsers[g.id] || ''} onChange={e => setMemberUsers(prev => ({ ...prev, [g.id]: e.target.value }))} placeholder="输入用户名" className="h-8 text-sm" onKeyDown={e => e.key === 'Enter' && handleAddMember(g.id)} />
<div className="flex-1 space-y-1"><Label className="text-xs"> {i18n.t('system.addUser')} </Label>
<Input value={memberUsers[g.id] || ''} onChange={e => setMemberUsers(prev => ({ ...prev, [g.id]: e.target.value }))} placeholder={i18n.t('pipeline.usernamePlaceholder')} className="h-8 text-sm" onKeyDown={e => e.key === 'Enter' && handleAddMember(g.id)} />
</div>
<Button size="sm" className="h-8" onClick={() => handleAddMember(g.id)}><UserPlus className="w-3.5 h-3.5 mr-1" />添加</Button>
<Button size="sm" className="h-8" onClick={() => handleAddMember(g.id)}><UserPlus className="w-3.5 h-3.5 mr-1" /> {i18n.t('monitor.dashboard.custom.addBtn')} </Button>
</div>
{/* Members list */}
{(g.members || []).length === 0 ? (
<div className="text-xs text-[var(--dg-text-secondary)] py-2">暂无成员</div>
<div className="text-xs text-[var(--dg-text-secondary)] py-2"> {i18n.t('deploy.rbac.noMembers')} </div>
) : (
<div className="flex flex-wrap gap-2">
{(g.members || []).map(m => (
@@ -162,14 +163,14 @@ export default function UserGroupsManager({ embedded = false }: { embedded?: boo
const formDialog = (
<Dialog open={formOpen} onOpenChange={setFormOpen}>
<DialogContent>
<DialogHeader><DialogTitle>{editId ? '编辑用户组' : '新建用户组'}</DialogTitle></DialogHeader>
<DialogHeader><DialogTitle>{editId ? i18n.t('deploy.rbac.groupEditTitle') : i18n.t('pipeline.newGroup')}</DialogTitle></DialogHeader>
<div className="space-y-3">
<div className="space-y-1"><Label>组名</Label><Input value={formName} onChange={e => setFormName(e.target.value)} placeholder="例如: 后端开发组" /></div>
<div className="space-y-1"><Label>描述</Label><Input value={formDesc} onChange={e => setFormDesc(e.target.value)} placeholder="可选" /></div>
<div className="space-y-1"><Label> {i18n.t('monitor.groupName')} </Label><Input value={formName} onChange={e => setFormName(e.target.value)} placeholder={i18n.t('pipeline.usergroupsmanag.s5a56328e')} /></div>
<div className="space-y-1"><Label> {i18n.t('aiops.llmModelDesc')} </Label><Input value={formDesc} onChange={e => setFormDesc(e.target.value)} placeholder={i18n.t('rbac.jobRole.placeholder.description')} /></div>
</div>
<div className="flex justify-end gap-2 mt-4 pt-4 border-t">
<Button variant="outline" onClick={() => setFormOpen(false)}>取消</Button>
<Button onClick={handleSave} disabled={saving}>{saving ? '...' : '保存'}</Button>
<Button variant="outline" onClick={() => setFormOpen(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
<Button onClick={handleSave} disabled={saving}>{saving ? '...' : i18n.t('task.script.save')}</Button>
</div>
</DialogContent>
</Dialog>
@@ -188,15 +189,15 @@ export default function UserGroupsManager({ embedded = false }: { embedded?: boo
return (
<PageShell
variant="list"
title="应用发布"
subtitle="用户组 — 批量授权流水线访问权限"
title={i18n.t('pipeline.releaseTitle')}
subtitle={i18n.t('pipeline.usergroupsmanag.sf80ecf33')}
icon={<Users className="h-5 w-5" />}
loading={loading}
metrics={(
<StatStrip
items={[
{ label: '用户组', value: groups.length, color: 'blue' },
{ label: '成员总数', value: memberCount, color: 'purple' },
{ label: i18n.t('pipeline.groupLabel'), value: groups.length, color: 'blue' },
{ label: i18n.t('pipeline.usergroupsmanag.sdf2b42fe'), value: memberCount, color: 'purple' },
]}
/>
)}
@@ -11,6 +11,7 @@ import { toast } from 'sonner';
import { useCredentialDetail } from '@/hooks/api';
import type { SecretType } from './types';
import { TYPE_CONFIG, TYPE_BADGE_CLASS } from './constants';
import i18n from '@/i18n';
export interface CredDetailDialogProps {
open: boolean;
@@ -26,7 +27,7 @@ export function CredDetailDialog({ open, onClose, credId }: CredDetailDialogProp
if (!open || !credId) return null;
const toggleShow = (key: string) => setShowMap(prev => ({ ...prev, [key]: !prev[key] }));
const copyValue = (val: string) => { navigator.clipboard.writeText(val); toast.success('已复制'); };
const copyValue = (val: string) => { navigator.clipboard.writeText(val); toast.success(i18n.t('aiops.agentCliCopied')); };
const st = detail?.secret_type as SecretType;
const cfg = TYPE_CONFIG[st];
@@ -77,39 +78,39 @@ export function CredDetailDialog({ open, onClose, credId }: CredDetailDialogProp
</DialogTitle>
</DialogHeader>
{loading ? (
<div className="text-center py-8 text-[var(--dg-text-muted)]">加载中...</div>
<div className="text-center py-8 text-[var(--dg-text-muted)]"> {i18n.t('monitor.logPanel.logResults.loading')} </div>
) : detail ? (
<div className="space-y-4">
{/* 基本信息 // Logan.Li */}
<div className="grid grid-cols-2 gap-3 text-sm">
<div><span className="text-[var(--dg-text-muted)]">名称:</span><span className="text-[var(--dg-text-primary)] font-medium">{detailRecord?.name as string}</span></div>
<div><span className="text-[var(--dg-text-muted)]">类型:</span>
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s5b478615')} </span><span className="text-[var(--dg-text-primary)] font-medium">{detailRecord?.name as string}</span></div>
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.se91f5a13')} </span>
<Badge variant="outline" className={`text-xs ml-1 ${TYPE_BADGE_CLASS[st] || ''}`}>{cfg?.label || st}</Badge>
</div>
<div><span className="text-[var(--dg-text-muted)]">状态:</span>
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('approval.detail.statusLabel')} </span>
{detailRecord?.is_active
? <Badge className="bg-green-500/20 dg-text-green text-xs ml-1"><CheckCircle2 className="h-3 w-3 mr-0.5" />启用</Badge>
: <Badge className="bg-red-500/20 dg-text-red text-xs ml-1"><XCircle className="h-3 w-3 mr-0.5" />禁用</Badge>}
? <Badge className="bg-green-500/20 dg-text-green text-xs ml-1"><CheckCircle2 className="h-3 w-3 mr-0.5" /> {i18n.t('system.userActive')} </Badge>
: <Badge className="bg-red-500/20 dg-text-red text-xs ml-1"><XCircle className="h-3 w-3 mr-0.5" /> {i18n.t('system.userInactive')} </Badge>}
{detailRecord?.expires_at && new Date(detailRecord.expires_at as string) < new Date() && (
<Badge className="bg-yellow-500/20 dg-text-yellow text-xs ml-1"><AlertTriangle className="h-3 w-3 mr-0.5" />已过期</Badge>
<Badge className="bg-yellow-500/20 dg-text-yellow text-xs ml-1"><AlertTriangle className="h-3 w-3 mr-0.5" /> {i18n.t('system.expired2')} </Badge>
)}
</div>
<div className="col-span-2 cursor-pointer" onClick={() => copyValue((detailRecord?.env_prefix as string) || '')}>
<span className="text-[var(--dg-text-muted)]">引用ID:</span>
<span className="text-[var(--dg-text-muted)]"> {i18n.t('deploy.cred.refIdLabel')} </span>
<code className="text-[var(--dg-blue)] font-mono text-xs">{detailRecord?.env_prefix as string || '无'}</code>
<Copy className="h-3 w-3 inline ml-1 text-[var(--dg-text-muted)]" />
</div>
{(detailRecord?.tags as string[])?.length > 0 && (
<div className="col-span-2">
<span className="text-[var(--dg-text-muted)]">标签:</span>
<span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s1655dd5f')} </span>
{(detailRecord?.tags as string[]).map((t: string) => (
<Badge key={t} variant="outline" className="text-xs ml-1 border-[var(--dg-border)]">{t}</Badge>
))}
</div>
)}
<div className="col-span-2"><span className="text-[var(--dg-text-muted)]">描述:</span><span className="text-[var(--dg-text-secondary)]">{(detailRecord?.description as string) || '无'}</span></div>
<div><span className="text-[var(--dg-text-muted)]">创建:</span><span className="text-[var(--dg-text-secondary)] text-xs">{new Date(detailRecord?.created_at as string).toLocaleString()}</span></div>
<div><span className="text-[var(--dg-text-muted)]">更新:</span><span className="text-[var(--dg-text-secondary)] text-xs">{new Date(detailRecord?.updated_at as string).toLocaleString()}</span></div>
<div className="col-span-2"><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s4a4e3b5a')} </span><span className="text-[var(--dg-text-secondary)]">{(detailRecord?.description as string) || '无'}</span></div>
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s76d768fb')} </span><span className="text-[var(--dg-text-secondary)] text-xs">{new Date(detailRecord?.created_at as string).toLocaleString()}</span></div>
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s3820b7e1')} </span><span className="text-[var(--dg-text-secondary)] text-xs">{new Date(detailRecord?.updated_at as string).toLocaleString()}</span></div>
</div>
{/* 密钥数据 // Logan.Li */}
@@ -118,41 +119,41 @@ export function CredDetailDialog({ open, onClose, credId }: CredDetailDialogProp
<Key className="h-4 w-4 text-yellow-500" />密钥数据
</div>
<div className="p-3 rounded-md bg-yellow-500/5 border border-yellow-500/20 mb-3">
<p className="text-xs dg-text-yellow/80">以下内容包含敏感信息,请妥善保管,不要泄露给他人。</p>
<p className="text-xs dg-text-yellow/80"> {i18n.t('pipeline.creddetaildialo.s03b218d4')} </p>
</div>
{st === 'ssh' && <>
<Field label="SSH 用户" value={content.ssh_user} />
<Field label="SSH 端口" value={content.ssh_port} />
<Field label="SSH 密码" value={content.password} sensitive fieldKey="ssh_password" />
<Field label="私钥" value={content.private_key} sensitive fieldKey="ssh_private_key" />
<Field label="公钥" value={content.public_key} fieldKey="ssh_public_key" />
<Field label={i18n.t('pipeline.creddetaildialo.s2272f5e9')} value={content.ssh_user} />
<Field label={i18n.t('pipeline.creddetaildialo.s3f284a59')} value={content.ssh_port} />
<Field label={i18n.t('system.sshPassword')} value={content.password} sensitive fieldKey="ssh_password" />
<Field label={i18n.t('system.sshPrivateKey')} value={content.private_key} sensitive fieldKey="ssh_private_key" />
<Field label={i18n.t('system.sshPublicKey')} value={content.public_key} fieldKey="ssh_public_key" />
</>}
{st === 'docker' && <>
<Field label="Registry 地址" value={content.server} />
<Field label="用户名" value={content.username} />
<Field label="密码" value={content.password} sensitive fieldKey="docker_password" />
<Field label={i18n.t('deploy.form.registryAddr')} value={content.server} />
<Field label={i18n.t('system.userName')} value={content.username} />
<Field label={i18n.t('system.passwordLabel')} value={content.password} sensitive fieldKey="docker_password" />
</>}
{st === 'tls' && <>
<Field label="证书" value={content.cert} sensitive fieldKey="tls_cert" />
<Field label="私钥" value={content.key} sensitive fieldKey="tls_key" />
<Field label="CA 证书" value={content.ca} sensitive fieldKey="tls_ca" />
<Field label={i18n.t('pipeline.creddetaildialo.sd40b5115')} value={content.cert} sensitive fieldKey="tls_cert" />
<Field label={i18n.t('system.sshPrivateKey')} value={content.key} sensitive fieldKey="tls_key" />
<Field label={i18n.t('pipeline.credformdialogt.sa24356be')} value={content.ca} sensitive fieldKey="tls_ca" />
</>}
{st === 'aksk' && <>
<Field label="Access Key ID" value={content.access_key} />
<Field label="Secret Access Key" value={content.secret_key} sensitive fieldKey="aksk_secret_key" />
</>}
{st === 'database' && <>
<Field label="数据库类型" value={String(content.db_type || '').toUpperCase()} />
<Field label="主机" value={content.host} />
<Field label="端口" value={content.port} />
<Field label="数据库名" value={content.db_name} />
<Field label="用户名" value={content.username} />
<Field label="密码" value={content.password} sensitive fieldKey="db_password" />
<Field label={i18n.t('pipeline.credformdialogt.s84b916da')} value={String(content.db_type || '').toUpperCase()} />
<Field label={i18n.t('agent.agentHost')} value={content.host} />
<Field label={i18n.t('system.ldapPort')} value={content.port} />
<Field label={i18n.t('pipeline.credformdialogt.s5ccbbd01')} value={content.db_name} />
<Field label={i18n.t('system.userName')} value={content.username} />
<Field label={i18n.t('system.passwordLabel')} value={content.password} sensitive fieldKey="db_password" />
</>}
{st === 'api' && <>
<Field label="API Key" value={content.api_key} sensitive fieldKey="api_key" />
<Field label="API 端点" value={content.endpoint} />
<Field label={i18n.t('pipeline.creddetaildialo.s48ec3494')} value={content.endpoint} />
</>}
{st === 'generic' && <>
<Field label="Key" value={content.generic_key} />
@@ -162,17 +163,17 @@ export function CredDetailDialog({ open, onClose, credId }: CredDetailDialogProp
{/* 引用提示 // Logan.Li */}
<div className="border-t border-[var(--dg-border)] pt-3">
<div className="text-xs text-[var(--dg-text-muted)] mb-2">在流水线/脚本中引用此凭据:</div>
<div className="text-xs text-[var(--dg-text-muted)] mb-2"> {i18n.t('deploy.cred.refHint')} </div>
<code className="text-xs bg-[var(--dg-bg-tertiary)] px-3 py-1.5 rounded block text-[var(--dg-blue)]">
{'${'}{String(detailRecord?.variable_name || String(detailRecord?.name || '').toUpperCase().replace(/[^A-Z0-9]/g, '_'))}{'}'}
</code>
</div>
</div>
) : (
<div className="text-center py-8 text-[var(--dg-text-muted)]">未找到凭据数据</div>
<div className="text-center py-8 text-[var(--dg-text-muted)]"> {i18n.t('deploy.cred.noCredData')} </div>
)}
<DialogFooter>
<Button variant="outline" onClick={onClose}>关闭</Button>
<Button variant="outline" onClick={onClose}> {i18n.t('agent.portClosed')} </Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -4,15 +4,16 @@ import {
Key, Server, Container, Shield, Cloud, Database, FileText, Globe,
} from 'lucide-react';
import type { SecretType } from './types';
import i18n from '@/i18n';
export const TYPE_CONFIG: Record<SecretType, { label: string; icon: typeof Key; color: string; desc: string }> = {
ssh: { label: 'SSH 密钥', icon: Server, color: 'dg-text-yellow', desc: 'SSH 私钥/公钥/密码' },
docker: { label: 'Docker 凭证', icon: Container, color: 'dg-text-cyan', desc: 'Docker Registry 登录凭据' },
tls: { label: 'TLS 证书', icon: Shield, color: 'dg-text-green', desc: 'TLS/SSL 证书与私钥' },
aksk: { label: 'AKSK 密钥', icon: Cloud, color: 'dg-text-purple', desc: '云服务 AccessKey/SecretKey' },
database: { label: '数据库凭证', icon: Database, color: 'dg-text-blue', desc: '数据库连接信息' },
api: { label: 'API 密钥', icon: Globe, color: 'dg-text-orange', desc: 'API Token/Key + 端点' },
generic: { label: '通用凭证', icon: FileText, color: 'dg-text-secondary', desc: '自定义 Key/Value 键值对' },
ssh: { label: i18n.t('system.sshKeyLabel'), icon: Server, color: 'dg-text-yellow', desc: i18n.t('pipeline.constantsts.sedc7f9e9') },
docker: { label: i18n.t('system.dockerCredential'), icon: Container, color: 'dg-text-cyan', desc: i18n.t('pipeline.constantsts.sbd6cf171') },
tls: { label: i18n.t('pipeline.constantsts.sfa5a64de'), icon: Shield, color: 'dg-text-green', desc: i18n.t('pipeline.constantsts.s43f00c61') },
aksk: { label: i18n.t('pipeline.constantsts.sb82698a1'), icon: Cloud, color: 'dg-text-purple', desc: i18n.t('pipeline.constantsts.s564e98a9') },
database: { label: i18n.t('system.dbCredential'), icon: Database, color: 'dg-text-blue', desc: i18n.t('pipeline.constantsts.se47498d3') },
api: { label: i18n.t('system.apiKeys'), icon: Globe, color: 'dg-text-orange', desc: i18n.t('pipeline.constantsts.sb3edf0a4') },
generic: { label: i18n.t('pipeline.constantsts.s1d319bb7'), icon: FileText, color: 'dg-text-secondary', desc: i18n.t('pipeline.constantsts.s51b5b280') },
};
export const TYPE_BADGE_CLASS: Record<SecretType, string> = {
@@ -8,6 +8,7 @@ import { Search, Server, Network, Loader2, Check } from 'lucide-react';
import { cn } from '@/lib/utils';
import { serverApi, kubernetesApi } from '@/api';
import { getPublicApiBase } from '@/utils/request';
import i18n from '@/i18n';
export interface DeployTargetValue {
deployTarget: 'ssh' | 'k8s';
@@ -148,8 +149,8 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
<div className="space-y-3">
<div className="flex gap-1 p-1 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
{([
{ key: 'ssh' as const, label: '主机发布', icon: Server },
{ key: 'k8s' as const, label: 'Kubernetes 发布', icon: Network },
{ key: 'ssh' as const, label: i18n.t('pipeline.deploytargetfie.secf81967'), icon: Server },
{ key: 'k8s' as const, label: i18n.t('pipeline.deploytargetfie.s7ff062d5'), icon: Network },
]).map((item) => {
const active = value.deployTarget === item.key;
const Icon = item.icon;
@@ -199,7 +200,7 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
<Input
value={serverSearch}
onChange={(e) => setServerSearch(e.target.value)}
placeholder="搜索主机名或 IP"
placeholder={i18n.t('pipeline.deploytargetfie.sc7624940')}
className="pl-8 h-8 text-xs dg-input"
/>
</div>
@@ -209,7 +210,7 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
<Loader2 className="h-4 w-4 animate-spin mr-1.5" />加载中
</div>
) : filteredServers.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] text-center py-8">暂无可用主机</p>
<p className="text-xs text-[var(--dg-text-muted)] text-center py-8"> {i18n.t('pipeline.deploytargetfie.sb4240171')} </p>
) : (
<div className="p-1.5 space-y-1">
{filteredServers.map((srv) => {
@@ -241,10 +242,10 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
</div>
)}
</ScrollArea>
<p className="text-[10px] text-[var(--dg-text-muted)]">构建完成后 SCP 上传 jar/tar.gz 到远程目录并执行部署命令</p>
<p className="text-[10px] text-[var(--dg-text-muted)]"> {i18n.t('pipeline.deploytargetfie.sb2b6db98')} </p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">远程目录</Label>
<Label className="text-xs"> {i18n.t('pipeline.deploytargetfie.s5ca05a9b')} </Label>
<Input
value={value.deployRemotePath}
onChange={(e) => onChange({ deployRemotePath: e.target.value })}
@@ -253,7 +254,7 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
/>
</div>
<div className="space-y-1">
<Label className="text-xs">部署命令</Label>
<Label className="text-xs"> {i18n.t('pipeline.deploytargetfie.s65b20ba7')} </Label>
<Input
value={value.deployScript}
onChange={(e) => onChange({ deployScript: e.target.value })}
@@ -280,10 +281,10 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
})}
>
<SelectTrigger className="h-8 text-xs dg-input">
<SelectValue placeholder="选择集群" />
<SelectValue placeholder={i18n.t('kubernetes.topology.selectCluster')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">请选择</SelectItem>
<SelectItem value="__none"> {i18n.t('k8s.storage.pleaseSelect')} </SelectItem>
{clusters.map((c) => (
<SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
))}
@@ -291,11 +292,11 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">已选 Deployment</Label>
<Label className="text-xs"> {i18n.t('pipeline.deploytargetfie.s806e4e01')} </Label>
<Input
readOnly
value={value.k8sDeployment ? `${value.k8sNamespace}/${value.k8sDeployment}` : ''}
placeholder="从下方列表选择"
placeholder={i18n.t('pipeline.deploytargetfie.sbb1671f3')}
className="h-8 text-xs dg-input font-mono"
/>
</div>
@@ -307,7 +308,7 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
<Input
value={depSearch}
onChange={(e) => setDepSearch(e.target.value)}
placeholder="搜索 Deployment 或命名空间"
placeholder={i18n.t('pipeline.deploytargetfie.saa98b28a')}
className="pl-8 h-8 text-xs dg-input"
/>
</div>
@@ -317,7 +318,7 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
<Loader2 className="h-4 w-4 animate-spin mr-1.5" />加载 Deployment
</div>
) : filteredDeployments.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] text-center py-8">暂无 Deployment</p>
<p className="text-xs text-[var(--dg-text-muted)] text-center py-8"> {i18n.t('k8s.workloads.noDeployment')} </p>
) : (
<div className="p-1.5 space-y-1">
{filteredDeployments.map((dep) => {
@@ -348,11 +349,11 @@ export function DeployTargetFields({ value, onChange, requireTarget = true }: De
)}
</ScrollArea>
<div className="space-y-1">
<Label className="text-xs">容器名 <span className="text-[var(--dg-text-muted)]">(多容器时填写)</span></Label>
<Label className="text-xs"> {i18n.t('kubernetes.container.containerName')} <span className="text-[var(--dg-text-muted)]">(多容器时填写)</span></Label>
<Input
value={value.k8sContainerName}
onChange={(e) => onChange({ k8sContainerName: e.target.value })}
placeholder="默认首个容器"
placeholder={i18n.t('pipeline.deploytargetfie.s389076f8')}
className="h-8 text-xs font-mono dg-input"
/>
</div>
@@ -13,6 +13,7 @@ import {
import { pipelineApi } from '@/api';
import { toast } from 'sonner';
import { fmtDuration, type LogEntry } from './constants';
import i18n from '@/i18n';
interface PipelineRunPanelProps {
pipeline: Record<string, unknown>;
@@ -49,10 +50,10 @@ function PipelineQueuePanel({ pipeline }: { pipeline: Record<string, unknown> })
const handleCancel = async (runId: number) => {
try {
await pipelineApi.removeFromQueue(code, runId);
toast.success('已取消');
toast.success(i18n.t('pipeline.statusCancelled'));
loadQueue();
} catch {
toast.error('取消失败');
toast.error(i18n.t('deploy.toast.cancelFailed'));
}
};
@@ -68,7 +69,7 @@ function PipelineQueuePanel({ pipeline }: { pipeline: Record<string, unknown> })
return (
<div className="px-4 py-2 space-y-2">
<div className="flex items-center gap-2 text-xs">
<span className="text-[var(--dg-text-secondary)]">构建执行状态</span>
<span className="text-[var(--dg-text-secondary)]"> {i18n.t('deploy.run.buildStatus')} </span>
<span className="font-bold text-[var(--dg-text-primary)]">{running}/{max}</span>
{queued > 0 && (
<span className="text-[var(--dg-text-secondary)] ml-2">
@@ -141,17 +142,17 @@ export function PipelineRunPanel({ pipeline, pipelineRuns, isLoadingRuns, onStop
<Loader2 className="h-5 w-5 animate-spin text-[var(--dg-text-secondary)]" />
</div>
) : pipelineRuns.length === 0 ? (
<div className="py-6 text-center text-[var(--dg-text-secondary)] text-sm">暂无执行记录</div>
<div className="py-6 text-center text-[var(--dg-text-secondary)] text-sm"> {i18n.t('automation.noExecHistory')} </div>
) : (
<table className="w-full text-sm">
<thead className="border-b border-[var(--dg-border)]">
<tr className="bg-[var(--dg-bg-tertiary)]">
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">#</th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">状态</th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">触发人</th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">耗时</th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">时间</th>
<th className="px-4 py-2 text-right text-xs font-medium text-[var(--dg-text-secondary)]">操作</th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('system.terminalStatus')} </th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('pipeline.triggerBy')} </th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('pipeline.duration')} </th>
<th className="px-4 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('system.auditTime')} </th>
<th className="px-4 py-2 text-right text-xs font-medium text-[var(--dg-text-secondary)]"> {i18n.t('system.auditAction')} </th>
</tr>
</thead>
<tbody>
@@ -217,7 +218,7 @@ export function PipelineRunPanel({ pipeline, pipelineRuns, isLoadingRuns, onStop
</div>
<div className="bg-[var(--dg-bg-primary)] rounded-md p-3 max-h-[50vh] overflow-y-auto font-mono text-xs">
{logs.length === 0 ? (
<div className="dg-text-muted py-4 text-center">暂无日志</div>
<div className="dg-text-muted py-4 text-center"> {i18n.t('pipeline.noLog')} </div>
) : (
logs.map((l, i) => (
<div key={i} className="flex gap-2 py-0.5">
@@ -1,3 +1,4 @@
import i18n from '@/i18n';
import {
GitBranch,
Box,
@@ -9,12 +10,12 @@ import {
// 流水线阶段定义(固定顺序) // Logan.Li
export const STAGE_DEFS = [
{ key: 'checkout', label: '代码检出', icon: GitBranch, color: '#6366F1', order: 1, needs: ['repo'], desc: '从 Git 仓库拉取代码' },
{ key: 'build', label: '构建', icon: Box, color: '#F59E0B', order: 2, needs: [], desc: '编译/打包项目' },
{ key: 'test', label: '测试', icon: CheckCircle, color: '#10B981', order: 3, needs: [], desc: '运行单元/集成测试' },
{ key: 'image', label: '镜像打包', icon: Container, color: '#06B6D4', order: 4, needs: ['registry'], desc: 'Docker 构建 & 推送' },
{ key: 'deploy', label: '部署', icon: Rocket, color: '#EF4444', order: 5, needs: [], desc: '部署到目标环境' },
{ key: 'verify', label: '验证', icon: ShieldCheck, color: '#3B82F6', order: 6, needs: [], desc: '部署后健康检查' },
{ key: 'checkout', label: i18n.t('pipeline.checkout'), icon: GitBranch, color: '#6366F1', order: 1, needs: ['repo'], desc: i18n.t('pipeline.constantsts.s4769cecb') },
{ key: 'build', label: i18n.t('pipeline.build'), icon: Box, color: '#F59E0B', order: 2, needs: [], desc: i18n.t('pipeline.constantsts.s3318af6f') },
{ key: 'test', label: i18n.t('pipeline.testTool'), icon: CheckCircle, color: '#10B981', order: 3, needs: [], desc: i18n.t('pipeline.constantsts.s350a98e1') },
{ key: 'image', label: i18n.t('pipeline.imageBuild'), icon: Container, color: '#06B6D4', order: 4, needs: ['registry'], desc: i18n.t('pipeline.constantsts.s98ea1149') },
{ key: 'deploy', label: i18n.t('pipeline.deploy2'), icon: Rocket, color: '#EF4444', order: 5, needs: [], desc: i18n.t('pipeline.constantsts.s45f0eba0') },
{ key: 'verify', label: i18n.t('system.credentialVerify'), icon: ShieldCheck, color: '#3B82F6', order: 6, needs: [], desc: i18n.t('pipeline.constantsts.seb164cd5') },
].sort((a, b) => a.order - b.order);
// 运行日志条目 // Logan.Li