import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; 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 { Rocket, Plus, GitBranch, Globe, Container, Loader2, Trash2, ChevronDown, Settings2, } from 'lucide-react'; import { buildNodeApi, credentialApi, deployApi, pipelineApi, type BuildNode } from '@/api'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { nameToCode } from '../pipeline-helpers'; import { STAGE_DEFS } from './constants'; import { DeployTargetFields, type DeployTargetValue } from './DeployTargetFields'; interface PipelineCreateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onCreated: () => void; } const PRESETS = [ { key: 'ssh', label: t('deploy.presetHost'), desc: t('deploy.presetHostDesc'), stages: ['checkout', 'build', 'deploy'], deployTarget: 'ssh' as const, }, { key: 'k8s', label: t('deploy.presetK8s'), desc: t('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) { const { t } = useTranslation(); const [step, setStep] = useState<1 | 2>(1); const [preset, setPreset] = useState('ssh'); const [newName, setNewName] = useState(''); const [newDesc, setNewDesc] = useState(''); const [newSource, setNewSource] = useState(''); const [newBranch, setNewBranch] = useState('main'); const [newDockerfile, setNewDockerfile] = useState(''); const [selectedStages, setSelectedStages] = useState(['checkout', 'build', 'deploy']); const [deployTarget, setDeployTarget] = useState(DEFAULT_TARGET); const [newEnvironmentId, setNewEnvironmentId] = useState(''); const [saving, setSaving] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); const [gitCredentials, setGitCredentials] = useState[]>([]); const [dockerCredentials, setDockerCredentials] = useState[]>([]); const [newGitCredentialId, setNewGitCredentialId] = useState(''); const [newRegistryCredentialId, setNewRegistryCredentialId] = useState(''); const [newJobName, setNewJobName] = useState(''); const [newTriggerMode, setNewTriggerMode] = useState('manual'); const [newWatchBranches, setNewWatchBranches] = useState(''); const [newCronExpr, setNewCronExpr] = useState(''); const [newEnvVars, setNewEnvVars] = useState<{ key: string; value: string }[]>([]); const [environments, setEnvironments] = useState[]>([]); const [buildNodes, setBuildNodes] = useState([]); const [buildNodeId, setBuildNodeId] = useState(''); const loadRefs = useCallback(async () => { try { const [rEnv, rCred, rNodes] = await Promise.all([ deployApi.getEnvironments().catch(() => ({ list: [] })), credentialApi.list({ page_size: 100 }).catch(() => ({ list: [] })), buildNodeApi.list().catch(() => ({ list: [] })), ] as const); setEnvironments(Array.isArray((rEnv as Record)?.list) ? (rEnv as Record).list as Record[] : []); const allCreds: Record[] = (rCred as Record)?.list as Record[] || []; setGitCredentials(allCreds.filter((c) => c.secret_type === 'api')); setDockerCredentials(allCreds.filter((c) => c.secret_type === 'docker')); const nodeData = rNodes as { list?: BuildNode[] }; setBuildNodes(Array.isArray(nodeData?.list) ? nodeData.list.filter((n) => n.enabled) : []); } catch { /* silent */ } }, []); useEffect(() => { if (open) loadRefs(); }, [open, loadRefs]); const applyPreset = (key: string) => { const p = PRESETS.find((x) => x.key === key); if (!p) return; setPreset(key); setSelectedStages(p.stages); setDeployTarget({ ...DEFAULT_TARGET, deployTarget: p.deployTarget, }); }; const resetForm = () => { setStep(1); setPreset('ssh'); setNewName(''); setNewDesc(''); setNewSource(''); setNewBranch('main'); setNewDockerfile(''); setSelectedStages(['checkout', 'build', 'deploy']); setDeployTarget(DEFAULT_TARGET); setNewEnvironmentId(''); setNewGitCredentialId(''); setNewRegistryCredentialId(''); setNewJobName(''); setNewTriggerMode('manual'); setNewWatchBranches(''); setNewCronExpr(''); setNewEnvVars([]); setAdvancedOpen(false); setBuildNodeId(''); }; const validateStep1 = () => { if (!newName.trim()) { toast.error(t('deploy.enterPipelineName')); return false; } return true; }; const validateStep2 = () => { if (selectedStages.includes('checkout') && !newSource.trim()) { toast.error(t('deploy.stage.checkoutNeedGit')); return false; } if (selectedStages.includes('image') && !newRegistryCredentialId) { toast.error(t('deploy.stage.imageNeedDocker')); return false; } if (selectedStages.includes('deploy')) { if (deployTarget.deployTarget === 'ssh' && deployTarget.serverIds.length === 0) { toast.error(t('deploy.needHost')); return false; } if (deployTarget.deployTarget === 'k8s' && (!deployTarget.k8sClusterId || !deployTarget.k8sDeployment)) { toast.error(t('deploy.needK8s')); return false; } } return true; }; const handleCreate = async () => { if (!validateStep1() || !validateStep2()) return; setSaving(true); try { const stageNames = selectedStages.map((k) => STAGE_DEFS.find((s) => s.key === k)?.label || k); const stages = JSON.stringify(stageNames.map((name, i) => ({ id: `stage-${i + 1}`, name, type: selectedStages[i] || 'custom', tasks: [], }))); await pipelineApi.createPipeline({ name: newName, code: nameToCode(newName), job_name: newJobName || undefined, description: newDesc, source: newSource, branch: newBranch, dockerfile: newDockerfile, type: 'deploy', stages, deploy_mode: 'branch', deploy_target: deployTarget.deployTarget, server_ids: deployTarget.serverIds.length > 0 ? JSON.stringify(deployTarget.serverIds) : undefined, deploy_remote_path: deployTarget.deployRemotePath || undefined, deploy_script: deployTarget.deployScript || undefined, k8s_cluster_id: deployTarget.k8sClusterId ? Number(deployTarget.k8sClusterId) : null, k8s_namespace: deployTarget.k8sNamespace || undefined, k8s_deployment: deployTarget.k8sDeployment || undefined, k8s_container_name: deployTarget.k8sContainerName || undefined, environment_id: newEnvironmentId ? Number(newEnvironmentId) : null, git_credential_id: newGitCredentialId ? Number(newGitCredentialId) : null, registry_credential_id: newRegistryCredentialId ? Number(newRegistryCredentialId) : null, trigger_mode: newTriggerMode, watch_branches: newWatchBranches || undefined, cron_expr: newCronExpr || undefined, variables: newEnvVars.length > 0 ? JSON.stringify(newEnvVars) : undefined, build_node_id: buildNodeId ? Number(buildNodeId) : null, }); toast.success(t('deploy.pipeline.created')); onOpenChange(false); resetForm(); onCreated(); } catch { toast.error(t('deploy.pipeline.createFailed')); } finally { setSaving(false); } }; const toggleStage = (key: string) => { setSelectedStages((prev) => ( prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key] )); }; return ( { if (!v) resetForm(); onOpenChange(v); }}> {t('deploy.newPipelineTitle')} {t('deploy.newPipelineDesc')} {/* 步骤指示 */}
/
{step === 1 ? (
{PRESETS.map((p) => ( ))}
setNewName(e.target.value)} placeholder={t('deploy.pipeNamePlaceholder')} className="h-9 dg-input" />