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:
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user