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