三方系统密码加密

This commit is contained in:
dengqichen 2025-11-11 16:05:30 +08:00
parent d0538de830
commit 8b15f8ae71
15 changed files with 381 additions and 379 deletions

View File

@ -277,5 +277,5 @@ http://localhost:3000/form-designer/workflow-example
---
Made with ❤️ for Deploy Ease Platform
Made with ❤️ for 链宇Deploy Ease平台

View File

@ -6,19 +6,32 @@ import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
>(({ className, children, ...props }, ref) => {
const viewportRef = React.useRef<HTMLDivElement>(null);
return (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
ref={viewportRef}
className="h-full w-full rounded-[inherit]"
onWheel={(e) => {
// 允许滚轮在整个viewport区域工作
if (viewportRef.current) {
viewportRef.current.scrollTop += e.deltaY;
}
}}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
})
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<

View File

@ -1,10 +1,7 @@
import React, {useEffect, useState} from 'react';
import type {Application, RepositoryProject} from '../types';
import type {Application} from '../types';
import {DevelopmentLanguageTypeEnum} from '../types';
import {createApplication, updateApplication, getRepositoryProjectsBySystem} from '../service';
import type {ExternalSystemResponse} from '@/pages/Resource/External/List/types';
import {SystemType} from '@/pages/Resource/External/List/types';
import {getExternalSystems} from '@/pages/Resource/External/List/service';
import {createApplication, updateApplication} from '../service';
import {getEnabledCategories} from '../../Category/service';
import type {ApplicationCategoryResponse} from '../../Category/types';
import {
@ -37,16 +34,6 @@ import {useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import {applicationFormSchema, type ApplicationFormValues} from "../schema";
import {Textarea} from "@/components/ui/textarea";
import {ScrollArea} from "@/components/ui/scroll-area";
import {Check, ChevronDown, Search} from "lucide-react";
import {cn} from "@/lib/utils";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
interface ApplicationModalProps {
open: boolean;
@ -62,8 +49,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
initialValues,
}) => {
const [categories, setCategories] = useState<ApplicationCategoryResponse[]>([]);
const [externalSystems, setExternalSystems] = useState<ExternalSystemResponse[]>([]);
const [repositoryProjects, setRepositoryProjects] = useState<RepositoryProject[]>([]);
const {toast} = useToast();
const isEdit = !!initialValues?.id;
@ -77,8 +62,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
language: DevelopmentLanguageTypeEnum.JAVA,
enabled: true,
sort: 0,
externalSystemId: undefined,
repoProjectId: undefined,
},
});
@ -97,29 +80,8 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
}
};
// 加载Gitlab列表
const loadExternalSystems = async () => {
try {
const response = await getExternalSystems({
type: SystemType.GIT,
enabled: true,
});
if (response?.content) {
setExternalSystems(response.content);
}
} catch (error) {
toast({
variant: "destructive",
title: "加载Gitlab失败",
description: error instanceof Error ? error.message : undefined,
duration: 3000,
});
}
};
useEffect(() => {
loadCategories();
loadExternalSystems();
}, []);
useEffect(() => {
@ -133,36 +95,13 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
language: initialValues.language,
enabled: initialValues.enabled,
sort: initialValues.sort,
externalSystemId: initialValues.externalSystemId,
repoProjectId: initialValues.repoProjectId
});
// 如果有Gitlab ID加载仓库项目
if (initialValues.externalSystemId) {
fetchRepositoryProjects(initialValues.externalSystemId);
}
}
}, [initialValues]);
// 当选择Gitlab时获取对应的仓库项目列表
const handleExternalSystemChange = (externalSystemId: number | undefined) => {
form.setValue('repoProjectId', undefined);
setRepositoryProjects([]);
if (externalSystemId) {
fetchRepositoryProjects(externalSystemId);
}
};
const fetchRepositoryProjects = async (externalSystemId: number | undefined) => {
if (!externalSystemId) return;
const response = await getRepositoryProjectsBySystem(externalSystemId);
setRepositoryProjects(response || []);
};
const handleSubmit = async (values: ApplicationFormValues) => {
console.log('Form submitted with values:', values);
try {
// 保留 externalSystemId 字段,传给后端
const submitData = values;
if (isEdit) {
@ -203,9 +142,9 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
<DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form className="flex flex-col flex-1 overflow-hidden">
<ScrollArea className="flex-1 px-6">
<div className="space-y-4 pr-4">
<form className="space-y-6">
<div className="px-6">
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<FormField
control={form.control}
@ -298,166 +237,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
/>
</div>
{/* Git 配置区域(可折叠) */}
<Accordion type="single" collapsible className="border rounded-lg">
<AccordionItem value="git-config" className="border-0">
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<span className="text-sm font-medium">Git </span>
</AccordionTrigger>
<AccordionContent className="px-4 pb-4">
<div className="grid grid-cols-3 gap-4">
<FormField
control={form.control}
name="externalSystemId"
render={({field}) => (
<FormItem>
<FormLabel>Gitlab</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(Number(value));
handleExternalSystemChange(Number(value));
}}
value={field.value?.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="请选择Gitlab"/>
</SelectTrigger>
</FormControl>
<SelectContent>
{externalSystems.map((system) => (
<SelectItem
key={system.id}
value={system.id.toString()}
>
{system.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="repoProjectId"
render={({field}) => {
const [searchValue, setSearchValue] = React.useState("");
const [open, setOpen] = React.useState(false);
const filteredProjects = repositoryProjects.filter(project =>
project.name.toLowerCase().includes(searchValue.toLowerCase())
);
return (
<FormItem>
<FormLabel></FormLabel>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
disabled={!form.watch('externalSystemId')}
className={cn(
"w-full justify-between",
!field.value && "text-muted-foreground"
)}
>
{field.value
? (() => {
const project = repositoryProjects.find(
(p) => p.repoProjectId === field.value
);
if (!project) return '';
// 如果有组别名称,显示组别/项目名
if (project.repoGroupName) {
return (
<>
<span className="text-muted-foreground">{project.repoGroupName}/</span>
{project.name}
</>
);
}
return project.name;
})()
: !form.watch('externalSystemId')
? "请先选择Gitlab"
: "请选择项目"}
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] 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) => setSearchValue(e.target.value)}
/>
</div>
<div className="relative">
<ScrollArea className="h-[200px] w-full">
<div className="p-1">
{filteredProjects.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
</div>
) : (
filteredProjects.map((project) => (
<div
key={project.repoProjectId}
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",
project.repoProjectId === field.value && "bg-accent text-accent-foreground"
)}
onClick={() => {
form.setValue("repoProjectId", project.repoProjectId);
setSearchValue("");
setOpen(false);
}}
onWheel={(e) => {
const scrollArea = e.currentTarget.closest('[data-radix-scroll-area-viewport]');
if (scrollArea) {
scrollArea.scrollTop += e.deltaY;
}
}}
>
<div className="flex-1 truncate">
{project.repoGroupName ? (
<>
<span className="text-muted-foreground">{project.repoGroupName}/</span>
{project.name}
</>
) : (
project.name
)}
</div>
{project.repoProjectId === field.value && (
<Check className="ml-auto h-4 w-4 flex-shrink-0" />
)}
</div>
))
)}
</div>
</ScrollArea>
</div>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<FormField
control={form.control}
name="appDesc"
@ -514,7 +293,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
/>
</div>
</ScrollArea>
</div>
<DialogFooter className="px-6 py-4 border-t mt-0">
<Button type="button" variant="outline" onClick={onCancel}>

View File

@ -242,8 +242,8 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
<Table minWidth="890px">
<TableHeader>
<TableRow>
<TableHead width="160px"></TableHead>
<TableHead width="140px"></TableHead>
<TableHead width="160px"></TableHead>
<TableHead width="60px"></TableHead>
<TableHead width="120px"></TableHead>
<TableHead width="80px"></TableHead>
@ -265,14 +265,14 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
) : data?.content && data.content.length > 0 ? (
data.content.map((record) => (
<TableRow key={record.id}>
<TableCell width="160px" className="font-medium">
{record.name}
</TableCell>
<TableCell width="140px">
<code className="text-xs bg-muted px-2 py-0.5 rounded whitespace-nowrap">
{record.code}
</code>
</TableCell>
<TableCell width="160px" className="font-medium">
{record.name}
</TableCell>
<TableCell width="60px">
{record.icon ? (
<div className="flex items-center justify-center">
@ -350,13 +350,13 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="name"
rules={{ required: '请输入分类名称' }}
name="code"
rules={{ required: '请输入分类代码' }}
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="输入分类名称" {...field} />
<Input placeholder="输入分类代码" {...field} disabled={!!editRecord} />
</FormControl>
<FormMessage />
</FormItem>
@ -365,13 +365,13 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
<FormField
control={form.control}
name="code"
rules={{ required: '请输入分类代码' }}
name="name"
rules={{ required: '请输入分类名称' }}
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="输入分类代码" {...field} disabled={!!editRecord} />
<Input placeholder="输入分类名称" {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@ -228,48 +228,14 @@ const ApplicationList: React.FC = () => {
},
{
id: 'teamCount',
header: '团队数量',
size: 100,
header: '团队使用数量',
size: 120,
cell: ({ row }) => (
<div className="text-center">
<span className="font-medium">{row.original.teamCount || 0}</span>
</div>
),
},
{
id: 'externalSystem',
header: 'Gitlab',
size: 150,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<span>{row.original.externalSystem?.name || '-'}</span>
</div>
),
},
{
id: 'repository',
header: '代码仓库',
size: 280,
cell: ({ row }) => {
const project = row.original.repositoryProject;
return project ? (
<div className="flex items-center gap-2">
<GithubOutlined />
<a
href={project.webUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700 flex items-center gap-1"
>
{project.name}
<ExternalLink className="h-3 w-3" />
</a>
</div>
) : (
'-'
);
},
},
{
accessorKey: 'language',
header: '开发语言',

View File

@ -16,8 +16,6 @@ export const applicationFormSchema = z.object({
required_error: '请选择应用分类',
invalid_type_error: '应用分类必须是数字',
}),
externalSystemId: z.number().optional(),
repoProjectId: z.number().optional(),
language: z.nativeEnum(DevelopmentLanguageTypeEnum, {
required_error: "请选择开发语言",
}),

View File

@ -28,6 +28,8 @@ import type {
} from '../types';
import type { RepositoryBranchResponse } from '@/pages/Resource/Git/List/types';
import type { WorkflowDefinition } from '@/pages/Workflow/Definition/List/types';
import { getExternalSystemList } from '@/pages/Resource/External/List/service';
import { getRepositoryProjectsList, getRepositoryBranchesList } from '@/pages/Resource/Git/List/service';
interface TeamApplicationDialogProps {
open: boolean;
@ -48,6 +50,8 @@ interface TeamApplicationDialogProps {
deploySystemId: number | null;
deployJob: string;
workflowDefinitionId: number | null;
codeSourceSystemId: number | null;
codeSourceProjectId: number | null;
}) => Promise<void>;
onLoadBranches: (appId: number, app: Application) => Promise<RepositoryBranchResponse[]>;
onLoadJenkinsJobs: (systemId: number) => Promise<any[]>;
@ -78,6 +82,8 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: null as number | null,
deployJob: '',
workflowDefinitionId: null as number | null,
codeSourceSystemId: null as number | null,
codeSourceProjectId: null as number | null,
});
// 加载状态
@ -86,10 +92,15 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
const [jenkinsJobs, setJenkinsJobs] = useState<any[]>([]);
const [loadingJobs, setLoadingJobs] = useState(false);
const [saving, setSaving] = useState(false);
const [gitSystems, setGitSystems] = useState<any[]>([]);
const [repoProjects, setRepoProjects] = useState<any[]>([]);
const [loadingRepoProjects, setLoadingRepoProjects] = useState(false);
// 搜索和弹窗状态
const [branchSearchValue, setBranchSearchValue] = useState('');
const [branchPopoverOpen, setBranchPopoverOpen] = useState(false);
const [projectSearchValue, setProjectSearchValue] = useState('');
const [projectPopoverOpen, setProjectPopoverOpen] = useState(false);
const [jobSearchValue, setJobSearchValue] = useState('');
const [jobPopoverOpen, setJobPopoverOpen] = useState(false);
@ -104,13 +115,28 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: application.deploySystemId || null,
deployJob: application.deployJob || '',
workflowDefinitionId: application.workflowDefinitionId || null,
codeSourceSystemId: application.codeSourceSystemId || null,
codeSourceProjectId: application.codeSourceProjectId || null,
});
// 加载分支和Jobs
const app = applications.find(a => a.id === application.applicationId);
if (app) {
loadBranches(application.applicationId, app);
// 加载仓库项目
if (application.codeSourceSystemId) {
loadRepoProjects(application.codeSourceSystemId);
}
// 加载分支(优先使用代码源信息,向后兼容旧数据)
if (application.codeSourceSystemId && application.codeSourceProjectId) {
// 使用代码源信息加载分支
loadBranchesFromCodeSource(application.codeSourceSystemId, application.codeSourceProjectId);
} else {
// 向后兼容:使用应用信息加载分支
const app = applications.find(a => a.id === application.applicationId);
if (app) {
loadBranches(application.applicationId, app);
}
}
// 加载Jenkins Jobs
if (application.deploySystemId) {
loadJenkinsJobs(application.deploySystemId);
}
@ -122,6 +148,8 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: null,
deployJob: '',
workflowDefinitionId: null,
codeSourceSystemId: null,
codeSourceProjectId: null,
});
setBranches([]);
setJenkinsJobs([]);
@ -157,21 +185,51 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
}
};
// 加载Git系统列表
const loadGitSystems = async () => {
try {
const systems = await getExternalSystemList({ type: 'GIT' });
setGitSystems(systems || []);
} catch (error) {
console.error('加载Git系统失败:', error);
setGitSystems([]);
}
};
// 加载仓库项目列表
const loadRepoProjects = async (externalSystemId: number) => {
setLoadingRepoProjects(true);
try {
const projects = await getRepositoryProjectsList({ externalSystemId });
setRepoProjects(projects || []);
} catch (error) {
console.error('加载仓库项目失败:', error);
setRepoProjects([]);
} finally {
setLoadingRepoProjects(false);
}
};
// 初始化加载Git系统列表
useEffect(() => {
if (open) {
loadGitSystems();
}
}, [open]);
// 处理应用选择
const handleAppChange = (appId: number) => {
const app = applications.find(a => a.id === appId);
setFormData({
appId: appId,
branch: '',
deploySystemId: null,
deployJob: '',
workflowDefinitionId: null,
codeSourceSystemId: null,
codeSourceProjectId: null,
});
// 加载该应用的分支列表
if (app) {
loadBranches(appId, app);
}
// 清空分支列表(分支现在基于代码源,不基于应用)
setBranches([]);
};
// 处理 Jenkins 系统选择
@ -184,6 +242,45 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
loadJenkinsJobs(systemId);
};
// 加载基于代码源的分支列表
const loadBranchesFromCodeSource = async (externalSystemId: number, repoProjectId: number) => {
setLoadingBranches(true);
try {
const branchList = await getRepositoryBranchesList({ externalSystemId, repoProjectId });
setBranches(branchList || []);
} catch (error) {
console.error('加载分支失败:', error);
setBranches([]);
} finally {
setLoadingBranches(false);
}
};
// 处理代码源系统选择
const handleCodeSourceSystemChange = (systemId: number) => {
setFormData({
...formData,
codeSourceSystemId: systemId,
codeSourceProjectId: null,
branch: '', // 清空分支
});
setBranches([]); // 清空分支列表
loadRepoProjects(systemId);
};
// 处理仓库项目选择
const handleCodeSourceProjectChange = (projectId: number) => {
setFormData({
...formData,
codeSourceProjectId: projectId,
branch: '', // 清空分支
});
// 加载该项目的分支
if (formData.codeSourceSystemId) {
loadBranchesFromCodeSource(formData.codeSourceSystemId, projectId);
}
};
// 保存
const handleSave = async () => {
// 表单验证
@ -204,6 +301,8 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: formData.deploySystemId,
deployJob: formData.deployJob,
workflowDefinitionId: formData.workflowDefinitionId,
codeSourceSystemId: formData.codeSourceSystemId,
codeSourceProjectId: formData.codeSourceProjectId,
});
toast({
@ -279,10 +378,124 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
)}
</div>
{/* 代码源选择 */}
<div className="space-y-2">
<Label></Label>
<Select
value={formData.codeSourceSystemId?.toString() || ''}
onValueChange={(value) => handleCodeSourceSystemChange(Number(value))}
>
<SelectTrigger>
<SelectValue placeholder="选择代码源" />
</SelectTrigger>
<SelectContent>
{gitSystems.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
Git系统
</div>
) : (
gitSystems.map((system) => (
<SelectItem key={system.id} value={system.id.toString()}>
{system.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
{/* 仓库项目选择 */}
<div className="space-y-2">
<Label></Label>
{formData.codeSourceSystemId ? (
<Popover
open={projectPopoverOpen}
onOpenChange={setProjectPopoverOpen}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={loadingRepoProjects || repoProjects.length === 0}
className={cn(
'w-full justify-between',
!formData.codeSourceProjectId && 'text-muted-foreground'
)}
>
{formData.codeSourceProjectId
? (() => {
const selectedProject = repoProjects.find(
(p) => p.repoProjectId === formData.codeSourceProjectId
);
return selectedProject
? (selectedProject.repoGroupName
? `${selectedProject.repoGroupName} / ${selectedProject.name}`
: selectedProject.name)
: '选择仓库项目';
})()
: loadingRepoProjects
? '加载中...'
: repoProjects.length === 0
? '暂无项目'
: '选择仓库项目'}
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] 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"
value={projectSearchValue}
onChange={(e) => setProjectSearchValue(e.target.value)}
/>
</div>
<ScrollArea className="h-[200px]">
<div className="p-1">
{repoProjects
.filter((project) =>
project.name.toLowerCase().includes(projectSearchValue.toLowerCase()) ||
(project.repoGroupName?.toLowerCase().includes(projectSearchValue.toLowerCase()))
)
.map((project) => (
<div
key={project.repoProjectId}
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',
project.repoProjectId === formData.codeSourceProjectId &&
'bg-accent text-accent-foreground'
)}
onClick={() => {
handleCodeSourceProjectChange(project.repoProjectId);
setProjectSearchValue('');
setProjectPopoverOpen(false);
}}
>
<div className="flex-1 truncate">
{project.repoGroupName && (
<span className="text-muted-foreground">{project.repoGroupName} / </span>
)}
{project.name}
</div>
{project.repoProjectId === formData.codeSourceProjectId && (
<Check className="ml-2 h-4 w-4" />
)}
</div>
))}
</div>
</ScrollArea>
</PopoverContent>
</Popover>
) : (
<Input placeholder="请先选择代码源" disabled />
)}
</div>
{/* 分支选择 */}
<div className="space-y-2">
<Label></Label>
{formData.appId ? (
{formData.codeSourceProjectId ? (
<Popover
open={branchPopoverOpen}
onOpenChange={setBranchPopoverOpen}
@ -371,7 +584,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
</PopoverContent>
</Popover>
) : (
<Input placeholder="请先选择应用" disabled />
<Input placeholder="请先选择仓库项目" disabled />
)}
</div>

View File

@ -140,6 +140,8 @@ export const TeamApplicationManageDialog: React.FC<
deploySystemId: number | null;
deployJob: string;
workflowDefinitionId: number | null;
codeSourceSystemId: number | null;
codeSourceProjectId: number | null;
}) => {
if (!editingEnvironment) return;
@ -151,6 +153,8 @@ export const TeamApplicationManageDialog: React.FC<
deploySystemId: data.deploySystemId || undefined,
deployJob: data.deployJob,
workflowDefinitionId: data.workflowDefinitionId || undefined,
codeSourceSystemId: data.codeSourceSystemId || undefined,
codeSourceProjectId: data.codeSourceProjectId || undefined,
};
if (appDialogMode === 'edit' && data.id) {

View File

@ -7,7 +7,9 @@ import {
Dialog,
DialogContent,
DialogHeader,
DialogBody,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
@ -35,14 +37,14 @@ import { Textarea } from "@/components/ui/textarea";
interface TeamModalProps {
open: boolean;
onCancel: () => void;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
initialValues?: TeamResponse;
}
const TeamModal: React.FC<TeamModalProps> = ({
open,
onCancel,
onOpenChange,
onSuccess,
initialValues,
}) => {
@ -120,7 +122,7 @@ const TeamModal: React.FC<TeamModalProps> = ({
});
}
onSuccess();
onCancel();
onOpenChange(false);
} catch (error) {
console.error(error);
toast({
@ -133,15 +135,14 @@ const TeamModal: React.FC<TeamModalProps> = ({
};
return (
<Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className="max-w-[600px] max-h-[85vh] flex flex-col p-0">
<DialogHeader className="px-6 pt-6 pb-4">
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form className="flex flex-col flex-1 overflow-hidden">
<div className="flex-1 overflow-y-auto px-6">
<div className="space-y-4 pb-4">
<DialogBody>
<Form {...form}>
<form className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
@ -277,27 +278,25 @@ const TeamModal: React.FC<TeamModalProps> = ({
/>
</div>
<div className="h-6" />
</div>
</div>
<div className="px-6 py-4 border-t bg-muted/30 flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
</Button>
<Button
type="button"
onClick={async () => {
const isValid = await form.trigger();
if (isValid) {
handleSubmit(form.getValues());
}
}}
>
</Button>
</div>
</form>
</Form>
</form>
</Form>
</DialogBody>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button
type="button"
onClick={async () => {
const isValid = await form.trigger();
if (isValid) {
handleSubmit(form.getValues());
}
}}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View File

@ -189,9 +189,11 @@ const TeamList: React.FC = () => {
setEnvManageDialogOpen(true);
};
const handleModalClose = () => {
setModalVisible(false);
setCurrentTeam(undefined);
const handleModalOpenChange = (open: boolean) => {
setModalVisible(open);
if (!open) {
setCurrentTeam(undefined);
}
};
const handlePageChange = (newPage: number) => {
@ -461,14 +463,12 @@ const TeamList: React.FC = () => {
</Card>
{/* 对话框 */}
{modalVisible && (
<TeamModal
open={modalVisible}
onCancel={handleModalClose}
onSuccess={handleSuccess}
initialValues={currentTeam}
/>
)}
<TeamModal
open={modalVisible}
onOpenChange={handleModalOpenChange}
onSuccess={handleSuccess}
initialValues={currentTeam}
/>
<DeleteDialog
open={deleteDialogOpen}
record={currentTeam}

View File

@ -90,12 +90,16 @@ export interface TeamApplication extends BaseResponse {
deploySystemId?: number;
deployJob?: string;
workflowDefinitionId?: number;
codeSourceSystemId?: number; // 代码源系统ID
codeSourceProjectId?: number; // 代码源项目ID
teamName?: string;
applicationName?: string;
applicationCode?: string;
environmentName?: string;
deploySystemName?: string;
workflowDefinitionName?: string;
codeSourceSystemName?: string; // 代码源系统名称
codeSourceProjectName?: string; // 代码源项目名称
}
/**
@ -109,5 +113,7 @@ export interface TeamApplicationRequest {
deploySystemId?: number;
deployJob?: string;
workflowDefinitionId?: number;
codeSourceSystemId?: number; // 代码源系统ID
codeSourceProjectId?: number; // 代码源项目ID
}

View File

@ -40,16 +40,22 @@ const EditDialog: React.FC<EditDialogProps> = ({
authType: 'BASIC' as AuthType,
});
// 保存原始密码和Token用于判断是否被修改
const [originalPassword, setOriginalPassword] = useState<string | undefined>(undefined);
const [originalToken, setOriginalToken] = useState<string | undefined>(undefined);
// 标记是否有原始密码和Token用于判断是否需要提交
const [hasOriginalPassword, setHasOriginalPassword] = useState(false);
const [hasOriginalToken, setHasOriginalToken] = useState(false);
// 掩码常量
const MASK = '********';
useEffect(() => {
if (open) {
if (record) {
// 保存原始掩码值
setOriginalPassword(record.password);
setOriginalToken(record.token);
// 判断是否有密码/Token无论返回什么值只要不为空就认为有
const hasPwd = !!record.password && record.password.trim() !== '';
const hasTkn = !!record.token && record.token.trim() !== '';
setHasOriginalPassword(hasPwd);
setHasOriginalToken(hasTkn);
setFormData({
name: record.name,
@ -57,15 +63,17 @@ const EditDialog: React.FC<EditDialogProps> = ({
url: record.url,
authType: record.authType,
username: record.username,
password: record.password || '', // 显示掩码或空
token: record.token || '', // 显示掩码或空
// 如果有密码,显示掩码;否则显示空
password: hasPwd ? MASK : '',
// 如果有Token显示掩码否则显示空
token: hasTkn ? MASK : '',
sort: record.sort,
remark: record.remark,
enabled: record.enabled,
});
} else {
setOriginalPassword(undefined);
setOriginalToken(undefined);
setHasOriginalPassword(false);
setHasOriginalToken(false);
setFormData({ enabled: true, sort: 1, authType: 'BASIC' as AuthType });
}
}
@ -104,9 +112,9 @@ const EditDialog: React.FC<EditDialogProps> = ({
toast({ title: '提示', description: '请输入密码', variant: 'destructive' });
return;
}
// 编辑时,如果清空了原有密码(掩码),也需要输入新密码
if (record && originalPassword === '********' && !formData.password?.trim()) {
toast({ title: '提示', description: '请输入新密码或保持原密码不变', variant: 'destructive' });
// 编辑时,如果清空了原有密码(掩码),需要提示
if (record && hasOriginalPassword && !formData.password?.trim()) {
toast({ title: '提示', description: '密码不能为空,请输入新密码或保持原密码不变', variant: 'destructive' });
return;
}
}
@ -117,9 +125,9 @@ const EditDialog: React.FC<EditDialogProps> = ({
toast({ title: '提示', description: '请输入访问令牌', variant: 'destructive' });
return;
}
// 编辑时如果清空了原有Token掩码也需要输入新Token
if (record && originalToken === '********' && !formData.token?.trim()) {
toast({ title: '提示', description: '请输入新令牌或保持原令牌不变', variant: 'destructive' });
// 编辑时如果清空了原有Token掩码需要提示
if (record && hasOriginalToken && !formData.token?.trim()) {
toast({ title: '提示', description: '令牌不能为空,请输入新令牌或保持原令牌不变', variant: 'destructive' });
return;
}
}
@ -129,22 +137,22 @@ const EditDialog: React.FC<EditDialogProps> = ({
...formData as ExternalSystemRequest,
};
// 处理密码:如果用户没有修改,保持掩码;如果修改了,提交新值
// 处理密码:如果值还是掩码,说明没修改,提交掩码;否则提交新值
if (formData.authType === 'BASIC') {
if (record && formData.password === originalPassword) {
if (record && formData.password === MASK) {
// 没有修改,保持掩码
submitData.password = originalPassword;
submitData.password = MASK;
} else {
// 修改了或新建,提交新密码
submitData.password = formData.password;
}
}
// 处理Token如果用户没有修改,保持掩码;如果修改了,提交新值
// 处理Token如果值还是掩码,说明没修改,提交掩码;否则提交新值
if (formData.authType === 'TOKEN') {
if (record && formData.token === originalToken) {
if (record && formData.token === MASK) {
// 没有修改,保持掩码
submitData.token = originalToken;
submitData.token = MASK;
} else {
// 修改了或新建提交新Token
submitData.token = formData.token;
@ -243,7 +251,7 @@ const EditDialog: React.FC<EditDialogProps> = ({
<div className="grid gap-2">
<Label htmlFor="password">
{!record && '*'}
{record && originalPassword === '********' && (
{record && hasOriginalPassword && (
<span className="ml-2 text-xs text-muted-foreground">
()
</span>
@ -254,7 +262,7 @@ const EditDialog: React.FC<EditDialogProps> = ({
type="password"
value={formData.password || ''}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder={record ? (originalPassword === '********' ? '保持不变或输入新密码' : '请输入密码') : '请输入密码'}
placeholder={record ? (hasOriginalPassword ? '保持不变或输入新密码' : '请输入密码') : '请输入密码'}
/>
</div>
</>
@ -264,7 +272,7 @@ const EditDialog: React.FC<EditDialogProps> = ({
<div className="grid gap-2">
<Label htmlFor="token">
访 {!record && '*'}
{record && originalToken === '********' && (
{record && hasOriginalToken && (
<span className="ml-2 text-xs text-muted-foreground">
()
</span>
@ -275,7 +283,7 @@ const EditDialog: React.FC<EditDialogProps> = ({
type="password"
value={formData.token || ''}
onChange={(e) => setFormData({ ...formData, token: e.target.value })}
placeholder={record ? (originalToken === '********' ? '保持不变或输入新令牌' : '请输入访问令牌') : '请输入访问令牌'}
placeholder={record ? (hasOriginalToken ? '保持不变或输入新令牌' : '请输入访问令牌') : '请输入访问令牌'}
/>
</div>
)}

View File

@ -93,6 +93,8 @@ const ExternalPage: React.FC = () => {
description: success ? '外部系统连接正常' : '无法连接到外部系统',
variant: success ? 'default' : 'destructive',
});
// 刷新列表以更新最后连接时间等信息
loadData();
} catch (error) {
toast({ title: '测试失败', description: '无法测试连接', variant: 'destructive' });
}
@ -263,6 +265,10 @@ const ExternalPage: React.FC = () => {
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-1"
onClick={() => {
// 点击链接后刷新列表
setTimeout(() => loadData(), 100);
}}
>
<Link className="h-3 w-3" />
{item.url}

View File

@ -30,3 +30,7 @@ export const testConnection = (id: number) =>
// 更新状态
export const updateStatus = (id: number, enabled: boolean) =>
request.put(`${BASE_URL}/${id}/status`, null, { params: { enabled } });
// 获取外部系统列表(不分页)
export const getExternalSystemList = (params?: { type?: string }) =>
request.get<ExternalSystemResponse[]>(`${BASE_URL}/list`, { params });

View File

@ -61,6 +61,17 @@ export const getRepositoryProjects = (params: {
},
});
/**
*
*/
export const getRepositoryProjectsList = (params: {
externalSystemId: number;
repoGroupId?: number;
}) =>
request.get<RepositoryProjectResponse[]>(`${PROJECT_URL}/list`, {
params,
});
/**
*
* @param externalSystemId ID
@ -94,19 +105,14 @@ 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,
},
params,
});
/**