diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx new file mode 100644 index 00000000..e992bc9e --- /dev/null +++ b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx @@ -0,0 +1,375 @@ +import React, { useEffect, useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogBody, +} from '@/components/ui/dialog'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import { Plus, Edit, Trash2, Loader2 } from 'lucide-react'; +import type { Environment } from '@/pages/Deploy/Environment/List/types'; +import type { TeamApplication, Application } from '../types'; +import type { WorkflowDefinition } from '@/pages/Workflow/Definition/List/types'; +import { + getTeamApplications, + deleteTeamApplication, + getApplicationList, + getJenkinsSystems, + getJenkinsJobs, + getWorkflowDefinitions, + createTeamApplication, + updateTeamApplication, +} from '../service'; +import { getRepositoryBranches } from '@/pages/Resource/Git/List/service'; +import TeamApplicationDialog from './TeamApplicationDialog'; + +interface TeamApplicationManageDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + teamId: number; + environmentId?: number; // 可选:如果指定,则只管理该环境的应用 + environments: Environment[]; + onSuccess?: () => void; +} + +export const TeamApplicationManageDialog: React.FC< + TeamApplicationManageDialogProps +> = ({ open, onOpenChange, teamId, environmentId, environments, onSuccess }) => { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [teamApplications, setTeamApplications] = useState([]); + // 如果从环境管理进入,只显示该环境的应用;否则显示所有应用 + const selectedEnvironmentId = environmentId || null; + + // 基础数据状态 + const [applications, setApplications] = useState([]); + const [jenkinsSystems, setJenkinsSystems] = useState([]); + const [workflowDefinitions, setWorkflowDefinitions] = useState([]); + + // 应用配置对话框状态 + const [appDialogOpen, setAppDialogOpen] = useState(false); + const [appDialogMode, setAppDialogMode] = useState<'create' | 'edit'>('create'); + const [editingApp, setEditingApp] = useState(null); + const [editingEnvironment, setEditingEnvironment] = useState(null); + + // 删除确认对话框状态 + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deletingApp, setDeletingApp] = useState(null); + const [deleting, setDeleting] = useState(false); + + // 加载基础数据和应用列表 + useEffect(() => { + if (open) { + loadTeamApplications(); + loadBaseData(); + } + }, [open, teamId, selectedEnvironmentId]); + + const loadBaseData = async () => { + try { + const [appsData, jenkinsData, workflowsData] = await Promise.all([ + getApplicationList(), + getJenkinsSystems(), + getWorkflowDefinitions(), + ]); + setApplications(appsData || []); + setJenkinsSystems(jenkinsData || []); + setWorkflowDefinitions(workflowsData || []); + } catch (error: any) { + toast({ + title: '加载基础数据失败', + description: error.message || '无法加载应用、Jenkins和工作流数据', + variant: 'destructive', + }); + } + }; + + // 后端已经根据 selectedEnvironmentId 筛选了数据,前端不需要再次筛选 + + const loadTeamApplications = async () => { + setLoading(true); + try { + // 如果指定了环境ID,只加载该环境的应用;否则加载所有 + const data = await getTeamApplications(teamId, selectedEnvironmentId || undefined); + setTeamApplications(data); + } catch (error: any) { + toast({ + title: '加载失败', + description: error.message || '无法加载应用列表', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + const handleOpenAddAppDialog = () => { + // 如果指定了环境,使用指定的环境;否则使用第一个环境 + const defaultEnv = selectedEnvironmentId + ? environments.find(e => e.id === selectedEnvironmentId) || environments[0] + : environments[0]; + setAppDialogMode('create'); + setEditingApp(null); + setEditingEnvironment(defaultEnv); + setAppDialogOpen(true); + }; + + const handleOpenEditAppDialog = (app: TeamApplication) => { + const env = environments.find(e => e.id === app.environmentId); + setAppDialogMode('edit'); + setEditingApp(app); + setEditingEnvironment(env || null); + setAppDialogOpen(true); + }; + + const handleSaveApplication = async (data: { + id?: number; + appId: number; + branch: string; + deploySystemId: number | null; + deployJob: string; + workflowDefinitionId: number | null; + }) => { + if (!editingEnvironment) return; + + const payload = { + teamId, + applicationId: data.appId, + environmentId: editingEnvironment.id, + branch: data.branch, + deploySystemId: data.deploySystemId || undefined, + deployJob: data.deployJob, + workflowDefinitionId: data.workflowDefinitionId || undefined, + }; + + if (appDialogMode === 'edit' && data.id) { + await updateTeamApplication(data.id, payload); + } else { + await createTeamApplication(payload); + } + + // 刷新应用列表 + await loadTeamApplications(); + }; + + const handleLoadBranches = async (_appId: number, app: Application) => { + if (!app.repoProjectId || !app.externalSystemId) { + return []; + } + const result = await getRepositoryBranches({ + externalSystemId: app.externalSystemId, + repoProjectId: app.repoProjectId, + }); + // getRepositoryBranches 返回 Page 类型,需要提取 content + return Array.isArray(result) ? result : (result as any).content || []; + }; + + const handleLoadJenkinsJobs = async (systemId: number) => { + return await getJenkinsJobs(systemId); + }; + + const handleOpenDeleteDialog = (app: TeamApplication) => { + setDeletingApp(app); + setDeleteDialogOpen(true); + }; + + const handleConfirmDelete = async () => { + if (!deletingApp) return; + + setDeleting(true); + try { + await deleteTeamApplication(deletingApp.id!); + toast({ + title: '删除成功', + description: '应用配置已删除', + }); + setDeleteDialogOpen(false); + setDeletingApp(null); + loadTeamApplications(); + onSuccess?.(); + } catch (error: any) { + toast({ + title: '删除失败', + description: error.message || '删除应用配置失败', + variant: 'destructive', + }); + } finally { + setDeleting(false); + } + }; + + const getEnvironmentName = (environmentId: number) => { + return ( + environments.find((env) => env.id === environmentId)?.envName || '未知环境' + ); + }; + + return ( + <> + + + + 管理应用配置 + + + + {/* 工具栏 */} +
+ +
+ + {/* 应用列表表格 */} +
+ {loading ? ( +
+ +
+ ) : teamApplications.length === 0 ? ( +
+

暂无应用配置

+ +
+ ) : ( + + + + 应用名称 + 环境 + 分支 + Jenkins系统 + Jenkins Job + 工作流 + 操作 + + + + {teamApplications.map((app) => ( + + + {app.applicationName || `应用 ${app.applicationId}`} + + + {getEnvironmentName(app.environmentId)} + + {app.branch || '-'} + + {app.deploySystemName || `系统 ${app.deploySystemId}`} + + {app.deployJob || '-'} + + {app.workflowDefinitionName || + (app.workflowDefinitionId + ? `工作流 ${app.workflowDefinitionId}` + : '-')} + + +
+ + +
+
+
+ ))} +
+
+ )} +
+
+ + + + +
+
+ + {/* 应用配置对话框 */} + {editingEnvironment && ( + + )} + + {/* 删除确认对话框 */} + + + + 确认删除 + + 确定要删除应用配置 " + {deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗? + 此操作无法撤销。 + + + + 取消 + + {deleting && } + 删除 + + + + + + ); +}; + diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamConfigDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamConfigDialog.tsx deleted file mode 100644 index 2c13479f..00000000 --- a/frontend/src/pages/Deploy/Team/List/components/TeamConfigDialog.tsx +++ /dev/null @@ -1,1474 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { Search, Check, ChevronDown } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { useToast } from '@/components/ui/use-toast'; -import { Plus, Trash2, Loader2, AlertCircle } from 'lucide-react'; -import type { - TeamConfig, - TeamConfigRequest, - TeamApplication, - TeamApplicationRequest, - Environment, - Application, -} from '../types'; -import type { UserResponse } from '@/pages/System/User/List/types'; -import type { RepositoryBranchResponse } from '@/pages/Resource/Git/List/types'; -import type { WorkflowDefinition } from '@/pages/Workflow/Definition/List/types'; -import { - getTeamConfig, - createTeamConfig, - updateTeamConfig, - getTeamApplications, - createTeamApplication, - deleteTeamApplication, - getJenkinsSystems, - getJenkinsJobs, -} from '../service'; -import { getRepositoryBranchesList } from '@/pages/Resource/Git/List/service'; -import { getPublishedDefinitions } from '@/pages/Workflow/Definition/List/service'; - -interface TeamConfigDialogProps { - open: boolean; - teamId: number; - teamName: string; - users: UserResponse[]; - environments: Environment[]; - applications: Application[]; - onOpenChange: (open: boolean) => void; - onSuccess: () => void; -} - -const TeamConfigDialog: React.FC = ({ - open, - teamId, - teamName, - users, - environments, - applications, - onOpenChange, - onSuccess, -}) => { - const { toast } = useToast(); - - // 状态管理 - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [teamConfig, setTeamConfig] = useState(null); - const [teamApplications, setTeamApplications] = useState([]); - - // 步骤管理 - const [currentStep, setCurrentStep] = useState(1); // 1: 环境选择, 2: 环境配置, 3: 配置预览 - const [currentConfigEnvId, setCurrentConfigEnvId] = useState(null); // 当前正在配置的环境ID - - // 表单状态 - 改为以环境ID为key的对象结构 - const [selectedEnvs, setSelectedEnvs] = useState([]); - const [envConfigs, setEnvConfigs] = useState>({}); // 每个环境的审批配置 - const [configuredEnvs, setConfiguredEnvs] = useState([]); // 已配置的环境列表 - - // 每个环境的添加表单状态 - const [addForms, setAddForms] = useState>({}); - - // 分支列表状态 - 以应用ID为key - const [branchesMap, setBranchesMap] = useState>({}); - const [loadingBranches, setLoadingBranches] = useState>({}); - - // 分支选择器的搜索和打开状态 - 以环境ID为key - const [branchSearchValues, setBranchSearchValues] = useState>({}); - const [branchPopoverOpen, setBranchPopoverOpen] = useState>({}); - - // Jenkins 系统和 Job 列表 - const [jenkinsSystems, setJenkinsSystems] = useState([]); - const [jenkinsJobsMap, setJenkinsJobsMap] = useState>({}); - const [loadingJobs, setLoadingJobs] = useState>({}); - - // Jenkins Job 选择器的搜索和打开状态 - 以环境ID为key - const [jobSearchValues, setJobSearchValues] = useState>({}); - const [jobPopoverOpen, setJobPopoverOpen] = useState>({}); - - // 工作流定义列表 - const [workflowDefinitions, setWorkflowDefinitions] = useState([]); - - // 加载数据 - useEffect(() => { - if (open && teamId) { - loadData(); - setCurrentStep(1); // 重置为第一步 - } - }, [open, teamId]); - - const loadData = async () => { - setLoading(true); - try { - await Promise.all([ - loadTeamConfig(), - loadTeamApplications(), - loadJenkinsSystems(), - loadWorkflowDefinitions() - ]); - } catch (error) { - console.error('加载数据失败:', error); - } finally { - setLoading(false); - } - }; - - // 加载 Jenkins 系统列表 - const loadJenkinsSystems = async () => { - try { - const systems = await getJenkinsSystems(); - setJenkinsSystems(systems || []); - } catch (error) { - console.error('加载Jenkins系统失败:', error); - setJenkinsSystems([]); - } - }; - - // 加载工作流定义列表(已发布) - const loadWorkflowDefinitions = async () => { - try { - const definitions = await getPublishedDefinitions(); - setWorkflowDefinitions(definitions || []); - } catch (error) { - console.error('加载工作流定义失败:', error); - setWorkflowDefinitions([]); - } - }; - - const loadTeamConfig = async () => { - try { - const config = await getTeamConfig(teamId); - if (config) { - setTeamConfig(config); - setSelectedEnvs(config.allowedEnvironmentIds || []); - - // 转换为新的数据结构 - const configs: Record = {}; - config.allowedEnvironmentIds.forEach((envId, index) => { - configs[envId] = { - approvalRequired: config.environmentApprovalRequired[index] || false, - approverUserIds: config.approverUserIds[index] || [], - }; - }); - setEnvConfigs(configs); - setConfiguredEnvs(config.allowedEnvironmentIds); - } else { - // 没有配置数据,重置为空 - setSelectedEnvs([]); - setEnvConfigs({}); - setConfiguredEnvs([]); - } - } catch (error: any) { - // 404 表示配置不存在,这是正常的,重置为空 - if (error.response?.status === 404) { - setTeamConfig(null); - setSelectedEnvs([]); - setEnvConfigs({}); - setConfiguredEnvs([]); - } else { - console.error('加载团队配置失败:', error); - } - } - }; - - const loadTeamApplications = async () => { - try { - const response = await getTeamApplications({ teamId, pageNum: 0, size: 1000 }); - if (response) { - setTeamApplications(response.content || []); - } - } catch (error) { - console.error('加载团队应用失败:', error); - } - }; - - // 当团队应用加载后,自动加载所需的 Jenkins Jobs (用于编辑时的下拉选择) - useEffect(() => { - if (teamApplications.length > 0) { - // 收集所有需要加载的 deploySystemId - const systemIds = new Set(); - teamApplications.forEach(app => { - if (app.deploySystemId) { - systemIds.add(app.deploySystemId); - } - }); - - // 加载所有需要的 Jenkins Jobs - systemIds.forEach(systemId => { - if (!jenkinsJobsMap[systemId]) { - loadJenkinsJobs(systemId); - } - }); - } - }, [teamApplications]); - - // 处理环境选择 - const handleEnvToggle = (envId: number, checked: boolean) => { - if (checked) { - setSelectedEnvs([...selectedEnvs, envId]); - // 初始化该环境的配置 - if (!envConfigs[envId]) { - setEnvConfigs({ - ...envConfigs, - [envId]: { approvalRequired: false, approverUserIds: [] }, - }); - } - } else { - setSelectedEnvs(selectedEnvs.filter((id) => id !== envId)); - // 移除该环境的配置 - const newConfigs = { ...envConfigs }; - delete newConfigs[envId]; - setEnvConfigs(newConfigs); - setConfiguredEnvs(configuredEnvs.filter((id) => id !== envId)); - } - }; - - // 处理当前环境的审批开关 - const handleCurrentEnvApprovalToggle = (checked: boolean) => { - if (!currentConfigEnvId) return; - - setEnvConfigs({ - ...envConfigs, - [currentConfigEnvId]: { - ...envConfigs[currentConfigEnvId], - approvalRequired: checked, - approverUserIds: checked ? envConfigs[currentConfigEnvId]?.approverUserIds || [] : [], - }, - }); - }; - - // 处理当前环境的审批人选择 - const handleCurrentEnvApproverToggle = (userId: number, checked: boolean) => { - if (!currentConfigEnvId) return; - - const currentApprovers = envConfigs[currentConfigEnvId]?.approverUserIds || []; - const newApprovers = checked - ? [...currentApprovers, userId] - : currentApprovers.filter((id) => id !== userId); - - setEnvConfigs({ - ...envConfigs, - [currentConfigEnvId]: { - ...envConfigs[currentConfigEnvId], - approverUserIds: newApprovers, - }, - }); - }; - - // 步骤导航 - const handleNext = () => { - // Step 1: 环境选择 → Step 2: 环境配置 或 直接保存 - if (currentStep === 1) { - if (selectedEnvs.length === 0) { - // 没有选择环境,直接保存空配置 - handleSaveConfig(); - return; - } - // 进入 Step 2,默认选择第一个环境 - setCurrentConfigEnvId(selectedEnvs[0]); - setCurrentStep(2); - return; - } - - // Step 2: 环境配置 → Step 3: 配置预览 - if (currentStep === 2) { - // 校验当前环境的配置 - if (currentConfigEnvId) { - const config = envConfigs[currentConfigEnvId]; - if (config?.approvalRequired && (!config.approverUserIds || config.approverUserIds.length === 0)) { - const env = environments.find((e) => e.id === currentConfigEnvId); - toast({ - variant: 'destructive', - title: `环境"${env?.envName}"需要审批,请至少选择一个审批人`, - }); - return; - } - - // 标记当前环境已配置 - if (!configuredEnvs.includes(currentConfigEnvId)) { - setConfiguredEnvs([...configuredEnvs, currentConfigEnvId]); - } - } - - // 检查是否所有环境都已配置 - const unconfiguredEnvs = selectedEnvs.filter((id) => !configuredEnvs.includes(id) && id !== currentConfigEnvId); - - if (unconfiguredEnvs.length > 0) { - // 还有未配置的环境,切换到下一个 - setCurrentConfigEnvId(unconfiguredEnvs[0]); - toast({ - title: '当前环境配置已保存', - description: `请继续配置 "${environments.find((e) => e.id === unconfiguredEnvs[0])?.envName}"`, - }); - } else { - // 所有环境都配置完了,进入预览 - setCurrentStep(3); - } - return; - } - }; - - const handlePrev = () => { - if (currentStep > 1) { - setCurrentStep(currentStep - 1); - } - }; - - // 切换配置的环境 - const handleSwitchConfigEnv = (envId: number) => { - // 先保存当前环境的配置状态 - if (currentConfigEnvId && !configuredEnvs.includes(currentConfigEnvId)) { - const config = envConfigs[currentConfigEnvId]; - // 如果已经有配置内容,标记为已配置 - if (config && (config.approvalRequired || (envApps(currentConfigEnvId).length > 0))) { - setConfiguredEnvs([...configuredEnvs, currentConfigEnvId]); - } - } - setCurrentConfigEnvId(envId); - }; - - // 跳过配置(直接进入预览) - const handleSkipToPreview = () => { - if (currentConfigEnvId && !configuredEnvs.includes(currentConfigEnvId)) { - setConfiguredEnvs([...configuredEnvs, currentConfigEnvId]); - } - setCurrentStep(3); - }; - - // 保存最终配置 - const handleSaveConfig = async () => { - setSaving(true); - try { - // 如果没有选择任何环境,清空所有配置 - if (selectedEnvs.length === 0) { - const configData: TeamConfigRequest = { - teamId, - allowedEnvironmentIds: [], - environmentApprovalRequired: [], - approverUserIds: [], - }; - - if (teamConfig) { - await updateTeamConfig(teamConfig.id, configData); - } else { - const newConfig = await createTeamConfig(configData); - setTeamConfig(newConfig); - } - - toast({ - title: '保存成功', - description: '已清空团队配置', - }); - onSuccess(); - onOpenChange(false); - return; - } - - // 只保存已勾选环境的配置 - const allowedEnvironmentIds = selectedEnvs; - const environmentApprovalRequired = selectedEnvs.map( - (envId) => envConfigs[envId]?.approvalRequired || false - ); - const approverUserIds = selectedEnvs.map( - (envId) => envConfigs[envId]?.approverUserIds || null - ); - - const configData: TeamConfigRequest = { - teamId, - allowedEnvironmentIds, - environmentApprovalRequired, - approverUserIds, - }; - - if (teamConfig) { - // 更新 - await updateTeamConfig(teamConfig.id, configData); - } else { - // 创建 - const newConfig = await createTeamConfig(configData); - setTeamConfig(newConfig); - } - - toast({ - title: '保存成功', - description: '团队配置已保存', - }); - onSuccess(); - onOpenChange(false); - } catch (error: any) { - toast({ - variant: 'destructive', - title: '保存失败', - description: error.response?.data?.message || '保存配置失败', - }); - } finally { - setSaving(false); - } - }; - - // 获取某个环境的应用列表(辅助函数) - const envApps = (envId: number) => { - return teamApplications.filter((app) => app.environmentId === envId); - }; - - // 加载 Jenkins Jobs - const loadJenkinsJobs = async (systemId: number) => { - if (jenkinsJobsMap[systemId]) { - return; // 已加载过 - } - - setLoadingJobs(prev => ({ ...prev, [systemId]: true })); - try { - const jobs = await getJenkinsJobs(systemId); - setJenkinsJobsMap(prev => ({ - ...prev, - [systemId]: jobs || [], - })); - } catch (error) { - console.error('加载Jenkins Jobs失败:', error); - setJenkinsJobsMap(prev => ({ - ...prev, - [systemId]: [], - })); - } finally { - setLoadingJobs(prev => ({ ...prev, [systemId]: false })); - } - }; - - // 加载分支列表 - 只在用户选择应用时调用 - const loadBranches = async (appId: number, app: Application) => { - // 如果应用没有关联仓库项目,跳过 - if (!app.repoProjectId || !app.externalSystemId) { - console.log('应用未关联仓库项目,跳过分支加载'); - setBranchesMap(prev => ({ - ...prev, - [appId]: [], - })); - return; - } - - // 如果已经加载过,跳过 - if (branchesMap[appId]) { - return; - } - - setLoadingBranches(prev => ({ ...prev, [appId]: true })); - try { - // 调用 /api/v1/repository-branch/list 接口 - const branches = await getRepositoryBranchesList({ - externalSystemId: app.externalSystemId, - repoProjectId: app.repoProjectId, - }); - - setBranchesMap(prev => ({ - ...prev, - [appId]: branches || [], - })); - } catch (error) { - console.error('加载分支列表失败:', error); - toast({ - variant: 'destructive', - title: '加载分支列表失败', - description: '无法获取该应用的分支信息', - }); - setBranchesMap(prev => ({ - ...prev, - [appId]: [], - })); - } finally { - setLoadingBranches(prev => ({ ...prev, [appId]: false })); - } - }; - - // 添加应用到环境 - const handleAddApplication = async (envId: number) => { - const form = addForms[envId]; - if (!form?.appId) { - toast({ - variant: 'destructive', - title: '请选择应用', - }); - return; - } - - // 检查是否已存在 - const exists = teamApplications.some( - (app) => app.environmentId === envId && app.applicationId === form.appId - ); - if (exists) { - toast({ - variant: 'destructive', - title: '该应用已添加到此环境', - }); - return; - } - - try { - const data: TeamApplicationRequest = { - teamId, - applicationId: form.appId, - environmentId: envId, - branch: form.branch || undefined, - deploySystemId: form.deploySystemId || undefined, - deployJob: form.deployJob || undefined, - workflowDefinitionId: form.workflowDefinitionId || undefined, - }; - - await createTeamApplication(data); - - toast({ - title: '添加成功', - }); - - // 清空表单 - setAddForms({ - ...addForms, - [envId]: { appId: null, branch: '', deploySystemId: null, deployJob: '', workflowDefinitionId: null }, - }); - - // 重新加载 - loadTeamApplications(); - onSuccess(); - } catch (error: any) { - toast({ - variant: 'destructive', - title: '添加失败', - description: error.response?.data?.message || '添加应用失败', - }); - } - }; - - // 删除应用 - const handleDeleteApplication = async (id: number) => { - try { - await deleteTeamApplication(id); - - toast({ - title: '删除成功', - }); - - // 重新加载 - loadTeamApplications(); - onSuccess(); - } catch (error: any) { - toast({ - variant: 'destructive', - title: '删除失败', - description: error.response?.data?.message || '删除应用失败', - }); - } - }; - - // 获取某个环境下的应用列表 - const getEnvApplications = (envId: number) => { - return teamApplications.filter((app) => app.environmentId === envId); - }; - - - // 获取可添加的应用列表(排除已添加的) - const getAvailableApplications = (envId: number) => { - const addedAppIds = getEnvApplications(envId).map((app) => app.applicationId); - return applications.filter((app) => !addedAppIds.includes(app.id)); - }; - - // 渲染步骤指示器 - const renderStepIndicator = () => ( -
- {[ - { step: 1, label: '环境选择' }, - { step: 2, label: '环境配置' }, - { step: 3, label: '配置预览' }, - ].map((item, index) => ( - -
-
item.step - ? 'bg-primary/20 text-primary' - : 'bg-muted text-muted-foreground' - }`} - > - {item.step} -
- item.step - ? 'text-primary' - : 'text-muted-foreground' - }`} - > - {item.label} - -
- {index < 2 && ( -
item.step ? 'bg-primary' : 'bg-muted' - }`} - /> - )} - - ))} -
- ); - - return ( - - - - 团队配置:{teamName} - - - {loading ? ( -
- -
- ) : ( - <> - {/* 步骤指示器 */} -
- {renderStepIndicator()} -
- - -
- {/* Step 1: 环境选择 */} - {currentStep === 1 && ( - - - 选择可访问的环境 -

- 选择该团队可以访问哪些环境 -

-
- - {environments.length === 0 ? ( -
- -

暂无可用环境

-

- 请先前往"环境管理"创建环境 -

-
- ) : ( -
- {environments.map((env) => ( -
handleEnvToggle(env.id, !selectedEnvs.includes(env.id))} - > - - handleEnvToggle(env.id, checked as boolean) - } - onClick={(e) => e.stopPropagation()} - /> -
-
- -
-

- {env.envCode} -

-
-
- ))} -
- )} - {selectedEnvs.length > 0 ? ( -
-

- 已选择 {selectedEnvs.length} 个环境 -

-
- ) : ( -
-

- 未选择任何环境,点击"下一步"将清空该团队的环境配置 -

-
- )} -
-
- )} - - {/* Step 2: 环境配置(侧边导航式) */} - {currentStep === 2 && currentConfigEnvId && ( -
- {/* 左侧:环境列表 */} - - - 环境列表 - - - {selectedEnvs.map((envId) => { - const env = environments.find((e) => e.id === envId); - if (!env) return null; - const isConfigured = configuredEnvs.includes(envId); - const isCurrent = envId === currentConfigEnvId; - - return ( -
handleSwitchConfigEnv(envId)} - > - - {env.envName} - - {isConfigured && !isCurrent && ( - - )} -
- ); - })} -
-
- - {/* 右侧:当前环境配置 */} -
- {(() => { - const currentEnv = environments.find((e) => e.id === currentConfigEnvId); - const currentConfig = envConfigs[currentConfigEnvId] || { approvalRequired: false, approverUserIds: [] }; - - return ( - <> - {/* 环境审批配置 */} - - -
- {currentEnv?.envName} -
-

- 配置审批规则和应用配置 -

-
- - {/* 审批开关 */} -
- - -
- - {/* 审批人选择 */} - {currentConfig.approvalRequired && ( -
- -
- {users.map((user) => ( -
- - handleCurrentEnvApproverToggle(user.id, checked as boolean) - } - /> - -
- ))} -
-
- )} -
-
- - {/* 应用配置 */} - - - 应用配置 - - - {applications.length === 0 ? ( -
- -

暂无可用应用

-
- ) : ( -
- {/* 已配置的应用列表 - 卡片式 */} - {envApps(currentConfigEnvId).map((app) => { - // 查找Jenkins系统名称 - const deploySystemName = app.deploySystemName || - jenkinsSystems.find(s => s.id === app.deploySystemId)?.name; - - // Job名称直接从后端获取 - const deployJob = app.deployJob; - - // 工作流定义名称 - const workflowName = app.workflowDefinitionName || - workflowDefinitions.find(w => w.id === app.workflowDefinitionId)?.name; - - return ( -
-
-
- {/* 应用名称 */} -
- {app.applicationName} - {app.branch && ( - - {app.branch} - - )} -
- - {/* 配置信息 - 网格布局 */} -
- {deploySystemName && ( -
- Jenkins: - {deploySystemName} -
- )} - {deployJob && ( -
- Job: - {deployJob} -
- )} - {workflowName && ( -
- 工作流: - {workflowName} -
- )} -
-
- - {/* 删除按钮 */} - -
-
- ); - })} - - {/* 添加新应用表单 */} - {getAvailableApplications(currentConfigEnvId).length > 0 && ( -
-
-
- - 添加应用配置 -
- - {/* 应用选择 */} -
- - -
- - {/* 分支选择 */} -
- - {addForms[currentConfigEnvId]?.appId ? ( - (() => { - const appId = addForms[currentConfigEnvId].appId!; - const branches = branchesMap[appId] || []; - const isLoading = loadingBranches[appId]; - const searchValue = branchSearchValues[currentConfigEnvId] || ""; - const open = branchPopoverOpen[currentConfigEnvId] || false; - const filteredBranches = branches.filter(branch => - branch.name.toLowerCase().includes(searchValue.toLowerCase()) - ); - - return ( - { - setBranchPopoverOpen({ - ...branchPopoverOpen, - [currentConfigEnvId]: isOpen, - }); - }} - > - - - - -
- - { - setBranchSearchValues({ - ...branchSearchValues, - [currentConfigEnvId]: e.target.value, - }); - }} - /> -
-
- -
- {filteredBranches.length === 0 ? ( -
- 未找到分支 -
- ) : ( - filteredBranches.map((branch) => ( -
{ - setAddForms({ - ...addForms, - [currentConfigEnvId]: { - ...addForms[currentConfigEnvId], - branch: branch.name, - }, - }); - setBranchSearchValues({ - ...branchSearchValues, - [currentConfigEnvId]: "", - }); - setBranchPopoverOpen({ - ...branchPopoverOpen, - [currentConfigEnvId]: false, - }); - }} - > - - {branch.name} - {branch.isDefaultBranch && ( - (默认) - )} - - {branch.name === addForms[currentConfigEnvId]?.branch && ( - - )} -
- )) - )} -
-
-
-
-
- ); - })() - ) : ( - - )} -
- - {/* Jenkins配置 - 网格布局 */} -
- {/* Jenkins 系统 */} -
- - -
- - {/* Jenkins Job */} -
- - {addForms[currentConfigEnvId]?.deploySystemId ? ( - (() => { - const systemId = addForms[currentConfigEnvId].deploySystemId!; - const jobs = jenkinsJobsMap[systemId] || []; - const isLoading = loadingJobs[systemId]; - const searchValue = jobSearchValues[currentConfigEnvId] || ""; - const open = jobPopoverOpen[currentConfigEnvId] || false; - const filteredJobs = jobs.filter(job => - job.jobName.toLowerCase().includes(searchValue.toLowerCase()) - ); - - return ( - { - setJobPopoverOpen({ - ...jobPopoverOpen, - [currentConfigEnvId]: isOpen, - }); - }} - > - - - - -
- - { - setJobSearchValues({ - ...jobSearchValues, - [currentConfigEnvId]: e.target.value, - }); - }} - /> -
-
- -
- {filteredJobs.length === 0 ? ( -
- 未找到Job -
- ) : ( - filteredJobs.map((job) => ( -
{ - setAddForms({ - ...addForms, - [currentConfigEnvId]: { - ...addForms[currentConfigEnvId], - deployJob: job.jobName, - }, - }); - setJobSearchValues({ - ...jobSearchValues, - [currentConfigEnvId]: "", - }); - setJobPopoverOpen({ - ...jobPopoverOpen, - [currentConfigEnvId]: false, - }); - }} - > - - {job.jobName} - - {job.jobName === addForms[currentConfigEnvId]?.deployJob && ( - - )} -
- )) - )} -
-
-
-
-
- ); - })() - ) : ( - - )} -
-
- - {/* 工作流定义 */} -
- - -
- - {/* 添加按钮 */} -
- -
-
-
- )} - - {/* 空状态提示 */} - {envApps(currentConfigEnvId).length === 0 && getAvailableApplications(currentConfigEnvId).length === 0 && ( -
-

所有应用已添加

-
- )} -
- )} -
-
- - ); - })()} -
-
- )} - - {/* Step 3: 配置预览 */} - {currentStep === 3 && ( - - - 配置预览 -

- 请检查所有配置,确认无误后点击"完成配置" -

-
- -
- {selectedEnvs.map((envId) => { - const env = environments.find((e) => e.id === envId); - if (!env) return null; - const config = envConfigs[envId] || { approvalRequired: false, approverUserIds: [] }; - const apps = envApps(envId); - - return ( - - -
-
- {env.envName} -
- -
-
- - {/* 审批配置信息 */} -
- -
- {config.approvalRequired ? ( - <> - 需要审批 - {config.approverUserIds.length > 0 && ( - - 审批人: - {config.approverUserIds - .map((userId) => { - const user = users.find((u) => u.id === userId); - return user?.nickname || user?.username; - }) - .filter(Boolean) - .join('、')} - - )} - - ) : ( - 无需审批 - )} -
-
- - {/* 应用配置信息 */} -
- - {apps.length > 0 ? ( -
- {apps.map((app) => { - // 查找Jenkins系统名称 - const deploySystemName = app.deploySystemName || - jenkinsSystems.find(s => s.id === app.deploySystemId)?.name; - - // Job名称直接从后端获取 - const deployJob = app.deployJob; - - // 工作流定义名称 - const workflowName = app.workflowDefinitionName || - workflowDefinitions.find(w => w.id === app.workflowDefinitionId)?.name; - - return ( -
-
- {app.applicationName} - - {app.branch || '-'} - -
- {(deploySystemName || deployJob || workflowName) && ( -
- {deploySystemName && ( -
- Jenkins: - {deploySystemName} -
- )} - {deployJob && ( -
- Job: - {deployJob} -
- )} - {workflowName && ( -
- 工作流: - {workflowName} -
- )} -
- )} -
- ); - })} -
- ) : ( -

暂无应用配置

- )} -
-
-
- ); - })} -
-
-
- )} -
-
- - {/* 底部导航按钮 */} - - - -
- {currentStep === 2 && ( - - )} - {currentStep < 3 ? ( - - ) : ( - - )} -
-
- - )} -
-
- ); -}; - -export default TeamConfigDialog; - diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentConfigDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentConfigDialog.tsx new file mode 100644 index 00000000..ffe3293e --- /dev/null +++ b/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentConfigDialog.tsx @@ -0,0 +1,599 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogBody, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Badge } from '@/components/ui/badge'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { useToast } from '@/components/ui/use-toast'; +import { Check, ChevronsUpDown, Loader2, X } from 'lucide-react'; +import type { Environment } from '@/pages/Deploy/Environment/List/types'; +import { + getTeamEnvironmentConfig, + createTeamEnvironmentConfig, + updateTeamEnvironmentConfig, + getNotificationChannels, +} from '../service'; + +interface User { + id: number; + username: string; + realName?: string; +} + +// 表单验证 Schema +const formSchema = z.object({ + environmentId: z.number().min(1, '请选择环境'), + approvalRequired: z.boolean().default(false), + approverUserIds: z.array(z.number()).default([]), + notificationChannelId: z.number().optional(), + notificationEnabled: z.boolean().default(false), + requireCodeReview: z.boolean().default(false), + remark: z.string().max(100, '备注最多100个字符').optional(), +}).refine( + (data) => { + // 如果启用了通知,则通知渠道必填 + if (data.notificationEnabled && !data.notificationChannelId) { + return false; + } + return true; + }, + { + message: '启用通知时必须选择通知渠道', + path: ['notificationChannelId'], + } +).refine( + (data) => { + // 如果需要审批,则审批人必填 + if (data.approvalRequired && (!data.approverUserIds || data.approverUserIds.length === 0)) { + return false; + } + return true; + }, + { + message: '需要审批时必须选择审批人', + path: ['approverUserIds'], + } +); + +type FormData = z.infer; + +interface NotificationChannel { + id: number; + name: string; + type: string; +} + +interface TeamEnvironmentConfigDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + teamId: number; + environments: Environment[]; + users?: User[]; + onSuccess?: () => void; + // 编辑模式:传入要编辑的环境ID + editEnvironmentId?: number; + // 旧的初始数据模式(向后兼容) + initialData?: { + environmentId: number; + workflowDefinitionId?: number; + notificationChannelId?: number; + notificationEnabled?: boolean; + requireCodeReview?: boolean; + remark?: string; + }; +} + +export const TeamEnvironmentConfigDialog: React.FC< + TeamEnvironmentConfigDialogProps +> = ({ + open, + onOpenChange, + teamId, + environments, + users = [], + onSuccess, + editEnvironmentId, + initialData, +}) => { + const { toast } = useToast(); + const [submitting, setSubmitting] = useState(false); + const [loading, setLoading] = useState(false); + const [configId, setConfigId] = useState(null); + const [notificationChannels, setNotificationChannels] = useState< + NotificationChannel[] + >([]); + const [loadingChannels, setLoadingChannels] = useState(false); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + environmentId: initialData?.environmentId || editEnvironmentId || 0, + approvalRequired: false, + approverUserIds: [], + notificationChannelId: initialData?.notificationChannelId, + notificationEnabled: initialData?.notificationEnabled || false, + requireCodeReview: initialData?.requireCodeReview || false, + remark: initialData?.remark || '', + }, + }); + + // 对话框打开时初始化 + useEffect(() => { + if (open) { + loadNotificationChannels(); + + if (editEnvironmentId) { + // 编辑模式或绑定新环境:设置环境ID并加载配置 + form.setValue('environmentId', editEnvironmentId); + loadEnvironmentConfig(); + } else if (initialData) { + // 旧模式兼容 + setConfigId(null); + form.reset({ + environmentId: initialData.environmentId, + approvalRequired: false, + approverUserIds: [], + notificationChannelId: initialData.notificationChannelId, + notificationEnabled: initialData.notificationEnabled || false, + requireCodeReview: initialData.requireCodeReview || false, + remark: initialData.remark || '', + }); + } else { + // 创建模式:清空表单 + setConfigId(null); + form.reset({ + environmentId: 0, + approvalRequired: false, + approverUserIds: [], + notificationChannelId: undefined, + notificationEnabled: false, + requireCodeReview: false, + remark: '', + }); + } + } + }, [open, editEnvironmentId, teamId]); + + const loadEnvironmentConfig = async () => { + if (!editEnvironmentId) return; + + setLoading(true); + try { + const config = await getTeamEnvironmentConfig(teamId, editEnvironmentId); + + if (config) { + setConfigId(config.id); + form.reset({ + environmentId: config.environmentId, + approvalRequired: config.approvalRequired || false, + approverUserIds: config.approverUserIds || [], + notificationChannelId: config.notificationChannelId, + notificationEnabled: config.notificationEnabled || false, + requireCodeReview: config.requireCodeReview || false, + remark: config.remark || '', + }); + } + } catch (error: any) { + // 如果找不到配置(404),说明是首次配置该环境 + if (error.response?.status === 404) { + setConfigId(null); + form.reset({ + environmentId: editEnvironmentId, + approvalRequired: false, + approverUserIds: [], + notificationChannelId: undefined, + notificationEnabled: false, + requireCodeReview: false, + remark: '', + }); + } else { + toast({ + title: '加载失败', + description: error.message || '无法加载环境配置', + variant: 'destructive', + }); + } + } finally { + setLoading(false); + } + }; + + const loadNotificationChannels = async () => { + setLoadingChannels(true); + try { + const channels = await getNotificationChannels(); + setNotificationChannels(channels); + } catch (error) { + toast({ + title: '加载失败', + description: '无法加载通知渠道列表', + variant: 'destructive', + }); + } finally { + setLoadingChannels(false); + } + }; + + const handleSubmit = async (data: FormData) => { + setSubmitting(true); + try { + const payload = { + teamId, + environmentId: data.environmentId, + approvalRequired: data.approvalRequired, + approverUserIds: data.approverUserIds, + notificationChannelId: data.notificationChannelId, + notificationEnabled: data.notificationEnabled, + requireCodeReview: data.requireCodeReview, + remark: data.remark, + }; + + if (configId) { + // 更新已有配置 + await updateTeamEnvironmentConfig(configId, payload); + toast({ + title: '保存成功', + description: '环境配置已更新', + }); + } else { + // 创建新配置 + await createTeamEnvironmentConfig(payload); + toast({ + title: '保存成功', + description: '环境配置已创建', + }); + } + + onOpenChange(false); + onSuccess?.(); + } catch (error: any) { + toast({ + title: '保存失败', + description: error.message || '保存环境配置失败', + variant: 'destructive', + }); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + {editEnvironmentId ? '编辑环境配置' : '配置环境'} + + + + + {loading ? ( +
+ +
+ ) : ( +
+ + {/* 环境选择 */} + ( + + 环境 * + + + + )} + /> + + {/* 是否需要审批 */} + ( + +
+ 需要审批 +
+ 部署前需要通过审批流程 +
+
+ + + +
+ )} + /> + + {/* 审批人多选 - 仅在需要审批时显示 */} + {form.watch('approvalRequired') && ( + { + const [open, setOpen] = useState(false); + const selectedUsers = (users || []).filter(u => field.value?.includes(u.id)); + + return ( + + 审批人 * + + + + + + ))} +
+ ) : ( + "请选择审批人" + )} + + + + + + + + + 未找到审批人 + + {(users || []).map((user) => { + const isSelected = field.value?.includes(user.id); + return ( + { + const newValue = isSelected + ? field.value?.filter(id => id !== user.id) || [] + : [...(field.value || []), user.id]; + field.onChange(newValue); + }} + > + + {user.realName || user.username} + + ); + })} + + + + + + + + ); + }} + /> + )} + + {/* 启用通知 */} + ( + +
+ 启用通知 +
+ 部署状态变更时发送通知 +
+
+ + + +
+ )} + /> + + {/* 通知渠道选择 - 仅在启用通知时显示 */} + {form.watch('notificationEnabled') && ( + ( + + 通知渠道 * + + + + )} + /> + )} + + {/* 需要代码审查 */} + ( + +
+ 需要代码审查 +
+ 部署前需要通过代码审查 +
+
+ + + +
+ )} + /> + + {/* 备注 */} + ( + + 备注 + +