更换变量显示组件
This commit is contained in:
parent
e27bbca4b3
commit
e4fe82d7ea
@ -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<TeamApplication[]>([]);
|
||||
// 如果从环境管理进入,只显示该环境的应用;否则显示所有应用
|
||||
const selectedEnvironmentId = environmentId || null;
|
||||
|
||||
// 基础数据状态
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [jenkinsSystems, setJenkinsSystems] = useState<any[]>([]);
|
||||
const [workflowDefinitions, setWorkflowDefinitions] = useState<WorkflowDefinition[]>([]);
|
||||
|
||||
// 应用配置对话框状态
|
||||
const [appDialogOpen, setAppDialogOpen] = useState(false);
|
||||
const [appDialogMode, setAppDialogMode] = useState<'create' | 'edit'>('create');
|
||||
const [editingApp, setEditingApp] = useState<TeamApplication | null>(null);
|
||||
const [editingEnvironment, setEditingEnvironment] = useState<Environment | null>(null);
|
||||
|
||||
// 删除确认对话框状态
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deletingApp, setDeletingApp] = useState<TeamApplication | null>(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 (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>管理应用配置</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
{/* 工具栏 */}
|
||||
<div className="flex items-center justify-end mb-4">
|
||||
<Button onClick={handleOpenAddAppDialog}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加应用
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 应用列表表格 */}
|
||||
<div className="border rounded-lg">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : teamApplications.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
||||
<p>暂无应用配置</p>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={handleOpenAddAppDialog}
|
||||
className="mt-2"
|
||||
>
|
||||
立即添加
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>应用名称</TableHead>
|
||||
<TableHead>环境</TableHead>
|
||||
<TableHead>分支</TableHead>
|
||||
<TableHead>Jenkins系统</TableHead>
|
||||
<TableHead>Jenkins Job</TableHead>
|
||||
<TableHead>工作流</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{teamApplications.map((app) => (
|
||||
<TableRow key={app.id}>
|
||||
<TableCell className="font-medium">
|
||||
{app.applicationName || `应用 ${app.applicationId}`}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getEnvironmentName(app.environmentId)}
|
||||
</TableCell>
|
||||
<TableCell>{app.branch || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{app.deploySystemName || `系统 ${app.deploySystemId}`}
|
||||
</TableCell>
|
||||
<TableCell>{app.deployJob || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{app.workflowDefinitionName ||
|
||||
(app.workflowDefinitionId
|
||||
? `工作流 ${app.workflowDefinitionId}`
|
||||
: '-')}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleOpenEditAppDialog(app)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleOpenDeleteDialog(app)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 应用配置对话框 */}
|
||||
{editingEnvironment && (
|
||||
<TeamApplicationDialog
|
||||
open={appDialogOpen}
|
||||
onOpenChange={setAppDialogOpen}
|
||||
mode={appDialogMode}
|
||||
teamId={teamId}
|
||||
environmentId={editingEnvironment.id}
|
||||
environmentName={editingEnvironment.envName}
|
||||
application={editingApp || undefined}
|
||||
applications={applications}
|
||||
jenkinsSystems={jenkinsSystems}
|
||||
workflowDefinitions={workflowDefinitions}
|
||||
onSave={handleSaveApplication}
|
||||
onLoadBranches={handleLoadBranches}
|
||||
onLoadJenkinsJobs={handleLoadJenkinsJobs}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除应用配置 "
|
||||
{deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗?
|
||||
此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleting}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={deleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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<typeof formSchema>;
|
||||
|
||||
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<number | null>(null);
|
||||
const [notificationChannels, setNotificationChannels] = useState<
|
||||
NotificationChannel[]
|
||||
>([]);
|
||||
const [loadingChannels, setLoadingChannels] = useState(false);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editEnvironmentId ? '编辑环境配置' : '配置环境'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* 环境选择 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="environmentId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>环境 *</FormLabel>
|
||||
<Select
|
||||
value={field.value?.toString() || ''}
|
||||
onValueChange={(value) => field.onChange(Number(value))}
|
||||
disabled={!!initialData || !!editEnvironmentId}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择环境" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{environments.map((env) => (
|
||||
<SelectItem key={env.id} value={env.id.toString()}>
|
||||
{env.envName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 是否需要审批 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="approvalRequired"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">需要审批</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
部署前需要通过审批流程
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 审批人多选 - 仅在需要审批时显示 */}
|
||||
{form.watch('approvalRequired') && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="approverUserIds"
|
||||
render={({ field }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selectedUsers = (users || []).filter(u => field.value?.includes(u.id));
|
||||
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>审批人 *</FormLabel>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{selectedUsers.length > 0 ? (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{selectedUsers.map((user) => (
|
||||
<Badge
|
||||
key={user.id}
|
||||
variant="secondary"
|
||||
className="mr-1"
|
||||
>
|
||||
{user.realName || user.username}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
field.onChange(field.value?.filter(id => id !== user.id) || []);
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={() => {
|
||||
field.onChange(field.value?.filter(id => id !== user.id) || []);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
"请选择审批人"
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="搜索审批人..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>未找到审批人</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{(users || []).map((user) => {
|
||||
const isSelected = field.value?.includes(user.id);
|
||||
return (
|
||||
<CommandItem
|
||||
key={user.id}
|
||||
value={`${user.id}-${user.username}-${user.realName || ''}`}
|
||||
onSelect={() => {
|
||||
const newValue = isSelected
|
||||
? field.value?.filter(id => id !== user.id) || []
|
||||
: [...(field.value || []), user.id];
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
isSelected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{user.realName || user.username}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 启用通知 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">启用通知</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
部署状态变更时发送通知
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 通知渠道选择 - 仅在启用通知时显示 */}
|
||||
{form.watch('notificationEnabled') && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationChannelId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>通知渠道 *</FormLabel>
|
||||
<Select
|
||||
value={field.value?.toString() || ''}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value ? Number(value) : undefined)
|
||||
}
|
||||
disabled={loadingChannels}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingChannels
|
||||
? '加载中...'
|
||||
: '请选择通知渠道'
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{notificationChannels.map((channel) => (
|
||||
<SelectItem
|
||||
key={channel.id}
|
||||
value={channel.id.toString()}
|
||||
>
|
||||
{channel.name} ({channel.type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 需要代码审查 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireCodeReview"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">需要代码审查</FormLabel>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
部署前需要通过代码审查
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 备注 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remark"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>备注</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="请输入备注信息(最多100字符)"
|
||||
className="resize-none"
|
||||
rows={3}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={submitting}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={form.handleSubmit(handleSubmit)}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,393 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} 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 { Badge } from '@/components/ui/badge';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Settings, AppWindow, Trash2, Plus, Loader2 } from 'lucide-react';
|
||||
import type { Environment } from '@/pages/Deploy/Environment/List/types';
|
||||
import type { TeamEnvironmentConfig } from '../types';
|
||||
import { getTeamEnvironmentConfigs, deleteTeamEnvironmentConfig } from '../service';
|
||||
import { TeamEnvironmentConfigDialog } from './TeamEnvironmentConfigDialog';
|
||||
import { TeamApplicationManageDialog } from './TeamApplicationManageDialog';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
realName?: string;
|
||||
}
|
||||
|
||||
interface TeamEnvironmentManageDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
environments: Environment[];
|
||||
users: User[];
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export const TeamEnvironmentManageDialog: React.FC<
|
||||
TeamEnvironmentManageDialogProps
|
||||
> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
teamId,
|
||||
teamName,
|
||||
environments,
|
||||
users,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [configuredEnvs, setConfiguredEnvs] = useState<TeamEnvironmentConfig[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
// 环境配置对话框状态
|
||||
const [envConfigDialogOpen, setEnvConfigDialogOpen] = useState(false);
|
||||
const [editEnvironmentId, setEditEnvironmentId] = useState<
|
||||
number | undefined
|
||||
>();
|
||||
|
||||
// 应用管理对话框状态
|
||||
const [appManageDialogOpen, setAppManageDialogOpen] = useState(false);
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<
|
||||
number | undefined
|
||||
>();
|
||||
|
||||
// 删除确认对话框状态
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deletingConfig, setDeletingConfig] = useState<
|
||||
TeamEnvironmentConfig | null
|
||||
>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// 加载团队的环境配置列表
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadEnvironmentConfigs();
|
||||
}
|
||||
}, [open, teamId]);
|
||||
|
||||
const loadEnvironmentConfigs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const configs = await getTeamEnvironmentConfigs(teamId);
|
||||
// 关联环境名称
|
||||
const configsWithEnvName = configs.map((config: any) => ({
|
||||
...config,
|
||||
environmentName:
|
||||
environments.find((env) => env.id === config.environmentId)
|
||||
?.envName || '未知环境',
|
||||
}));
|
||||
setConfiguredEnvs(configsWithEnvName);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: error.message || '无法加载环境配置列表',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 未配置的环境
|
||||
const unconfiguredEnvs = environments.filter(
|
||||
(env) => !configuredEnvs.some((config) => config.environmentId === env.id)
|
||||
);
|
||||
|
||||
const handleOpenConfigDialog = (environmentId?: number) => {
|
||||
setEditEnvironmentId(environmentId);
|
||||
setEnvConfigDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenAppManageDialog = (environmentId: number) => {
|
||||
setSelectedEnvironmentId(environmentId);
|
||||
setAppManageDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenDeleteDialog = (config: TeamEnvironmentConfig) => {
|
||||
setDeletingConfig(config);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!deletingConfig) return;
|
||||
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deleteTeamEnvironmentConfig(deletingConfig.id);
|
||||
|
||||
toast({
|
||||
title: '删除成功',
|
||||
description: '环境配置已删除',
|
||||
});
|
||||
setDeleteDialogOpen(false);
|
||||
setDeletingConfig(null);
|
||||
loadEnvironmentConfigs();
|
||||
onSuccess?.();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: '删除失败',
|
||||
description: error.message || '删除环境配置失败',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfigSuccess = () => {
|
||||
loadEnvironmentConfigs();
|
||||
onSuccess?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>环境管理 - {teamName}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody className="overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* 已配置的环境 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">已配置的环境</h3>
|
||||
{configuredEnvs.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground border rounded-lg">
|
||||
暂无已配置的环境
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>环境名称</TableHead>
|
||||
<TableHead>审批人</TableHead>
|
||||
<TableHead>通知渠道</TableHead>
|
||||
<TableHead>应用数量</TableHead>
|
||||
<TableHead>代码审查</TableHead>
|
||||
<TableHead>备注</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{configuredEnvs.map((config) => (
|
||||
<TableRow key={config.id}>
|
||||
<TableCell className="font-medium">
|
||||
{config.environmentName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{config.approvalRequired && config.approverUserIds && config.approverUserIds.length > 0 ? (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{config.approverUserIds.map((userId) => {
|
||||
const user = users.find(u => u.id === userId);
|
||||
return user ? (
|
||||
<Badge key={userId} variant="secondary" className="text-xs">
|
||||
{user.realName || user.username}
|
||||
</Badge>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
无需审批
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{config.notificationEnabled ? (
|
||||
<Badge variant="outline">
|
||||
{config.notificationChannelName || '已启用'}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
未启用
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">
|
||||
{config.applicationCount || 0} 个应用
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
config.requireCodeReview
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
>
|
||||
{config.requireCodeReview ? '需要' : '不需要'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{config.remark || '-'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleOpenConfigDialog(
|
||||
config.environmentId
|
||||
)
|
||||
}
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
配置
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleOpenAppManageDialog(
|
||||
config.environmentId
|
||||
)
|
||||
}
|
||||
>
|
||||
<AppWindow className="h-4 w-4 mr-1" />
|
||||
应用
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleOpenDeleteDialog(config)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 未配置的环境 */}
|
||||
{unconfiguredEnvs.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
未配置的环境
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{unconfiguredEnvs.map((env) => (
|
||||
<div
|
||||
key={env.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent"
|
||||
>
|
||||
<span className="font-medium">{env.envName}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleOpenConfigDialog(env.id)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
绑定
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 环境配置对话框 */}
|
||||
<TeamEnvironmentConfigDialog
|
||||
open={envConfigDialogOpen}
|
||||
onOpenChange={setEnvConfigDialogOpen}
|
||||
teamId={teamId}
|
||||
environments={environments}
|
||||
users={users}
|
||||
editEnvironmentId={editEnvironmentId}
|
||||
onSuccess={handleConfigSuccess}
|
||||
/>
|
||||
|
||||
{/* 应用管理对话框 */}
|
||||
{selectedEnvironmentId && (
|
||||
<TeamApplicationManageDialog
|
||||
open={appManageDialogOpen}
|
||||
onOpenChange={setAppManageDialogOpen}
|
||||
teamId={teamId}
|
||||
environmentId={selectedEnvironmentId}
|
||||
environments={environments}
|
||||
onSuccess={handleConfigSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除环境"{deletingConfig?.environmentName}"的配置吗?
|
||||
此操作将同时删除该环境下的所有应用配置,且无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleting}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={deleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,9 +7,9 @@ import DeleteDialog from './components/DeleteDialog';
|
||||
import MemberManageDialog from './components/MemberManageDialog';
|
||||
import { getUserList } from '@/pages/System/User/List/service';
|
||||
import type { UserResponse } from '@/pages/System/User/List/types';
|
||||
import { getEnvironmentList, getApplicationList } from './service';
|
||||
import type { Environment, Application } from './types';
|
||||
import TeamConfigDialog from './components/TeamConfigDialog';
|
||||
import { getEnvironmentList } from './service';
|
||||
import type { Environment } from './types';
|
||||
import { TeamEnvironmentManageDialog } from './components/TeamEnvironmentManageDialog';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
@ -35,6 +35,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@ -50,9 +56,11 @@ import {
|
||||
Activity,
|
||||
Server,
|
||||
Database,
|
||||
MoreHorizontal,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
|
||||
|
||||
type Column = {
|
||||
accessorKey?: keyof TeamResponse;
|
||||
id?: string;
|
||||
@ -73,10 +81,9 @@ const TeamList: React.FC = () => {
|
||||
const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [memberDialogOpen, setMemberDialogOpen] = useState(false);
|
||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||
const [envManageDialogOpen, setEnvManageDialogOpen] = useState(false);
|
||||
const [users, setUsers] = useState<UserResponse[]>([]);
|
||||
const [environments, setEnvironments] = useState<Environment[]>([]);
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
|
||||
const form = useForm<SearchFormValues>({
|
||||
resolver: zodResolver(searchFormSchema),
|
||||
@ -124,7 +131,6 @@ const TeamList: React.FC = () => {
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
loadEnvironments();
|
||||
loadApplications();
|
||||
}, []);
|
||||
|
||||
const loadUsers = async () => {
|
||||
@ -149,16 +155,6 @@ const TeamList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadApplications = async () => {
|
||||
try {
|
||||
const response = await getApplicationList();
|
||||
if (response) {
|
||||
setApplications(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载应用列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@ -188,9 +184,9 @@ const TeamList: React.FC = () => {
|
||||
setMemberDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleConfig = (record: TeamResponse) => {
|
||||
const handleManageEnvironments = (record: TeamResponse) => {
|
||||
setCurrentTeam(record);
|
||||
setConfigDialogOpen(true);
|
||||
setEnvManageDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
@ -276,30 +272,38 @@ const TeamList: React.FC = () => {
|
||||
{
|
||||
id: 'actions',
|
||||
header: '操作',
|
||||
size: 260,
|
||||
size: 180,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleManageMembers(row.original)}>
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
成员
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleConfig(row.original)}>
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
配置
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(row.original)}>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(row.original)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
操作
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleManageEnvironments(row.original)}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
环境管理
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleManageMembers(row.original)}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
管理成员
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
编辑团队
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(row.original)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除团队
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@ -482,14 +486,13 @@ const TeamList: React.FC = () => {
|
||||
onOpenChange={setMemberDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
<TeamConfigDialog
|
||||
open={configDialogOpen}
|
||||
<TeamEnvironmentManageDialog
|
||||
open={envManageDialogOpen}
|
||||
onOpenChange={setEnvManageDialogOpen}
|
||||
teamId={currentTeam.id}
|
||||
teamName={currentTeam.teamName}
|
||||
users={users}
|
||||
environments={environments}
|
||||
applications={applications}
|
||||
onOpenChange={setConfigDialogOpen}
|
||||
users={users}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -4,8 +4,8 @@ import type {
|
||||
TeamQuery,
|
||||
TeamResponse,
|
||||
TeamRequest,
|
||||
TeamConfig,
|
||||
TeamConfigRequest,
|
||||
TeamEnvironmentConfig,
|
||||
TeamEnvironmentConfigRequest,
|
||||
TeamApplication,
|
||||
TeamApplicationRequest,
|
||||
} from './types';
|
||||
@ -64,30 +64,25 @@ export { getApplicationListByCondition as getApplicationList } from '@/pages/Dep
|
||||
|
||||
// ==================== 团队配置相关 ====================
|
||||
|
||||
/**
|
||||
* 获取团队配置
|
||||
*/
|
||||
export const getTeamConfig = (teamId: number) =>
|
||||
request.get<TeamConfig>(`${TEAM_CONFIG_URL}/team/${teamId}`);
|
||||
|
||||
/**
|
||||
* 创建团队配置
|
||||
*/
|
||||
export const createTeamConfig = (data: TeamConfigRequest) =>
|
||||
request.post<TeamConfig>(TEAM_CONFIG_URL, data);
|
||||
|
||||
/**
|
||||
* 更新团队配置
|
||||
*/
|
||||
export const updateTeamConfig = (id: number, data: TeamConfigRequest) =>
|
||||
request.put<TeamConfig>(`${TEAM_CONFIG_URL}/${id}`, data);
|
||||
// 旧的团队配置 API(已废弃,使用 TeamEnvironmentConfig 代替)
|
||||
// export const getTeamConfig = (teamId: number) => ...
|
||||
// export const createTeamConfig = (data: TeamConfigRequest) => ...
|
||||
// export const updateTeamConfig = (id: number, data: TeamConfigRequest) => ...
|
||||
|
||||
// ==================== 团队应用关联相关 ====================
|
||||
|
||||
/**
|
||||
* 获取团队应用关联列表(支持分页和列表模式)
|
||||
*/
|
||||
export const getTeamApplications = (teamId: number, environmentId?: number) =>
|
||||
request.get<TeamApplication[]>(`${TEAM_APPLICATION_URL}/list`, {
|
||||
params: { teamId, environmentId }
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取团队应用关联列表(分页)
|
||||
*/
|
||||
export const getTeamApplications = (params?: { teamId?: number; environmentId?: number; pageNum?: number; size?: number }) =>
|
||||
export const getTeamApplicationsPage = (params?: { teamId?: number; environmentId?: number; pageNum?: number; size?: number }) =>
|
||||
request.get<Page<TeamApplication>>(`${TEAM_APPLICATION_URL}/page`, { params });
|
||||
|
||||
/**
|
||||
@ -126,3 +121,74 @@ export const getJenkinsJobs = (externalSystemId: number) =>
|
||||
params: { externalSystemId }
|
||||
});
|
||||
|
||||
// ==================== 工作流相关 ====================
|
||||
|
||||
/**
|
||||
* 获取已发布的工作流定义列表
|
||||
*/
|
||||
export const getWorkflowDefinitions = () =>
|
||||
request.get<any[]>('/api/v1/workflow/definition/published');
|
||||
|
||||
// ==================== 通知渠道相关 ====================
|
||||
|
||||
/**
|
||||
* 获取通知渠道列表
|
||||
*/
|
||||
export const getNotificationChannels = () =>
|
||||
request.get<any[]>('/api/v1/notification-channel/list');
|
||||
|
||||
// ==================== 团队环境配置相关 ====================
|
||||
|
||||
/**
|
||||
* 查询团队的所有环境配置列表
|
||||
*/
|
||||
export const getTeamEnvironmentConfigs = (teamId: number) =>
|
||||
request.get<any[]>(`/api/v1/team-environment-config/team/${teamId}`);
|
||||
|
||||
/**
|
||||
* 根据团队ID和环境ID查询配置
|
||||
*/
|
||||
export const getTeamEnvironmentConfig = (teamId: number, environmentId: number) =>
|
||||
request.get<any>(
|
||||
`/api/v1/team-environment-config/team/${teamId}/environment/${environmentId}`
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建团队环境配置
|
||||
*/
|
||||
export const createTeamEnvironmentConfig = (data: {
|
||||
teamId: number;
|
||||
environmentId: number;
|
||||
approvalRequired?: boolean;
|
||||
approverUserIds?: number[];
|
||||
notificationChannelId?: number;
|
||||
notificationEnabled?: boolean;
|
||||
requireCodeReview?: boolean;
|
||||
remark?: string;
|
||||
}) =>
|
||||
request.post('/api/v1/team-environment-config', data);
|
||||
|
||||
/**
|
||||
* 更新团队环境配置
|
||||
*/
|
||||
export const updateTeamEnvironmentConfig = (
|
||||
id: number,
|
||||
data: {
|
||||
teamId: number;
|
||||
environmentId: number;
|
||||
approvalRequired?: boolean;
|
||||
approverUserIds?: number[];
|
||||
notificationChannelId?: number;
|
||||
notificationEnabled?: boolean;
|
||||
requireCodeReview?: boolean;
|
||||
remark?: string;
|
||||
}
|
||||
) =>
|
||||
request.put(`/api/v1/team-environment-config/${id}`, data);
|
||||
|
||||
/**
|
||||
* 删除团队环境配置
|
||||
*/
|
||||
export const deleteTeamEnvironmentConfig = (id: number) =>
|
||||
request.delete(`/api/v1/team-environment-config/${id}`);
|
||||
|
||||
|
||||
@ -43,26 +43,38 @@ export interface TeamRequest {
|
||||
sort?: number;
|
||||
}
|
||||
|
||||
// ==================== 团队配置相关 ====================
|
||||
// ==================== 团队环境配置相关 ====================
|
||||
|
||||
/**
|
||||
* 团队配置响应
|
||||
* 团队环境配置响应
|
||||
*/
|
||||
export interface TeamConfig extends BaseResponse {
|
||||
export interface TeamEnvironmentConfig extends BaseResponse {
|
||||
teamId: number;
|
||||
allowedEnvironmentIds: number[]; // [1, 2, 3]
|
||||
environmentApprovalRequired: boolean[]; // [false, false, true]
|
||||
approverUserIds: (number[] | null)[]; // [null, null, [1, 4]]
|
||||
environmentId: number;
|
||||
approvalRequired?: boolean;
|
||||
approverUserIds?: number[];
|
||||
notificationChannelId?: number;
|
||||
notificationEnabled?: boolean;
|
||||
requireCodeReview?: boolean;
|
||||
remark?: string;
|
||||
// 关联数据
|
||||
environmentName?: string;
|
||||
notificationChannelName?: string;
|
||||
applicationCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队配置请求
|
||||
* 团队环境配置请求
|
||||
*/
|
||||
export interface TeamConfigRequest {
|
||||
export interface TeamEnvironmentConfigRequest {
|
||||
teamId: number;
|
||||
allowedEnvironmentIds: number[]; // [1, 2, 3]
|
||||
environmentApprovalRequired: boolean[]; // [false, false, true]
|
||||
approverUserIds: (number[] | null)[]; // [null, null, [1, 4]]
|
||||
environmentId: number;
|
||||
approvalRequired?: boolean;
|
||||
approverUserIds?: number[];
|
||||
notificationChannelId?: number;
|
||||
notificationEnabled?: boolean;
|
||||
requireCodeReview?: boolean;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// ==================== 团队应用关联相关 ====================
|
||||
|
||||
@ -40,8 +40,9 @@ type EdgeConditionFormValues = z.infer<typeof edgeConditionSchema>;
|
||||
|
||||
/**
|
||||
* 边条件配置弹窗
|
||||
* ⚠️ 重要规范:只用于配置网关节点的出口连线
|
||||
* 非网关节点的多分支场景请使用网关节点
|
||||
* 支持两种场景:
|
||||
* 1. 网关节点的出口连线(推荐用于复杂分支逻辑)
|
||||
* 2. 普通节点的条件分支连线(简单场景可直接在线上配置)
|
||||
*/
|
||||
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||
visible,
|
||||
|
||||
@ -36,23 +36,7 @@ const validateBeforeSave = (nodes: FlowNode[], edges: FlowEdge[]): { valid: bool
|
||||
return { valid: false, message: '流程图中必须包含结束节点' };
|
||||
}
|
||||
|
||||
// 4. ⚠️ 规范检查:禁止非网关节点有多个条件出口
|
||||
const nonGatewayNodes = nodes.filter(node => node.data.nodeType !== NodeType.GATEWAY_NODE);
|
||||
for (const node of nonGatewayNodes) {
|
||||
const outgoingEdges = edges.filter(e => e.source === node.id);
|
||||
|
||||
// 检查是否有多个出口且配置了条件
|
||||
const conditionalEdges = outgoingEdges.filter(e => e.data?.condition?.type === 'EXPRESSION');
|
||||
if (conditionalEdges.length > 0 || outgoingEdges.length > 2) {
|
||||
const nodeName = node.data.label || '未命名节点';
|
||||
return {
|
||||
valid: false,
|
||||
message: `节点「${nodeName}」存在多分支逻辑。为符合 BPMN 标准,请使用网关节点来处理分支。提示:在节点后插入"排他网关"或"并行网关"。`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 网关节点验证
|
||||
// 4. 网关节点验证
|
||||
const gatewayErrors = getGatewayErrors(nodes, edges);
|
||||
if (gatewayErrors.length > 0) {
|
||||
return {
|
||||
@ -61,7 +45,7 @@ const validateBeforeSave = (nodes: FlowNode[], edges: FlowEdge[]): { valid: bool
|
||||
};
|
||||
}
|
||||
|
||||
// 6. 网关节点警告(不阻止保存,只提示)
|
||||
// 5. 网关节点警告(不阻止保存,只提示)
|
||||
const gatewayWarnings = getGatewayWarnings(nodes, edges);
|
||||
if (gatewayWarnings.length > 0) {
|
||||
console.warn('网关节点警告:', gatewayWarnings);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user