import { useState, useEffect, useCallback, useMemo } from 'react'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Search, Server, Network, Loader2, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; import { serverApi, kubernetesApi } from '@/api'; import { getPublicApiBase } from '@/utils/request'; import i18n from '@/i18n'; export interface DeployTargetValue { deployTarget: 'ssh' | 'k8s'; serverIds: number[]; deployRemotePath: string; deployScript: string; k8sClusterId: number | ''; k8sNamespace: string; k8sDeployment: string; k8sContainerName: string; } interface DeployTargetFieldsProps { value: DeployTargetValue; onChange: (patch: Partial) => void; requireTarget?: boolean; } interface ServerItem { id: number; hostname: string; ip?: string; status?: string; } interface ClusterItem { id: number; name: string; code: string; } interface DeploymentItem { namespace: string; name: string; image?: string; ready_replicas?: number; replicas?: number; } // 发布目标选择:SSH 关联主机 / K8s 关联 Deployment // Logan.Li export function DeployTargetFields({ value, onChange, requireTarget = true }: DeployTargetFieldsProps) { const [servers, setServers] = useState([]); const [clusters, setClusters] = useState([]); const [deployments, setDeployments] = useState([]); const [serverSearch, setServerSearch] = useState(''); const [depSearch, setDepSearch] = useState(''); const [loadingServers, setLoadingServers] = useState(false); const [loadingDeps, setLoadingDeps] = useState(false); const loadServers = useCallback(async () => { setLoadingServers(true); try { const res = await serverApi.getServers({ page_size: 200 }); const list = (res?.list ?? []) as ServerItem[]; setServers(list); } catch { setServers([]); } finally { setLoadingServers(false); } }, []); const loadClusters = useCallback(async () => { try { const res = await kubernetesApi.getClusters(); setClusters((res?.list ?? []) as ClusterItem[]); } catch { setClusters([]); } }, []); const loadDeployments = useCallback(async (clusterId: number | '') => { if (!clusterId) { setDeployments([]); return; } const cluster = clusters.find((c) => c.id === clusterId); if (!cluster?.code) { setDeployments([]); return; } setLoadingDeps(true); try { const res = await kubernetesApi.getAllDeployments(cluster.code); const list = Array.isArray(res) ? res : []; setDeployments(list as DeploymentItem[]); } catch { setDeployments([]); } finally { setLoadingDeps(false); } }, [clusters]); useEffect(() => { loadServers(); loadClusters(); }, [loadServers, loadClusters]); useEffect(() => { if (value.deployTarget === 'k8s' && value.k8sClusterId) { loadDeployments(value.k8sClusterId); } }, [value.deployTarget, value.k8sClusterId, loadDeployments]); const filteredServers = useMemo(() => { const q = serverSearch.trim().toLowerCase(); if (!q) return servers; return servers.filter( (s) => s.hostname?.toLowerCase().includes(q) || s.ip?.toLowerCase().includes(q), ); }, [servers, serverSearch]); const filteredDeployments = useMemo(() => { const q = depSearch.trim().toLowerCase(); if (!q) return deployments; return deployments.filter( (d) => d.name.toLowerCase().includes(q) || d.namespace.toLowerCase().includes(q), ); }, [deployments, depSearch]); const toggleServer = (id: number) => { const next = value.serverIds.includes(id) ? value.serverIds.filter((x) => x !== id) : [...value.serverIds, id]; onChange({ serverIds: next }); }; const selectDeployment = (dep: DeploymentItem) => { onChange({ k8sNamespace: dep.namespace, k8sDeployment: dep.name, }); }; const selectedCluster = clusters.find((c) => c.id === value.k8sClusterId); return (
{([ { key: 'ssh' as const, label: i18n.t('pipeline.deploytargetfie.secf81967'), icon: Server }, { key: 'k8s' as const, label: i18n.t('pipeline.deploytargetfie.s7ff062d5'), icon: Network }, ]).map((item) => { const active = value.deployTarget === item.key; const Icon = item.icon; return ( ); })}
{value.deployTarget === 'ssh' ? (
{value.serverIds.length > 0 && ( 已选 {value.serverIds.length} 台 )}
setServerSearch(e.target.value)} placeholder={i18n.t('pipeline.deploytargetfie.sc7624940')} className="pl-8 h-8 text-xs dg-input" />
{loadingServers ? (
加载中
) : filteredServers.length === 0 ? (

{i18n.t('pipeline.deploytargetfie.sb4240171')}

) : (
{filteredServers.map((srv) => { const selected = value.serverIds.includes(srv.id); return ( ); })}
)}

{i18n.t('pipeline.deploytargetfie.sb2b6db98')}

onChange({ deployRemotePath: e.target.value })} placeholder="/opt/apps/my-app" className="h-8 text-xs font-mono dg-input" />
onChange({ deployScript: e.target.value })} placeholder="./restart.sh" className="h-8 text-xs font-mono dg-input" />
) : (
{selectedCluster && ( <>
setDepSearch(e.target.value)} placeholder={i18n.t('pipeline.deploytargetfie.saa98b28a')} className="pl-8 h-8 text-xs dg-input" />
{loadingDeps ? (
加载 Deployment
) : filteredDeployments.length === 0 ? (

{i18n.t('k8s.workloads.noDeployment')}

) : (
{filteredDeployments.map((dep) => { const selected = value.k8sDeployment === dep.name && value.k8sNamespace === dep.namespace; return ( ); })}
)}
onChange({ k8sContainerName: e.target.value })} placeholder={i18n.t('pipeline.deploytargetfie.s389076f8')} className="h-8 text-xs font-mono dg-input" />
{value.k8sDeployment && selectedCluster && (

流水线部署将调用 CI 接口: POST {getPublicApiBase()}/kubernetes/{selectedCluster.code}/namespace/{value.k8sNamespace}/deployments/{value.k8sDeployment}/update-image

)} )}
)}
); }