feat(i18n): 增量国际化迁移 — deploy/kubernetes/monitor/task 全模块 + 补全 28 缺失翻译键
迁移范围: - deploy 模块 (PipelineEditor/PipelineDetailDialog/PipelineFormDialog/PipelineRunDialog/ReleaseCreateDialog/ReleaseDetailDialog/ReleaseManager/ToolConfigTab/JobRoleCreateDialog 等 14 文件) - kubernetes 模块 (YamlEditorToolbar/DeploymentHpaTab/DeploymentLogsTab/DeploymentPodsTab/DnsHostSection/SchedulingSection/EventsTab/HPACronHPATab/HistoryTab/PodsTab/ServicesTab/ActionModals 等 12 文件) - monitor 模块 (AlertEventsTab/AlertCenterDetail/DatasourcePage/LogAlertBell/LogCenter/LogCenterArchBanner/StatChartPanel/LogRoleCreateDialog 等 8 文件) - task 模块 (CronJobStatsPanel/CronJobTable/CronJobViewDialog/ExecutionHistoryDialog 4 文件) - layout (PageEmpty/PageLoading 2 文件) - site-monitor/snmp-monitor 模块 (6 文件) 翻译键: 3975 → 4003 (+28 缺失键补全), 22 命名空间, zh-CN/en-US 完全对齐 新增命名空间: agent/aiops/cloud/kubernetes/monitor/pipeline/rbac/system/task/ui 验证: go build✅ tsc -b=0 npm build✅ (20.78s) 17/17 集成测试通过
This commit is contained in:
1 parent
eddf01dd79
commit
63e4c220d4
59 files changed
+6713
-641
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -39,6 +40,7 @@ import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
|
||||
export function AppReleaseManager() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [envFilter, setEnvFilter] = useState<string>('all');
|
||||
@@ -188,8 +190,8 @@ export function AppReleaseManager() {
|
||||
{release.version}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{getEnvBadge(release.environment)}</TableCell>
|
||||
<TableCell>{getStatusBadge(release.status)}</TableCell>
|
||||
<TableCell>{getEnvBadge(release.environment, t)}</TableCell>
|
||||
<TableCell>{getStatusBadge(release.status, t)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1 text-xs dg-text-secondary">
|
||||
<GitBranch className="h-3 w-3" />
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GitBranch, Settings2, Shield, Users, Server } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ path: '/pipelines', label: '流水线', icon: GitBranch },
|
||||
{ path: '/deploy/build-nodes', label: '构建节点', icon: Server },
|
||||
{ path: '/deploy/configureTools', label: '发布配置', icon: Settings2 },
|
||||
{ path: '/deploy/rbac', label: 'Job 权限', icon: Shield },
|
||||
{ path: '/deploy/groups', label: '用户组', icon: Users },
|
||||
{ path: '/pipelines', labelKey: 'deploy.pipeline.title', icon: GitBranch },
|
||||
{ path: '/deploy/build-nodes', labelKey: 'deploy.deployNav.buildNodes', icon: Server },
|
||||
{ path: '/deploy/configureTools', labelKey: 'deploy.label.releaseConfig', icon: Settings2 },
|
||||
{ path: '/deploy/rbac', labelKey: 'deploy.deployNav.jobPermission', icon: Shield },
|
||||
{ path: '/deploy/groups', labelKey: 'deploy.label.userGroups', icon: Users },
|
||||
];
|
||||
|
||||
// 应用发布模块二级导航 // Logan.Li
|
||||
export function DeployNav() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<nav className="flex items-center gap-1 p-1 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] w-fit flex-wrap">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
@@ -28,7 +30,7 @@ export function DeployNav() {
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{item.label}
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -36,6 +37,7 @@ export function PipelineDetailDialog({
|
||||
activeStage: string | null;
|
||||
setActiveStage: (id: string | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
@@ -48,19 +50,19 @@ export function PipelineDetailDialog({
|
||||
<Tabs defaultValue="stages">
|
||||
<TabsList className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
|
||||
<TabsTrigger value="stages" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
|
||||
执行阶段
|
||||
{t('deploy.run.execStage')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="config" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
|
||||
配置
|
||||
{t('deploy.detailDialog.config')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="history" className="data-[state=active]:bg-[var(--dg-bg-tertiary)]">
|
||||
历史记录{pipelineRuns.length > 0 && ` (${pipelineRuns.length})`}
|
||||
{t('deploy.detailDialog.history')}{pipelineRuns.length > 0 && ` (${pipelineRuns.length})`}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="stages" className="mt-4">
|
||||
<div className="space-y-2">
|
||||
{parseStages(selectedPipeline.stages).length === 0 ? (
|
||||
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无阶段配置</div>
|
||||
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">{t('deploy.pipeline.noStages')}</div>
|
||||
) : (
|
||||
parseStages(selectedPipeline.stages).map((stage: PipelineStage, index: number) => (
|
||||
<div
|
||||
@@ -103,21 +105,21 @@ export function PipelineDetailDialog({
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium w-32">代码仓库</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium w-32">{t('deploy.table.codeCol')}</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.source || '--'}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium">默认分支</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium">{t('deploy.table.defaultBranch')}</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.branch || '--'}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium">流水线类型</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium">{t('deploy.pipeline.type')}</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-primary)]">{selectedPipeline.type || 'deploy'}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium">运行次数</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-secondary)] font-medium">{t('deploy.pipeline.runs')}</TableCell>
|
||||
<TableCell className="text-[var(--dg-text-primary)]">
|
||||
{selectedPipeline.run_count || 0} 次(成功 {selectedPipeline.success_count || 0} / 失败 {selectedPipeline.fail_count || 0})
|
||||
{selectedPipeline.run_count || 0}{t('deploy.detailDialog.runCountPrefix')} {selectedPipeline.success_count || 0} / {t('deploy.detailDialog.runCountMid')} {selectedPipeline.fail_count || 0}{t('deploy.detailDialog.runCountSuffix')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
@@ -129,7 +131,7 @@ export function PipelineDetailDialog({
|
||||
<Loader2 className="h-5 w-5 text-[var(--dg-text-secondary)] animate-spin" />
|
||||
</div>
|
||||
) : pipelineRuns.length === 0 ? (
|
||||
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无执行记录</div>
|
||||
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">{t('deploy.run.noRecords')}</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{pipelineRuns.map((run: JsonData) => (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
|
||||
Plus,
|
||||
@@ -60,7 +61,13 @@ interface LogEntry {
|
||||
time: string; level: string; message: string; stage: string;
|
||||
}
|
||||
|
||||
const stageTypeLabels: Record<string, string> = { source: '拉取代码', build: '构建', deploy: '部署', test: '测试', custom: '自定义' };
|
||||
const stageTypeLabelKeys: Record<string, string> = {
|
||||
source: 'deploy.stage.checkoutDesc',
|
||||
build: 'deploy.stage.build',
|
||||
deploy: 'deploy.stage.deploy',
|
||||
test: 'deploy.stage.test',
|
||||
custom: 'deploy.stage.custom',
|
||||
};
|
||||
const taskIcons: JsonData = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
|
||||
|
||||
function uid() { return `tsk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; }
|
||||
@@ -97,24 +104,25 @@ function TaskCard({ task, selected, onSelect, onDelete }: { task: Task; selected
|
||||
|
||||
// ---- Task Editor Panel ----
|
||||
function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task) => void; onClose: () => void }) {
|
||||
if (!task) return <div className="p-6 text-center text-[var(--dg-text-secondary)] text-sm">选择一个任务开始编辑</div>;
|
||||
const { t } = useTranslation();
|
||||
if (!task) return <div className="p-6 text-center text-[var(--dg-text-secondary)] text-sm">{t('deploy.pipeline.selectTaskHint')}</div>;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1"><Label className="text-xs">任务名称</Label><Input value={task.name} onChange={e => onUpdate({ ...task, name: e.target.value })} className="h-8 text-sm" /></div>
|
||||
<div className="space-y-1"><Label className="text-xs">{t('pipeline.taskName')}</Label><Input value={task.name} onChange={e => onUpdate({ ...task, name: e.target.value })} className="h-8 text-sm" /></div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1"><Label className="text-xs">类型</Label>
|
||||
<div className="space-y-1"><Label className="text-xs">{t('common.type')}</Label>
|
||||
<Select value={task.type} onValueChange={v => onUpdate({ ...task, type: v })}><SelectTrigger className="h-8 text-sm"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="build">构建</SelectItem><SelectItem value="deploy">部署</SelectItem><SelectItem value="test">测试</SelectItem><SelectItem value="custom">自定义</SelectItem></SelectContent></Select>
|
||||
<SelectContent><SelectItem value="build">{t('pipeline.build')}</SelectItem><SelectItem value="deploy">{t('pipeline.deploy')}</SelectItem><SelectItem value="test">{t('pipeline.test')}</SelectItem><SelectItem value="custom">{t('pipeline.custom')}</SelectItem></SelectContent></Select>
|
||||
</div>
|
||||
<div className="space-y-1"><Label className="text-xs">图标</Label>
|
||||
<div className="space-y-1"><Label className="text-xs">{t('pipeline.icon')}</Label>
|
||||
<Select value={task.icon} onValueChange={v => onUpdate({ ...task, icon: v })}><SelectTrigger className="h-8 text-sm"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{Object.entries(taskIcons).map(([k]) => (<SelectItem key={k} value={k}>{k}</SelectItem>))}</SelectContent></Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1"><Label className="text-xs">脚本类型</Label>
|
||||
<div className="flex gap-2">{['shell', 'python'].map(t => (<Badge key={t} variant={task.script_type === t ? 'default' : 'outline'} className="cursor-pointer text-xs" onClick={() => onUpdate({ ...task, script_type: t })}>{t}</Badge>))}</div>
|
||||
<div className="space-y-1"><Label className="text-xs">{t('automation.scriptType')}</Label>
|
||||
<div className="flex gap-2">{['shell', 'python'].map(s => (<Badge key={s} variant={task.script_type === s ? 'default' : 'outline'} className="cursor-pointer text-xs" onClick={() => onUpdate({ ...task, script_type: s })}>{s}</Badge>))}</div>
|
||||
</div>
|
||||
<div className="space-y-1"><Label className="text-xs">脚本内容</Label>
|
||||
<div className="space-y-1"><Label className="text-xs">{t('automation.scriptContent')}</Label>
|
||||
<Textarea value={task.script} onChange={e => onUpdate({ ...task, script: e.target.value })} className="font-mono text-xs min-h-[200px] bg-[var(--dg-bg-primary)] dg-text-green" placeholder="#!/bin/bash\nset -e\necho 'hello'" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,20 +131,21 @@ function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task)
|
||||
|
||||
// ====== Log Dialog ======
|
||||
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run?: JsonData; logs: LogEntry[] }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader><DialogTitle className="flex items-center gap-2"><ScrollText className="w-5 h-5" />执行日志 #{run?.build_number || run?.id}</DialogTitle></DialogHeader>
|
||||
<DialogHeader><DialogTitle className="flex items-center gap-2"><ScrollText className="w-5 h-5" />{t('automation.execLog')} #{run?.build_number || run?.id}</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap gap-3 text-sm text-[var(--dg-text-secondary)]">
|
||||
<span>触发方式: {run?.triggered_by || 'manual'}</span>
|
||||
<span>分支: {run?.branch || '-'}</span>
|
||||
<span>耗时: {fmtDuration(run?.duration)}</span>
|
||||
<span>{t('pipeline.triggerMode')}: {run?.triggered_by || 'manual'}</span>
|
||||
<span>{t('pipeline.branch')}: {run?.branch || '-'}</span>
|
||||
<span>{t('pipeline.duration')}: {fmtDuration(run?.duration)}</span>
|
||||
<span>{run?.created_at ? new Date(run.created_at).toLocaleString('zh-CN') : '-'}</span>
|
||||
</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">{t('pipeline.noLog')}</div>
|
||||
) : (
|
||||
logs.map((l, i) => (
|
||||
<div key={i} className="flex gap-2 py-0.5">
|
||||
@@ -161,6 +170,7 @@ function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () =>
|
||||
|
||||
// ====== Main Component ======
|
||||
export default function PipelineEditor() {
|
||||
const { t } = useTranslation();
|
||||
const { code } = useParams<{ code: string }>();
|
||||
const pipelineCode = code || '';
|
||||
const [pipeline, setPipeline] = useState<JsonData>(null);
|
||||
@@ -172,7 +182,7 @@ export default function PipelineEditor() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [runs, setRuns] = useState<JsonData[]>([]);
|
||||
const [environments, setEnvironments] = useState<JsonData[]>([]);
|
||||
const [members, setMembers] = useState<JsonData[]>([]);
|
||||
const [members, setMembers] = useState<JsonData>([]);
|
||||
const [loadingMembers, setLoadingMembers] = useState(false);
|
||||
|
||||
// trigger config
|
||||
@@ -246,7 +256,7 @@ export default function PipelineEditor() {
|
||||
stageList.forEach((stg: Stage) => { expanded[stg.id] = true; });
|
||||
setExpandedStages(expanded);
|
||||
} catch { setStages([]); }
|
||||
} catch { toast.error('加载失败'); }
|
||||
} catch { toast.error(t('common.loadFailed')); }
|
||||
finally { setLoading(false); }
|
||||
})();
|
||||
loadEnvironments();
|
||||
@@ -256,16 +266,16 @@ export default function PipelineEditor() {
|
||||
const data = res as { list?: BuildNode[] };
|
||||
setBuildNodes(Array.isArray(data?.list) ? data.list.filter((n) => n.enabled) : []);
|
||||
}).catch(() => { /* silent */ });
|
||||
}, [pipelineCode, loadEnvironments, loadMembers, loadRuns]);
|
||||
}, [pipelineCode, loadEnvironments, loadMembers, loadRuns, t]);
|
||||
|
||||
const selectedTask = stages.flatMap(s => s.tasks).find(t => t.id === selectedTaskId) || null;
|
||||
const handleUpdateTask = (t: Task) => { setStages(prev => prev.map(s => ({ ...s, tasks: s.tasks.map(x => x.id === t.id ? t : x) }))); };
|
||||
const handleUpdateTask = (tk: Task) => { setStages(prev => prev.map(s => ({ ...s, tasks: s.tasks.map(x => x.id === tk.id ? tk : x) }))); };
|
||||
const handleAddTask = (stageIdx: number) => {
|
||||
const newTask: Task = { id: uid(), name: '新任务', type: 'build', icon: 'Wrench', config: {}, script: '', script_type: 'shell' };
|
||||
const newTask: Task = { id: uid(), name: t('pipeline.newTask'), type: 'build', icon: 'Wrench', config: {}, script: '', script_type: 'shell' };
|
||||
setStages(prev => prev.map((s, i) => i === stageIdx ? { ...s, tasks: [...s.tasks, newTask] } : s));
|
||||
};
|
||||
const handleDeleteTask = (taskId: string) => { setStages(prev => prev.map(s => ({ ...s, tasks: s.tasks.filter(t => t.id !== taskId) }))); if (selectedTaskId === taskId) setSelectedTaskId(null); };
|
||||
const handleAddStage = () => { const newStage: Stage = { id: `stg-${Date.now()}`, name: '新阶段', type: 'custom', tasks: [] }; setStages(prev => [...prev, newStage]); };
|
||||
const handleAddStage = () => { const newStage: Stage = { id: `stg-${Date.now()}`, name: t('pipeline.newStage'), type: 'custom', tasks: [] }; setStages(prev => [...prev, newStage]); };
|
||||
const handleDeleteStage = (idx: number) => { setStages(prev => prev.filter((_, i) => i !== idx)); };
|
||||
const handleMoveStage = (idx: number, dir: -1 | 1) => { const nx = idx + dir; if (nx < 0 || nx >= stages.length) return; const arr = [...stages]; [arr[idx], arr[nx]] = [arr[nx], arr[idx]]; setStages(arr); };
|
||||
|
||||
@@ -282,27 +292,27 @@ export default function PipelineEditor() {
|
||||
variables: JSON.stringify(Object.fromEntries(variables.filter(([k]) => k.trim()))),
|
||||
build_node_id: buildNodeId ? Number(buildNodeId) : null,
|
||||
});
|
||||
toast.success('已保存');
|
||||
} catch { toast.error('保存失败'); }
|
||||
toast.success(t('common.savedSuccessfully'));
|
||||
} catch { toast.error(t('common.saveFailed')); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const handleTrigger = async () => {
|
||||
try {
|
||||
await pipelineApi.triggerPipeline(pipelineCode);
|
||||
toast.success('已触发');
|
||||
toast.success(t('deploy.pipeline.triggered'));
|
||||
loadRuns();
|
||||
} catch { toast.error('触发失败'); }
|
||||
} catch { toast.error(t('deploy.pipeline.triggerFailed')); }
|
||||
};
|
||||
|
||||
const handleStop = async (runId: number | string) => {
|
||||
const ok = await _confirm({ title: '停止运行', description: '确定要停止此运行吗?', variant: 'destructive' });
|
||||
const ok = await _confirm({ title: t('pipeline.stopRun'), description: t('deploy.run.stopConfirm'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try {
|
||||
await pipelineApi.stopRun(pipelineCode, runId);
|
||||
toast.success('已停止');
|
||||
toast.success(t('pipeline.stopped'));
|
||||
loadRuns();
|
||||
} catch { toast.error('停止失败'); }
|
||||
} catch { toast.error(t('pipeline.stopFailed')); }
|
||||
};
|
||||
|
||||
const handleViewLog = async (run: JsonData) => {
|
||||
@@ -315,36 +325,36 @@ export default function PipelineEditor() {
|
||||
};
|
||||
|
||||
const handleAddMember = async () => {
|
||||
if (!newMemberUser.trim()) { toast.error('请输入用户名'); return; }
|
||||
if (!newMemberUser.trim()) { toast.error(t('pipeline.usernameRequired')); return; }
|
||||
try {
|
||||
await pipelineApi.addMember(pipelineCode, { username: newMemberUser.trim(), role: newMemberRole });
|
||||
toast.success('已添加成员');
|
||||
toast.success(t('pipeline.memberAdded'));
|
||||
setNewMemberUser('');
|
||||
loadMembers();
|
||||
} catch (e) { toast.error(e?.message || '添加失败'); }
|
||||
} catch (e) { toast.error(e?.message || t('common.operationFailed')); }
|
||||
};
|
||||
|
||||
const handleRemoveMember = async (memberId: number) => {
|
||||
const ok = await _confirm({ title: '移除成员', description: '确定要移除此成员吗?', variant: 'destructive' });
|
||||
const ok = await _confirm({ title: t('pipeline.removeMember'), description: t('pipeline.removeMemberConfirm'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try { await pipelineApi.removeMember(pipelineCode, memberId); toast.success('已移除'); loadMembers(); }
|
||||
catch { toast.error('移除失败'); }
|
||||
try { await pipelineApi.removeMember(pipelineCode, memberId); toast.success(t('pipeline.memberRemoved')); loadMembers(); }
|
||||
catch { toast.error(t('common.operationFailed')); }
|
||||
};
|
||||
|
||||
const envBadge = environments.find(e => String(e.id) === String(environmentId));
|
||||
|
||||
const editorTabs = [
|
||||
{ id: 'flow', label: '流程配置' },
|
||||
{ id: 'triggers', label: '触发设置' },
|
||||
{ id: 'variables', label: '变量' },
|
||||
{ id: 'runs', label: '发布记录', badge: runs.length || undefined },
|
||||
{ id: 'members', label: '成员管理', badge: members.length || undefined },
|
||||
{ id: 'flow', label: t('pipeline.flowConfig') },
|
||||
{ id: 'triggers', label: t('pipeline.triggerSettings') },
|
||||
{ id: 'variables', label: t('pipeline.variables') },
|
||||
{ id: 'runs', label: t('pipeline.releaseRecords'), badge: runs.length || undefined },
|
||||
{ id: 'members', label: t('pipeline.memberManage'), badge: members.length || undefined },
|
||||
];
|
||||
|
||||
if (!loading && !pipeline) {
|
||||
return (
|
||||
<PageShell variant="workspace" title="流水线编辑" icon={<GitBranch className="h-5 w-5" />}>
|
||||
<PageEmpty title="流水线不存在" description="请检查链接或返回列表重试" />
|
||||
<PageShell variant="workspace" title={t('pipeline.pipelineEditor')} icon={<GitBranch className="h-5 w-5" />}>
|
||||
<PageEmpty title={t('pipeline.pipelineNotExist')} description={t('pipeline.checkLinkOrRetry')} />
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -352,7 +362,7 @@ export default function PipelineEditor() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title={pipeline?.name || '流水线编辑'}
|
||||
title={pipeline?.name || t('pipeline.pipelineEditor')}
|
||||
subtitle={pipeline ? `code: ${pipeline.code} · ${pipeline.description || ''}` : undefined}
|
||||
icon={<GitBranch className="h-5 w-5" />}
|
||||
loading={loading}
|
||||
@@ -367,10 +377,10 @@ export default function PipelineEditor() {
|
||||
</Badge>
|
||||
)}
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={handleSave} disabled={saving}>
|
||||
<Save className="w-4 h-4 mr-1" />{saving ? '保存中...' : '保存'}
|
||||
<Save className="w-4 h-4 mr-1" />{saving ? t('common.loading') : t('common.save')}
|
||||
</Button>
|
||||
<Button className="dg-btn-primary" onClick={handleTrigger}>
|
||||
<Play className="w-4 h-4 mr-1" />运行
|
||||
<Play className="w-4 h-4 mr-1" />{t('pipeline.run')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -379,11 +389,11 @@ export default function PipelineEditor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 flex items-center gap-3">
|
||||
<Shield className="w-4 h-4 text-[var(--dg-text-secondary)]" />
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">部署环境:</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{t('pipeline.deployEnv')}:</span>
|
||||
<Select value={String(environmentId)} onValueChange={v => setEnvironmentId(v === '__none' ? '' : v)}>
|
||||
<SelectTrigger className="w-40 h-8 text-sm"><SelectValue placeholder="选择环境" /></SelectTrigger>
|
||||
<SelectTrigger className="w-40 h-8 text-sm"><SelectValue placeholder={t('pipeline.selectEnv')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none">未指定</SelectItem>
|
||||
<SelectItem value="__none">{t('pipeline.unspecified')}</SelectItem>
|
||||
{environments.map(env => (
|
||||
<SelectItem key={env.id} value={String(env.id)}>
|
||||
<span className="flex items-center gap-1.5">
|
||||
@@ -395,11 +405,11 @@ export default function PipelineEditor() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Server className="w-4 h-4 text-[var(--dg-text-secondary)] ml-2" />
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">构建节点:</span>
|
||||
<span className="text-sm text-[var(--dg-text-secondary)]">{t('pipeline.buildNode')}:</span>
|
||||
<Select value={buildNodeId || '__auto'} onValueChange={v => setBuildNodeId(v === '__auto' ? '' : v)}>
|
||||
<SelectTrigger className="w-48 h-8 text-sm"><SelectValue placeholder="自动调度" /></SelectTrigger>
|
||||
<SelectTrigger className="w-48 h-8 text-sm"><SelectValue placeholder={t('pipeline.autoSchedule')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__auto">自动调度(Master 优先)</SelectItem>
|
||||
<SelectItem value="__auto">{t('pipeline.autoScheduleMaster')}</SelectItem>
|
||||
{buildNodes.map(n => (
|
||||
<SelectItem key={n.id} value={String(n.id)}>
|
||||
{n.name} ({n.running_count || 0}/{n.max_executors})
|
||||
@@ -407,7 +417,7 @@ export default function PipelineEditor() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)] opacity-70">Master 满载时自动调度到空闲远程节点</span>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)] opacity-70">{t('pipeline.masterFullAutoSchedule')}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -417,11 +427,11 @@ export default function PipelineEditor() {
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-[var(--dg-text-primary)]">流水线阶段 ({stages.length})</span>
|
||||
<Button size="sm" onClick={handleAddStage}><Plus className="w-3.5 h-3.5 mr-1" />添加阶段</Button>
|
||||
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{t('deploy.pipeline.stages', { count: stages.length })}</span>
|
||||
<Button size="sm" onClick={handleAddStage}><Plus className="w-3.5 h-3.5 mr-1" />{t('pipeline.addStage')}</Button>
|
||||
</div>
|
||||
{stages.length === 0 ? (
|
||||
<div className="text-center py-12 border-2 border-dashed rounded-lg text-[var(--dg-text-secondary)]">点击「添加阶段」开始</div>
|
||||
<div className="text-center py-12 border-2 border-dashed rounded-lg text-[var(--dg-text-secondary)]">{t('deploy.pipeline.clickToAddStage')}</div>
|
||||
) : (
|
||||
stages.map((stage, idx) => {
|
||||
const isExpanded = expandedStages[stage.id] !== false; // default expanded
|
||||
@@ -440,16 +450,16 @@ export default function PipelineEditor() {
|
||||
className="h-7 text-sm font-medium border-none bg-transparent shadow-none flex-1 px-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{stage.tasks.length} 任务</span>
|
||||
<Badge variant="secondary" className="text-xs">{stageTypeLabels[stage.type] || stage.type}</Badge>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{stage.tasks.length} {t('pipeline.tasks')}</span>
|
||||
<Badge variant="secondary" className="text-xs">{stageTypeLabelKeys[stage.type] ? t(stageTypeLabelKeys[stage.type]) : stage.type}</Badge>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={(e) => { e.stopPropagation(); handleMoveStage(idx, -1); }} disabled={idx === 0}><ChevronUp className="w-3.5 h-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={(e) => { e.stopPropagation(); handleMoveStage(idx, 1); }} disabled={idx === stages.length - 1}><ChevronDown className="w-3.5 h-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 text-red-500" onClick={(e) => { e.stopPropagation(); handleDeleteStage(idx); }}><Trash2 className="w-3.5 h-3.5" /></Button>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="border border-[var(--dg-border)] rounded-b-md p-3 space-y-2">
|
||||
{stage.tasks.map(t => (<TaskCard key={t.id} task={t} selected={selectedTaskId === t.id} onSelect={() => setSelectedTaskId(selectedTaskId === t.id ? null : t.id)} onDelete={() => handleDeleteTask(t.id)} />))}
|
||||
<Button variant="outline" size="sm" className="w-full h-8 text-xs" onClick={() => handleAddTask(idx)}><Plus className="w-3 h-3 mr-1" />添加任务</Button>
|
||||
{stage.tasks.map(tk => (<TaskCard key={tk.id} task={tk} selected={selectedTaskId === tk.id} onSelect={() => setSelectedTaskId(selectedTaskId === tk.id ? null : tk.id)} onDelete={() => handleDeleteTask(tk.id)} />))}
|
||||
<Button variant="outline" size="sm" className="w-full h-8 text-xs" onClick={() => handleAddTask(idx)}><Plus className="w-3 h-3 mr-1" />{t('pipeline.addTask')}</Button>
|
||||
</div>
|
||||
)}
|
||||
{idx < stages.length - 1 && <div className="flex justify-center py-1"><ChevronRight className="w-4 h-4 rotate-90 text-[var(--dg-text-secondary)] opacity-30" /></div>}
|
||||
@@ -459,7 +469,7 @@ export default function PipelineEditor() {
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[360px] flex-shrink-0 border border-[var(--dg-border)] rounded-lg p-4 bg-[var(--dg-bg-secondary)]">
|
||||
<div className="flex items-center justify-between mb-3"><span className="text-sm font-medium text-[var(--dg-text-primary)]">任务编辑</span>{selectedTask && <Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => setSelectedTaskId(null)}>关闭</Button>}</div>
|
||||
<div className="flex items-center justify-between mb-3"><span className="text-sm font-medium text-[var(--dg-text-primary)]">{t('pipeline.taskEdit')}</span>{selectedTask && <Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => setSelectedTaskId(null)}>{t('common.close')}</Button>}</div>
|
||||
<TaskEditor task={selectedTask} onUpdate={handleUpdateTask} onClose={() => setSelectedTaskId(null)} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -469,18 +479,18 @@ export default function PipelineEditor() {
|
||||
<TabsContent value="triggers" className="mt-3">
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><GitBranch className="w-5 h-5 dg-text-blue" /><span className="font-medium">Git Push 触发</span></div><Switch checked={triggerMode === 'git_push'} onCheckedChange={v => setTriggerMode(v ? 'git_push' : 'manual')} /></div>
|
||||
{triggerMode === 'git_push' && <div className="space-y-1"><Label>监听分支</Label><Input value={watchBranches} onChange={e => setWatchBranches(e.target.value)} placeholder="master, release/*" className="h-9" /></div>}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><GitBranch className="w-5 h-5 dg-text-blue" /><span className="font-medium">{t('pipeline.gitPushTrigger')}</span></div><Switch checked={triggerMode === 'git_push'} onCheckedChange={v => setTriggerMode(v ? 'git_push' : 'manual')} /></div>
|
||||
{triggerMode === 'git_push' && <div className="space-y-1"><Label>{t('pipeline.listenBranch')}</Label><Input value={watchBranches} onChange={e => setWatchBranches(e.target.value)} placeholder="master, release/*" className="h-9" /></div>}
|
||||
</CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Clock className="w-5 h-5 dg-text-blue" /><span className="font-medium">定时触发</span></div><Switch checked={triggerMode === 'cron'} onCheckedChange={v => setTriggerMode(v ? 'cron' : 'manual')} /></div>
|
||||
{triggerMode === 'cron' && <div className="space-y-1"><Label>Cron 表达式</Label><Input value={cronExpr} onChange={e => setCronExpr(e.target.value)} placeholder="0 2 * * *" className="h-9 font-mono" /></div>}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Clock className="w-5 h-5 dg-text-blue" /><span className="font-medium">{t('pipeline.cronTrigger')}</span></div><Switch checked={triggerMode === 'cron'} onCheckedChange={v => setTriggerMode(v ? 'cron' : 'manual')} /></div>
|
||||
{triggerMode === 'cron' && <div className="space-y-1"><Label>{t('pipeline.cronExpr')}</Label><Input value={cronExpr} onChange={e => setCronExpr(e.target.value)} placeholder="0 2 * * *" className="h-9 font-mono" /></div>}
|
||||
</CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Webhook className="w-5 h-5 dg-text-blue" /><span className="font-medium">Webhook 触发</span></div><Switch checked={triggerMode === 'webhook'} onCheckedChange={v => setTriggerMode(v ? 'webhook' : 'manual')} /></div>
|
||||
{triggerMode === 'webhook' && <div className="space-y-1"><Label>Secret Token</Label><Input value={webhookToken} onChange={e => setWebhookToken(e.target.value)} type="password" placeholder="输入签名密钥" className="h-9" /></div>}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Webhook className="w-5 h-5 dg-text-blue" /><span className="font-medium">{t('pipeline.webhookTrigger')}</span></div><Switch checked={triggerMode === 'webhook'} onCheckedChange={v => setTriggerMode(v ? 'webhook' : 'manual')} /></div>
|
||||
{triggerMode === 'webhook' && <div className="space-y-1"><Label>Secret Token</Label><Input value={webhookToken} onChange={e => setWebhookToken(e.target.value)} type="password" placeholder={t('pipeline.inputSignKey')} className="h-9" /></div>}
|
||||
</CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Hand className="w-5 h-5 dg-text-blue" /><span className="font-medium">手动触发</span><Badge variant="secondary" className="text-xs">默认启用</Badge></div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Hand className="w-5 h-5 dg-text-blue" /><span className="font-medium">{t('pipeline.manualTrigger')}</span><Badge variant="secondary" className="text-xs">{t('pipeline.defaultEnabled')}</Badge></div></CardContent></Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -495,28 +505,28 @@ export default function PipelineEditor() {
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-500" onClick={() => setVariables(variables.filter((_, j) => j !== i))}><Trash2 className="w-3.5 h-3.5" /></Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={() => setVariables([...variables, ['', '']])}><Plus className="w-3.5 h-3.5 mr-1" />添加变量</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setVariables([...variables, ['', '']])}><Plus className="w-3.5 h-3.5 mr-1" />{t('pipeline.addVar')}</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* ====== Runs Tab ====== */}
|
||||
<TabsContent value="runs" className="mt-3">
|
||||
{runs.length === 0 ? (
|
||||
<div className="text-center py-12 text-[var(--dg-text-secondary)]">暂无执行记录</div>
|
||||
<div className="text-center py-12 text-[var(--dg-text-secondary)]">{t('pipeline.noRecords')}</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-[var(--dg-border)] overflow-hidden">
|
||||
<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 text-left text-[var(--dg-text-secondary)]">#</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">状态</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">触发方式</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">触发人</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">节点</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">分支</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">耗时</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">时间</th>
|
||||
<th className="px-3 py-2 text-right text-[var(--dg-text-secondary)]">操作</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">{t('common.status')}</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">{t('pipeline.triggerMode')}</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">{t('pipeline.triggerBy')}</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">{t('pipeline.buildNode')}</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">{t('pipeline.branch')}</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">{t('pipeline.duration')}</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--dg-text-secondary)]">{t('common.createdAt')}</th>
|
||||
<th className="px-3 py-2 text-right text-[var(--dg-text-secondary)]">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -524,18 +534,18 @@ export default function PipelineEditor() {
|
||||
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">#{r.build_number || r.id}</td>
|
||||
<td className="px-3 py-2">
|
||||
{r.status === 'success' ? <Badge className="bg-emerald-500/20 dg-text-green border-emerald-500/30"><Check className="w-3 h-3 mr-0.5" />成功</Badge> :
|
||||
r.status === 'failed' ? <Badge className="bg-red-500/20 dg-text-red border-red-500/30"><X className="w-3 h-3 mr-0.5" />失败</Badge> :
|
||||
r.status === 'running' ? <Badge className="bg-blue-500/20 dg-text-blue border-blue-500/30"><Loader2 className="w-3 h-3 animate-spin mr-0.5" />执行中</Badge> :
|
||||
r.status === 'cancelled' ? <Badge className="bg-yellow-500/20 dg-text-yellow border-yellow-500/30"><StopCircle className="w-3 h-3 mr-0.5" />已停止</Badge> :
|
||||
{r.status === 'success' ? <Badge className="bg-emerald-500/20 dg-text-green border-emerald-500/30"><Check className="w-3 h-3 mr-0.5" />{t('common.success')}</Badge> :
|
||||
r.status === 'failed' ? <Badge className="bg-red-500/20 dg-text-red border-red-500/30"><X className="w-3 h-3 mr-0.5" />{t('common.failed')}</Badge> :
|
||||
r.status === 'running' ? <Badge className="bg-blue-500/20 dg-text-blue border-blue-500/30"><Loader2 className="w-3 h-3 animate-spin mr-0.5" />{t('common.executing')}</Badge> :
|
||||
r.status === 'cancelled' ? <Badge className="bg-yellow-500/20 dg-text-yellow border-yellow-500/30"><StopCircle className="w-3 h-3 mr-0.5" />{t('pipeline.stopped')}</Badge> :
|
||||
<Badge variant="secondary" className="text-[var(--dg-text-secondary)]">{r.status}</Badge>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] text-xs">
|
||||
{r.trigger_mode === 'manual' && <span className="flex items-center gap-1"><Hand className="w-3 h-3" />手动</span>}
|
||||
{r.trigger_mode === 'cron' && <span className="flex items-center gap-1"><Clock className="w-3 h-3" />定时</span>}
|
||||
{r.trigger_mode === 'manual' && <span className="flex items-center gap-1"><Hand className="w-3 h-3" />{t('deploy.trigger.manual')}</span>}
|
||||
{r.trigger_mode === 'cron' && <span className="flex items-center gap-1"><Clock className="w-3 h-3" />{t('deploy.trigger.cron')}</span>}
|
||||
{r.trigger_mode === 'webhook' && <span className="flex items-center gap-1"><Webhook className="w-3 h-3" />Webhook</span>}
|
||||
{r.trigger_mode === 'git_push' && <span className="flex items-center gap-1"><GitBranch className="w-3 h-3" />Git Push</span>}
|
||||
{!r.trigger_mode && (r.triggered_by || '手动')}
|
||||
{!r.trigger_mode && (r.triggered_by || t('deploy.trigger.manual'))}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] text-xs">{r.triggered_by || 'admin'}</td>
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] text-xs">{r.build_node_name || 'Master'}</td>
|
||||
@@ -544,9 +554,9 @@ export default function PipelineEditor() {
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] text-xs">{r.created_at ? new Date(r.created_at).toLocaleString('zh-CN') : '-'}</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-blue hover:text-blue-300" onClick={() => handleViewLog(r)}><FileText className="w-3.5 h-3.5 mr-1" />日志</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-blue hover:text-blue-300" onClick={() => handleViewLog(r)}><FileText className="w-3.5 h-3.5 mr-1" />{t('pipeline.log')}</Button>
|
||||
{(r.status === 'running' || r.status === 'pending') && (
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleStop(r.id)}><StopCircle className="w-3.5 h-3.5 mr-1" />停止</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleStop(r.id)}><StopCircle className="w-3.5 h-3.5 mr-1" />{t('common.stop')}</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -566,25 +576,25 @@ export default function PipelineEditor() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<UserPlus className="w-4 h-4 dg-text-blue" />
|
||||
<span className="text-sm font-medium text-[var(--dg-text-primary)]">添加用户</span>
|
||||
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{t('pipeline.addUser')}</span>
|
||||
</div>
|
||||
<div className="flex items-end gap-3 mt-3">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label className="text-xs">用户名</Label>
|
||||
<Input value={newMemberUser} onChange={e => setNewMemberUser(e.target.value)} placeholder="输入用户名" className="h-9" />
|
||||
<Label className="text-xs">{t('pipeline.username')}</Label>
|
||||
<Input value={newMemberUser} onChange={e => setNewMemberUser(e.target.value)} placeholder={t('pipeline.usernamePlaceholder')} className="h-9" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">角色</Label>
|
||||
<Label className="text-xs">{t('pipeline.role')}</Label>
|
||||
<Select value={newMemberRole} onValueChange={setNewMemberRole}>
|
||||
<SelectTrigger className="h-9 w-28"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="owner">拥有者</SelectItem>
|
||||
<SelectItem value="editor">编辑者</SelectItem>
|
||||
<SelectItem value="viewer">查看者</SelectItem>
|
||||
<SelectItem value="owner">{t('pipeline.owner')}</SelectItem>
|
||||
<SelectItem value="editor">{t('pipeline.editor')}</SelectItem>
|
||||
<SelectItem value="viewer">{t('pipeline.viewer')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button onClick={handleAddMember} className="h-9"><Plus className="w-4 h-4 mr-1" />添加</Button>
|
||||
<Button onClick={handleAddMember} className="h-9"><Plus className="w-4 h-4 mr-1" />{t('common.add')}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -599,12 +609,12 @@ export default function PipelineEditor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-0">
|
||||
<div className="px-4 py-2 text-xs font-medium text-[var(--dg-text-secondary)] border-b border-[var(--dg-border)]">
|
||||
👤 直接用户授权 ({directUsers.length})
|
||||
👤 {t('pipeline.directUserAuth')} ({directUsers.length})
|
||||
</div>
|
||||
{loadingMembers ? (
|
||||
<div className="flex items-center justify-center py-6"><Loader2 className="w-5 h-5 animate-spin text-[var(--dg-text-secondary)]" /></div>
|
||||
) : directUsers.length === 0 ? (
|
||||
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">暂无直接用户</div>
|
||||
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">{t('pipeline.noDirectUser')}</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--dg-border)]">
|
||||
{directUsers.map((m: JsonData) => (
|
||||
@@ -613,10 +623,10 @@ export default function PipelineEditor() {
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 dg-text-blue" /></div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{m.username}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="dg-text-yellow">拥有者</span> : m.role === 'editor' ? <span className="dg-text-blue">编辑者</span> : <span className="dg-text-secondary">查看者</span>}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="dg-text-yellow">{t('pipeline.owner')}</span> : m.role === 'editor' ? <span className="dg-text-blue">{t('pipeline.editor')}</span> : <span className="dg-text-secondary">{t('pipeline.viewer')}</span>}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />移除</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />{t('pipeline.removeMember')}</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -628,11 +638,11 @@ export default function PipelineEditor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-0">
|
||||
<div className="px-4 py-2 text-xs font-medium text-[var(--dg-text-secondary)] border-b border-[var(--dg-border)]">
|
||||
👥 用户组授权 ({groupAssignments.length})
|
||||
👥 {t('pipeline.groupAuth')} ({groupAssignments.length})
|
||||
</div>
|
||||
{groupAssignments.length === 0 ? (
|
||||
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">
|
||||
暂无组授权 — 在「<a href="/deploy/groups" className="dg-text-blue underline">用户组管理</a>」页面创建组后,可在此授权
|
||||
{t('pipeline.noGroupAuthHint', { defaultValue: t('pipeline.noGroupAuthHint') })}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--dg-border)]">
|
||||
@@ -641,11 +651,11 @@ export default function PipelineEditor() {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 dg-text-purple" /></div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{m.group?.name || `组 #${m.group_id}`}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="dg-text-yellow">拥有者</span> : m.role === 'editor' ? <span className="dg-text-blue">编辑者</span> : <span className="dg-text-secondary">查看者</span>} · 组内全体成员</div>
|
||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{m.group?.name || `${t('pipeline.groupLabel')} #${m.group_id}`}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="dg-text-yellow">{t('pipeline.owner')}</span> : m.role === 'editor' ? <span className="dg-text-blue">{t('pipeline.editor')}</span> : <span className="dg-text-secondary">{t('pipeline.viewer')}</span>} · {t('pipeline.allGroupMembers')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />撤销授权</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />{t('pipeline.revokeAuth')}</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -39,36 +40,37 @@ export function PipelineFormDialog({
|
||||
setFormData: (data: PipelineFormData) => void;
|
||||
onSave: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editMode ? '编辑流水线' : '新建流水线'}</DialogTitle>
|
||||
<DialogTitle>{editMode ? t('deploy.pipeline.editPipeline') : t('deploy.pipeline.newPipeline')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||
{editMode ? '修改流水线配置' : '配置新的 CI/CD 流水线'}
|
||||
{editMode ? t('deploy.pipeline.modifyConfig') : t('deploy.pipeline.configureNew')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">流水线名称</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.pipeline.name')}</Label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="例如: frontend-build"
|
||||
placeholder={t('deploy.formDialog.namePlaceholder')}
|
||||
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('common.description')}</Label>
|
||||
<Textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="流水线描述..."
|
||||
placeholder={t('deploy.formDialog.descPlaceholder')}
|
||||
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">代码仓库</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.table.codeCol')}</Label>
|
||||
<Input
|
||||
value={formData.source}
|
||||
onChange={(e) => setFormData({ ...formData, source: e.target.value })}
|
||||
@@ -77,7 +79,7 @@ export function PipelineFormDialog({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">默认分支</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.table.defaultBranch')}</Label>
|
||||
<Input
|
||||
value={formData.branch}
|
||||
onChange={(e) => setFormData({ ...formData, branch: e.target.value })}
|
||||
@@ -87,7 +89,7 @@ export function PipelineFormDialog({
|
||||
</div>
|
||||
{!editMode && (
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">选择模板</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.template.selectTemplate')}</Label>
|
||||
<div className="grid grid-cols-2 gap-3 mt-1">
|
||||
{pipelineTemplates.map((template) => (
|
||||
<Card
|
||||
@@ -123,10 +125,10 @@ export function PipelineFormDialog({
|
||||
</div>
|
||||
<DialogFooter className="mt-4 pt-4 border-t border-[var(--dg-border)]">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={onSave}>
|
||||
{editMode ? '保存修改' : '创建流水线'}
|
||||
{editMode ? t('deploy.pipeline.editMode') : t('deploy.pipeline.createMode')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -25,24 +26,25 @@ export function PipelineRunDialog({
|
||||
selectedPipeline?: JsonData;
|
||||
onRun: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>执行流水线</DialogTitle>
|
||||
<DialogTitle>{t('deploy.runDialog.title')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">{selectedPipeline?.name}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">分支</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.run.branch')}</Label>
|
||||
<Input defaultValue={selectedPipeline?.branch} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">Commit ID (可选)</Label>
|
||||
<Input placeholder="留空则使用最新提交" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1" />
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.form.commitId')}</Label>
|
||||
<Input placeholder={t('deploy.runDialog.commitPlaceholder')} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">环境变量 (可选)</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.var.envVarOptional')}</Label>
|
||||
<Textarea
|
||||
placeholder="KEY=value KEY2=value2"
|
||||
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1 font-mono text-sm"
|
||||
@@ -51,11 +53,11 @@ export function PipelineRunDialog({
|
||||
</div>
|
||||
<DialogFooter className="mt-4 pt-4 border-t border-[var(--dg-border)]">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
取消
|
||||
{t('deploy.form.cancel')}
|
||||
</Button>
|
||||
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={onRun}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
开始执行
|
||||
{t('deploy.runDialog.startExec')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// 新建发布弹窗(应用/版本/环境/Commit/说明 + 提交申请) // Logan.Li
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -39,6 +40,7 @@ const EMPTY_FORM = {
|
||||
};
|
||||
|
||||
export function ReleaseCreateDialog({ open, onOpenChange, onSuccess }: ReleaseCreateDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [form, setForm] = useState({ ...EMPTY_FORM });
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
@@ -48,7 +50,7 @@ export function ReleaseCreateDialog({ open, onOpenChange, onSuccess }: ReleaseCr
|
||||
// 提交发布申请 // Logan.Li
|
||||
const handleSubmit = async () => {
|
||||
if (!form.appName.trim() || !form.version.trim()) {
|
||||
toast.error('请填写应用名称和版本号');
|
||||
toast.error(t('deploy.release.nameVerRequired'));
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
@@ -61,12 +63,12 @@ export function ReleaseCreateDialog({ open, onOpenChange, onSuccess }: ReleaseCr
|
||||
commit_msg: form.commitMsg.trim(),
|
||||
description: form.description.trim(),
|
||||
});
|
||||
toast.success('发布申请已提交');
|
||||
toast.success(t('deploy.toast.releaseSubmitted'));
|
||||
onOpenChange(false);
|
||||
setForm({ ...EMPTY_FORM });
|
||||
onSuccess?.();
|
||||
} catch {
|
||||
toast.error('创建失败');
|
||||
toast.error(t('deploy.toast.createFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -76,22 +78,22 @@ export function ReleaseCreateDialog({ open, onOpenChange, onSuccess }: ReleaseCr
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl dg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">新建发布</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">创建新的应用版本发布申请</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">{t('deploy.release.newRelease')}</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">{t('deploy.release.createNew')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">应用名称</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.release.appName')}</Label>
|
||||
<Input
|
||||
value={form.appName}
|
||||
onChange={(e) => update('appName', e.target.value)}
|
||||
placeholder="如:用户中心服务"
|
||||
placeholder={t('deploy.releaseCreate.appPlaceholder')}
|
||||
className="mt-1 dg-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">版本号</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.release.versionNo')}</Label>
|
||||
<Input
|
||||
value={form.version}
|
||||
onChange={(e) => update('version', e.target.value)}
|
||||
@@ -100,7 +102,7 @@ export function ReleaseCreateDialog({ open, onOpenChange, onSuccess }: ReleaseCr
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">目标环境</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.release.targetEnv')}</Label>
|
||||
<Select
|
||||
value={form.environment}
|
||||
onValueChange={(value) => update('environment', value)}
|
||||
@@ -109,10 +111,10 @@ export function ReleaseCreateDialog({ open, onOpenChange, onSuccess }: ReleaseCr
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="dev">开发环境</SelectItem>
|
||||
<SelectItem value="test">测试环境</SelectItem>
|
||||
<SelectItem value="staging">预发布</SelectItem>
|
||||
<SelectItem value="prod">生产环境</SelectItem>
|
||||
<SelectItem value="dev">{t('deploy.env.devEnv')}</SelectItem>
|
||||
<SelectItem value="test">{t('deploy.env.testEnv')}</SelectItem>
|
||||
<SelectItem value="staging">{t('deploy.env.staging')}</SelectItem>
|
||||
<SelectItem value="prod">{t('deploy.env.prodEnv')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -128,32 +130,32 @@ export function ReleaseCreateDialog({ open, onOpenChange, onSuccess }: ReleaseCr
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">提交信息</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.releaseCreate.commitMsg')}</Label>
|
||||
<Input
|
||||
value={form.commitMsg}
|
||||
onChange={(e) => update('commitMsg', e.target.value)}
|
||||
placeholder="feat: 新增功能"
|
||||
placeholder={t('deploy.releaseCreate.commitMsgPlaceholder')}
|
||||
className="mt-1 dg-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-secondary)]">发布说明</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)]">{t('deploy.release.releaseDesc')}</Label>
|
||||
<Textarea
|
||||
value={form.description}
|
||||
onChange={(e) => update('description', e.target.value)}
|
||||
placeholder="描述本次发布的内容..."
|
||||
placeholder={t('deploy.releaseCreate.descPlaceholder')}
|
||||
className="mt-1 dg-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
{t('deploy.form.cancel')}
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Plus className="h-4 w-4 mr-2" />}
|
||||
提交申请
|
||||
{t('deploy.release.submitApp')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// 发布详情弹窗(流水线进度 + 构建日志 + 重新部署) // Logan.Li
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
import { Rocket, CheckCircle, Loader2, RotateCcw } from 'lucide-react';
|
||||
import { releaseApi } from '@/api';
|
||||
import { toast } from 'sonner';
|
||||
import { PIPELINE_STEPS } from './releaseHelpers';
|
||||
import { getPipelineSteps } from './releaseHelpers';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface ReleaseDetailDialogProps {
|
||||
@@ -25,6 +26,7 @@ interface ReleaseDetailDialogProps {
|
||||
}
|
||||
|
||||
export function ReleaseDetailDialog({ release, open, onOpenChange, onSuccess }: ReleaseDetailDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// 重新触发部署 // Logan.Li
|
||||
@@ -33,16 +35,18 @@ export function ReleaseDetailDialog({ release, open, onOpenChange, onSuccess }:
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await releaseApi.deployRelease(release.id);
|
||||
toast.success('已重新触发部署');
|
||||
toast.success(t('deploy.toast.redeployed'));
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} catch {
|
||||
toast.error('重新部署失败');
|
||||
toast.error(t('deploy.toast.redeployFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const pipelineSteps = getPipelineSteps(t);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl dg-card">
|
||||
@@ -55,16 +59,16 @@ export function ReleaseDetailDialog({ release, open, onOpenChange, onSuccess }:
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="dg-text-muted">
|
||||
环境: {release?.environment}
|
||||
{t('deploy.releaseDialog.envLabel')} {release?.environment}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Pipeline Steps */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)]">发布进度</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)]">{t('deploy.release.releaseProgress')}</h4>
|
||||
<div className="space-y-2">
|
||||
{PIPELINE_STEPS.map((step, index) => (
|
||||
{pipelineSteps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
@@ -99,7 +103,7 @@ export function ReleaseDetailDialog({ release, open, onOpenChange, onSuccess }:
|
||||
|
||||
{/* Build Logs */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)]">构建日志</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)]">{t('deploy.run.buildLog')}</h4>
|
||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4 font-mono text-xs dg-text-secondary h-48 overflow-auto">
|
||||
<div className="space-y-1">
|
||||
<p>[10:00:00] Starting pipeline for {release?.app_name}...</p>
|
||||
@@ -129,7 +133,7 @@ export function ReleaseDetailDialog({ release, open, onOpenChange, onSuccess }:
|
||||
disabled={submitting}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
重新部署
|
||||
{t('deploy.release.redeploy')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -42,6 +43,7 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
|
||||
export function ReleaseManager() {
|
||||
const { t } = useTranslation();
|
||||
const _confirm = useConfirm();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [envFilter, setEnvFilter] = useState<string>('all');
|
||||
@@ -188,8 +190,8 @@ export function ReleaseManager() {
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-medium text-[var(--dg-text-primary)]">{release.app_name}</h3>
|
||||
{getStatusBadge(release.status)}
|
||||
{getEnvironmentBadge(release.environment)}
|
||||
{getStatusBadge(release.status, t)}
|
||||
{getEnvironmentBadge(release.environment, t)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-sm text-[var(--dg-text-secondary)]">
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -302,8 +304,8 @@ export function ReleaseManager() {
|
||||
<ScrollArea className="max-h-[60vh]">
|
||||
<div className="space-y-3 py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusBadge(selectedRelease.status)}
|
||||
{getEnvironmentBadge(selectedRelease.environment)}
|
||||
{getStatusBadge(selectedRelease.status, t)}
|
||||
{getEnvironmentBadge(selectedRelease.environment, t)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshCw, Loader2, Download, CheckCircle, XCircle, Clock, Settings2, ChevronDown, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -21,8 +22,8 @@ interface VersionOption {
|
||||
version: string; label: string;
|
||||
}
|
||||
|
||||
const categoryLabel: Record<string, string> = {
|
||||
build: '构建', deploy: '部署', automation: '自动化', other: '其他',
|
||||
const categoryLabelKey: Record<string, string> = {
|
||||
build: 'pipeline.build', deploy: 'pipeline.deploy', automation: 'pipeline.automation', other: 'pipeline.other',
|
||||
};
|
||||
const categoryColor: Record<string, string> = {
|
||||
build: 'bg-amber-500/20 dg-text-yellow border-amber-500/30',
|
||||
@@ -34,6 +35,7 @@ const categoryColor: Record<string, string> = {
|
||||
const VERSIONED_TOOLS = ['jdk', 'golang', 'nodejs', 'maven', 'nvm'];
|
||||
|
||||
export default function ToolConfigTab() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const [tools, setTools] = useState<ToolConfig[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -202,7 +204,7 @@ export default function ToolConfigTab() {
|
||||
</td>
|
||||
<td className="px-3 py-2 hidden sm:table-cell">
|
||||
<Badge className={`text-xs ${categoryColor[tool.category] || categoryColor.other}`}>
|
||||
{categoryLabel[tool.category] || tool.category}
|
||||
{categoryLabelKey[tool.category] || tool.category}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -50,40 +51,41 @@ export function JobRoleCreateDialog({
|
||||
onDescriptionChange,
|
||||
onSubmit,
|
||||
}: JobRoleCreateDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const preview = useMemo(() => matchPipelines(pattern, pipelines), [pattern, pipelines]);
|
||||
const patternHint = useMemo(() => {
|
||||
if (!pattern.trim()) return '输入正则,匹配流水线 code 或 Job 名称';
|
||||
if (!pattern.trim()) return t('rbac.jobRole.patternHint.empty');
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new RegExp(pattern);
|
||||
return preview.length > 0 ? `命中 ${preview.length} 个发布任务` : '无匹配任务';
|
||||
return preview.length > 0 ? t('rbac.jobRole.patternHint.matched', { count: preview.length }) : t('rbac.jobRole.patternHint.noMatch');
|
||||
} catch {
|
||||
return '正则语法错误';
|
||||
return t('rbac.jobRole.patternHint.syntaxError');
|
||||
}
|
||||
}, [pattern, preview.length]);
|
||||
}, [pattern, preview.length, t]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建 Job 权限</DialogTitle>
|
||||
<DialogTitle>{t('rbac.jobRole.newTitle')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">
|
||||
按正则匹配流水线 code 或 Job 名称,分配 owner / editor / viewer 权限
|
||||
{t('rbac.jobRole.desc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">名称</Label>
|
||||
<Input value={name} onChange={(e) => onNameChange(e.target.value)} placeholder="后端发布" className="h-8 text-sm dg-input" />
|
||||
<Label className="text-xs">{t('rbac.jobRole.label.name')}</Label>
|
||||
<Input value={name} onChange={(e) => onNameChange(e.target.value)} placeholder={t('rbac.jobRole.placeholder.name')} className="h-8 text-sm dg-input" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">标识</Label>
|
||||
<Input value={code} onChange={(e) => onCodeChange(e.target.value)} placeholder="backend-jobs" className="h-8 text-sm font-mono dg-input" />
|
||||
<Label className="text-xs">{t('rbac.jobRole.label.code')}</Label>
|
||||
<Input value={code} onChange={(e) => onCodeChange(e.target.value)} placeholder={t('rbac.jobRole.placeholder.code')} className="h-8 text-sm font-mono dg-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">匹配正则</Label>
|
||||
<Label className="text-xs">{t('rbac.jobRole.label.pattern')}</Label>
|
||||
<Input
|
||||
value={pattern}
|
||||
onChange={(e) => onPatternChange(e.target.value)}
|
||||
@@ -93,26 +95,26 @@ export function JobRoleCreateDialog({
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{patternHint}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">权限级别</Label>
|
||||
<Label className="text-xs">{t('rbac.jobRole.label.roleLevel')}</Label>
|
||||
<Select value={role} onValueChange={onRoleChange}>
|
||||
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="owner">拥有者 — 配置 + 执行 + 授权</SelectItem>
|
||||
<SelectItem value="editor">编辑者 — 配置 + 执行</SelectItem>
|
||||
<SelectItem value="viewer">查看者 — 只读</SelectItem>
|
||||
<SelectItem value="owner">{t('rbac.jobRole.role.owner')}</SelectItem>
|
||||
<SelectItem value="editor">{t('rbac.jobRole.role.editor')}</SelectItem>
|
||||
<SelectItem value="viewer">{t('rbac.jobRole.role.viewer')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">说明</Label>
|
||||
<Input value={description} onChange={(e) => onDescriptionChange(e.target.value)} placeholder="可选" className="h-8 text-sm dg-input" />
|
||||
<Label className="text-xs">{t('rbac.jobRole.label.description')}</Label>
|
||||
<Input value={description} onChange={(e) => onDescriptionChange(e.target.value)} placeholder={t('rbac.jobRole.placeholder.description')} className="h-8 text-sm dg-input" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('rbac.jobRole.cancel')}</Button>
|
||||
<Button onClick={onSubmit} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Save className="h-4 w-4 mr-1" />}
|
||||
创建
|
||||
{t('rbac.jobRole.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -51,6 +52,7 @@ export function PipelineCard({
|
||||
onStopRun,
|
||||
}: PipelineCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const stages = parseStages(pipeline.stages);
|
||||
const repo = pipeline.repo as Record<string, unknown> | undefined;
|
||||
const registry = pipeline.registry as Record<string, unknown> | undefined;
|
||||
@@ -92,12 +94,12 @@ export function PipelineCard({
|
||||
)}
|
||||
{pipeline.build_node_id && (
|
||||
<Badge variant="outline" className="text-[10px] gap-1">
|
||||
<Server className="h-3 w-3" />指定节点
|
||||
<Server className="h-3 w-3" />{t('deploy.pipelineCard.assignedNode')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--dg-text-secondary)] mt-1 line-clamp-1">
|
||||
{String(pipeline.description || '暂无描述')}
|
||||
{String(pipeline.description || t('deploy.pipeline.desc'))}
|
||||
</p>
|
||||
{/* 最近运行摘要行:branch + commit → image:tag // Logan.Li */}
|
||||
{(pipeline.last_run_branch || pipeline.last_commit_id || pipeline.last_image_tag) && (
|
||||
@@ -134,18 +136,18 @@ export function PipelineCard({
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs text-[var(--dg-text-secondary)] opacity-60">
|
||||
<span className="flex items-center gap-1">
|
||||
<GitBranch className="h-3 w-3" />
|
||||
{pipeline.deploy_mode === 'tag' ? 'Tag发版' : String(pipeline.branch || 'main')}
|
||||
{pipeline.deploy_mode === 'tag' ? t('deploy.pipelineCard.tagRelease') : String(pipeline.branch || 'main')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{deployTarget === 'k8s' ? (
|
||||
<>
|
||||
<Network className="h-3 w-3" />
|
||||
{k8sDep ? `${k8sNs}/${k8sDep}` : 'Kubernetes Deployment'}
|
||||
{k8sDep ? `${k8sNs}/${k8sDep}` : t('deploy.pipelineCard.k8sDeployment')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Server className="h-3 w-3" />
|
||||
{serverCount > 0 ? `${serverCount} 台主机` : 'SSH 主机'}
|
||||
{serverCount > 0 ? `${serverCount} ${t('deploy.pipelineCard.hostCountUnit')}` : t('deploy.banner.sshHost')}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -159,16 +161,16 @@ export function PipelineCard({
|
||||
<div className="flex items-center gap-1 ml-4 shrink-0">
|
||||
{pipeline.last_status === 'running' ? (
|
||||
<Button variant="destructive" size="sm" className="h-8" onClick={onStopRunning}>
|
||||
<StopCircle className="h-3.5 w-3.5 mr-1" />停止
|
||||
<StopCircle className="h-3.5 w-3.5 mr-1" />{t('deploy.pipelineCard.stop')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="default" size="sm" className="h-8" onClick={onTrigger}>
|
||||
<Play className="h-3.5 w-3.5 mr-1" />执行
|
||||
<Play className="h-3.5 w-3.5 mr-1" />{t('deploy.label.execute')}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={() => navigate(`/pipelines/${code}`)}>编辑</Button>
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={() => navigate(`/pipelines/${code}`)}>{t('common.edit')}</Button>
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={() => navigate(`/pipelines/${code}/runs`)}>
|
||||
<History className="h-3.5 w-3.5 mr-1" />记录
|
||||
<History className="h-3.5 w-3.5 mr-1" />{t('deploy.label.record')}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-8" onClick={onToggleExpand}>
|
||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 发布相关共享工具函数与常量 // Logan.Li
|
||||
import type { TFunction } from 'i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
CheckCircle,
|
||||
@@ -9,48 +10,48 @@ import {
|
||||
} from 'lucide-react';
|
||||
|
||||
// 发布状态徽章(列表 + 详情共用) // Logan.Li
|
||||
export function getStatusBadge( status: string) {
|
||||
export function getStatusBadge(status: string, t: TFunction) {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return (
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
成功
|
||||
{t('deploy.status.success')}
|
||||
</Badge>
|
||||
);
|
||||
case 'failed':
|
||||
return (
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
失败
|
||||
{t('deploy.status.failed')}
|
||||
</Badge>
|
||||
);
|
||||
case 'deploying':
|
||||
return (
|
||||
<Badge variant="outline" className="border-blue-500/30 dg-text-blue bg-blue-500/10 animate-pulse">
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
发布中
|
||||
{t('deploy.status.deploying')}
|
||||
</Badge>
|
||||
);
|
||||
case 'building':
|
||||
return (
|
||||
<Badge variant="outline" className="border-amber-500/30 dg-text-yellow bg-amber-500/10">
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
构建中
|
||||
{t('deploy.status.building')}
|
||||
</Badge>
|
||||
);
|
||||
case 'rolled_back':
|
||||
return (
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10">
|
||||
<RotateCcw className="h-3 w-3 mr-1" />
|
||||
已回滚
|
||||
{t('deploy.status.rolledBack')}
|
||||
</Badge>
|
||||
);
|
||||
case 'pending':
|
||||
return (
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
待发布
|
||||
{t('deploy.status.pendingRelease')}
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
@@ -59,34 +60,36 @@ export function getStatusBadge( status: string) {
|
||||
}
|
||||
|
||||
// 环境徽章 // Logan.Li
|
||||
export function getEnvBadge( env: string) {
|
||||
export function getEnvBadge(env: string, t: TFunction) {
|
||||
const colors: Record<string, string> = {
|
||||
dev: 'border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10',
|
||||
test: 'border-blue-500/30 dg-text-blue bg-blue-500/10',
|
||||
staging: 'border-amber-500/30 dg-text-yellow bg-amber-500/10',
|
||||
prod: 'border-emerald-500/30 dg-text-green bg-emerald-500/10',
|
||||
};
|
||||
const labels: Record<string, string> = {
|
||||
dev: '开发环境',
|
||||
test: '测试环境',
|
||||
staging: '预发布',
|
||||
prod: '生产环境',
|
||||
const labelKeys: Record<string, string> = {
|
||||
dev: 'deploy.env.devEnv',
|
||||
test: 'deploy.env.testEnv',
|
||||
staging: 'deploy.env.staging',
|
||||
prod: 'deploy.env.prodEnv',
|
||||
};
|
||||
return (
|
||||
<Badge variant="outline" className={colors[env] || colors.dev}>
|
||||
{labels[env] || env}
|
||||
{labelKeys[env] ? t(labelKeys[env]) : env}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// 模拟流水线步骤(详情弹窗使用) // Logan.Li
|
||||
export const PIPELINE_STEPS = [
|
||||
{ name: '代码检出', status: 'success', duration: '15s' },
|
||||
{ name: '依赖安装', status: 'success', duration: '45s' },
|
||||
{ name: '代码检查', status: 'success', duration: '30s' },
|
||||
{ name: '单元测试', status: 'success', duration: '2m 15s' },
|
||||
{ name: '构建镜像', status: 'success', duration: '3m 30s' },
|
||||
{ name: '推送镜像', status: 'success', duration: '1m 20s' },
|
||||
{ name: '部署到Kubernetes', status: 'running', duration: '--' },
|
||||
{ name: '健康检查', status: 'pending', duration: '--' },
|
||||
];
|
||||
export function getPipelineSteps(t: TFunction) {
|
||||
return [
|
||||
{ name: t('deploy.stage.checkout'), status: 'success', duration: '15s' },
|
||||
{ name: t('deploy.template.dependencyInstall'), status: 'success', duration: '45s' },
|
||||
{ name: t('deploy.releaseHelpers.pipelineStep.codeCheck'), status: 'success', duration: '30s' },
|
||||
{ name: t('deploy.template.unitTest'), status: 'success', duration: '2m 15s' },
|
||||
{ name: t('deploy.releaseHelpers.pipelineStep.buildImage'), status: 'success', duration: '3m 30s' },
|
||||
{ name: t('deploy.releaseHelpers.pipelineStep.pushImage'), status: 'success', duration: '1m 20s' },
|
||||
{ name: t('deploy.stage.deployDesc'), status: 'running', duration: '--' },
|
||||
{ name: t('deploy.template.healthCheck'), status: 'pending', duration: '--' },
|
||||
];
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// 发布管理(审批流)共享工具函数与常量 // Logan.Li
|
||||
// 注意:本文件专用于 ReleaseManager(含审批流,状态标签与 AppRelease 的 releaseHelpers 不同)
|
||||
import type { TFunction } from 'i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Clock,
|
||||
@@ -18,16 +19,23 @@ export const ENVIRONMENTS: Record<string, { label: string; color: string }> = {
|
||||
prod: { label: '生产环境', color: 'border-red-500/30 dg-text-red bg-red-500/10' },
|
||||
};
|
||||
|
||||
const ENV_LABEL_KEY: Record<string, string> = {
|
||||
dev: 'deploy.env.devEnv',
|
||||
test: 'deploy.env.testEnv',
|
||||
staging: 'deploy.env.stagingEnv',
|
||||
prod: 'deploy.env.prodEnv',
|
||||
};
|
||||
|
||||
// 发布状态徽章(审批流:待审批/已审批/已驳回/发布中/发布成功/发布失败/已回滚) // Logan.Li
|
||||
export function getStatusBadge( status: string) {
|
||||
const map: Record<string, { icon: any; label: string; cls: string }> = {
|
||||
pending: { icon: Clock, label: '待审批', cls: 'border-amber-500/30 dg-text-yellow bg-amber-500/10' },
|
||||
approved: { icon: Check, label: '已审批', cls: 'border-purple-500/30 dg-text-purple bg-purple-500/10' },
|
||||
rejected: { icon: XCircle, label: '已驳回', cls: 'border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10' },
|
||||
deploying: { icon: Loader2, label: '发布中', cls: 'border-blue-500/30 dg-text-blue bg-blue-500/10 animate-pulse' },
|
||||
success: { icon: CheckCircle, label: '发布成功', cls: 'border-emerald-500/30 dg-text-green bg-emerald-500/10' },
|
||||
failed: { icon: XCircle, label: '发布失败', cls: 'border-red-500/30 dg-text-red bg-red-500/10' },
|
||||
rolled_back: { icon: RotateCcw, label: '已回滚', cls: 'border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10' },
|
||||
export function getStatusBadge(status: string, t: TFunction) {
|
||||
const map: Record<string, { icon: any; labelKey: string; cls: string }> = {
|
||||
pending: { icon: Clock, labelKey: 'deploy.status.pendingApproval', cls: 'border-amber-500/30 dg-text-yellow bg-amber-500/10' },
|
||||
approved: { icon: Check, labelKey: 'deploy.status.approved', cls: 'border-purple-500/30 dg-text-purple bg-purple-500/10' },
|
||||
rejected: { icon: XCircle, labelKey: 'deploy.status.rejected', cls: 'border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10' },
|
||||
deploying: { icon: Loader2, labelKey: 'deploy.status.deploying', cls: 'border-blue-500/30 dg-text-blue bg-blue-500/10 animate-pulse' },
|
||||
success: { icon: CheckCircle, labelKey: 'deploy.status.succeeded', cls: 'border-emerald-500/30 dg-text-green bg-emerald-500/10' },
|
||||
failed: { icon: XCircle, labelKey: 'deploy.status.failed_deploy', cls: 'border-red-500/30 dg-text-red bg-red-500/10' },
|
||||
rolled_back: { icon: RotateCcw, labelKey: 'deploy.status.rolledBack', cls: 'border-[var(--dg-border)] dg-text-secondary bg-[var(--dg-bg-active)]/10' },
|
||||
};
|
||||
const cfg = map[status];
|
||||
if (!cfg) return null;
|
||||
@@ -35,17 +43,17 @@ export function getStatusBadge( status: string) {
|
||||
return (
|
||||
<Badge variant="outline" className={cfg.cls}>
|
||||
<Icon className="h-3 w-3 mr-1" />
|
||||
{cfg.label}
|
||||
{t(cfg.labelKey)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// 环境徽章 // Logan.Li
|
||||
export function getEnvironmentBadge( env: string) {
|
||||
export function getEnvironmentBadge(env: string, t: TFunction) {
|
||||
const cfg = ENVIRONMENTS[env];
|
||||
return (
|
||||
<Badge variant="outline" className={cfg?.color || 'border-[var(--dg-border)] dg-text-secondary'}>
|
||||
{cfg?.label || env}
|
||||
{ENV_LABEL_KEY[env] ? t(ENV_LABEL_KEY[env]) : env}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user