三方系统密码加密

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

View File

@ -1,10 +1,7 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import type {Application, RepositoryProject} from '../types'; import type {Application} from '../types';
import {DevelopmentLanguageTypeEnum} from '../types'; import {DevelopmentLanguageTypeEnum} from '../types';
import {createApplication, updateApplication, getRepositoryProjectsBySystem} from '../service'; import {createApplication, updateApplication} 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 {getEnabledCategories} from '../../Category/service'; import {getEnabledCategories} from '../../Category/service';
import type {ApplicationCategoryResponse} from '../../Category/types'; import type {ApplicationCategoryResponse} from '../../Category/types';
import { import {
@ -37,16 +34,6 @@ import {useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod"; import {zodResolver} from "@hookform/resolvers/zod";
import {applicationFormSchema, type ApplicationFormValues} from "../schema"; import {applicationFormSchema, type ApplicationFormValues} from "../schema";
import {Textarea} from "@/components/ui/textarea"; 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 { interface ApplicationModalProps {
open: boolean; open: boolean;
@ -62,8 +49,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
initialValues, initialValues,
}) => { }) => {
const [categories, setCategories] = useState<ApplicationCategoryResponse[]>([]); const [categories, setCategories] = useState<ApplicationCategoryResponse[]>([]);
const [externalSystems, setExternalSystems] = useState<ExternalSystemResponse[]>([]);
const [repositoryProjects, setRepositoryProjects] = useState<RepositoryProject[]>([]);
const {toast} = useToast(); const {toast} = useToast();
const isEdit = !!initialValues?.id; const isEdit = !!initialValues?.id;
@ -77,8 +62,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
language: DevelopmentLanguageTypeEnum.JAVA, language: DevelopmentLanguageTypeEnum.JAVA,
enabled: true, enabled: true,
sort: 0, 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(() => { useEffect(() => {
loadCategories(); loadCategories();
loadExternalSystems();
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -133,36 +95,13 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
language: initialValues.language, language: initialValues.language,
enabled: initialValues.enabled, enabled: initialValues.enabled,
sort: initialValues.sort, sort: initialValues.sort,
externalSystemId: initialValues.externalSystemId,
repoProjectId: initialValues.repoProjectId
}); });
// 如果有Gitlab ID加载仓库项目
if (initialValues.externalSystemId) {
fetchRepositoryProjects(initialValues.externalSystemId);
}
} }
}, [initialValues]); }, [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) => { const handleSubmit = async (values: ApplicationFormValues) => {
console.log('Form submitted with values:', values); console.log('Form submitted with values:', values);
try { try {
// 保留 externalSystemId 字段,传给后端
const submitData = values; const submitData = values;
if (isEdit) { if (isEdit) {
@ -203,9 +142,9 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
<DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle> <DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form className="flex flex-col flex-1 overflow-hidden"> <form className="space-y-6">
<ScrollArea className="flex-1 px-6"> <div className="px-6">
<div className="space-y-4 pr-4"> <div className="space-y-4">
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<FormField <FormField
control={form.control} control={form.control}
@ -298,166 +237,6 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
/> />
</div> </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 <FormField
control={form.control} control={form.control}
name="appDesc" name="appDesc"
@ -514,7 +293,7 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
/> />
</div> </div>
</ScrollArea> </div>
<DialogFooter className="px-6 py-4 border-t mt-0"> <DialogFooter className="px-6 py-4 border-t mt-0">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>

View File

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

View File

@ -228,48 +228,14 @@ const ApplicationList: React.FC = () => {
}, },
{ {
id: 'teamCount', id: 'teamCount',
header: '团队数量', header: '团队使用数量',
size: 100, size: 120,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-center"> <div className="text-center">
<span className="font-medium">{row.original.teamCount || 0}</span> <span className="font-medium">{row.original.teamCount || 0}</span>
</div> </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', accessorKey: 'language',
header: '开发语言', header: '开发语言',

View File

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

View File

@ -28,6 +28,8 @@ import type {
} from '../types'; } from '../types';
import type { RepositoryBranchResponse } from '@/pages/Resource/Git/List/types'; import type { RepositoryBranchResponse } from '@/pages/Resource/Git/List/types';
import type { WorkflowDefinition } from '@/pages/Workflow/Definition/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 { interface TeamApplicationDialogProps {
open: boolean; open: boolean;
@ -48,6 +50,8 @@ interface TeamApplicationDialogProps {
deploySystemId: number | null; deploySystemId: number | null;
deployJob: string; deployJob: string;
workflowDefinitionId: number | null; workflowDefinitionId: number | null;
codeSourceSystemId: number | null;
codeSourceProjectId: number | null;
}) => Promise<void>; }) => Promise<void>;
onLoadBranches: (appId: number, app: Application) => Promise<RepositoryBranchResponse[]>; onLoadBranches: (appId: number, app: Application) => Promise<RepositoryBranchResponse[]>;
onLoadJenkinsJobs: (systemId: number) => Promise<any[]>; onLoadJenkinsJobs: (systemId: number) => Promise<any[]>;
@ -78,6 +82,8 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: null as number | null, deploySystemId: null as number | null,
deployJob: '', deployJob: '',
workflowDefinitionId: null as number | null, 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 [jenkinsJobs, setJenkinsJobs] = useState<any[]>([]);
const [loadingJobs, setLoadingJobs] = useState(false); const [loadingJobs, setLoadingJobs] = useState(false);
const [saving, setSaving] = 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 [branchSearchValue, setBranchSearchValue] = useState('');
const [branchPopoverOpen, setBranchPopoverOpen] = useState(false); const [branchPopoverOpen, setBranchPopoverOpen] = useState(false);
const [projectSearchValue, setProjectSearchValue] = useState('');
const [projectPopoverOpen, setProjectPopoverOpen] = useState(false);
const [jobSearchValue, setJobSearchValue] = useState(''); const [jobSearchValue, setJobSearchValue] = useState('');
const [jobPopoverOpen, setJobPopoverOpen] = useState(false); const [jobPopoverOpen, setJobPopoverOpen] = useState(false);
@ -104,13 +115,28 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: application.deploySystemId || null, deploySystemId: application.deploySystemId || null,
deployJob: application.deployJob || '', deployJob: application.deployJob || '',
workflowDefinitionId: application.workflowDefinitionId || null, workflowDefinitionId: application.workflowDefinitionId || null,
codeSourceSystemId: application.codeSourceSystemId || null,
codeSourceProjectId: application.codeSourceProjectId || null,
}); });
// 加载分支和Jobs // 加载仓库项目
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); const app = applications.find(a => a.id === application.applicationId);
if (app) { if (app) {
loadBranches(application.applicationId, app); loadBranches(application.applicationId, app);
} }
}
// 加载Jenkins Jobs
if (application.deploySystemId) { if (application.deploySystemId) {
loadJenkinsJobs(application.deploySystemId); loadJenkinsJobs(application.deploySystemId);
} }
@ -122,6 +148,8 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: null, deploySystemId: null,
deployJob: '', deployJob: '',
workflowDefinitionId: null, workflowDefinitionId: null,
codeSourceSystemId: null,
codeSourceProjectId: null,
}); });
setBranches([]); setBranches([]);
setJenkinsJobs([]); 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 handleAppChange = (appId: number) => {
const app = applications.find(a => a.id === appId);
setFormData({ setFormData({
appId: appId, appId: appId,
branch: '', branch: '',
deploySystemId: null, deploySystemId: null,
deployJob: '', deployJob: '',
workflowDefinitionId: null, workflowDefinitionId: null,
codeSourceSystemId: null,
codeSourceProjectId: null,
}); });
// 清空分支列表(分支现在基于代码源,不基于应用)
// 加载该应用的分支列表 setBranches([]);
if (app) {
loadBranches(appId, app);
}
}; };
// 处理 Jenkins 系统选择 // 处理 Jenkins 系统选择
@ -184,6 +242,45 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
loadJenkinsJobs(systemId); 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 () => { const handleSave = async () => {
// 表单验证 // 表单验证
@ -204,6 +301,8 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
deploySystemId: formData.deploySystemId, deploySystemId: formData.deploySystemId,
deployJob: formData.deployJob, deployJob: formData.deployJob,
workflowDefinitionId: formData.workflowDefinitionId, workflowDefinitionId: formData.workflowDefinitionId,
codeSourceSystemId: formData.codeSourceSystemId,
codeSourceProjectId: formData.codeSourceProjectId,
}); });
toast({ toast({
@ -279,10 +378,124 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
)} )}
</div> </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"> <div className="space-y-2">
<Label></Label> <Label></Label>
{formData.appId ? ( {formData.codeSourceProjectId ? (
<Popover <Popover
open={branchPopoverOpen} open={branchPopoverOpen}
onOpenChange={setBranchPopoverOpen} onOpenChange={setBranchPopoverOpen}
@ -371,7 +584,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) : ( ) : (
<Input placeholder="请先选择应用" disabled /> <Input placeholder="请先选择仓库项目" disabled />
)} )}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,3 +30,7 @@ export const testConnection = (id: number) =>
// 更新状态 // 更新状态
export const updateStatus = (id: number, enabled: boolean) => export const updateStatus = (id: number, enabled: boolean) =>
request.put(`${BASE_URL}/${id}/status`, null, { params: { enabled } }); 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 * @param externalSystemId ID
@ -94,19 +105,14 @@ export const getRepositoryBranches = (params: {
}); });
/** /**
* *
*/ */
export const getRepositoryBranchesList = (params: { export const getRepositoryBranchesList = (params: {
externalSystemId: number; externalSystemId: number;
repoProjectId: number; repoProjectId: number;
}) => }) =>
request.get<RepositoryBranchResponse[]>(`${BRANCH_URL}/list`, { request.get<RepositoryBranchResponse[]>(`${BRANCH_URL}/list`, {
params: { params,
sortField: 'lastUpdateTime',
sortOrder: 'DESC',
externalSystemId: params.externalSystemId,
repoProjectId: params.repoProjectId,
},
}); });
/** /**