更换变量显示组件

This commit is contained in:
dengqichen 2025-11-04 16:30:46 +08:00
parent e27bbca4b3
commit e4fe82d7ea
9 changed files with 1529 additions and 1570 deletions

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
};

View File

@ -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" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</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"
</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 hover:text-destructive"
className="text-destructive focus:text-destructive"
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
<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}
/>
</>

View File

@ -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}`);

View File

@ -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;
}
// ==================== 团队应用关联相关 ====================

View File

@ -40,8 +40,9 @@ type EdgeConditionFormValues = z.infer<typeof edgeConditionSchema>;
/**
*
* 线
* 使
*
* 1. 线
* 2. 线线
*/
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
visible,

View File

@ -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);