增加代码编辑器表单组件
This commit is contained in:
parent
87b8023e1c
commit
a0b189854d
@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
|
||||
@ -41,6 +41,9 @@ importers:
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collapsible':
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
11
frontend/src/components/ui/collapsible.tsx
Normal file
11
frontend/src/components/ui/collapsible.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import * as React from "react"
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
|
||||
@ -1,67 +1,21 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {Progress} from "@/components/ui/progress";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Clock,
|
||||
Search,
|
||||
Edit,
|
||||
Package,
|
||||
ChevronRight,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
Shield,
|
||||
Loader2,
|
||||
ServerCrash,
|
||||
Server
|
||||
Rocket,
|
||||
GitBranch,
|
||||
Users,
|
||||
Server,
|
||||
CheckCircle2
|
||||
} from "lucide-react";
|
||||
import {getEnvironmentList} from '@/pages/Deploy/Environment/List/service';
|
||||
import {getDevelopmentLanguages} from '@/pages/Deploy/Application/List/service';
|
||||
import {getDeploymentConfigPage} from '@/pages/Deploy/Deployment/List/service';
|
||||
import type {Environment} from '@/pages/Deploy/Environment/List/types';
|
||||
import type {DevelopmentLanguageType} from '@/pages/Deploy/Application/List/types';
|
||||
import type {DeploymentConfig} from '@/pages/Deploy/Deployment/List/types';
|
||||
import type {Page} from '@/types/base';
|
||||
import type { JsonNode } from '@/types/common';
|
||||
import DeploymentFormModal from './components/DeploymentFormModal';
|
||||
import {message} from "antd";
|
||||
|
||||
type EnvironmentStatus = 'success' | 'warning' | 'error';
|
||||
|
||||
// 扩展环境类型,添加监控数据
|
||||
interface EnhancedEnvironment extends Environment {
|
||||
projectCount: number;
|
||||
status: EnvironmentStatus;
|
||||
lastDeployment: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
storage: number;
|
||||
}
|
||||
|
||||
const EmptyState: React.FC<{ title: string; description: string; icon: React.ReactNode }> = ({
|
||||
title,
|
||||
description,
|
||||
icon
|
||||
}) => (
|
||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||
<div className="rounded-full bg-muted p-3 mb-4">
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-sm">{description}</p>
|
||||
</div>
|
||||
);
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { getDeployEnvironments, startDeployment } from './service';
|
||||
import type { DeployTeam, ApplicationConfig } from './types';
|
||||
|
||||
const LoadingState = () => (
|
||||
<div className="flex-1 p-8">
|
||||
@ -73,404 +27,291 @@ const LoadingState = () => (
|
||||
);
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [projectType, setProjectType] = useState("ALL");
|
||||
const [status, setStatus] = useState("");
|
||||
const [environments, setEnvironments] = useState<EnhancedEnvironment[]>([]);
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [languages, setLanguages] = useState<DevelopmentLanguageType[]>([]);
|
||||
const [deployConfigs, setDeployConfigs] = useState<DeploymentConfig[]>([]);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [currentEnvId, setCurrentEnvId] = useState<number>();
|
||||
const [deployModalOpen, setDeployModalOpen] = useState(false);
|
||||
const [selectedConfig, setSelectedConfig] = useState<DeploymentConfig | null>(null);
|
||||
const [teams, setTeams] = useState<DeployTeam[]>([]);
|
||||
const [currentTeamId, setCurrentTeamId] = useState<number | null>(null);
|
||||
const [currentEnvId, setCurrentEnvId] = useState<number | null>(null);
|
||||
const [deploying, setDeploying] = useState<Set<number>>(new Set());
|
||||
|
||||
// 获取环境和语言数据
|
||||
// 加载部署环境数据
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [envResponse, langResponse] = await Promise.all([
|
||||
getEnvironmentList(),
|
||||
getDevelopmentLanguages()
|
||||
]);
|
||||
const response = await getDeployEnvironments();
|
||||
|
||||
if (envResponse) {
|
||||
const enrichedEnvironments = envResponse.map(env => {
|
||||
const randomStatus: EnvironmentStatus = Math.random() > 0.7 ? 'warning' : 'success';
|
||||
return {
|
||||
...env,
|
||||
projectCount: 0, // 初始化为0,后续更新
|
||||
status: randomStatus,
|
||||
lastDeployment: '暂无部署',
|
||||
cpu: Math.floor(Math.random() * 40) + 40,
|
||||
memory: Math.floor(Math.random() * 30) + 50,
|
||||
storage: Math.floor(Math.random() * 40) + 30,
|
||||
};
|
||||
});
|
||||
setEnvironments(enrichedEnvironments);
|
||||
if (enrichedEnvironments.length > 0) {
|
||||
setCurrentEnvId(enrichedEnvironments[0].id);
|
||||
if (response && response.teams) {
|
||||
setTeams(response.teams);
|
||||
|
||||
// 默认选中第一个团队和第一个环境
|
||||
if (response.teams.length > 0) {
|
||||
setCurrentTeamId(response.teams[0].teamId);
|
||||
|
||||
if (response.teams[0].environments.length > 0) {
|
||||
setCurrentEnvId(response.teams[0].environments[0].environmentId);
|
||||
}
|
||||
}
|
||||
|
||||
if (langResponse) {
|
||||
setLanguages(langResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data:', error);
|
||||
console.error('加载数据失败:', error);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '无法加载部署环境数据,请稍后重试',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
loadData();
|
||||
}, [toast]);
|
||||
|
||||
// 获取部署配置数据
|
||||
useEffect(() => {
|
||||
const fetchDeployConfigs = async () => {
|
||||
if (!currentEnvId) return;
|
||||
// 切换团队时,自动选中第一个环境
|
||||
const handleTeamChange = (teamId: string) => {
|
||||
const newTeamId = Number(teamId);
|
||||
setCurrentTeamId(newTeamId);
|
||||
|
||||
const team = teams.find(t => t.teamId === newTeamId);
|
||||
if (team && team.environments.length > 0) {
|
||||
setCurrentEnvId(team.environments[0].environmentId);
|
||||
} else {
|
||||
setCurrentEnvId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理部署
|
||||
const handleDeploy = async (app: ApplicationConfig, requiresApproval: boolean) => {
|
||||
try {
|
||||
const response = await getDeploymentConfigPage({
|
||||
pageSize: 100,
|
||||
pageNum: 1,
|
||||
environmentId: currentEnvId,
|
||||
workflowDefinitionId: 0,
|
||||
enabled: undefined
|
||||
setDeploying((prev) => new Set(prev).add(app.teamApplicationId));
|
||||
|
||||
await startDeployment(app.teamApplicationId);
|
||||
|
||||
toast({
|
||||
title: requiresApproval ? '部署申请已提交' : '部署任务已创建',
|
||||
description: requiresApproval
|
||||
? '您的部署申请已提交,等待审批人审核'
|
||||
: '部署任务已成功创建并开始执行',
|
||||
});
|
||||
|
||||
if (response) {
|
||||
setDeployConfigs(response.content);
|
||||
// 更新环境的项目数量
|
||||
setEnvironments(prevEnvs =>
|
||||
prevEnvs.map(env =>
|
||||
env.id === currentEnvId
|
||||
? {...env, projectCount: response.totalElements}
|
||||
: env
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch deployment configs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDeployConfigs();
|
||||
}, [currentEnvId]);
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!currentEnvId) return;
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
pageSize: 100,
|
||||
pageNum: 1,
|
||||
environmentId: currentEnvId,
|
||||
enabled: status === 'active' ? true : status === 'paused' ? false : undefined
|
||||
};
|
||||
|
||||
// 如果选择了具体语言(不是"所有"),添加到查询参数
|
||||
if (projectType !== 'ALL') {
|
||||
params.languageType = projectType;
|
||||
}
|
||||
|
||||
const response = await getDeploymentConfigPage(params);
|
||||
|
||||
if (response) {
|
||||
setDeployConfigs(response.content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to search deployment configs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchText("");
|
||||
setProjectType("ALL");
|
||||
setStatus("");
|
||||
if (currentEnvId) {
|
||||
getDeploymentConfigPage({
|
||||
pageSize: 100,
|
||||
pageNum: 1,
|
||||
environmentId: currentEnvId,
|
||||
workflowDefinitionId: 0,
|
||||
enabled: undefined
|
||||
}).then(response => {
|
||||
if (response) {
|
||||
setDeployConfigs(response.content);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '操作失败',
|
||||
description: error.response?.data?.message || '部署失败,请稍后重试',
|
||||
});
|
||||
} finally {
|
||||
setDeploying((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(app.teamApplicationId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (envCode: string) => {
|
||||
const env = environments.find(e => e.envCode === envCode);
|
||||
if (env) {
|
||||
setCurrentEnvId(env.id);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircle className="h-5 w-5 text-green-500"/>;
|
||||
case 'warning':
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500"/>;
|
||||
case 'error':
|
||||
return <XCircle className="h-5 w-5 text-red-500"/>;
|
||||
default:
|
||||
return <Clock className="h-5 w-5 text-blue-500"/>;
|
||||
}
|
||||
};
|
||||
|
||||
const getBuildStatusBadge = (enabled: boolean) => {
|
||||
const config = enabled
|
||||
? {className: "bg-green-100 text-green-800", text: "活跃"}
|
||||
: {className: "bg-red-100 text-red-800", text: "暂停"};
|
||||
|
||||
return (
|
||||
<Badge className={cn("rounded-full", config.className)}>
|
||||
{config.text}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeploy = (config: DeploymentConfig) => {
|
||||
if (!config?.formVariablesSchema) {
|
||||
message.error('工作流配置有误,请检查工作流定义');
|
||||
return;
|
||||
}
|
||||
setSelectedConfig(config);
|
||||
setDeployModalOpen(true);
|
||||
};
|
||||
// 获取当前团队和环境
|
||||
const currentTeam = teams.find(t => t.teamId === currentTeamId);
|
||||
const currentEnv = currentTeam?.environments.find(e => e.environmentId === currentEnvId);
|
||||
const currentApps = currentEnv?.applications || [];
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState/>;
|
||||
}
|
||||
|
||||
const hasEnvironments = environments.length > 0;
|
||||
const hasProjects = deployConfigs.length > 0;
|
||||
|
||||
if (teams.length === 0) {
|
||||
return (
|
||||
<div className="flex-1 p-8">
|
||||
<h2 className="text-2xl font-semibold mb-6">部署环境概览</h2>
|
||||
|
||||
{!hasEnvironments ? (
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-6">部署管理</h2>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<EmptyState
|
||||
icon={<ServerCrash className="h-8 w-8 text-muted-foreground"/>}
|
||||
title="暂无环境数据"
|
||||
description="当前系统中还没有配置任何环境。请先添加部署环境,然后开始使用部署功能。"
|
||||
/>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Package className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-sm text-muted-foreground">您还没有加入任何团队</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{environments.map((env) => (
|
||||
<Card key={env.id} className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{env.envName}</CardTitle>
|
||||
{getStatusIcon(env.status)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{env.projectCount}</div>
|
||||
<p className="text-xs text-muted-foreground">个项目</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center text-sm">
|
||||
<Clock className="mr-2 h-4 w-4 text-muted-foreground"/>
|
||||
最近部署: {env.lastDeployment}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>CPU</span>
|
||||
<span>{env.cpu}%</span>
|
||||
</div>
|
||||
<Progress value={env.cpu} className="h-1"/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>内存</span>
|
||||
<span>{env.memory}%</span>
|
||||
</div>
|
||||
<Progress value={env.memory} className="h-1"/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>存储</span>
|
||||
<span>{env.storage}%</span>
|
||||
</div>
|
||||
<Progress value={env.storage} className="h-1"/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
return (
|
||||
<div className="flex-1 p-8 space-y-6">
|
||||
{/* 页面标题和团队选择器 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">部署管理</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
管理和执行应用部署任务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<Tabs
|
||||
defaultValue={environments[0]?.envCode}
|
||||
className="w-full"
|
||||
onValueChange={handleTabChange}
|
||||
>
|
||||
<div className="border-b px-6">
|
||||
<TabsList className="h-16">
|
||||
{environments.map((env) => (
|
||||
<TabsTrigger
|
||||
key={env.id}
|
||||
value={env.envCode}
|
||||
className="px-6 py-3 data-[state=active]:border-b-2 data-[state=active]:border-blue-600"
|
||||
>
|
||||
{env.envName}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">当前团队:</span>
|
||||
</div>
|
||||
|
||||
{environments.map((env) => (
|
||||
<TabsContent key={env.id} value={env.envCode} className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Input
|
||||
placeholder="项目名称或代码"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
<Select value={projectType} onValueChange={setProjectType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="开发语言"/>
|
||||
<Select value={currentTeamId?.toString()} onValueChange={handleTeamChange}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="选择团队" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">所有</SelectItem>
|
||||
{languages.map(lang => (
|
||||
<SelectItem key={lang.code} value={lang.code}>
|
||||
{lang.name}
|
||||
{teams.map((team) => (
|
||||
<SelectItem key={team.teamId} value={team.teamId.toString()}>
|
||||
{team.teamName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="项目状态"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">活跃</SelectItem>
|
||||
<SelectItem value="paused">暂停</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex space-x-2">
|
||||
<Button className="flex-1" onClick={handleSearch}>
|
||||
<Search className="w-4 h-4 mr-2"/>
|
||||
搜索
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasProjects ? (
|
||||
{/* 当前团队信息 */}
|
||||
{currentTeam && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{currentTeam.teamName}</span>
|
||||
{currentTeam.teamRole && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{currentTeam.teamRole}</span>
|
||||
</>
|
||||
)}
|
||||
{currentTeam.description && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{currentTeam.description}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 环境切换和应用列表 */}
|
||||
{currentTeam && (
|
||||
currentTeam.environments.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<EmptyState
|
||||
icon={<Server className="h-8 w-8 text-muted-foreground"/>}
|
||||
title="暂无项目数据"
|
||||
description="当前环境中还没有配置任何项目。请先创建项目,然后开始部署。"
|
||||
/>
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<Server className="h-16 w-16 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">暂无部署环境</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
当前团队「{currentTeam.teamName}」还没有配置任何部署环境
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{deployConfigs.map((config) => (
|
||||
<Card key={config.id} className="hover:shadow-md transition-shadow duration-300">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{config.application.appName}</h3>
|
||||
<p className="text-sm text-gray-500">{config.application.appCode}</p>
|
||||
<Card>
|
||||
<CardHeader className="border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle>选择部署环境</CardTitle>
|
||||
<Select value={currentEnvId?.toString()} onValueChange={(value) => setCurrentEnvId(Number(value))}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="选择环境" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currentTeam.environments.map((env) => (
|
||||
<SelectItem key={env.environmentId} value={env.environmentId.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
{env.requiresApproval ? (
|
||||
<Shield className="h-3 w-3 text-amber-600" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||||
)}
|
||||
{env.environmentName}
|
||||
<span className="text-xs text-muted-foreground">({env.applications.length})</span>
|
||||
</div>
|
||||
{getBuildStatusBadge(config.enabled)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm mb-4">
|
||||
<div>
|
||||
<p className="text-gray-500">构建类型</p>
|
||||
<p className="font-medium">{config.buildType}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">开发语言</p>
|
||||
<p className="font-medium">{config.languageType}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">工作流</p>
|
||||
<p className="font-medium">{config.publishedWorkflowDefinition?.name || '未配置'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">最后部署时间</p>
|
||||
<p className="font-medium">{config.lastBuildStartTime ? new Date(config.lastBuildStartTime).toLocaleString() : '暂无部署'}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground min-h-[20px]">
|
||||
{currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 ? (
|
||||
<>
|
||||
<Users className="h-4 w-4" />
|
||||
<span>审批人: {currentEnv.approvers.map((a) => a.realName).join('、')}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="opacity-0">占位</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t pt-2 mt-2">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-500">构建状态</p>
|
||||
<p className={cn(
|
||||
"font-medium",
|
||||
config.lastBuildStatus === 'COMPLETED' ? 'text-green-600' :
|
||||
config.lastBuildStatus === 'FAILED' ? 'text-red-600' :
|
||||
config.lastBuildStatus === 'RUNNING' ? 'text-blue-600' : 'text-gray-600'
|
||||
)}>
|
||||
{config.lastBuildStatus === 'COMPLETED' ? '构建成功' :
|
||||
config.lastBuildStatus === 'FAILED' ? '构建失败' :
|
||||
config.lastBuildStatus === 'RUNNING' ? '构建中' : '未构建'}
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
|
||||
{/* 应用列表 */}
|
||||
{currentApps.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<Package className="h-16 w-16 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">暂无可部署应用</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
环境「{currentEnv?.environmentName}」暂未配置任何应用
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500">构建时长</p>
|
||||
<p className="font-medium">
|
||||
{config.lastBuildStartTime && config.lastBuildEndTime ? (
|
||||
`${Math.round((new Date(config.lastBuildEndTime).getTime() - new Date(config.lastBuildStartTime).getTime()) / 1000 / 60)} 分钟`
|
||||
) : '暂无数据'}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
{currentApps.map((app) => {
|
||||
const isDeploying = deploying.has(app.teamApplicationId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={app.teamApplicationId}
|
||||
className="flex flex-col p-3 rounded-lg border hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
|
||||
<code className="text-xs text-muted-foreground truncate block">
|
||||
{app.applicationCode}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 mb-3 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<GitBranch className="h-3 w-3 shrink-0" />
|
||||
<code className="truncate font-mono">{app.branch}</code>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="outline" size="sm" onClick={() => handleDeploy(config)}>
|
||||
<Package className="h-4 w-4 mr-1"/>
|
||||
部署
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<ChevronRight className="h-4 w-4"/>
|
||||
详情
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Rocket className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">{app.workflowDefinitionName || '未配置'}</span>
|
||||
</div>
|
||||
|
||||
{app.deploySystemName && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Server className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">{app.deploySystemName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)}
|
||||
disabled={!currentEnv?.enabled || isDeploying}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'w-full text-xs h-8',
|
||||
currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
|
||||
)}
|
||||
>
|
||||
{isDeploying ? (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
处理中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Rocket className="h-3 w-3 mr-1" />
|
||||
{currentEnv?.requiresApproval ? '申请部署' : '立即部署'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)}
|
||||
{selectedConfig && selectedConfig.formVariablesSchema && (
|
||||
<DeploymentFormModal
|
||||
open={deployModalOpen}
|
||||
onClose={() => {
|
||||
setDeployModalOpen(false);
|
||||
setSelectedConfig(null);
|
||||
}}
|
||||
formSchema={selectedConfig.formVariablesSchema}
|
||||
deployConfig={selectedConfig}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
import { DeployAppBuildDTO } from './types';
|
||||
import request from "@/utils/request.ts";
|
||||
import request from '@/utils/request';
|
||||
import type { DeployEnvironmentsResponse, StartDeploymentResponse } from './types';
|
||||
|
||||
const DEPLOY_URL = '/api/v1/deploy';
|
||||
|
||||
/**
|
||||
* 部署应用
|
||||
* @param data 部署参数
|
||||
* 获取用户的部署环境列表
|
||||
*/
|
||||
export const deployApp = (data: DeployAppBuildDTO) =>
|
||||
request.post<void>('/api/v1/deploy-app-config/deploy', data);
|
||||
export const getDeployEnvironments = () =>
|
||||
request.get<DeployEnvironmentsResponse>(`${DEPLOY_URL}/environments`);
|
||||
|
||||
/**
|
||||
* 发起部署
|
||||
*/
|
||||
export const startDeployment = (teamApplicationId: number) =>
|
||||
request.post<StartDeploymentResponse>(`${DEPLOY_URL}/execute`, { teamApplicationId });
|
||||
|
||||
@ -1,38 +1,64 @@
|
||||
import { JsonNode } from '@/types/common';
|
||||
import type { BaseResponse } from '@/types/base';
|
||||
|
||||
export interface DeployAppBuildDTO {
|
||||
/**
|
||||
* 构建类型
|
||||
*/
|
||||
buildType: string;
|
||||
|
||||
/**
|
||||
* 应用语言
|
||||
*/
|
||||
languageType: string;
|
||||
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
formVariables?: JsonNode;
|
||||
|
||||
/**
|
||||
* 构建配置
|
||||
*/
|
||||
buildVariables: JsonNode;
|
||||
|
||||
/**
|
||||
* 环境ID
|
||||
*/
|
||||
environmentId: number;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
applicationId: number;
|
||||
|
||||
/**
|
||||
* 已发布的流程定义ID
|
||||
*/
|
||||
workflowDefinitionId: number;
|
||||
export interface Approver {
|
||||
userId: number;
|
||||
username: string;
|
||||
realName: string;
|
||||
}
|
||||
|
||||
export interface ApplicationConfig {
|
||||
teamApplicationId: number;
|
||||
applicationId: number;
|
||||
applicationCode: string;
|
||||
applicationName: string;
|
||||
applicationDesc?: string;
|
||||
branch: string;
|
||||
deploySystemId?: number;
|
||||
deploySystemName?: string;
|
||||
deployJob?: string;
|
||||
workflowDefinitionId?: number;
|
||||
workflowDefinitionName?: string;
|
||||
workflowDefinitionKey?: string;
|
||||
}
|
||||
|
||||
export interface DeployEnvironment {
|
||||
environmentId: number;
|
||||
environmentCode: string;
|
||||
environmentName: string;
|
||||
environmentDesc?: string;
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
requiresApproval: boolean;
|
||||
approvers: Approver[];
|
||||
applications: ApplicationConfig[];
|
||||
}
|
||||
|
||||
export interface DeployTeam {
|
||||
teamId: number;
|
||||
teamCode: string;
|
||||
teamName: string;
|
||||
teamRole: string;
|
||||
description?: string;
|
||||
environments: DeployEnvironment[];
|
||||
}
|
||||
|
||||
export interface DeployEnvironmentsResponse {
|
||||
userId: number;
|
||||
username: string;
|
||||
realName: string;
|
||||
teams: DeployTeam[];
|
||||
}
|
||||
|
||||
export interface StartDeploymentRequest {
|
||||
teamApplicationId: number;
|
||||
workflowDefinitionKey: string;
|
||||
formData?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface StartDeploymentResponse {
|
||||
instanceId: string;
|
||||
processDefinitionId: string;
|
||||
businessKey: string;
|
||||
processKey: string;
|
||||
startTime?: string;
|
||||
}
|
||||
@ -97,7 +97,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 加载外部系统列表
|
||||
// 加载Gitlab列表
|
||||
const loadExternalSystems = async () => {
|
||||
try {
|
||||
const response = await getExternalSystems({
|
||||
@ -110,7 +110,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "加载外部系统失败",
|
||||
title: "加载Gitlab失败",
|
||||
description: error instanceof Error ? error.message : undefined,
|
||||
duration: 3000,
|
||||
});
|
||||
@ -137,14 +137,14 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
repoProjectId: initialValues.repoProjectId
|
||||
});
|
||||
|
||||
// 如果有外部系统ID,加载仓库项目
|
||||
// 如果有Gitlab ID,加载仓库项目
|
||||
if (initialValues.externalSystemId) {
|
||||
fetchRepositoryProjects(initialValues.externalSystemId);
|
||||
}
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
// 当选择外部系统时,获取对应的仓库项目列表
|
||||
// 当选择Gitlab时,获取对应的仓库项目列表
|
||||
const handleExternalSystemChange = (externalSystemId: number | undefined) => {
|
||||
form.setValue('repoProjectId', undefined);
|
||||
setRepositoryProjects([]);
|
||||
@ -162,8 +162,8 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
const handleSubmit = async (values: ApplicationFormValues) => {
|
||||
console.log('Form submitted with values:', values);
|
||||
try {
|
||||
// 去掉 externalSystemId 字段,不传给后端
|
||||
const { externalSystemId, ...submitData } = values;
|
||||
// 保留 externalSystemId 字段,传给后端
|
||||
const submitData = values;
|
||||
|
||||
if (isEdit) {
|
||||
await updateApplication({
|
||||
@ -311,7 +311,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
name="externalSystemId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>外部系统</FormLabel>
|
||||
<FormLabel>Gitlab</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(Number(value));
|
||||
@ -321,7 +321,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择外部系统"/>
|
||||
<SelectValue placeholder="请选择Gitlab"/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@ -370,7 +370,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
|
||||
(project) => project.repoProjectId === field.value
|
||||
)?.name
|
||||
: !form.watch('externalSystemId')
|
||||
? "请先选择外部系统"
|
||||
? "请先选择Gitlab"
|
||||
: "请选择项目"}
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
|
||||
@ -238,7 +238,7 @@ const ApplicationList: React.FC = () => {
|
||||
},
|
||||
{
|
||||
id: 'externalSystem',
|
||||
header: '外部系统',
|
||||
header: 'Gitlab',
|
||||
size: 150,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@ -51,7 +51,7 @@ export const getRepositoryProjects = (repoGroupId: number) =>
|
||||
params: { repoGroupId: repoGroupId },
|
||||
});
|
||||
|
||||
// 获取项目列表(根据外部系统)
|
||||
// 获取项目列表(根据Gitlab)
|
||||
export const getRepositoryProjectsBySystem = (externalSystemId: number) =>
|
||||
request.get<RepositoryProject[]>(`${REPOSITORY_PROJECT_URL}/list`, {
|
||||
params: { externalSystemId },
|
||||
|
||||
@ -19,14 +19,9 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Search, Check, ChevronDown } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Plus, Trash2, Loader2, AlertCircle } from 'lucide-react';
|
||||
@ -39,6 +34,8 @@ import type {
|
||||
Application,
|
||||
} from '../types';
|
||||
import type { UserResponse } from '@/pages/System/User/List/types';
|
||||
import type { RepositoryBranchResponse } from '@/pages/Resource/Git/List/types';
|
||||
import type { WorkflowDefinition } from '@/pages/Workflow/Definition/List/types';
|
||||
import {
|
||||
getTeamConfig,
|
||||
createTeamConfig,
|
||||
@ -46,7 +43,11 @@ import {
|
||||
getTeamApplications,
|
||||
createTeamApplication,
|
||||
deleteTeamApplication,
|
||||
getJenkinsSystems,
|
||||
getJenkinsJobs,
|
||||
} from '../service';
|
||||
import { getRepositoryBranchesList } from '@/pages/Resource/Git/List/service';
|
||||
import { getPublishedDefinitions } from '@/pages/Workflow/Definition/List/service';
|
||||
|
||||
interface TeamConfigDialogProps {
|
||||
open: boolean;
|
||||
@ -90,7 +91,33 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
const [configuredEnvs, setConfiguredEnvs] = useState<number[]>([]); // 已配置的环境列表
|
||||
|
||||
// 每个环境的添加表单状态
|
||||
const [addForms, setAddForms] = useState<Record<number, { appId: number | null; branch: string }>>({});
|
||||
const [addForms, setAddForms] = useState<Record<number, {
|
||||
appId: number | null;
|
||||
branch: string;
|
||||
deploySystemId: number | null;
|
||||
deployJob: string;
|
||||
workflowDefinitionId: number | null;
|
||||
}>>({});
|
||||
|
||||
// 分支列表状态 - 以应用ID为key
|
||||
const [branchesMap, setBranchesMap] = useState<Record<number, RepositoryBranchResponse[]>>({});
|
||||
const [loadingBranches, setLoadingBranches] = useState<Record<number, boolean>>({});
|
||||
|
||||
// 分支选择器的搜索和打开状态 - 以环境ID为key
|
||||
const [branchSearchValues, setBranchSearchValues] = useState<Record<number, string>>({});
|
||||
const [branchPopoverOpen, setBranchPopoverOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// Jenkins 系统和 Job 列表
|
||||
const [jenkinsSystems, setJenkinsSystems] = useState<any[]>([]);
|
||||
const [jenkinsJobsMap, setJenkinsJobsMap] = useState<Record<number, any[]>>({});
|
||||
const [loadingJobs, setLoadingJobs] = useState<Record<number, boolean>>({});
|
||||
|
||||
// Jenkins Job 选择器的搜索和打开状态 - 以环境ID为key
|
||||
const [jobSearchValues, setJobSearchValues] = useState<Record<number, string>>({});
|
||||
const [jobPopoverOpen, setJobPopoverOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// 工作流定义列表
|
||||
const [workflowDefinitions, setWorkflowDefinitions] = useState<WorkflowDefinition[]>([]);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
@ -103,7 +130,12 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await Promise.all([loadTeamConfig(), loadTeamApplications()]);
|
||||
await Promise.all([
|
||||
loadTeamConfig(),
|
||||
loadTeamApplications(),
|
||||
loadJenkinsSystems(),
|
||||
loadWorkflowDefinitions()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
} finally {
|
||||
@ -111,6 +143,28 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 加载 Jenkins 系统列表
|
||||
const loadJenkinsSystems = async () => {
|
||||
try {
|
||||
const systems = await getJenkinsSystems();
|
||||
setJenkinsSystems(systems || []);
|
||||
} catch (error) {
|
||||
console.error('加载Jenkins系统失败:', error);
|
||||
setJenkinsSystems([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载工作流定义列表(已发布)
|
||||
const loadWorkflowDefinitions = async () => {
|
||||
try {
|
||||
const definitions = await getPublishedDefinitions();
|
||||
setWorkflowDefinitions(definitions || []);
|
||||
} catch (error) {
|
||||
console.error('加载工作流定义失败:', error);
|
||||
setWorkflowDefinitions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTeamConfig = async () => {
|
||||
try {
|
||||
const config = await getTeamConfig(teamId);
|
||||
@ -158,6 +212,26 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 当团队应用加载后,自动加载所需的 Jenkins Jobs (用于编辑时的下拉选择)
|
||||
useEffect(() => {
|
||||
if (teamApplications.length > 0) {
|
||||
// 收集所有需要加载的 deploySystemId
|
||||
const systemIds = new Set<number>();
|
||||
teamApplications.forEach(app => {
|
||||
if (app.deploySystemId) {
|
||||
systemIds.add(app.deploySystemId);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载所有需要的 Jenkins Jobs
|
||||
systemIds.forEach(systemId => {
|
||||
if (!jenkinsJobsMap[systemId]) {
|
||||
loadJenkinsJobs(systemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [teamApplications]);
|
||||
|
||||
// 处理环境选择
|
||||
const handleEnvToggle = (envId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
@ -367,6 +441,75 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
return teamApplications.filter((app) => app.environmentId === envId);
|
||||
};
|
||||
|
||||
// 加载 Jenkins Jobs
|
||||
const loadJenkinsJobs = async (systemId: number) => {
|
||||
if (jenkinsJobsMap[systemId]) {
|
||||
return; // 已加载过
|
||||
}
|
||||
|
||||
setLoadingJobs(prev => ({ ...prev, [systemId]: true }));
|
||||
try {
|
||||
const jobs = await getJenkinsJobs(systemId);
|
||||
setJenkinsJobsMap(prev => ({
|
||||
...prev,
|
||||
[systemId]: jobs || [],
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('加载Jenkins Jobs失败:', error);
|
||||
setJenkinsJobsMap(prev => ({
|
||||
...prev,
|
||||
[systemId]: [],
|
||||
}));
|
||||
} finally {
|
||||
setLoadingJobs(prev => ({ ...prev, [systemId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 加载分支列表 - 只在用户选择应用时调用
|
||||
const loadBranches = async (appId: number, app: Application) => {
|
||||
// 如果应用没有关联仓库项目,跳过
|
||||
if (!app.repoProjectId || !app.externalSystemId) {
|
||||
console.log('应用未关联仓库项目,跳过分支加载');
|
||||
setBranchesMap(prev => ({
|
||||
...prev,
|
||||
[appId]: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经加载过,跳过
|
||||
if (branchesMap[appId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingBranches(prev => ({ ...prev, [appId]: true }));
|
||||
try {
|
||||
// 调用 /api/v1/repository-branch/list 接口
|
||||
const branches = await getRepositoryBranchesList({
|
||||
externalSystemId: app.externalSystemId,
|
||||
repoProjectId: app.repoProjectId,
|
||||
});
|
||||
|
||||
setBranchesMap(prev => ({
|
||||
...prev,
|
||||
[appId]: branches || [],
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('加载分支列表失败:', error);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载分支列表失败',
|
||||
description: '无法获取该应用的分支信息',
|
||||
});
|
||||
setBranchesMap(prev => ({
|
||||
...prev,
|
||||
[appId]: [],
|
||||
}));
|
||||
} finally {
|
||||
setLoadingBranches(prev => ({ ...prev, [appId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 添加应用到环境
|
||||
const handleAddApplication = async (envId: number) => {
|
||||
const form = addForms[envId];
|
||||
@ -396,6 +539,9 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
applicationId: form.appId,
|
||||
environmentId: envId,
|
||||
branch: form.branch || undefined,
|
||||
deploySystemId: form.deploySystemId || undefined,
|
||||
deployJob: form.deployJob || undefined,
|
||||
workflowDefinitionId: form.workflowDefinitionId || undefined,
|
||||
};
|
||||
|
||||
await createTeamApplication(data);
|
||||
@ -407,7 +553,7 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
// 清空表单
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[envId]: { appId: null, branch: '' },
|
||||
[envId]: { appId: null, branch: '', deploySystemId: null, deployJob: '', workflowDefinitionId: null },
|
||||
});
|
||||
|
||||
// 重新加载
|
||||
@ -449,16 +595,6 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
};
|
||||
|
||||
|
||||
// 获取环境图标
|
||||
const getEnvIcon = (envCode?: string) => {
|
||||
const code = envCode?.toUpperCase();
|
||||
if (code?.includes('DEV')) return '🟢';
|
||||
if (code?.includes('TEST') || code?.includes('QA')) return '🟡';
|
||||
if (code?.includes('UAT') || code?.includes('PRE')) return '🟠';
|
||||
if (code?.includes('PROD')) return '🔴';
|
||||
return '⚪';
|
||||
};
|
||||
|
||||
// 获取可添加的应用列表(排除已添加的)
|
||||
const getAvailableApplications = (envId: number) => {
|
||||
const addedAppIds = getEnvApplications(envId).map((app) => app.applicationId);
|
||||
@ -512,7 +648,7 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] p-0">
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-0">
|
||||
<DialogTitle>团队配置:{teamName}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -570,7 +706,6 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{getEnvIcon(env.envCode)}</span>
|
||||
<label
|
||||
htmlFor={`env-${env.id}`}
|
||||
className="font-medium cursor-pointer"
|
||||
@ -595,7 +730,7 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
) : (
|
||||
<div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-800 rounded-md">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
💡 未选择任何环境,点击"下一步"将清空该团队的环境配置
|
||||
未选择任何环境,点击"下一步"将清空该团队的环境配置
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -628,7 +763,6 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
}`}
|
||||
onClick={() => handleSwitchConfigEnv(envId)}
|
||||
>
|
||||
<span className="text-sm">{getEnvIcon(env.envCode)}</span>
|
||||
<span className="flex-1 text-sm font-medium truncate">
|
||||
{env.envName}
|
||||
</span>
|
||||
@ -653,11 +787,10 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{getEnvIcon(currentEnv?.envCode)}</span>
|
||||
<CardTitle className="text-base">{currentEnv?.envName}</CardTitle>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
配置审批规则和应用分支
|
||||
配置审批规则和应用配置
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@ -698,10 +831,10 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 应用分支配置 */}
|
||||
{/* 应用配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">应用分支</CardTitle>
|
||||
<CardTitle className="text-base">应用配置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{applications.length === 0 ? (
|
||||
@ -710,52 +843,104 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
<p className="text-sm text-muted-foreground">暂无可用应用</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>应用名称</TableHead>
|
||||
<TableHead>分支</TableHead>
|
||||
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{envApps(currentConfigEnvId).map((app) => (
|
||||
<TableRow key={app.id}>
|
||||
<TableCell className="font-medium">
|
||||
{app.applicationName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-muted px-2 py-1 rounded">
|
||||
{app.branch || '-'}
|
||||
<div className="space-y-4">
|
||||
{/* 已配置的应用列表 - 卡片式 */}
|
||||
{envApps(currentConfigEnvId).map((app) => {
|
||||
// 查找Jenkins系统名称
|
||||
const deploySystemName = app.deploySystemName ||
|
||||
jenkinsSystems.find(s => s.id === app.deploySystemId)?.name;
|
||||
|
||||
// Job名称直接从后端获取
|
||||
const deployJob = app.deployJob;
|
||||
|
||||
// 工作流定义名称
|
||||
const workflowName = app.workflowDefinitionName ||
|
||||
workflowDefinitions.find(w => w.id === app.workflowDefinitionId)?.name;
|
||||
|
||||
return (
|
||||
<div key={app.id} className="p-4 border rounded-lg bg-card hover:bg-accent/5 transition-colors">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* 应用名称 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{app.applicationName}</span>
|
||||
{app.branch && (
|
||||
<code className="text-xs bg-muted px-2 py-0.5 rounded">
|
||||
{app.branch}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 配置信息 - 网格布局 */}
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||
{deploySystemName && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Jenkins:</span>
|
||||
<span className="truncate" title={deploySystemName}>{deploySystemName}</span>
|
||||
</div>
|
||||
)}
|
||||
{deployJob && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Job:</span>
|
||||
<span className="truncate" title={deployJob}>{deployJob}</span>
|
||||
</div>
|
||||
)}
|
||||
{workflowName && (
|
||||
<div className="flex items-center gap-2 col-span-2">
|
||||
<span className="text-muted-foreground">工作流:</span>
|
||||
<span className="truncate" title={workflowName}>{workflowName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteApplication(app.id)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 添加行 */}
|
||||
{/* 添加新应用表单 */}
|
||||
{getAvailableApplications(currentConfigEnvId).length > 0 && (
|
||||
<TableRow className="bg-muted/20">
|
||||
<TableCell>
|
||||
<div className="p-4 border-2 border-dashed rounded-lg bg-muted/10">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="font-medium text-sm">添加应用配置</span>
|
||||
</div>
|
||||
|
||||
{/* 应用选择 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">应用名称 <span className="text-destructive">*</span></Label>
|
||||
<Select
|
||||
value={addForms[currentConfigEnvId]?.appId?.toString() || ''}
|
||||
onValueChange={(value) => {
|
||||
const appId = Number(value);
|
||||
const app = applications.find(a => a.id === appId);
|
||||
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[currentConfigEnvId]: {
|
||||
...addForms[currentConfigEnvId],
|
||||
appId: Number(value),
|
||||
appId: appId,
|
||||
branch: '',
|
||||
deploySystemId: null,
|
||||
deployJob: '',
|
||||
workflowDefinitionId: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 加载该应用的分支列表
|
||||
if (app) {
|
||||
loadBranches(appId, app);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
@ -769,46 +954,340 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
placeholder="分支名"
|
||||
className="h-8"
|
||||
value={addForms[currentConfigEnvId]?.branch || ''}
|
||||
</div>
|
||||
|
||||
{/* 分支选择 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">分支</Label>
|
||||
{addForms[currentConfigEnvId]?.appId ? (
|
||||
(() => {
|
||||
const appId = addForms[currentConfigEnvId].appId!;
|
||||
const branches = branchesMap[appId] || [];
|
||||
const isLoading = loadingBranches[appId];
|
||||
const searchValue = branchSearchValues[currentConfigEnvId] || "";
|
||||
const open = branchPopoverOpen[currentConfigEnvId] || false;
|
||||
const filteredBranches = branches.filter(branch =>
|
||||
branch.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setBranchPopoverOpen({
|
||||
...branchPopoverOpen,
|
||||
[currentConfigEnvId]: isOpen,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={isLoading || branches.length === 0}
|
||||
className={cn(
|
||||
"w-full justify-between h-8",
|
||||
!addForms[currentConfigEnvId]?.branch && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{addForms[currentConfigEnvId]?.branch
|
||||
? (() => {
|
||||
const selectedBranch = branches.find(
|
||||
(b) => b.name === addForms[currentConfigEnvId]?.branch
|
||||
);
|
||||
return (
|
||||
<span className="flex items-center gap-2 truncate">
|
||||
{selectedBranch?.name}
|
||||
{selectedBranch?.isDefaultBranch && (
|
||||
<span className="text-xs text-muted-foreground">(默认)</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})()
|
||||
: isLoading
|
||||
? '加载中...'
|
||||
: branches.length === 0
|
||||
? '无分支'
|
||||
: '选择分支'}
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<div className="flex items-center border-b px-3">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
placeholder="搜索分支..."
|
||||
className="flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
setBranchSearchValues({
|
||||
...branchSearchValues,
|
||||
[currentConfigEnvId]: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<ScrollArea className="h-[200px] w-full">
|
||||
<div className="p-1">
|
||||
{filteredBranches.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||
未找到分支
|
||||
</div>
|
||||
) : (
|
||||
filteredBranches.map((branch) => (
|
||||
<div
|
||||
key={branch.id}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
|
||||
branch.name === addForms[currentConfigEnvId]?.branch && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
onClick={() => {
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[currentConfigEnvId]: {
|
||||
...addForms[currentConfigEnvId],
|
||||
branch: e.target.value,
|
||||
branch: branch.name,
|
||||
},
|
||||
});
|
||||
setBranchSearchValues({
|
||||
...branchSearchValues,
|
||||
[currentConfigEnvId]: "",
|
||||
});
|
||||
setBranchPopoverOpen({
|
||||
...branchPopoverOpen,
|
||||
[currentConfigEnvId]: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="flex-1 truncate">
|
||||
{branch.name}
|
||||
{branch.isDefaultBranch && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">(默认)</span>
|
||||
)}
|
||||
</span>
|
||||
{branch.name === addForms[currentConfigEnvId]?.branch && (
|
||||
<Check className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
placeholder="请先选择应用"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Jenkins配置 - 网格布局 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Jenkins 系统 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Jenkins系统</Label>
|
||||
<Select
|
||||
value={addForms[currentConfigEnvId]?.deploySystemId?.toString() || ''}
|
||||
onValueChange={(value) => {
|
||||
const systemId = Number(value);
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[currentConfigEnvId]: {
|
||||
...addForms[currentConfigEnvId],
|
||||
deploySystemId: systemId,
|
||||
deployJob: '',
|
||||
},
|
||||
});
|
||||
// 加载Jobs
|
||||
loadJenkinsJobs(systemId);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择Jenkins" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{jenkinsSystems.map((system) => (
|
||||
<SelectItem key={system.id} value={system.id.toString()}>
|
||||
{system.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Jenkins Job */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Jenkins Job</Label>
|
||||
{addForms[currentConfigEnvId]?.deploySystemId ? (
|
||||
(() => {
|
||||
const systemId = addForms[currentConfigEnvId].deploySystemId!;
|
||||
const jobs = jenkinsJobsMap[systemId] || [];
|
||||
const isLoading = loadingJobs[systemId];
|
||||
const searchValue = jobSearchValues[currentConfigEnvId] || "";
|
||||
const open = jobPopoverOpen[currentConfigEnvId] || false;
|
||||
const filteredJobs = jobs.filter(job =>
|
||||
job.jobName.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setJobPopoverOpen({
|
||||
...jobPopoverOpen,
|
||||
[currentConfigEnvId]: isOpen,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={isLoading || jobs.length === 0}
|
||||
className={cn(
|
||||
"w-full justify-between h-8",
|
||||
!addForms[currentConfigEnvId]?.deployJob && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{addForms[currentConfigEnvId]?.deployJob ||
|
||||
(isLoading
|
||||
? '加载中...'
|
||||
: jobs.length === 0
|
||||
? '无Job'
|
||||
: '选择Job')}
|
||||
</span>
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<div className="flex items-center border-b px-3">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
placeholder="搜索Job..."
|
||||
className="flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
setJobSearchValues({
|
||||
...jobSearchValues,
|
||||
[currentConfigEnvId]: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
</div>
|
||||
<div className="relative">
|
||||
<ScrollArea className="h-[200px] w-full">
|
||||
<div className="p-1">
|
||||
{filteredJobs.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||
未找到Job
|
||||
</div>
|
||||
) : (
|
||||
filteredJobs.map((job) => (
|
||||
<div
|
||||
key={job.id}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
|
||||
job.jobName === addForms[currentConfigEnvId]?.deployJob && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
onClick={() => {
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[currentConfigEnvId]: {
|
||||
...addForms[currentConfigEnvId],
|
||||
deployJob: job.jobName,
|
||||
},
|
||||
});
|
||||
setJobSearchValues({
|
||||
...jobSearchValues,
|
||||
[currentConfigEnvId]: "",
|
||||
});
|
||||
setJobPopoverOpen({
|
||||
...jobPopoverOpen,
|
||||
[currentConfigEnvId]: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="flex-1 truncate">
|
||||
{job.jobName}
|
||||
</span>
|
||||
{job.jobName === addForms[currentConfigEnvId]?.deployJob && (
|
||||
<Check className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
placeholder="请先选择Jenkins"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 工作流定义 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">工作流定义</Label>
|
||||
<Select
|
||||
value={addForms[currentConfigEnvId]?.workflowDefinitionId?.toString() || ''}
|
||||
onValueChange={(value) => {
|
||||
setAddForms({
|
||||
...addForms,
|
||||
[currentConfigEnvId]: {
|
||||
...addForms[currentConfigEnvId],
|
||||
workflowDefinitionId: Number(value),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择工作流(可选)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{workflowDefinitions.map((workflow) => (
|
||||
<SelectItem key={workflow.id} value={workflow.id.toString()}>
|
||||
{workflow.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 添加按钮 */}
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => handleAddApplication(currentConfigEnvId)}
|
||||
disabled={!addForms[currentConfigEnvId]?.appId}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
添加
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加应用配置
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 空状态提示 */}
|
||||
{envApps(currentConfigEnvId).length === 0 && getAvailableApplications(currentConfigEnvId).length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center text-sm text-muted-foreground py-4">
|
||||
所有应用已添加
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<div className="p-4 border border-dashed rounded-lg text-center">
|
||||
<p className="text-sm text-muted-foreground">所有应用已添加</p>
|
||||
</div>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -841,7 +1320,6 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{getEnvIcon(env.envCode)}</span>
|
||||
<CardTitle className="text-base">{env.envName}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
@ -887,15 +1365,52 @@ const TeamConfigDialog: React.FC<TeamConfigDialogProps> = ({
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-muted-foreground">应用配置</Label>
|
||||
{apps.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{apps.map((app) => (
|
||||
<div key={app.id} className="flex items-center justify-between text-xs p-2 bg-muted/30 rounded">
|
||||
<span>{app.applicationName}</span>
|
||||
<div className="space-y-2">
|
||||
{apps.map((app) => {
|
||||
// 查找Jenkins系统名称
|
||||
const deploySystemName = app.deploySystemName ||
|
||||
jenkinsSystems.find(s => s.id === app.deploySystemId)?.name;
|
||||
|
||||
// Job名称直接从后端获取
|
||||
const deployJob = app.deployJob;
|
||||
|
||||
// 工作流定义名称
|
||||
const workflowName = app.workflowDefinitionName ||
|
||||
workflowDefinitions.find(w => w.id === app.workflowDefinitionId)?.name;
|
||||
|
||||
return (
|
||||
<div key={app.id} className="p-3 bg-muted/30 rounded space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">{app.applicationName}</span>
|
||||
<code className="text-xs bg-background px-2 py-0.5 rounded">
|
||||
{app.branch || '-'}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
{(deploySystemName || deployJob || workflowName) && (
|
||||
<div className="flex flex-col gap-1 text-xs text-muted-foreground pl-2 border-l-2 border-primary/20">
|
||||
{deploySystemName && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">Jenkins:</span>
|
||||
<span>{deploySystemName}</span>
|
||||
</div>
|
||||
)}
|
||||
{deployJob && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">Job:</span>
|
||||
<span>{deployJob}</span>
|
||||
</div>
|
||||
)}
|
||||
{workflowName && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">工作流:</span>
|
||||
<span>{workflowName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">暂无应用配置</p>
|
||||
|
||||
@ -108,3 +108,21 @@ export const updateTeamApplication = (id: number, data: TeamApplicationRequest)
|
||||
export const deleteTeamApplication = (id: number) =>
|
||||
request.delete(`${TEAM_APPLICATION_URL}/${id}`);
|
||||
|
||||
// ==================== Jenkins 相关 ====================
|
||||
|
||||
/**
|
||||
* 获取 Jenkins 系统列表
|
||||
*/
|
||||
export const getJenkinsSystems = () =>
|
||||
request.get<any[]>('/api/v1/external-system/list', {
|
||||
params: { type: 'JENKINS' }
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取 Jenkins Job 列表
|
||||
*/
|
||||
export const getJenkinsJobs = (externalSystemId: number) =>
|
||||
request.get<any[]>('/api/v1/jenkins-job/list', {
|
||||
params: { externalSystemId }
|
||||
});
|
||||
|
||||
|
||||
@ -75,10 +75,15 @@ export interface TeamApplication extends BaseResponse {
|
||||
applicationId: number;
|
||||
environmentId: number;
|
||||
branch?: string;
|
||||
deploySystemId?: number;
|
||||
deployJob?: string;
|
||||
workflowDefinitionId?: number;
|
||||
teamName?: string;
|
||||
applicationName?: string;
|
||||
applicationCode?: string;
|
||||
environmentName?: string;
|
||||
deploySystemName?: string;
|
||||
workflowDefinitionName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,5 +94,8 @@ export interface TeamApplicationRequest {
|
||||
applicationId: number;
|
||||
environmentId: number;
|
||||
branch?: string;
|
||||
deploySystemId?: number;
|
||||
deployJob?: string;
|
||||
workflowDefinitionId?: number;
|
||||
}
|
||||
|
||||
|
||||
@ -93,6 +93,22 @@ export const getRepositoryBranches = (params: {
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取仓库分支列表(不分页,按最后更新时间倒序排序)
|
||||
*/
|
||||
export const getRepositoryBranchesList = (params: {
|
||||
externalSystemId: number;
|
||||
repoProjectId: number;
|
||||
}) =>
|
||||
request.get<RepositoryBranchResponse[]>(`${BRANCH_URL}/list`, {
|
||||
params: {
|
||||
sortField: 'lastUpdateTime',
|
||||
sortOrder: 'DESC',
|
||||
externalSystemId: params.externalSystemId,
|
||||
repoProjectId: params.repoProjectId,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 同步仓库分支数据
|
||||
* @param externalSystemId 外部系统ID(必填)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {ConfigurableNodeDefinition, NodeType, NodeCategory, defineNodeOutputs} from './types';
|
||||
import { DataSourceType } from '@/domain/dataSource';
|
||||
|
||||
/**
|
||||
* Jenkins构建节点定义(纯配置)
|
||||
@ -44,20 +43,20 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
||||
title: "输入",
|
||||
description: "当前节点所需数据配置",
|
||||
properties: {
|
||||
jenkinsServerId: {
|
||||
type: "number",
|
||||
serverId: {
|
||||
type: "string",
|
||||
title: "服务器",
|
||||
description: "选择要使用的服务器",
|
||||
'x-dataSource': DataSourceType.JENKINS_SERVERS
|
||||
description: "输入要使用的服务器",
|
||||
default: "${jenkins.serverId}"
|
||||
},
|
||||
project: {
|
||||
jobName: {
|
||||
type: "string",
|
||||
title: "项目",
|
||||
description: "要触发构建的项目",
|
||||
default: ""
|
||||
default: "${jenkins.jobName}"
|
||||
},
|
||||
},
|
||||
required: ["jenkinsServerId"]
|
||||
required: ["serverId", "jobName"]
|
||||
},
|
||||
outputs: defineNodeOutputs(
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user