refactor(quality): Q25 前端巨型组件拆分 — PipelineCreateDialog 559L→498L (-11%), 提取 pipeline-create/ 2 个文件 (types(PRESETS/DEFAULT_TARGET/PipelineCreateDialogProps/EnvVar) + AdvancedSettingsPanel(172L 受控展开面板: 构建节点/Job名/触发模式/环境变量)), 零逻辑变更, ESLint errors=0/warnings=0/any=0, tsc -b=0, npm build✓ (17.45s), 17/17 测试通过

This commit is contained in:
attacker committed 2026-07-09 01:19:05 +08:00
1 parent adf1ac8f71
commit 2f8113cb8e
3 files changed
+352 -194

No files matched your search

@@ -13,60 +13,17 @@ import {
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Rocket, GitBranch, Globe, Container, Loader2 } from 'lucide-react';
import {
Rocket,
Plus,
GitBranch,
Globe,
Container,
Loader2,
Trash2,
ChevronDown,
Settings2,
} from 'lucide-react';
import { buildNodeApi, credentialApi, deployApi, pipelineApi, type BuildNode } from '@/api'; import { buildNodeApi, credentialApi, deployApi, pipelineApi, type BuildNode } from '@/api';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { nameToCode } from '../pipeline-helpers'; import { nameToCode } from '../pipeline-helpers';
import { STAGE_DEFS } from './constants'; import { STAGE_DEFS } from './constants';
import { DeployTargetFields, type DeployTargetValue } from './DeployTargetFields'; import { DeployTargetFields, type DeployTargetValue } from './DeployTargetFields';
import { PRESETS, DEFAULT_TARGET, type PipelineCreateDialogProps, type EnvVar } from './pipeline-create/types';
import { AdvancedSettingsPanel } from './pipeline-create/AdvancedSettingsPanel';
interface PipelineCreateDialogProps { // 新建流水线对话框 — 精简分步 + 双发布目标
open: boolean;
onOpenChange: (open: boolean) => void;
onCreated: () => void;
}
const PRESETS = [
{
key: 'ssh',
labelKey: 'deploy.presetHost',
descKey: 'deploy.presetHostDesc',
stages: ['checkout', 'build', 'deploy'],
deployTarget: 'ssh' as const,
},
{
key: 'k8s',
labelKey: 'deploy.presetK8s',
descKey: 'deploy.presetK8sDesc',
stages: ['checkout', 'build', 'image', 'deploy'],
deployTarget: 'k8s' as const,
},
];
const DEFAULT_TARGET: DeployTargetValue = {
deployTarget: 'ssh',
serverIds: [],
deployRemotePath: '',
deployScript: '',
k8sClusterId: '',
k8sNamespace: '',
k8sDeployment: '',
k8sContainerName: '',
};
// 新建流水线对话框 — 精简分步 + 双发布目标 // Logan.Li
export function PipelineCreateDialog({ open, onOpenChange, onCreated }: PipelineCreateDialogProps) { export function PipelineCreateDialog({ open, onOpenChange, onCreated }: PipelineCreateDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [step, setStep] = useState<1 | 2>(1); const [step, setStep] = useState<1 | 2>(1);
@@ -91,7 +48,7 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
const [newTriggerMode, setNewTriggerMode] = useState('manual'); const [newTriggerMode, setNewTriggerMode] = useState('manual');
const [newWatchBranches, setNewWatchBranches] = useState(''); const [newWatchBranches, setNewWatchBranches] = useState('');
const [newCronExpr, setNewCronExpr] = useState(''); const [newCronExpr, setNewCronExpr] = useState('');
const [newEnvVars, setNewEnvVars] = useState<{ key: string; value: string }[]>([]); const [newEnvVars, setNewEnvVars] = useState<EnvVar[]>([]);
const [environments, setEnvironments] = useState<Record<string, unknown>[]>([]); const [environments, setEnvironments] = useState<Record<string, unknown>[]>([]);
const [buildNodes, setBuildNodes] = useState<BuildNode[]>([]); const [buildNodes, setBuildNodes] = useState<BuildNode[]>([]);
const [buildNodeId, setBuildNodeId] = useState(''); const [buildNodeId, setBuildNodeId] = useState('');
@@ -103,15 +60,20 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
credentialApi.list({ page_size: 100 }).catch(() => ({ list: [] })), credentialApi.list({ page_size: 100 }).catch(() => ({ list: [] })),
buildNodeApi.list().catch(() => ({ list: [] })), buildNodeApi.list().catch(() => ({ list: [] })),
] as const); ] as const);
setEnvironments(Array.isArray((rEnv as Record<string, unknown>)?.list) setEnvironments(
? (rEnv as Record<string, unknown>).list as Record<string, unknown>[] Array.isArray((rEnv as Record<string, unknown>)?.list)
: []); ? ((rEnv as Record<string, unknown>).list as Record<string, unknown>[])
const allCreds: Record<string, unknown>[] = (rCred as Record<string, unknown>)?.list as Record<string, unknown>[] || []; : [],
);
const allCreds: Record<string, unknown>[] =
((rCred as Record<string, unknown>)?.list as Record<string, unknown>[]) || [];
setGitCredentials(allCreds.filter((c) => c.secret_type === 'api')); setGitCredentials(allCreds.filter((c) => c.secret_type === 'api'));
setDockerCredentials(allCreds.filter((c) => c.secret_type === 'docker')); setDockerCredentials(allCreds.filter((c) => c.secret_type === 'docker'));
const nodeData = rNodes as { list?: BuildNode[] }; const nodeData = rNodes as { list?: BuildNode[] };
setBuildNodes(Array.isArray(nodeData?.list) ? nodeData.list.filter((n) => n.enabled) : []); setBuildNodes(Array.isArray(nodeData?.list) ? nodeData.list.filter((n) => n.enabled) : []);
} catch { /* silent */ } } catch {
/* silent */
}
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -186,12 +148,14 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
setSaving(true); setSaving(true);
try { try {
const stageNames = selectedStages.map((k) => STAGE_DEFS.find((s) => s.key === k)?.label || k); const stageNames = selectedStages.map((k) => STAGE_DEFS.find((s) => s.key === k)?.label || k);
const stages = JSON.stringify(stageNames.map((name, i) => ({ const stages = JSON.stringify(
id: `stage-${i + 1}`, stageNames.map((name, i) => ({
name, id: `stage-${i + 1}`,
type: selectedStages[i] || 'custom', name,
tasks: [], type: selectedStages[i] || 'custom',
}))); tasks: [],
})),
);
await pipelineApi.createPipeline({ await pipelineApi.createPipeline({
name: newName, name: newName,
code: nameToCode(newName), code: nameToCode(newName),
@@ -204,7 +168,8 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
stages, stages,
deploy_mode: 'branch', deploy_mode: 'branch',
deploy_target: deployTarget.deployTarget, deploy_target: deployTarget.deployTarget,
server_ids: deployTarget.serverIds.length > 0 ? JSON.stringify(deployTarget.serverIds) : undefined, server_ids:
deployTarget.serverIds.length > 0 ? JSON.stringify(deployTarget.serverIds) : undefined,
deploy_remote_path: deployTarget.deployRemotePath || undefined, deploy_remote_path: deployTarget.deployRemotePath || undefined,
deploy_script: deployTarget.deployScript || undefined, deploy_script: deployTarget.deployScript || undefined,
k8s_cluster_id: deployTarget.k8sClusterId ? Number(deployTarget.k8sClusterId) : null, k8s_cluster_id: deployTarget.k8sClusterId ? Number(deployTarget.k8sClusterId) : null,
@@ -232,16 +197,22 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
}; };
const toggleStage = (key: string) => { const toggleStage = (key: string) => {
setSelectedStages((prev) => ( setSelectedStages((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]));
prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]
));
}; };
return ( return (
<Dialog open={open} onOpenChange={(v) => { if (!v) resetForm(); onOpenChange(v); }}> <Dialog
open={open}
onOpenChange={(v) => {
if (!v) resetForm();
onOpenChange(v);
}}
>
<DialogContent className="max-w-lg max-h-[88vh] overflow-y-auto bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"> <DialogContent className="max-w-lg max-h-[88vh] overflow-y-auto bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">{t('deploy.newPipelineTitle')}</DialogTitle> <DialogTitle className="text-[var(--dg-text-primary)]">
{t('deploy.newPipelineTitle')}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]"> <DialogDescription className="text-[var(--dg-text-muted)]">
{t('deploy.newPipelineDesc')} {t('deploy.newPipelineDesc')}
</DialogDescription> </DialogDescription>
@@ -254,7 +225,9 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
onClick={() => setStep(1)} onClick={() => setStep(1)}
className={cn( className={cn(
'px-2.5 py-1 rounded-md transition-colors', 'px-2.5 py-1 rounded-md transition-colors',
step === 1 ? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)]' : 'text-[var(--dg-text-muted)]', step === 1
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)]'
: 'text-[var(--dg-text-muted)]',
)} )}
> >
{t('deploy.step1')} {t('deploy.step1')}
@@ -262,10 +235,14 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
<span className="text-[var(--dg-border)]">/</span> <span className="text-[var(--dg-border)]">/</span>
<button <button
type="button" type="button"
onClick={() => { if (validateStep1()) setStep(2); }} onClick={() => {
if (validateStep1()) setStep(2);
}}
className={cn( className={cn(
'px-2.5 py-1 rounded-md transition-colors', 'px-2.5 py-1 rounded-md transition-colors',
step === 2 ? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)]' : 'text-[var(--dg-text-muted)]', step === 2
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)]'
: 'text-[var(--dg-text-muted)]',
)} )}
> >
{t('deploy.step2')} {t('deploy.step2')}
@@ -287,14 +264,20 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
: 'border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]', : 'border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]',
)} )}
> >
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{t(p.labelKey)}</div> <div className="text-sm font-medium text-[var(--dg-text-primary)]">
<div className="text-[11px] text-[var(--dg-text-muted)] mt-0.5">{t(p.descKey)}</div> {t(p.labelKey)}
</div>
<div className="text-[11px] text-[var(--dg-text-muted)] mt-0.5">
{t(p.descKey)}
</div>
</button> </button>
))} ))}
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">{t('deploy.pipeName')} <span className="text-[var(--dg-red)]">*</span></Label> <Label className="text-xs">
{t('deploy.pipeName')} <span className="text-[var(--dg-red)]">*</span>
</Label>
<Input <Input
value={newName} value={newName}
onChange={(e) => setNewName(e.target.value)} onChange={(e) => setNewName(e.target.value)}
@@ -320,7 +303,9 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
value={String(newEnvironmentId)} value={String(newEnvironmentId)}
onValueChange={(v) => setNewEnvironmentId(v === '__none' ? '' : v)} onValueChange={(v) => setNewEnvironmentId(v === '__none' ? '' : v)}
> >
<SelectTrigger className="h-9 dg-input"><SelectValue placeholder={t('deploy.selectEnv')} /></SelectTrigger> <SelectTrigger className="h-9 dg-input">
<SelectValue placeholder={t('deploy.selectEnv')} />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__none">{t('deploy.notSpecified')}</SelectItem> <SelectItem value="__none">{t('deploy.notSpecified')}</SelectItem>
{environments.map((env) => ( {environments.map((env) => (
@@ -333,8 +318,16 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
</div> </div>
<div className="flex justify-end gap-2 pt-2"> <div className="flex justify-end gap-2 pt-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button> <Button variant="outline" onClick={() => onOpenChange(false)}>
<Button onClick={() => { if (validateStep1()) setStep(2); }}>下一步</Button> 取消
</Button>
<Button
onClick={() => {
if (validateStep1()) setStep(2);
}}
>
下一步
</Button>
</div> </div>
</div> </div>
) : ( ) : (
@@ -348,7 +341,10 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs"> <Label className="text-xs">
{t('deploy.gitRepo')} {selectedStages.includes('checkout') && <span className="text-[var(--dg-red)]">*</span>} {t('deploy.gitRepo')}{' '}
{selectedStages.includes('checkout') && (
<span className="text-[var(--dg-red)]">*</span>
)}
</Label> </Label>
<Input <Input
value={newSource} value={newSource}
@@ -359,20 +355,32 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">{t('deploy.pipeBranch')}</Label> <Label className="text-xs">{t('deploy.pipeBranch')}</Label>
<Input value={newBranch} onChange={(e) => setNewBranch(e.target.value)} className="h-8 text-xs dg-input" /> <Input
value={newBranch}
onChange={(e) => setNewBranch(e.target.value)}
className="h-8 text-xs dg-input"
/>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">{t('deploy.gitCredential')}</Label> <Label className="text-xs">{t('deploy.gitCredential')}</Label>
<Select value={String(newGitCredentialId)} onValueChange={(v) => setNewGitCredentialId(v === '__none' ? '' : v)}> <Select
<SelectTrigger className="h-8 text-xs dg-input"><SelectValue placeholder="选择凭证" /></SelectTrigger> value={String(newGitCredentialId)}
onValueChange={(v) => setNewGitCredentialId(v === '__none' ? '' : v)}
>
<SelectTrigger className="h-8 text-xs dg-input">
<SelectValue placeholder="选择凭证" />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__none">{t('deploy.notLinked')}</SelectItem> <SelectItem value="__none">{t('deploy.notLinked')}</SelectItem>
{gitCredentials.map((c) => ( {gitCredentials.map((c) => (
<SelectItem key={c.id as number} value={String(c.id)}> <SelectItem key={c.id as number} value={String(c.id)}>
<span className="flex items-center gap-1"><Globe className="h-3 w-3" />{String(c.name)}</span> <span className="flex items-center gap-1">
<Globe className="h-3 w-3" />
{String(c.name)}
</span>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -380,14 +388,24 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
</div> </div>
{selectedStages.includes('image') && ( {selectedStages.includes('image') && (
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">{t('deploy.imageCredential')} <span className="text-[var(--dg-red)]">*</span></Label> <Label className="text-xs">
<Select value={String(newRegistryCredentialId)} onValueChange={(v) => setNewRegistryCredentialId(v === '__none' ? '' : v)}> {t('deploy.imageCredential')} <span className="text-[var(--dg-red)]">*</span>
<SelectTrigger className="h-8 text-xs dg-input"><SelectValue placeholder="Docker 凭证" /></SelectTrigger> </Label>
<Select
value={String(newRegistryCredentialId)}
onValueChange={(v) => setNewRegistryCredentialId(v === '__none' ? '' : v)}
>
<SelectTrigger className="h-8 text-xs dg-input">
<SelectValue placeholder="Docker 凭证" />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__none">不关联</SelectItem> <SelectItem value="__none">不关联</SelectItem>
{dockerCredentials.map((c) => ( {dockerCredentials.map((c) => (
<SelectItem key={c.id as number} value={String(c.id)}> <SelectItem key={c.id as number} value={String(c.id)}>
<span className="flex items-center gap-1"><Container className="h-3 w-3" />{String(c.name)}</span> <span className="flex items-center gap-1">
<Container className="h-3 w-3" />
{String(c.name)}
</span>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -429,125 +447,45 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
{selectedStages.length > 0 && ( {selectedStages.length > 0 && (
<p className="text-[10px] text-[var(--dg-text-muted)] flex items-center gap-1 flex-wrap"> <p className="text-[10px] text-[var(--dg-text-muted)] flex items-center gap-1 flex-wrap">
<GitBranch className="h-3 w-3" /> <GitBranch className="h-3 w-3" />
{selectedStages.map((k) => STAGE_DEFS.find((d) => d.key === k)?.label || k).join(' → ')} {selectedStages
.map((k) => STAGE_DEFS.find((d) => d.key === k)?.label || k)
.join(' → ')}
</p> </p>
)} )}
</div> </div>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}> <AdvancedSettingsPanel
<CollapsibleTrigger asChild> open={advancedOpen}
<button onOpenChange={setAdvancedOpen}
type="button" buildNodeId={buildNodeId}
className="flex items-center gap-1.5 text-xs text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]" onBuildNodeIdChange={setBuildNodeId}
> buildNodes={buildNodes}
<Settings2 className="h-3.5 w-3.5" /> jobName={newJobName}
{t('deploy.advancedSettings')} onJobNameChange={setNewJobName}
<ChevronDown className={cn('h-3.5 w-3.5 transition-transform', advancedOpen && 'rotate-180')} /> triggerMode={newTriggerMode}
</button> onTriggerModeChange={setNewTriggerMode}
</CollapsibleTrigger> watchBranches={newWatchBranches}
<CollapsibleContent className="mt-3 space-y-3 pt-3 border-t border-[var(--dg-border)]"> onWatchBranchesChange={setNewWatchBranches}
<div className="space-y-1.5"> cronExpr={newCronExpr}
<Label className="text-xs">{t('deploy.buildNode')}</Label> onCronExprChange={setNewCronExpr}
<Select value={buildNodeId || '__auto'} onValueChange={(v) => setBuildNodeId(v === '__auto' ? '' : v)}> envVars={newEnvVars}
<SelectTrigger className="h-8 text-xs dg-input"><SelectValue /></SelectTrigger> onEnvVarsChange={setNewEnvVars}
<SelectContent> />
<SelectItem value="__auto">{t('deploy.autoSchedule')}</SelectItem>
{buildNodes.map((n) => (
<SelectItem key={n.id} value={String(n.id)}>
{n.name} ({n.running_count || 0}/{n.max_executors})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Job 名称 <span className="text-[var(--dg-text-muted)]">(Webhook)</span></Label>
<Input value={newJobName} onChange={(e) => setNewJobName(e.target.value)} className="h-8 text-xs font-mono dg-input" />
</div>
<div className="space-y-1.5">
<Label className="text-xs">{t('deploy.triggerMethod')}</Label>
<Select value={newTriggerMode} onValueChange={setNewTriggerMode}>
<SelectTrigger className="h-8 text-xs dg-input"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="manual">{t('deploy.triggerManual')}</SelectItem>
<SelectItem value="git_push">{t('deploy.codePush')}</SelectItem>
<SelectItem value="cron">{t('deploy.triggerCron')}</SelectItem>
<SelectItem value="webhook">Webhook</SelectItem>
</SelectContent>
</Select>
</div>
{newTriggerMode === 'git_push' && (
<Input
value={newWatchBranches}
onChange={(e) => setNewWatchBranches(e.target.value)}
placeholder="监听分支,如 main, release/*"
className="h-8 text-xs font-mono dg-input"
/>
)}
{newTriggerMode === 'cron' && (
<Input
value={newCronExpr}
onChange={(e) => setNewCronExpr(e.target.value)}
placeholder="0 2 * * *"
className="h-8 text-xs font-mono dg-input"
/>
)}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<Label className="text-xs">{t('deploy.envVars')}</Label>
<Button
type="button"
variant="ghost"
size="sm"
className="h-6 text-[10px]"
onClick={() => setNewEnvVars([...newEnvVars, { key: '', value: '' }])}
>
<Plus className="h-3 w-3 mr-0.5" />{t('deploy.addVar')}
</Button>
</div>
{newEnvVars.map((v, i) => (
<div key={i} className="flex gap-1.5">
<Input
value={v.key}
onChange={(e) => {
const nv = [...newEnvVars];
nv[i] = { ...nv[i], key: e.target.value };
setNewEnvVars(nv);
}}
placeholder="KEY"
className="h-7 text-xs font-mono dg-input flex-1"
/>
<Input
value={v.value}
onChange={(e) => {
const nv = [...newEnvVars];
nv[i] = { ...nv[i], value: e.target.value };
setNewEnvVars(nv);
}}
placeholder="value"
className="h-7 text-xs font-mono dg-input flex-[2]"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0"
onClick={() => setNewEnvVars(newEnvVars.filter((_, idx) => idx !== i))}
>
<Trash2 className="h-3.5 w-3.5 text-[var(--dg-red)]" />
</Button>
</div>
))}
</div>
</CollapsibleContent>
</Collapsible>
<div className="flex justify-between gap-2 pt-2 border-t border-[var(--dg-border)]"> <div className="flex justify-between gap-2 pt-2 border-t border-[var(--dg-border)]">
<Button variant="ghost" onClick={() => setStep(1)}>{t('deploy.prevStep')}</Button> <Button variant="ghost" onClick={() => setStep(1)}>
{t('deploy.prevStep')}
</Button>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button> <Button variant="outline" onClick={() => onOpenChange(false)}>
取消
</Button>
<Button onClick={handleCreate} disabled={saving}> <Button onClick={handleCreate} disabled={saving}>
{saving ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Rocket className="h-4 w-4 mr-1" />} {saving ? (
<Loader2 className="h-4 w-4 animate-spin mr-1" />
) : (
<Rocket className="h-4 w-4 mr-1" />
)}
{saving ? t('deploy.creating') : t('deploy.pipeCreate')} {saving ? t('deploy.creating') : t('deploy.pipeCreate')}
</Button> </Button>
</div> </div>
@@ -0,0 +1,172 @@
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Plus, ChevronDown, Settings2, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { BuildNode } from '@/api';
import type { EnvVar } from './types';
export interface AdvancedSettingsPanelProps {
open: boolean;
onOpenChange: (open: boolean) => void;
buildNodeId: string;
onBuildNodeIdChange: (v: string) => void;
buildNodes: BuildNode[];
jobName: string;
onJobNameChange: (v: string) => void;
triggerMode: string;
onTriggerModeChange: (v: string) => void;
watchBranches: string;
onWatchBranchesChange: (v: string) => void;
cronExpr: string;
onCronExprChange: (v: string) => void;
envVars: EnvVar[];
onEnvVarsChange: (vars: EnvVar[]) => void;
}
export function AdvancedSettingsPanel({
open,
onOpenChange,
buildNodeId,
onBuildNodeIdChange,
buildNodes,
jobName,
onJobNameChange,
triggerMode,
onTriggerModeChange,
watchBranches,
onWatchBranchesChange,
cronExpr,
onCronExprChange,
envVars,
onEnvVarsChange,
}: AdvancedSettingsPanelProps) {
const { t } = useTranslation();
return (
<Collapsible open={open} onOpenChange={onOpenChange}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex items-center gap-1.5 text-xs text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
>
<Settings2 className="h-3.5 w-3.5" />
{t('deploy.advancedSettings')}
<ChevronDown className={cn('h-3.5 w-3.5 transition-transform', open && 'rotate-180')} />
</button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-3 space-y-3 pt-3 border-t border-[var(--dg-border)]">
<div className="space-y-1.5">
<Label className="text-xs">{t('deploy.buildNode')}</Label>
<Select
value={buildNodeId || '__auto'}
onValueChange={(v) => onBuildNodeIdChange(v === '__auto' ? '' : v)}
>
<SelectTrigger className="h-8 text-xs dg-input">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="__auto">{t('deploy.autoSchedule')}</SelectItem>
{buildNodes.map((n) => (
<SelectItem key={n.id} value={String(n.id)}>
{n.name} ({n.running_count || 0}/{n.max_executors})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs">
Job 名称 <span className="text-[var(--dg-text-muted)]">(Webhook)</span>
</Label>
<Input
value={jobName}
onChange={(e) => onJobNameChange(e.target.value)}
className="h-8 text-xs font-mono dg-input"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">{t('deploy.triggerMethod')}</Label>
<Select value={triggerMode} onValueChange={onTriggerModeChange}>
<SelectTrigger className="h-8 text-xs dg-input">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">{t('deploy.triggerManual')}</SelectItem>
<SelectItem value="git_push">{t('deploy.codePush')}</SelectItem>
<SelectItem value="cron">{t('deploy.triggerCron')}</SelectItem>
<SelectItem value="webhook">Webhook</SelectItem>
</SelectContent>
</Select>
</div>
{triggerMode === 'git_push' && (
<Input
value={watchBranches}
onChange={(e) => onWatchBranchesChange(e.target.value)}
placeholder="监听分支,如 main, release/*"
className="h-8 text-xs font-mono dg-input"
/>
)}
{triggerMode === 'cron' && (
<Input
value={cronExpr}
onChange={(e) => onCronExprChange(e.target.value)}
placeholder="0 2 * * *"
className="h-8 text-xs font-mono dg-input"
/>
)}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<Label className="text-xs">{t('deploy.envVars')}</Label>
<Button
type="button"
variant="ghost"
size="sm"
className="h-6 text-[10px]"
onClick={() => onEnvVarsChange([...envVars, { key: '', value: '' }])}
>
<Plus className="h-3 w-3 mr-0.5" />
{t('deploy.addVar')}
</Button>
</div>
{envVars.map((v, i) => (
<div key={i} className="flex gap-1.5">
<Input
value={v.key}
onChange={(e) => {
const nv = [...envVars];
nv[i] = { ...nv[i], key: e.target.value };
onEnvVarsChange(nv);
}}
placeholder="KEY"
className="h-7 text-xs font-mono dg-input flex-1"
/>
<Input
value={v.value}
onChange={(e) => {
const nv = [...envVars];
nv[i] = { ...nv[i], value: e.target.value };
onEnvVarsChange(nv);
}}
placeholder="value"
className="h-7 text-xs font-mono dg-input flex-[2]"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0"
onClick={() => onEnvVarsChange(envVars.filter((_, idx) => idx !== i))}
>
<Trash2 className="h-3.5 w-3.5 text-[var(--dg-red)]" />
</Button>
</div>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
}
@@ -0,0 +1,48 @@
import type { DeployTargetValue } from '../DeployTargetFields';
export interface PipelineCreateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onCreated: () => void;
}
export interface Preset {
key: string;
labelKey: string;
descKey: string;
stages: string[];
deployTarget: 'ssh' | 'k8s';
}
export const PRESETS: Preset[] = [
{
key: 'ssh',
labelKey: 'deploy.presetHost',
descKey: 'deploy.presetHostDesc',
stages: ['checkout', 'build', 'deploy'],
deployTarget: 'ssh',
},
{
key: 'k8s',
labelKey: 'deploy.presetK8s',
descKey: 'deploy.presetK8sDesc',
stages: ['checkout', 'build', 'image', 'deploy'],
deployTarget: 'k8s',
},
];
export const DEFAULT_TARGET: DeployTargetValue = {
deployTarget: 'ssh',
serverIds: [],
deployRemotePath: '',
deployScript: '',
k8sClusterId: '',
k8sNamespace: '',
k8sDeployment: '',
k8sContainerName: '',
};
export interface EnvVar {
key: string;
value: string;
}