feat(i18n): deploy 模块(Pipeline/Credential)国际化增量迁移 — 翻译键 deploy 命名空间(455键) + CredentialManager 补全 3 处 UI 文案
- PipelineManager.tsx: 流水线全量翻译(标题/搜索/CRUD/阶段/任务/运行/横幅) - CredentialManager.tsx: 操作按钮 title 属性补全(编辑/禁用启用/删除) - zh-CN.json + en-US.json: deploy 命名空间 455 键完全对齐 - 总翻译键: 1019(zh) = 1019(en) - tsc -b 零错误, npm build 成功(17.42s)
This commit is contained in:
1 parent
ed7a5c07a7
commit
01967bc52b
4 files changed
+1053
-67
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Plus, Pencil, Trash2, RefreshCw, Key, Shield, Search,
|
||||
Clock, CheckCircle2, XCircle, Zap,
|
||||
@@ -17,6 +18,7 @@ import { CredFormDialog } from './credential-manager/CredFormDialog';
|
||||
|
||||
// 凭据管理页编排器 // Logan.Li
|
||||
export default function CredentialManager() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Credential | null>(null);
|
||||
@@ -38,28 +40,28 @@ export default function CredentialManager() {
|
||||
|
||||
const handleDelete = async (cred: Credential) => {
|
||||
const ok = await confirm({
|
||||
title: '删除凭据',
|
||||
description: `确定删除凭据「${cred.name}」?此操作不可撤销。`,
|
||||
confirmText: '删除',
|
||||
title: t('deploy.cred.deleteCred'),
|
||||
description: t('deploy.pipeline.deleteConfirm', { name: cred.name }),
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteMutation.mutateAsync(cred.id);
|
||||
toast.success('已删除');
|
||||
} catch { toast.error('删除失败'); }
|
||||
toast.success(t('deploy.toast.deleted'));
|
||||
} catch { toast.error(t('deploy.toast.deleteFailed')); }
|
||||
};
|
||||
|
||||
const handleToggleActive = async (cred: Credential) => {
|
||||
try {
|
||||
await toggleActiveMutation.mutateAsync({ id: cred.id, isActive: !cred.is_active });
|
||||
toast.success(cred.is_active ? '已禁用' : '已启用');
|
||||
} catch { toast.error('操作失败'); }
|
||||
toast.success(cred.is_active ? t('deploy.toast.disabled') : t('deploy.toast.enabled'));
|
||||
} catch { toast.error(t('deploy.toast.operationFailed')); }
|
||||
};
|
||||
|
||||
const handleCopyPrefix = (prefix: string) => {
|
||||
navigator.clipboard.writeText(prefix);
|
||||
toast.success('引用 ID 已复制');
|
||||
toast.success(t('deploy.cred.refIdCopied'));
|
||||
};
|
||||
|
||||
const typeOptions = Object.entries(TYPE_CONFIG) as [SecretType, typeof TYPE_CONFIG[SecretType]][];
|
||||
@@ -68,17 +70,17 @@ export default function CredentialManager() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)]">凭据管理</h2>
|
||||
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('deploy.cred.title')}</h2>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-0.5">
|
||||
统一管理 SSH 密钥、Docker 密码、AKSK、数据库等敏感凭据(AES 加密存储)
|
||||
{t('deploy.cred.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />刷新
|
||||
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => { setEditing(null); setDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-1" />新增凭据
|
||||
<Plus className="w-4 h-4 mr-1" />{t('deploy.cred.addCredential')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,10 +89,10 @@ export default function CredentialManager() {
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Select value={filterType} onValueChange={setFilterType}>
|
||||
<SelectTrigger className="w-[140px] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] h-9 text-sm">
|
||||
<SelectValue placeholder="全部类型" />
|
||||
<SelectValue placeholder={t('deploy.label.allType')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="all">{t('deploy.label.allType')}</SelectItem>
|
||||
{typeOptions.map(([k, v]) => (
|
||||
<SelectItem key={k} value={k}>
|
||||
<span className="flex items-center gap-2"><v.icon className="h-3.5 w-3.5" />{v.label}</span>
|
||||
@@ -101,23 +103,23 @@ export default function CredentialManager() {
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<Input value={filterSearch} onChange={e => setFilterSearch(e.target.value)}
|
||||
placeholder="搜索名称、描述..."
|
||||
placeholder={t('deploy.label.searchPlaceholder')}
|
||||
className="pl-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] h-9 text-sm" />
|
||||
</div>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{credentials.length} 个凭据</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{t('deploy.cred.credCount', { count: credentials.length })}</span>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-16 text-[var(--dg-text-secondary)]">
|
||||
<Zap className="w-6 h-6 animate-spin mx-auto mb-2 text-[var(--dg-blue)]" />加载中...
|
||||
<Zap className="w-6 h-6 animate-spin mx-auto mb-2 text-[var(--dg-blue)]" />{t('deploy.label.loading')}
|
||||
</div>
|
||||
) : credentials.length === 0 ? (
|
||||
<div className="text-center py-16 border border-dashed border-[var(--dg-border)] rounded-lg">
|
||||
<Shield className="w-12 h-12 mx-auto mb-3 text-[var(--dg-text-muted)]" />
|
||||
<p className="text-[var(--dg-text-secondary)] mb-2">暂无凭据</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-4">创建 SSH、Docker、AKSK 等类型的凭据,供流水线和服务器使用</p>
|
||||
<p className="text-[var(--dg-text-secondary)] mb-2">{t('deploy.cred.noCredentials')}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-4">{t('deploy.cred.subtitle')}</p>
|
||||
<Button onClick={() => { setEditing(null); setDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-1" />创建第一个凭据
|
||||
<Plus className="w-4 h-4 mr-1" />{t('deploy.cred.createFirst')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -125,13 +127,13 @@ export default function CredentialManager() {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">名称</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">类型</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">摘要</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden lg:table-cell">标签</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden xl:table-cell">引用ID</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">状态</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-[var(--dg-text-secondary)]">操作</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.name')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.type')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">{t('deploy.table.repoUrl')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden lg:table-cell">{t('deploy.table.tagLabel')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden xl:table-cell">{t('deploy.cred.refId')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.status')}</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -181,28 +183,28 @@ export default function CredentialManager() {
|
||||
<td className="px-3 py-2.5">
|
||||
<div className="flex items-center gap-1">
|
||||
{cred.is_active ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 text-xs border-0">启用</Badge>
|
||||
<Badge className="bg-green-500/20 text-green-400 text-xs border-0">{t('common.enable')}</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-500/20 text-red-400 text-xs border-0">禁用</Badge>
|
||||
<Badge className="bg-red-500/20 text-red-400 text-xs border-0">{t('common.disable')}</Badge>
|
||||
)}
|
||||
{isExpired && (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 text-xs border-0"><Clock className="h-2.5 w-2.5 mr-0.5" />过期</Badge>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 text-xs border-0"><Clock className="h-2.5 w-2.5 mr-0.5" />{t('deploy.status.expired')}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-right" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7"
|
||||
onClick={() => { setEditing(cred as Credential); setDialogOpen(true); }} title="编辑">
|
||||
onClick={() => { setEditing(cred as Credential); setDialogOpen(true); }} title={t('common.edit')}>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7"
|
||||
onClick={() => handleToggleActive(cred as Credential)}
|
||||
title={cred.is_active ? '禁用' : '启用'}>
|
||||
title={cred.is_active ? t('common.disable') : t('common.enable')}>
|
||||
{cred.is_active ? <XCircle className="w-3.5 h-3.5 text-yellow-500" /> : <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500"
|
||||
onClick={() => handleDelete(cred as Credential)} title="删除">
|
||||
onClick={() => handleDelete(cred as Credential)} title={t('common.delete')}>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
@@ -28,6 +29,7 @@ import { PipelineCreateDialog } from './pipeline-manager/PipelineCreateDialog';
|
||||
|
||||
// 流水线列表页编排器 // Logan.Li
|
||||
export function PipelineManager() {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { data: pipelines = [], isLoading: loading, refetch: refetchPipelines } = usePipelines();
|
||||
const { triggerMutation, deleteMutation, stopRunMutation } = usePipelineMutations();
|
||||
@@ -41,11 +43,11 @@ export function PipelineManager() {
|
||||
const confirm = useConfirm();
|
||||
|
||||
const ENV_TYPES = [
|
||||
{ key: 'all', label: '全部', icon: Layers, color: '#6B7280' },
|
||||
{ key: 'production', label: '生产', icon: ShieldCheck, color: '#EF4444' },
|
||||
{ key: 'testing', label: '测试', icon: CheckCircle, color: '#F59E0B' },
|
||||
{ key: 'staging', label: '预发布', icon: Globe, color: '#8B5CF6' },
|
||||
{ key: 'development', label: '开发', icon: Code, color: '#3B82F6' },
|
||||
{ key: 'all', label: t('deploy.env.all'), icon: Layers, color: '#6B7280' },
|
||||
{ key: 'production', label: t('deploy.env.production'), icon: ShieldCheck, color: '#EF4444' },
|
||||
{ key: 'testing', label: t('deploy.env.testing'), icon: CheckCircle, color: '#F59E0B' },
|
||||
{ key: 'staging', label: t('deploy.env.staging'), icon: Globe, color: '#8B5CF6' },
|
||||
{ key: 'development', label: t('deploy.env.development'), icon: Code, color: '#3B82F6' },
|
||||
];
|
||||
|
||||
// 同步运行中的任务 ID // Logan.Li
|
||||
@@ -86,20 +88,20 @@ export function PipelineManager() {
|
||||
const handleTrigger = async (pipelineId: number, code: string) => {
|
||||
try {
|
||||
await triggerMutation.mutateAsync(code);
|
||||
toast.success('已触发');
|
||||
toast.success(t('deploy.toast.triggered'));
|
||||
loadRuns(pipelineId, code);
|
||||
refetchPipelines();
|
||||
} catch {
|
||||
toast.error('触发失败');
|
||||
toast.error(t('deploy.toast.triggerFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = async (pipelineId: number, code: string, runId: number | string) => {
|
||||
const ok = await confirm({ title: '停止运行', description: '确定要停止此运行吗?', variant: 'destructive' });
|
||||
const ok = await confirm({ title: t('deploy.run.stopRun'), description: t('deploy.run.stopConfirm'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try {
|
||||
await stopRunMutation.mutateAsync({ code, runId });
|
||||
toast.success('已停止');
|
||||
toast.success(t('deploy.toast.stopped'));
|
||||
loadRuns(pipelineId, code);
|
||||
setRunningRunMap((prev) => {
|
||||
const n = { ...prev };
|
||||
@@ -107,7 +109,7 @@ export function PipelineManager() {
|
||||
return n;
|
||||
});
|
||||
} catch {
|
||||
toast.error('停止失败');
|
||||
toast.error(t('deploy.toast.stopFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,9 +122,9 @@ export function PipelineManager() {
|
||||
(r) => r.status === 'running' || r.status === 'pending',
|
||||
);
|
||||
if (running) await handleStop(pipelineId, code, running.id as number | string);
|
||||
else toast.error('未找到运行中的任务');
|
||||
else toast.error(t('deploy.pipeline.noRunningTask'));
|
||||
} catch {
|
||||
toast.error('停止失败');
|
||||
toast.error(t('deploy.toast.stopFailed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -131,17 +133,17 @@ export function PipelineManager() {
|
||||
|
||||
const handleDelete = async (pipeline: Record<string, unknown>) => {
|
||||
const ok = await confirm({
|
||||
title: '删除流水线',
|
||||
description: `确定删除「${pipeline.name}」?`,
|
||||
title: t('deploy.pipeline.deletePipeline'),
|
||||
description: t('deploy.pipeline.deleteConfirm', { name: pipeline.name }),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteMutation.mutateAsync(pipeline.id as number | string);
|
||||
toast.success('已删除');
|
||||
toast.success(t('deploy.toast.deleted'));
|
||||
refetchPipelines();
|
||||
} catch {
|
||||
toast.error('删除失败');
|
||||
toast.error(t('deploy.toast.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,14 +159,14 @@ export function PipelineManager() {
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">流水线</h1>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">CI/CD 构建 · 部署 · 发布一体化</p>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">{t('deploy.pipeline.title')}</h1>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">{t('deploy.pipeline.subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => refetchPipelines()}>
|
||||
<Clock className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />刷新
|
||||
<Clock className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button onClick={() => setNewOpen(true)}><Plus className="h-4 w-4 mr-2" />新建流水线</Button>
|
||||
<Button onClick={() => setNewOpen(true)}><Plus className="h-4 w-4 mr-2" />{t('deploy.pipeline.newPipeline')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -173,45 +175,45 @@ export function PipelineManager() {
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<GitBranch className="h-3 w-3 text-indigo-400" />
|
||||
<span className="text-[10px] font-semibold text-indigo-400">构建</span>
|
||||
<span className="text-[10px] font-semibold text-indigo-400">{t('deploy.banner.build')}</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<Rocket className="h-3 w-3 text-red-400" />
|
||||
<span className="text-[10px] font-semibold text-red-400">部署</span>
|
||||
<span className="text-[10px] font-semibold text-red-400">{t('deploy.banner.deploy')}</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 rounded bg-blue-400/10 border border-blue-400/20">
|
||||
<Server className="h-3 w-3 text-blue-400" />
|
||||
<span className="text-[10px] font-medium text-blue-400">SSH 主机</span>
|
||||
<span className="text-[10px] font-medium text-blue-400">{t('deploy.banner.sshHost')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[9px] text-[var(--dg-text-secondary)] opacity-40 pl-1">传统主机 SCP 部署 · 无需镜像仓库</p>
|
||||
<p className="text-[9px] text-[var(--dg-text-secondary)] opacity-40 pl-1">{t('deploy.banner.sshDesc')}</p>
|
||||
</div>
|
||||
<div className="w-px bg-[var(--dg-border)]" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<GitBranch className="h-3 w-3 text-indigo-400" />
|
||||
<span className="text-[10px] font-semibold text-indigo-400">构建</span>
|
||||
<span className="text-[10px] font-semibold text-indigo-400">{t('deploy.banner.build')}</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<Container className="h-3 w-3 text-cyan-400" />
|
||||
<span className="text-[10px] font-semibold text-cyan-400">镜像仓库</span>
|
||||
<span className="text-[10px] font-semibold text-cyan-400">{t('deploy.banner.imageRegistry')}</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<Rocket className="h-3 w-3 text-red-400" />
|
||||
<span className="text-[10px] font-semibold text-red-400">部署</span>
|
||||
<span className="text-[10px] font-semibold text-red-400">{t('deploy.banner.deploy')}</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 rounded bg-emerald-400/10 border border-emerald-400/20">
|
||||
<Network className="h-3 w-3 text-emerald-400" />
|
||||
<span className="text-[10px] font-medium text-emerald-400">Kubernetes</span>
|
||||
<span className="text-[10px] font-medium text-emerald-400">{t('deploy.banner.kubernetes')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[9px] text-[var(--dg-text-secondary)] opacity-40 pl-1">容器化部署 · 需镜像仓库推送</p>
|
||||
<p className="text-[9px] text-[var(--dg-text-secondary)] opacity-40 pl-1">{t('deploy.banner.k8sDesc')}</p>
|
||||
</div>
|
||||
<span className="self-center ml-2 text-[10px] text-[var(--dg-text-secondary)] opacity-30 tracking-wide whitespace-nowrap">全流程可视化</span>
|
||||
<span className="self-center ml-2 text-[10px] text-[var(--dg-text-secondary)] opacity-30 tracking-wide whitespace-nowrap">{t('deploy.banner.fullFlow')}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative max-w-md flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-secondary)]" />
|
||||
<Input
|
||||
placeholder="搜索流水线..."
|
||||
placeholder={t('deploy.pipeline.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
@@ -244,9 +246,9 @@ export function PipelineManager() {
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="text-center py-16 text-[var(--dg-text-secondary)]">
|
||||
<Workflow className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm">暂无流水线</p>
|
||||
<p className="text-sm">{t('deploy.pipeline.noData')}</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => setNewOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />创建第一个流水线
|
||||
<Plus className="h-4 w-4 mr-1" />{t('deploy.pipeline.createFirst')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -275,4 +277,4 @@ export function PipelineManager() {
|
||||
<PipelineCreateDialog open={newOpen} onOpenChange={setNewOpen} onCreated={refetchPipelines} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user