更换变量显示组件
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 MemberManageDialog from './components/MemberManageDialog';
|
||||||
import { getUserList } from '@/pages/System/User/List/service';
|
import { getUserList } from '@/pages/System/User/List/service';
|
||||||
import type { UserResponse } from '@/pages/System/User/List/types';
|
import type { UserResponse } from '@/pages/System/User/List/types';
|
||||||
import { getEnvironmentList, getApplicationList } from './service';
|
import { getEnvironmentList } from './service';
|
||||||
import type { Environment, Application } from './types';
|
import type { Environment } from './types';
|
||||||
import TeamConfigDialog from './components/TeamConfigDialog';
|
import { TeamEnvironmentManageDialog } from './components/TeamEnvironmentManageDialog';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@ -35,6 +35,12 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@ -50,9 +56,11 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Server,
|
Server,
|
||||||
Database,
|
Database,
|
||||||
|
MoreHorizontal,
|
||||||
Settings,
|
Settings,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
|
||||||
type Column = {
|
type Column = {
|
||||||
accessorKey?: keyof TeamResponse;
|
accessorKey?: keyof TeamResponse;
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -73,10 +81,9 @@ const TeamList: React.FC = () => {
|
|||||||
const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>();
|
const [currentTeam, setCurrentTeam] = useState<TeamResponse | undefined>();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [memberDialogOpen, setMemberDialogOpen] = useState(false);
|
const [memberDialogOpen, setMemberDialogOpen] = useState(false);
|
||||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
const [envManageDialogOpen, setEnvManageDialogOpen] = useState(false);
|
||||||
const [users, setUsers] = useState<UserResponse[]>([]);
|
const [users, setUsers] = useState<UserResponse[]>([]);
|
||||||
const [environments, setEnvironments] = useState<Environment[]>([]);
|
const [environments, setEnvironments] = useState<Environment[]>([]);
|
||||||
const [applications, setApplications] = useState<Application[]>([]);
|
|
||||||
|
|
||||||
const form = useForm<SearchFormValues>({
|
const form = useForm<SearchFormValues>({
|
||||||
resolver: zodResolver(searchFormSchema),
|
resolver: zodResolver(searchFormSchema),
|
||||||
@ -124,7 +131,6 @@ const TeamList: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
loadEnvironments();
|
loadEnvironments();
|
||||||
loadApplications();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadUsers = async () => {
|
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(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
@ -188,9 +184,9 @@ const TeamList: React.FC = () => {
|
|||||||
setMemberDialogOpen(true);
|
setMemberDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfig = (record: TeamResponse) => {
|
const handleManageEnvironments = (record: TeamResponse) => {
|
||||||
setCurrentTeam(record);
|
setCurrentTeam(record);
|
||||||
setConfigDialogOpen(true);
|
setEnvManageDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleModalClose = () => {
|
||||||
@ -276,30 +272,38 @@ const TeamList: React.FC = () => {
|
|||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: '操作',
|
header: '操作',
|
||||||
size: 260,
|
size: 180,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleManageMembers(row.original)}>
|
<DropdownMenu>
|
||||||
<Users className="h-4 w-4 mr-1" />
|
<DropdownMenuTrigger asChild>
|
||||||
成员
|
<Button variant="ghost" size="sm">
|
||||||
</Button>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleConfig(row.original)}>
|
操作
|
||||||
<Settings className="h-4 w-4 mr-1" />
|
</Button>
|
||||||
配置
|
</DropdownMenuTrigger>
|
||||||
</Button>
|
<DropdownMenuContent align="end">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(row.original)}>
|
<DropdownMenuItem onClick={() => handleManageEnvironments(row.original)}>
|
||||||
<Edit className="h-4 w-4 mr-1" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
编辑
|
环境管理
|
||||||
</Button>
|
</DropdownMenuItem>
|
||||||
<Button
|
<DropdownMenuItem onClick={() => handleManageMembers(row.original)}>
|
||||||
variant="ghost"
|
<Users className="mr-2 h-4 w-4" />
|
||||||
size="sm"
|
管理成员
|
||||||
onClick={() => handleDelete(row.original)}
|
</DropdownMenuItem>
|
||||||
className="text-destructive hover:text-destructive"
|
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
|
||||||
>
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
编辑团队
|
||||||
删除
|
</DropdownMenuItem>
|
||||||
</Button>
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDelete(row.original)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
删除团队
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -482,14 +486,13 @@ const TeamList: React.FC = () => {
|
|||||||
onOpenChange={setMemberDialogOpen}
|
onOpenChange={setMemberDialogOpen}
|
||||||
onSuccess={handleSuccess}
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
<TeamConfigDialog
|
<TeamEnvironmentManageDialog
|
||||||
open={configDialogOpen}
|
open={envManageDialogOpen}
|
||||||
|
onOpenChange={setEnvManageDialogOpen}
|
||||||
teamId={currentTeam.id}
|
teamId={currentTeam.id}
|
||||||
teamName={currentTeam.teamName}
|
teamName={currentTeam.teamName}
|
||||||
users={users}
|
|
||||||
environments={environments}
|
environments={environments}
|
||||||
applications={applications}
|
users={users}
|
||||||
onOpenChange={setConfigDialogOpen}
|
|
||||||
onSuccess={handleSuccess}
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import type {
|
|||||||
TeamQuery,
|
TeamQuery,
|
||||||
TeamResponse,
|
TeamResponse,
|
||||||
TeamRequest,
|
TeamRequest,
|
||||||
TeamConfig,
|
TeamEnvironmentConfig,
|
||||||
TeamConfigRequest,
|
TeamEnvironmentConfigRequest,
|
||||||
TeamApplication,
|
TeamApplication,
|
||||||
TeamApplicationRequest,
|
TeamApplicationRequest,
|
||||||
} from './types';
|
} from './types';
|
||||||
@ -64,30 +64,25 @@ export { getApplicationListByCondition as getApplicationList } from '@/pages/Dep
|
|||||||
|
|
||||||
// ==================== 团队配置相关 ====================
|
// ==================== 团队配置相关 ====================
|
||||||
|
|
||||||
/**
|
// 旧的团队配置 API(已废弃,使用 TeamEnvironmentConfig 代替)
|
||||||
* 获取团队配置
|
// export const getTeamConfig = (teamId: number) => ...
|
||||||
*/
|
// export const createTeamConfig = (data: TeamConfigRequest) => ...
|
||||||
export const getTeamConfig = (teamId: number) =>
|
// export const updateTeamConfig = (id: number, data: TeamConfigRequest) => ...
|
||||||
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);
|
|
||||||
|
|
||||||
// ==================== 团队应用关联相关 ====================
|
// ==================== 团队应用关联相关 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取团队应用关联列表(支持分页和列表模式)
|
||||||
|
*/
|
||||||
|
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 });
|
request.get<Page<TeamApplication>>(`${TEAM_APPLICATION_URL}/page`, { params });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,3 +121,74 @@ export const getJenkinsJobs = (externalSystemId: number) =>
|
|||||||
params: { externalSystemId }
|
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;
|
sort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 团队配置相关 ====================
|
// ==================== 团队环境配置相关 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 团队配置响应
|
* 团队环境配置响应
|
||||||
*/
|
*/
|
||||||
export interface TeamConfig extends BaseResponse {
|
export interface TeamEnvironmentConfig extends BaseResponse {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
allowedEnvironmentIds: number[]; // [1, 2, 3]
|
environmentId: number;
|
||||||
environmentApprovalRequired: boolean[]; // [false, false, true]
|
approvalRequired?: boolean;
|
||||||
approverUserIds: (number[] | null)[]; // [null, null, [1, 4]]
|
approverUserIds?: number[];
|
||||||
|
notificationChannelId?: number;
|
||||||
|
notificationEnabled?: boolean;
|
||||||
|
requireCodeReview?: boolean;
|
||||||
|
remark?: string;
|
||||||
|
// 关联数据
|
||||||
|
environmentName?: string;
|
||||||
|
notificationChannelName?: string;
|
||||||
|
applicationCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 团队配置请求
|
* 团队环境配置请求
|
||||||
*/
|
*/
|
||||||
export interface TeamConfigRequest {
|
export interface TeamEnvironmentConfigRequest {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
allowedEnvironmentIds: number[]; // [1, 2, 3]
|
environmentId: number;
|
||||||
environmentApprovalRequired: boolean[]; // [false, false, true]
|
approvalRequired?: boolean;
|
||||||
approverUserIds: (number[] | null)[]; // [null, null, [1, 4]]
|
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> = ({
|
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
|
|||||||
@ -36,23 +36,7 @@ const validateBeforeSave = (nodes: FlowNode[], edges: FlowEdge[]): { valid: bool
|
|||||||
return { valid: false, message: '流程图中必须包含结束节点' };
|
return { valid: false, message: '流程图中必须包含结束节点' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. ⚠️ 规范检查:禁止非网关节点有多个条件出口
|
// 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. 网关节点验证
|
|
||||||
const gatewayErrors = getGatewayErrors(nodes, edges);
|
const gatewayErrors = getGatewayErrors(nodes, edges);
|
||||||
if (gatewayErrors.length > 0) {
|
if (gatewayErrors.length > 0) {
|
||||||
return {
|
return {
|
||||||
@ -61,7 +45,7 @@ const validateBeforeSave = (nodes: FlowNode[], edges: FlowEdge[]): { valid: bool
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 网关节点警告(不阻止保存,只提示)
|
// 5. 网关节点警告(不阻止保存,只提示)
|
||||||
const gatewayWarnings = getGatewayWarnings(nodes, edges);
|
const gatewayWarnings = getGatewayWarnings(nodes, edges);
|
||||||
if (gatewayWarnings.length > 0) {
|
if (gatewayWarnings.length > 0) {
|
||||||
console.warn('网关节点警告:', gatewayWarnings);
|
console.warn('网关节点警告:', gatewayWarnings);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user