diff --git a/frontend/src/pages/Deploy/GitManager/List/index.tsx b/frontend/src/pages/Deploy/GitManager/List/index.tsx index 5099702a..81e37ce7 100644 --- a/frontend/src/pages/Deploy/GitManager/List/index.tsx +++ b/frontend/src/pages/Deploy/GitManager/List/index.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { Card, - CardContent, CardHeader, CardTitle, } from '@/components/ui/card'; @@ -14,6 +13,7 @@ import { SelectItem, SelectTrigger, SelectValue, + } from '@/components/ui/select'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useToast } from '@/components/ui/use-toast'; @@ -25,16 +25,19 @@ import { } from '@/components/ui/tooltip'; import { GitBranch, + FolderGit2, RefreshCw, Globe, Lock, Users, + Search, Loader2, ChevronRight, ChevronDown, ExternalLink, + Shield, Code2, Calendar, @@ -45,19 +48,20 @@ import type { RepositoryGroupResponse, RepositoryProjectResponse, RepositoryBranchResponse, - GitStatistics, } from './types'; import { getGitInstances, + getRepositoryGroups, getRepositoryProjects, getRepositoryBranches, syncAllGitData, + syncRepositoryGroups, syncRepositoryProjects, syncRepositoryBranches, - getGitStatistics, } from './service'; + import type { ExternalSystemResponse } from '@/pages/Deploy/External/types'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -66,15 +70,18 @@ import 'dayjs/locale/zh-cn'; dayjs.extend(relativeTime); dayjs.locale('zh-cn'); + // 可见性配置 const VISIBILITY_CONFIG = { public: { label: '公开', variant: 'default' as const, icon: Globe, color: 'text-green-600' }, + private: { label: '私有', variant: 'secondary' as const, icon: Lock, color: 'text-red-600' }, internal: { label: '内部', variant: 'outline' as const, icon: Users, color: 'text-yellow-600' }, }; const GitManager: React.FC = () => { const { toast } = useToast(); + // Git 实例选择 const [instances, setInstances] = useState([]); @@ -84,7 +91,6 @@ const GitManager: React.FC = () => { const [groupTree, setGroupTree] = useState([]); const [projects, setProjects] = useState([]); const [branches, setBranches] = useState([]); - const [statistics, setStatistics] = useState(); // 选中状态 const [selectedGroup, setSelectedGroup] = useState(); @@ -96,7 +102,6 @@ const GitManager: React.FC = () => { groups: false, projects: false, branches: false, - statistics: false, }); // 同步状态 @@ -107,14 +112,18 @@ const GitManager: React.FC = () => { branches: false, }); + // 搜索过滤 const [groupSearch, setGroupSearch] = useState(''); const [projectSearch, setProjectSearch] = useState(''); + const [branchSearch, setBranchSearch] = useState(''); + // 加载 Git 实例列表 const loadInstances = async () => { try { + const data = await getGitInstances(); if (data && Array.isArray(data)) { setInstances(data); @@ -124,6 +133,7 @@ const GitManager: React.FC = () => { } } catch (error) { toast({ + variant: 'destructive', title: '加载失败', description: '加载 Git 实例列表失败', @@ -131,6 +141,7 @@ const GitManager: React.FC = () => { } }; + // 构建树形结构 const buildTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { // 使用 repoGroupId 作为 key,因为 parentId 对应的是 Git 系统中的 repoGroupId @@ -226,6 +237,7 @@ const GitManager: React.FC = () => { setBranches(data || []); } catch (error) { toast({ + variant: 'destructive', title: '加载失败', description: '加载分支列表失败', @@ -236,38 +248,18 @@ const GitManager: React.FC = () => { } }; - // 加载统计信息 - const loadStatistics = async () => { - if (!selectedInstanceId) return; - - setLoading((prev) => ({ ...prev, statistics: true })); - try { - const data = await getGitStatistics(selectedInstanceId); - setStatistics(data); - } catch (error) { - // 静默失败 - } finally { - setLoading((prev) => ({ ...prev, statistics: false })); - } - }; - - // 同步所有数据 + // 同步所有数据(异步任务) const handleSyncAll = async () => { + if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, all: true })); try { await syncAllGitData(selectedInstanceId); toast({ - title: '同步成功', - description: '已开始同步所有数据', + title: '同步任务已启动', + description: '正在后台同步所有数据,请稍后手动刷新查看结果', }); - // 重新加载数据 - await Promise.all([ - loadGroupTree(), - loadProjects(selectedGroup?.repoGroupId || selectedGroup?.id), - loadStatistics(), - ]); } catch (error) { toast({ variant: 'destructive', @@ -279,31 +271,32 @@ const GitManager: React.FC = () => { } }; - // 同步仓库组 + // 同步仓库组(异步任务) const handleSyncGroups = async () => { + if (!selectedInstanceId) return; setSyncing((prev) => ({ ...prev, groups: true })); try { await syncRepositoryGroups(selectedInstanceId); toast({ - title: '同步成功', - description: '已开始同步仓库组', + title: '同步任务已启动', + description: '正在后台同步仓库组,请稍后手动刷新查看结果', }); - await loadGroupTree(); } catch (error) { toast({ variant: 'destructive', title: '同步失败', - description: '仓库组同步失败', + description: '仓库组同步任务启动失败', }); } finally { setSyncing((prev) => ({ ...prev, groups: false })); } }; - // 同步项目 + // 同步项目(异步任务) const handleSyncProjects = async () => { + if (!selectedInstanceId || !selectedGroup) return; setSyncing((prev) => ({ ...prev, projects: true })); @@ -314,43 +307,47 @@ const GitManager: React.FC = () => { selectedGroup.repoGroupId ); toast({ - title: '同步成功', - description: `已开始同步仓库组"${selectedGroup.name}"的项目`, + title: '同步任务已启动', + description: `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果`, }); - await loadProjects(selectedGroup.repoGroupId || selectedGroup.id); } catch (error) { toast({ variant: 'destructive', title: '同步失败', - description: '项目同步失败', + description: '项目同步任务启动失败', }); } finally { setSyncing((prev) => ({ ...prev, projects: false })); } }; - // 同步分支 + // 同步分支(异步任务) const handleSyncBranches = async () => { - if (!selectedInstanceId || !selectedProject || !selectedGroup) return; + + if (!selectedInstanceId || !selectedGroup) return; setSyncing((prev) => ({ ...prev, branches: true })); try { - // 传递 repoProjectId, repoGroupId (Git系统中的ID) 和 externalSystemId + // 如果选择了项目,只同步该项目的分支;否则同步该组下所有项目的分支 await syncRepositoryBranches( selectedInstanceId, - selectedProject.repoProjectId, + selectedProject?.repoProjectId, selectedGroup.repoGroupId ); + + const description = selectedProject + ? `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果` + : `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`; + toast({ - title: '同步成功', - description: `已开始同步项目"${selectedProject.name}"的分支`, + title: '同步任务已启动', + description, }); - await loadBranches(selectedProject.repoProjectId || selectedProject.id); } catch (error) { toast({ variant: 'destructive', title: '同步失败', - description: '分支同步失败', + description: '分支同步任务启动失败', }); } finally { setSyncing((prev) => ({ ...prev, branches: false })); @@ -370,6 +367,7 @@ const GitManager: React.FC = () => { }); }; + // 选中仓库组 const handleSelectGroup = (group: RepositoryGroupResponse) => { setSelectedGroup(group); @@ -386,6 +384,7 @@ const GitManager: React.FC = () => { // 格式化时间 const formatTime = (time?: string) => { + if (!time) return '-'; const diff = dayjs().diff(dayjs(time), 'day'); if (diff > 7) { @@ -394,6 +393,7 @@ const GitManager: React.FC = () => { return dayjs(time).fromNow(); }; + // 渲染可见性图标(带 Tooltip) const renderVisibilityIcon = (visibility: string) => { const config = @@ -421,6 +421,7 @@ const GitManager: React.FC = () => { return groups.filter((group) => { const matchesSearch = group.name.toLowerCase().includes(groupSearch.toLowerCase()) || + (group.path && group.path.toLowerCase().includes(groupSearch.toLowerCase())); const filteredChildren = group.children @@ -511,10 +512,12 @@ const GitManager: React.FC = () => { // 过滤项目列表 const filteredProjects = useMemo(() => { + if (!projectSearch) return projects; return projects.filter( (project) => project.name.toLowerCase().includes(projectSearch.toLowerCase()) || + (project.path && project.path.toLowerCase().includes(projectSearch.toLowerCase())) ); }, [projects, projectSearch]); @@ -533,9 +536,9 @@ const GitManager: React.FC = () => { }, []); useEffect(() => { + if (selectedInstanceId) { loadGroupTree(); - loadStatistics(); setSelectedGroup(undefined); setSelectedProject(undefined); setProjects([]); @@ -546,25 +549,31 @@ const GitManager: React.FC = () => { const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, groupSearch]); return ( + -
+
{/* 顶部标题栏 */}
+

Git 仓库管理

+

管理和浏览 Git 仓库、项目和分支

+
- {/* 统计卡片 */} - {statistics && ( -
- - - 仓库组 - - - -
{statistics.totalGroups}
-
-
- - - 项目 - - - -
{statistics.totalProjects}
-
-
- - - 分支 - - - -
{statistics.totalBranches}
-
-
-
- )} - {/* 三栏布局 */}
{/* 左栏:仓库组树 */} @@ -630,6 +607,7 @@ const GitManager: React.FC = () => { -
+
+
setGroupSearch(e.target.value)} + className="pl-8 h-8" />
+
@@ -664,9 +646,11 @@ const GitManager: React.FC = () => { filteredGroupTree.map((group) => renderGroupTreeNode(group)) )}
+
+ {/* 中栏:项目列表 */} @@ -676,6 +660,7 @@ const GitManager: React.FC = () => {
+
setProjectSearch(e.target.value)} + className="pl-8 h-8" />
+
{loading.projects ? (
+ ) : !selectedGroup ? (

请先选择仓库组

) : filteredProjects.length === 0 ? ( +

暂无项目

) : ( +
{filteredProjects.map((project) => ( +
{ {project.name}
+ {project.description && (

{project.description} @@ -742,23 +736,27 @@ const GitManager: React.FC = () => { {project.isDefaultBranch} + )} {project.lastActivityAt && ( {formatTime(project.lastActivityAt)} + )}

{project.webUrl && ( + e.stopPropagation()} className="flex-shrink-0" > @@ -792,7 +790,7 @@ const GitManager: React.FC = () => { size="icon" className="h-7 w-7" onClick={handleSyncBranches} - disabled={syncing.branches || !selectedProject} + disabled={syncing.branches || !selectedGroup} > { +

在 GitLab 中查看

@@ -912,8 +911,10 @@ const GitManager: React.FC = () => { +
); }; export default GitManager; + diff --git a/frontend/src/pages/Deploy/GitManager/List/service.ts b/frontend/src/pages/Deploy/GitManager/List/service.ts index 9a6ad29c..c604a06e 100644 --- a/frontend/src/pages/Deploy/GitManager/List/service.ts +++ b/frontend/src/pages/Deploy/GitManager/List/service.ts @@ -6,7 +6,6 @@ import type { RepositoryProjectQuery, RepositoryBranchResponse, RepositoryBranchQuery, - GitStatistics, } from './types'; import { getExternalSystems } from '@/pages/Deploy/External/service'; import { SystemType } from '@/pages/Deploy/External/types'; @@ -61,6 +60,8 @@ export const getProjectsByGroupId = (repoGroupId: number) => /** * 同步仓库项目数据 + * @param externalSystemId 外部系统ID(必填) + * @param repoGroupId 仓库组ID(可选)不传=全量同步,传值=只同步该组 */ export const syncRepositoryProjects = (externalSystemId: number, repoGroupId?: number) => request.post(`${PROJECT_URL}/sync`, null, { @@ -85,6 +86,9 @@ export const getBranchesByProjectId = (repoProjectId: number) => /** * 同步仓库分支数据 + * @param externalSystemId 外部系统ID(必填) + * @param repoProjectId 项目ID(可选)不传=按repoGroupId规则,传值=只同步该项目(传此参数时repoGroupId必传) + * @param repoGroupId 仓库组ID(可选)不传=全量同步,传值=同步该组所有项目的分支 */ export const syncRepositoryBranches = ( externalSystemId: number, @@ -95,16 +99,6 @@ export const syncRepositoryBranches = ( params: { externalSystemId, repoProjectId, repoGroupId }, }); -// ==================== 统计信息 ==================== - -/** - * 获取Git仓库统计信息 - */ -export const getGitStatistics = (externalSystemId: number) => - request.get(`${GROUP_URL}/statistics`, { - params: { externalSystemId }, - }); - // ==================== 批量操作 ==================== /** diff --git a/frontend/src/pages/Deploy/GitManager/List/types.ts b/frontend/src/pages/Deploy/GitManager/List/types.ts index 5785889c..519fde01 100644 --- a/frontend/src/pages/Deploy/GitManager/List/types.ts +++ b/frontend/src/pages/Deploy/GitManager/List/types.ts @@ -136,16 +136,3 @@ export interface RepositoryBranchQuery { isDefaultBranch?: boolean; } -/** - * 统计信息 - */ -export interface GitStatistics { - /** 总仓库组数 */ - totalGroups: number; - /** 总项目数 */ - totalProjects: number; - /** 总分支数 */ - totalBranches: number; - /** 最后同步时间 */ - lastSyncTime?: string; -} diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/components/CategoryManageDialog.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/components/CategoryManageDialog.tsx new file mode 100644 index 00000000..ee24bd79 --- /dev/null +++ b/frontend/src/pages/Deploy/ScheduleJob/List/components/CategoryManageDialog.tsx @@ -0,0 +1,561 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { useToast } from '@/components/ui/use-toast'; +import { useForm } from 'react-hook-form'; +import { DataTablePagination } from '@/components/ui/pagination'; +import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; +import { + Plus, + Edit, + Trash2, + Search, + Loader2, + FolderKanban, + CheckCircle2, + XCircle, +} from 'lucide-react'; +import DynamicIcon from '@/components/DynamicIcon'; +import LucideIconSelect from '@/components/LucideIconSelect'; +import { + getJobCategories, + createJobCategory, + updateJobCategory, + deleteJobCategory +} from '../service'; +import type { + JobCategoryResponse, + JobCategoryRequest, + JobCategoryQuery +} from '../types'; +import type { Page } from '@/types/base'; + +interface CategoryManageDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess?: () => void; +} + +const CategoryManageDialog: React.FC = ({ + open, + onOpenChange, + onSuccess +}) => { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [searchText, setSearchText] = useState(''); + const [editMode, setEditMode] = useState(false); + const [editRecord, setEditRecord] = useState(null); + const [iconSelectOpen, setIconSelectOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteRecord, setDeleteRecord] = useState(null); + + // 分页状态 + const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1); + const [pageSize] = useState(DEFAULT_PAGE_SIZE); + + const form = useForm({ + defaultValues: { + name: '', + code: '', + description: '', + icon: '', + color: '', + sort: 0, + enabled: true, + } + }); + + // 加载分类列表 + const loadCategories = async () => { + setLoading(true); + try { + const query: JobCategoryQuery = { + pageNum, + pageSize, + }; + if (searchText) { + query.name = searchText; + } + const result = await getJobCategories(query); + setData(result); + } catch (error) { + console.error('加载分类失败:', error); + toast({ + variant: 'destructive', + title: '加载失败', + description: '加载分类列表失败', + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (open) { + loadCategories(); + } + }, [open, searchText, pageNum, pageSize]); + + // 搜索时重置页码 + useEffect(() => { + if (searchText !== '') { + setPageNum(0); + } + }, [searchText]); + + // 新建 + const handleCreate = () => { + setEditRecord(null); + form.reset({ + name: '', + code: '', + description: '', + icon: '', + color: '', + sort: 0, + enabled: true, + }); + setEditMode(true); + }; + + // 编辑 + const handleEdit = (record: JobCategoryResponse) => { + setEditRecord(record); + form.reset({ + name: record.name, + code: record.code, + description: record.description || '', + icon: record.icon || '', + color: record.color || '', + sort: record.sort, + enabled: record.enabled, + }); + setEditMode(true); + }; + + // 打开删除确认对话框 + const handleDeleteClick = (record: JobCategoryResponse) => { + setDeleteRecord(record); + setDeleteDialogOpen(true); + }; + + // 确认删除 + const confirmDelete = async () => { + if (!deleteRecord) return; + + try { + await deleteJobCategory(deleteRecord.id); + toast({ + title: '删除成功', + description: `分类 "${deleteRecord.name}" 已删除`, + }); + loadCategories(); + onSuccess?.(); + setDeleteDialogOpen(false); + setDeleteRecord(null); + } catch (error) { + console.error('删除失败:', error); + toast({ + variant: 'destructive', + title: '删除失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 保存 + const handleSave = async (values: JobCategoryRequest) => { + try { + if (editRecord) { + await updateJobCategory(editRecord.id, values); + toast({ + title: '更新成功', + description: `分类 "${values.name}" 已更新`, + }); + } else { + await createJobCategory(values); + toast({ + title: '创建成功', + description: `分类 "${values.name}" 已创建`, + }); + } + setEditMode(false); + loadCategories(); + onSuccess?.(); + } catch (error) { + console.error('保存失败:', error); + toast({ + variant: 'destructive', + title: '保存失败', + description: error instanceof Error ? error.message : '未知错误', + }); + } + }; + + // 取消编辑 + const handleCancel = () => { + setEditMode(false); + setEditRecord(null); + form.reset(); + }; + + return ( + <> + + + + + + 分类管理 + + + + {!editMode ? ( +
+ {/* 搜索和新建 */} +
+
+ + setSearchText(e.target.value)} + className="pl-10" + /> +
+ +
+ + {/* 分类列表 */} +
+ + + + 分类名称 + 分类代码 + 图标 + 颜色 + 排序 + 状态 + 描述 + 操作 + + + + {loading ? ( + + + + + + ) : data && data.content.length > 0 ? ( + data.content.map((record) => ( + + {record.name} + + {record.code} + + + {record.icon ? ( + + ) : ( + - + )} + + + {record.color ? ( +
+
+ {record.color} +
+ ) : ( + - + )} + + {record.sort} + + {record.enabled ? ( + + + 启用 + + ) : ( + + + 禁用 + + )} + + + {record.description || '-'} + + +
+ + +
+
+ + )) + ) : ( + + + 暂无数据 + + + )} + +
+
+ + {/* 分页 */} + {data && data.content.length > 0 && ( + + )} +
+ ) : ( +
+ +
+ ( + + 分类名称 + + + + + + )} + /> + + ( + + 分类代码 + + + + + + )} + /> +
+ +
+ ( + + 图标 + +
+ + +
+
+ +
+ )} + /> + + ( + + 颜色 + +
+ + field.onChange(e.target.value)} + className="w-10 h-10 rounded border cursor-pointer" + /> +
+
+ +
+ )} + /> +
+ +
+ ( + + 排序号 + + field.onChange(Number(e.target.value))} + /> + + + + )} + /> + + ( + +
+ 是否启用 +
+ + + +
+ )} + /> +
+ + ( + + 描述 + +