增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-02 16:13:40 +08:00
parent 87b8023e1c
commit a0b189854d
15 changed files with 1026 additions and 581 deletions

View File

@ -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",

View File

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

View 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 }

View File

@ -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 React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
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()
]);
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);
const response = await getDeployEnvironments();
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;
try {
const response = await getDeploymentConfigPage({
pageSize: 100,
pageNum: 1,
environmentId: currentEnvId,
workflowDefinitionId: 0,
enabled: undefined
});
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 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 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);
}
// 处理部署
const handleDeploy = async (app: ApplicationConfig, requiresApproval: boolean) => {
try {
setDeploying((prev) => new Set(prev).add(app.teamApplicationId));
await startDeployment(app.teamApplicationId);
toast({
title: requiresApproval ? '部署申请已提交' : '部署任务已创建',
description: requiresApproval
? '您的部署申请已提交,等待审批人审核'
: '部署任务已成功创建并开始执行',
});
} 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;
return (
<div className="flex-1 p-8">
<h2 className="text-2xl font-semibold mb-6"></h2>
{!hasEnvironments ? (
if (teams.length === 0) {
return (
<div className="flex-1 p-8">
<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="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>
<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>
{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="开发语言"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL"></SelectItem>
{languages.map(lang => (
<SelectItem key={lang.code} value={lang.code}>
{lang.name}
</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 ? (
<Card>
<CardContent>
<EmptyState
icon={<Server className="h-8 w-8 text-muted-foreground"/>}
title="暂无项目数据"
description="当前环境中还没有配置任何项目。请先创建项目,然后开始部署。"
/>
</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>
</div>
{getBuildStatusBadge(config.enabled)}
</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>
</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' ? '构建中' : '未构建'}
</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>
</div>
</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"/>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
<Select value={currentTeamId?.toString()} onValueChange={handleTeamChange}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择团队" />
</SelectTrigger>
<SelectContent>
{teams.map((team) => (
<SelectItem key={team.teamId} value={team.teamId.toString()}>
{team.teamName}
</SelectItem>
))}
</Tabs>
</div>
</>
</SelectContent>
</Select>
</div>
</div>
{/* 当前团队信息 */}
{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>
)}
{selectedConfig && selectedConfig.formVariablesSchema && (
<DeploymentFormModal
open={deployModalOpen}
onClose={() => {
setDeployModalOpen(false);
setSelectedConfig(null);
}}
formSchema={selectedConfig.formVariablesSchema}
deployConfig={selectedConfig}
/>
{/* 环境切换和应用列表 */}
{currentTeam && (
currentTeam.environments.length === 0 ? (
<Card>
<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>
) : (
<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>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<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>
</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 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 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>
);

View File

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

View File

@ -1,38 +1,64 @@
import { JsonNode } from '@/types/common';
import type { BaseResponse } from '@/types/base';
export interface DeployAppBuildDTO {
/**
*
*/
buildType: string;
export interface Approver {
userId: number;
username: string;
realName: string;
}
/**
*
*/
languageType: 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;
}
/**
*
*/
formVariables?: JsonNode;
export interface DeployEnvironment {
environmentId: number;
environmentCode: string;
environmentName: string;
environmentDesc?: string;
enabled: boolean;
sort: number;
requiresApproval: boolean;
approvers: Approver[];
applications: ApplicationConfig[];
}
/**
*
*/
buildVariables: JsonNode;
export interface DeployTeam {
teamId: number;
teamCode: string;
teamName: string;
teamRole: string;
description?: string;
environments: DeployEnvironment[];
}
/**
* ID
*/
environmentId: number;
export interface DeployEnvironmentsResponse {
userId: number;
username: string;
realName: string;
teams: DeployTeam[];
}
/**
* ID
*/
applicationId: number;
export interface StartDeploymentRequest {
teamApplicationId: number;
workflowDefinitionKey: string;
formData?: Record<string, any>;
}
/**
* ID
*/
workflowDefinitionId: number;
}
export interface StartDeploymentResponse {
instanceId: string;
processDefinitionId: string;
businessKey: string;
processKey: string;
startTime?: string;
}

View File

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

View File

@ -238,7 +238,7 @@ const ApplicationList: React.FC = () => {
},
{
id: 'externalSystem',
header: '外部系统',
header: 'Gitlab',
size: 150,
cell: ({ row }) => (
<div className="flex items-center gap-2">

View File

@ -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 },

View File

@ -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 || '-'}
</code>
</TableCell>
<TableCell className="text-right">
<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>
)}
</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>
{/* 添加新应用表单 */}
{getAvailableApplications(currentConfigEnvId).length > 0 && (
<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 || ''}
onChange={(e) => {
</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: 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],
branch: e.target.value,
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,
});
}}
/>
</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),
},
});
}}
/>
</TableCell>
<TableCell className="text-right">
>
<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>
)}
</TableBody>
</Table>
{/* 空状态提示 */}
{envApps(currentConfigEnvId).length === 0 && getAvailableApplications(currentConfigEnvId).length === 0 && (
<div className="p-4 border border-dashed rounded-lg text-center">
<p className="text-sm text-muted-foreground"></p>
</div>
)}
</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>
<code className="text-xs bg-background px-2 py-0.5 rounded">
{app.branch || '-'}
</code>
</div>
))}
<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>

View File

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

View File

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

View File

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

View File

@ -351,16 +351,16 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
onClick={() => onOpenChange(false)}
disabled={submitting}
>
</Button>
</Button>
<Button
type="button"
onClick={handleSubmit}
disabled={submitting || loading}
>
{submitting ? '保存中...' : '确定'}
</Button>
</div>
{submitting ? '保存中...' : '确定'}
</Button>
</div>
</DialogContent>
</Dialog>
);

View File

@ -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(
{