增加团队管理页面
This commit is contained in:
parent
d5c4bb0c1f
commit
e087b6abd8
@ -42,6 +42,7 @@
|
||||
"@react-form-builder/designer": "^7.4.0",
|
||||
"@react-form-builder/designer-bundle": "^7.4.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@types/recharts": "^1.8.29",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@xyflow/react": "^12.8.6",
|
||||
|
||||
@ -104,6 +104,9 @@ importers:
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^2.0.1
|
||||
version: 2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.12
|
||||
version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@types/recharts':
|
||||
specifier: ^1.8.29
|
||||
version: 1.8.29
|
||||
@ -1950,6 +1953,15 @@ packages:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@tanstack/react-virtual@3.13.12':
|
||||
resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@tanstack/virtual-core@3.13.12':
|
||||
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
@ -6167,6 +6179,14 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.12
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@tanstack/virtual-core@3.13.12': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.3
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
@ -43,6 +43,7 @@ import {
|
||||
Calendar,
|
||||
User,
|
||||
GitCommit,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import type {
|
||||
RepositoryGroupResponse,
|
||||
@ -116,34 +117,39 @@ const GitManager: React.FC = () => {
|
||||
// 搜索过滤
|
||||
const [groupSearch, setGroupSearch] = useState('');
|
||||
const [projectSearch, setProjectSearch] = useState('');
|
||||
|
||||
const [branchSearch, setBranchSearch] = useState('');
|
||||
|
||||
// 防抖搜索值 - 优化大量数据过滤性能
|
||||
const [debouncedGroupSearch, setDebouncedGroupSearch] = useState('');
|
||||
const [debouncedProjectSearch, setDebouncedProjectSearch] = useState('');
|
||||
const [debouncedBranchSearch, setDebouncedBranchSearch] = useState('');
|
||||
|
||||
// 加载 Git 实例列表
|
||||
const loadInstances = async () => {
|
||||
try {
|
||||
// 仓库组搜索防抖
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedGroupSearch(groupSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [groupSearch]);
|
||||
|
||||
const data = await getGitInstances();
|
||||
if (data && Array.isArray(data)) {
|
||||
setInstances(data);
|
||||
if (data.length > 0 && !selectedInstanceId) {
|
||||
setSelectedInstanceId(data[0].id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '加载 Git 实例列表失败',
|
||||
});
|
||||
}
|
||||
};
|
||||
// 项目搜索防抖
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedProjectSearch(projectSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [projectSearch]);
|
||||
|
||||
// 分支搜索防抖
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedBranchSearch(branchSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [branchSearch]);
|
||||
|
||||
// 构建树形结构
|
||||
const buildTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => {
|
||||
const buildTree = useCallback((groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => {
|
||||
// 使用 repoGroupId 作为 key,因为 parentId 对应的是 Git 系统中的 repoGroupId
|
||||
const map = new Map<number, RepositoryGroupResponse>();
|
||||
const roots: RepositoryGroupResponse[] = [];
|
||||
@ -178,10 +184,10 @@ const GitManager: React.FC = () => {
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 加载仓库组列表并构建树
|
||||
const loadGroupTree = async () => {
|
||||
const loadGroupTree = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
|
||||
setLoading((prev) => ({ ...prev, groups: true }));
|
||||
@ -202,10 +208,10 @@ const GitManager: React.FC = () => {
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, groups: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, buildTree, toast]);
|
||||
|
||||
// 加载项目列表
|
||||
const loadProjects = async (repoGroupId?: number) => {
|
||||
// 加载项目列表(支持不传repoGroupId加载所有项目)
|
||||
const loadProjects = useCallback(async (repoGroupId?: number) => {
|
||||
if (!selectedInstanceId) return;
|
||||
|
||||
setLoading((prev) => ({ ...prev, projects: true }));
|
||||
@ -225,10 +231,10 @@ const GitManager: React.FC = () => {
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, projects: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
// 加载分支列表
|
||||
const loadBranches = async (repoProjectId: number) => {
|
||||
// 加载分支列表(支持不传repoProjectId加载该组所有分支)
|
||||
const loadBranches = useCallback(async (repoProjectId?: number) => {
|
||||
setLoading((prev) => ({ ...prev, branches: true }));
|
||||
try {
|
||||
const data = await getRepositoryBranches({
|
||||
@ -237,7 +243,6 @@ const GitManager: React.FC = () => {
|
||||
setBranches(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '加载分支列表失败',
|
||||
@ -246,11 +251,10 @@ const GitManager: React.FC = () => {
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, branches: false }));
|
||||
}
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
// 同步所有数据(异步任务)
|
||||
const handleSyncAll = async () => {
|
||||
|
||||
const handleSyncAll = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
|
||||
setSyncing((prev) => ({ ...prev, all: true }));
|
||||
@ -269,11 +273,10 @@ const GitManager: React.FC = () => {
|
||||
} finally {
|
||||
setSyncing((prev) => ({ ...prev, all: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
// 同步仓库组(异步任务)
|
||||
const handleSyncGroups = async () => {
|
||||
|
||||
const handleSyncGroups = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
|
||||
setSyncing((prev) => ({ ...prev, groups: true }));
|
||||
@ -292,23 +295,27 @@ const GitManager: React.FC = () => {
|
||||
} finally {
|
||||
setSyncing((prev) => ({ ...prev, groups: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
// 同步项目(异步任务)
|
||||
const handleSyncProjects = async () => {
|
||||
|
||||
if (!selectedInstanceId || !selectedGroup) return;
|
||||
// 同步项目(异步任务,支持未选择组时同步所有项目)
|
||||
const handleSyncProjects = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
|
||||
setSyncing((prev) => ({ ...prev, projects: true }));
|
||||
try {
|
||||
// 传递 repoGroupId (Git系统中的ID) 和 externalSystemId
|
||||
await syncRepositoryProjects(
|
||||
selectedInstanceId,
|
||||
selectedGroup.repoGroupId
|
||||
selectedGroup?.repoGroupId
|
||||
);
|
||||
|
||||
const description = selectedGroup
|
||||
? `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果`
|
||||
: '正在后台同步所有项目,请稍后手动刷新查看结果';
|
||||
|
||||
toast({
|
||||
title: '同步任务已启动',
|
||||
description: `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果`,
|
||||
description,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
@ -319,12 +326,11 @@ const GitManager: React.FC = () => {
|
||||
} finally {
|
||||
setSyncing((prev) => ({ ...prev, projects: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, selectedGroup, toast]);
|
||||
|
||||
// 同步分支(异步任务)
|
||||
const handleSyncBranches = async () => {
|
||||
|
||||
if (!selectedInstanceId || !selectedGroup) return;
|
||||
const handleSyncBranches = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
|
||||
setSyncing((prev) => ({ ...prev, branches: true }));
|
||||
try {
|
||||
@ -332,12 +338,15 @@ const GitManager: React.FC = () => {
|
||||
await syncRepositoryBranches(
|
||||
selectedInstanceId,
|
||||
selectedProject?.repoProjectId,
|
||||
selectedGroup.repoGroupId
|
||||
selectedGroup?.repoGroupId
|
||||
);
|
||||
|
||||
const description = selectedProject
|
||||
? `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果`
|
||||
: `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`;
|
||||
let description = '正在后台同步所有分支,请稍后手动刷新查看结果';
|
||||
if (selectedProject) {
|
||||
description = `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果`;
|
||||
} else if (selectedGroup) {
|
||||
description = `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '同步任务已启动',
|
||||
@ -352,10 +361,10 @@ const GitManager: React.FC = () => {
|
||||
} finally {
|
||||
setSyncing((prev) => ({ ...prev, branches: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, selectedGroup, selectedProject, toast]);
|
||||
|
||||
// 切换仓库组展开/折叠
|
||||
const toggleGroupExpand = (groupId: number) => {
|
||||
const toggleGroupExpand = useCallback((groupId: number) => {
|
||||
setExpandedGroupIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(groupId)) {
|
||||
@ -365,37 +374,44 @@ const GitManager: React.FC = () => {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
// 选中仓库组
|
||||
const handleSelectGroup = (group: RepositoryGroupResponse) => {
|
||||
setSelectedGroup(group);
|
||||
// 选中仓库组(支持反选取消)
|
||||
const handleSelectGroup = useCallback((group: RepositoryGroupResponse) => {
|
||||
// 如果点击的是已选中的组,则取消选择
|
||||
setSelectedGroup((prev) => {
|
||||
const newGroup = prev?.id === group.id ? undefined : group;
|
||||
setSelectedProject(undefined);
|
||||
setBranches([]);
|
||||
loadProjects(group.repoGroupId || group.id);
|
||||
};
|
||||
// 加载项目列表,如果取消选择则不传 repoGroupId(加载所有项目)
|
||||
loadProjects(newGroup?.repoGroupId || newGroup?.id);
|
||||
return newGroup;
|
||||
});
|
||||
}, [loadProjects]);
|
||||
|
||||
// 选中项目
|
||||
const handleSelectProject = (project: RepositoryProjectResponse) => {
|
||||
setSelectedProject(project);
|
||||
loadBranches(project.repoProjectId || project.id);
|
||||
};
|
||||
// 选中项目(支持反选取消)
|
||||
const handleSelectProject = useCallback((project: RepositoryProjectResponse) => {
|
||||
// 如果点击的是已选中的项目,则取消选择
|
||||
setSelectedProject((prev) => {
|
||||
const newProject = prev?.id === project.id ? undefined : project;
|
||||
// 加载分支列表,如果取消选择则不传 repoProjectId(加载所有分支)
|
||||
loadBranches(newProject?.repoProjectId || newProject?.id);
|
||||
return newProject;
|
||||
});
|
||||
}, [loadBranches]);
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
|
||||
const formatTime = useCallback((time?: string) => {
|
||||
if (!time) return '-';
|
||||
const diff = dayjs().diff(dayjs(time), 'day');
|
||||
if (diff > 7) {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm');
|
||||
}
|
||||
return dayjs(time).fromNow();
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
// 渲染可见性图标(带 Tooltip)
|
||||
const renderVisibilityIcon = (visibility: string) => {
|
||||
const renderVisibilityIcon = useCallback((visibility: string) => {
|
||||
const config =
|
||||
VISIBILITY_CONFIG[visibility.toLowerCase() as keyof typeof VISIBILITY_CONFIG] ||
|
||||
VISIBILITY_CONFIG.public;
|
||||
@ -412,17 +428,16 @@ const GitManager: React.FC = () => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 过滤仓库组树
|
||||
const filterGroupTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => {
|
||||
if (!groupSearch) return groups;
|
||||
// 过滤仓库组树(使用防抖搜索值)
|
||||
const filterGroupTree = useCallback((groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => {
|
||||
if (!debouncedGroupSearch) return groups;
|
||||
|
||||
return groups.filter((group) => {
|
||||
const matchesSearch =
|
||||
group.name.toLowerCase().includes(groupSearch.toLowerCase()) ||
|
||||
|
||||
(group.path && group.path.toLowerCase().includes(groupSearch.toLowerCase()));
|
||||
group.name.toLowerCase().includes(debouncedGroupSearch.toLowerCase()) ||
|
||||
(group.path && group.path.toLowerCase().includes(debouncedGroupSearch.toLowerCase()));
|
||||
|
||||
const filteredChildren = group.children
|
||||
? filterGroupTree(group.children)
|
||||
@ -433,7 +448,7 @@ const GitManager: React.FC = () => {
|
||||
...group,
|
||||
children: group.children ? filterGroupTree(group.children) : [],
|
||||
}));
|
||||
};
|
||||
}, [debouncedGroupSearch]);
|
||||
|
||||
// 渲染仓库组树节点
|
||||
const renderGroupTreeNode = (
|
||||
@ -516,33 +531,47 @@ const GitManager: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 过滤项目列表
|
||||
// 过滤项目列表(使用防抖搜索值)
|
||||
const filteredProjects = useMemo(() => {
|
||||
|
||||
if (!projectSearch) return projects;
|
||||
if (!debouncedProjectSearch) return projects;
|
||||
return projects.filter(
|
||||
(project) =>
|
||||
project.name.toLowerCase().includes(projectSearch.toLowerCase()) ||
|
||||
|
||||
(project.path && project.path.toLowerCase().includes(projectSearch.toLowerCase()))
|
||||
project.name.toLowerCase().includes(debouncedProjectSearch.toLowerCase()) ||
|
||||
(project.path && project.path.toLowerCase().includes(debouncedProjectSearch.toLowerCase()))
|
||||
);
|
||||
}, [projects, projectSearch]);
|
||||
}, [projects, debouncedProjectSearch]);
|
||||
|
||||
// 过滤分支列表
|
||||
// 过滤分支列表(使用防抖搜索值)
|
||||
const filteredBranches = useMemo(() => {
|
||||
if (!branchSearch) return branches;
|
||||
if (!debouncedBranchSearch) return branches;
|
||||
return branches.filter((branch) =>
|
||||
branch.name.toLowerCase().includes(branchSearch.toLowerCase())
|
||||
branch.name.toLowerCase().includes(debouncedBranchSearch.toLowerCase())
|
||||
);
|
||||
}, [branches, branchSearch]);
|
||||
}, [branches, debouncedBranchSearch]);
|
||||
|
||||
// 初始化加载
|
||||
// 加载 Git 实例列表
|
||||
useEffect(() => {
|
||||
loadInstances();
|
||||
}, []);
|
||||
const loadInstancesEffect = async () => {
|
||||
try {
|
||||
const data = await getGitInstances();
|
||||
if (data && Array.isArray(data)) {
|
||||
setInstances(data);
|
||||
if (data.length > 0 && !selectedInstanceId) {
|
||||
setSelectedInstanceId(data[0].id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '加载 Git 实例列表失败',
|
||||
});
|
||||
}
|
||||
};
|
||||
loadInstancesEffect();
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (selectedInstanceId) {
|
||||
loadGroupTree();
|
||||
setSelectedGroup(undefined);
|
||||
@ -550,9 +579,9 @@ const GitManager: React.FC = () => {
|
||||
setProjects([]);
|
||||
setBranches([]);
|
||||
}
|
||||
}, [selectedInstanceId]);
|
||||
}, [selectedInstanceId, loadGroupTree]);
|
||||
|
||||
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, groupSearch]);
|
||||
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, filterGroupTree]);
|
||||
|
||||
return (
|
||||
|
||||
@ -610,19 +639,31 @@ const GitManager: React.FC = () => {
|
||||
<CardHeader className="border-b flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">仓库组</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={loadGroupTree}
|
||||
disabled={loading.groups || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading.groups ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
className="h-7 w-7"
|
||||
onClick={handleSyncGroups}
|
||||
disabled={syncing.groups || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${syncing.groups ? 'animate-spin' : ''}`}
|
||||
<Download
|
||||
className={`h-4 w-4 ${syncing.groups ? 'animate-bounce' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-2">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
@ -662,30 +703,40 @@ const GitManager: React.FC = () => {
|
||||
<CardHeader className="border-b flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">
|
||||
项目 {selectedGroup && `(${filteredProjects.length})`}
|
||||
项目 ({filteredProjects.length}){selectedGroup && ` - ${selectedGroup.name}`}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => loadProjects(selectedGroup?.repoGroupId || selectedGroup?.id)}
|
||||
disabled={loading.projects || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading.projects ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={handleSyncProjects}
|
||||
disabled={syncing.projects || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${syncing.projects ? 'animate-spin' : ''}`}
|
||||
<Download
|
||||
className={`h-4 w-4 ${syncing.projects ? 'animate-bounce' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-2">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
|
||||
placeholder="搜索项目..."
|
||||
value={projectSearch}
|
||||
onChange={(e) => setProjectSearch(e.target.value)}
|
||||
|
||||
className="pl-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
@ -696,17 +747,10 @@ const GitManager: React.FC = () => {
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
) : !selectedGroup ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<Code2 className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">请先选择仓库组</p>
|
||||
</div>
|
||||
) : filteredProjects.length === 0 ? (
|
||||
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<Code2 className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">暂无项目</p>
|
||||
<p className="text-sm">{selectedGroup ? '当前仓库组暂无项目' : '暂无项目'}</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -796,20 +840,33 @@ const GitManager: React.FC = () => {
|
||||
<CardHeader className="border-b flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">
|
||||
分支 {selectedProject && `(${filteredBranches.length})`}
|
||||
分支 ({filteredBranches.length}){selectedProject && ` - ${selectedProject.name}`}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => loadBranches(selectedProject?.repoProjectId || selectedProject?.id)}
|
||||
disabled={loading.branches || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading.branches ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={handleSyncBranches}
|
||||
disabled={syncing.branches || !selectedGroup}
|
||||
disabled={syncing.branches || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`}
|
||||
<Download
|
||||
className={`h-4 w-4 ${syncing.branches ? 'animate-bounce' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
@ -825,15 +882,10 @@ const GitManager: React.FC = () => {
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : !selectedProject ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">请先选择项目</p>
|
||||
</div>
|
||||
) : filteredBranches.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">暂无分支</p>
|
||||
<p className="text-sm">{selectedProject ? '当前项目暂无分支' : '暂无分支'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
@ -37,6 +37,7 @@ import {
|
||||
AlertTriangle,
|
||||
Calendar,
|
||||
Activity,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import type {
|
||||
JenkinsInstance,
|
||||
@ -53,6 +54,7 @@ import {
|
||||
syncJenkinsJobs,
|
||||
syncJenkinsBuilds,
|
||||
} from './service';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
@ -109,6 +111,95 @@ const JenkinsManager: React.FC = () => {
|
||||
const [jobSearch, setJobSearch] = useState('');
|
||||
const [buildSearch, setBuildSearch] = useState('');
|
||||
|
||||
// 防抖搜索值 - 优化大量数据过滤性能
|
||||
const [debouncedViewSearch, setDebouncedViewSearch] = useState('');
|
||||
const [debouncedJobSearch, setDebouncedJobSearch] = useState('');
|
||||
const [debouncedBuildSearch, setDebouncedBuildSearch] = useState('');
|
||||
|
||||
// 视图搜索防抖
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedViewSearch(viewSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [viewSearch]);
|
||||
|
||||
// 任务搜索防抖
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedJobSearch(jobSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [jobSearch]);
|
||||
|
||||
// 构建搜索防抖(特别重要,因为有5000+条数据)
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedBuildSearch(buildSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [buildSearch]);
|
||||
|
||||
// 加载视图列表
|
||||
const loadViews = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
setLoading((prev) => ({ ...prev, views: true }));
|
||||
try {
|
||||
const data = await getJenkinsViews(selectedInstanceId);
|
||||
setViews(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '获取视图列表失败',
|
||||
});
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, views: false }));
|
||||
}
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
// 加载任务列表
|
||||
const loadJobs = useCallback(async (viewId?: number) => {
|
||||
if (!selectedInstanceId) return;
|
||||
setLoading((prev) => ({ ...prev, jobs: true }));
|
||||
try {
|
||||
const data = await getJenkinsJobs({
|
||||
externalSystemId: selectedInstanceId,
|
||||
viewId,
|
||||
});
|
||||
setJobs(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '获取任务列表失败',
|
||||
});
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, jobs: false }));
|
||||
}
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
// 加载构建列表
|
||||
const loadBuilds = useCallback(async (jobId?: number) => {
|
||||
if (!selectedInstanceId) return;
|
||||
setLoading((prev) => ({ ...prev, builds: true }));
|
||||
try {
|
||||
const data = await getJenkinsBuilds({
|
||||
externalSystemId: selectedInstanceId,
|
||||
jobId,
|
||||
});
|
||||
setBuilds(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '获取构建列表失败',
|
||||
});
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, builds: false }));
|
||||
}
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
// 加载 Jenkins 实例列表
|
||||
useEffect(() => {
|
||||
const loadInstances = async () => {
|
||||
@ -127,97 +218,10 @@ const JenkinsManager: React.FC = () => {
|
||||
}
|
||||
};
|
||||
loadInstances();
|
||||
}, []);
|
||||
|
||||
// 加载视图列表
|
||||
const loadViews = async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
setLoading((prev) => ({ ...prev, views: true }));
|
||||
try {
|
||||
const data = await getJenkinsViews(selectedInstanceId);
|
||||
setViews(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '获取视图列表失败',
|
||||
});
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, views: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 加载任务列表
|
||||
const loadJobs = async (viewId?: number) => {
|
||||
if (!selectedInstanceId) return;
|
||||
setLoading((prev) => ({ ...prev, jobs: true }));
|
||||
try {
|
||||
const data = await getJenkinsJobs({
|
||||
externalSystemId: selectedInstanceId,
|
||||
viewId,
|
||||
});
|
||||
setJobs(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '获取任务列表失败',
|
||||
});
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, jobs: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 加载构建列表
|
||||
const loadBuilds = async (jobId?: number) => {
|
||||
if (!selectedInstanceId) return;
|
||||
setLoading((prev) => ({ ...prev, builds: true }));
|
||||
try {
|
||||
const data = await getJenkinsBuilds({
|
||||
externalSystemId: selectedInstanceId,
|
||||
jobId,
|
||||
});
|
||||
setBuilds(data || []);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: '获取构建列表失败',
|
||||
});
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, builds: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 监听实例变化
|
||||
useEffect(() => {
|
||||
if (selectedInstanceId) {
|
||||
setSelectedView(undefined);
|
||||
setSelectedJob(undefined);
|
||||
setJobs([]);
|
||||
setBuilds([]);
|
||||
loadViews();
|
||||
}
|
||||
}, [selectedInstanceId]);
|
||||
|
||||
// 监听视图选择变化
|
||||
useEffect(() => {
|
||||
if (selectedView) {
|
||||
setSelectedJob(undefined);
|
||||
setBuilds([]);
|
||||
loadJobs(selectedView.id);
|
||||
}
|
||||
}, [selectedView]);
|
||||
|
||||
// 监听任务选择变化
|
||||
useEffect(() => {
|
||||
if (selectedJob) {
|
||||
loadBuilds(selectedJob.id);
|
||||
}
|
||||
}, [selectedJob]);
|
||||
}, [selectedInstanceId, toast]);
|
||||
|
||||
// 同步视图
|
||||
const handleSyncViews = async () => {
|
||||
const handleSyncViews = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
setSyncing((prev) => ({ ...prev, views: true }));
|
||||
try {
|
||||
@ -236,10 +240,10 @@ const JenkinsManager: React.FC = () => {
|
||||
} finally {
|
||||
setSyncing((prev) => ({ ...prev, views: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, toast, loadViews]);
|
||||
|
||||
// 同步任务
|
||||
const handleSyncJobs = async () => {
|
||||
const handleSyncJobs = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
setSyncing((prev) => ({ ...prev, jobs: true }));
|
||||
try {
|
||||
@ -249,12 +253,11 @@ const JenkinsManager: React.FC = () => {
|
||||
});
|
||||
toast({
|
||||
title: '同步成功',
|
||||
description: '任务同步任务已启动',
|
||||
description: selectedView ? '视图任务同步任务已启动' : '所有任务同步任务已启动',
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (selectedView) {
|
||||
loadJobs(selectedView.id);
|
||||
}
|
||||
// 无论是否选择视图,都重新加载任务列表
|
||||
loadJobs(selectedView?.id);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
toast({
|
||||
@ -265,10 +268,10 @@ const JenkinsManager: React.FC = () => {
|
||||
} finally {
|
||||
setSyncing((prev) => ({ ...prev, jobs: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, selectedView, toast, loadJobs]);
|
||||
|
||||
// 同步构建
|
||||
const handleSyncBuilds = async () => {
|
||||
const handleSyncBuilds = useCallback(async () => {
|
||||
if (!selectedInstanceId) return;
|
||||
setSyncing((prev) => ({ ...prev, builds: true }));
|
||||
try {
|
||||
@ -278,12 +281,11 @@ const JenkinsManager: React.FC = () => {
|
||||
});
|
||||
toast({
|
||||
title: '同步成功',
|
||||
description: '构建同步任务已启动',
|
||||
description: selectedJob ? '任务构建同步任务已启动' : '所有构建同步任务已启动',
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (selectedJob) {
|
||||
loadBuilds(selectedJob.id);
|
||||
}
|
||||
// 无论是否选择任务,都重新加载构建列表
|
||||
loadBuilds(selectedJob?.id);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
toast({
|
||||
@ -294,34 +296,65 @@ const JenkinsManager: React.FC = () => {
|
||||
} finally {
|
||||
setSyncing((prev) => ({ ...prev, builds: false }));
|
||||
}
|
||||
};
|
||||
}, [selectedInstanceId, selectedJob, toast, loadBuilds]);
|
||||
|
||||
// 过滤视图
|
||||
// 监听实例变化
|
||||
useEffect(() => {
|
||||
if (selectedInstanceId) {
|
||||
setSelectedView(undefined);
|
||||
setSelectedJob(undefined);
|
||||
setJobs([]);
|
||||
setBuilds([]);
|
||||
loadViews();
|
||||
}
|
||||
}, [selectedInstanceId, loadViews]);
|
||||
|
||||
// 监听视图选择变化
|
||||
useEffect(() => {
|
||||
if (selectedInstanceId) {
|
||||
setSelectedJob(undefined);
|
||||
setBuilds([]);
|
||||
// 如果选择了视图,传递viewId;否则不传,加载所有任务
|
||||
loadJobs(selectedView?.id);
|
||||
}
|
||||
}, [selectedView, selectedInstanceId, loadJobs]);
|
||||
|
||||
// 监听任务选择变化
|
||||
useEffect(() => {
|
||||
if (selectedInstanceId) {
|
||||
// 如果选择了任务,传递jobId;否则不传,加载所有构建
|
||||
loadBuilds(selectedJob?.id);
|
||||
}
|
||||
}, [selectedJob, selectedInstanceId, loadBuilds]);
|
||||
|
||||
// 过滤视图(使用防抖搜索值)
|
||||
const filteredViews = useMemo(() => {
|
||||
if (!debouncedViewSearch) return views;
|
||||
return views.filter((view) =>
|
||||
view.viewName.toLowerCase().includes(viewSearch.toLowerCase())
|
||||
view.viewName.toLowerCase().includes(debouncedViewSearch.toLowerCase())
|
||||
);
|
||||
}, [views, viewSearch]);
|
||||
}, [views, debouncedViewSearch]);
|
||||
|
||||
// 过滤任务
|
||||
// 过滤任务(使用防抖搜索值)
|
||||
const filteredJobs = useMemo(() => {
|
||||
if (!debouncedJobSearch) return jobs;
|
||||
return jobs.filter((job) =>
|
||||
job.jobName.toLowerCase().includes(jobSearch.toLowerCase())
|
||||
job.jobName.toLowerCase().includes(debouncedJobSearch.toLowerCase())
|
||||
);
|
||||
}, [jobs, jobSearch]);
|
||||
}, [jobs, debouncedJobSearch]);
|
||||
|
||||
// 过滤构建
|
||||
// 过滤构建(使用防抖搜索值 - 特别重要,5000+条数据)
|
||||
const filteredBuilds = useMemo(() => {
|
||||
if (!buildSearch) return builds;
|
||||
const searchLower = buildSearch.toLowerCase();
|
||||
if (!debouncedBuildSearch) return builds;
|
||||
const searchLower = debouncedBuildSearch.toLowerCase();
|
||||
return builds.filter((build) =>
|
||||
build.buildNumber.toString().includes(searchLower) ||
|
||||
build.buildStatus.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}, [builds, buildSearch]);
|
||||
}, [builds, debouncedBuildSearch]);
|
||||
|
||||
// 获取构建状态徽章
|
||||
const getBuildStatusBadge = (status: string) => {
|
||||
const getBuildStatusBadge = useCallback((status: string) => {
|
||||
const config = BUILD_STATUS_CONFIG[status] || BUILD_STATUS_CONFIG.NOT_BUILT;
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
@ -330,10 +363,10 @@ const JenkinsManager: React.FC = () => {
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (duration?: number) => {
|
||||
const formatDuration = useCallback((duration?: number) => {
|
||||
if (!duration) return '-';
|
||||
const seconds = Math.floor(duration / 1000);
|
||||
if (seconds < 60) return `${seconds}秒`;
|
||||
@ -341,7 +374,16 @@ const JenkinsManager: React.FC = () => {
|
||||
if (minutes < 60) return `${minutes}分钟`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${hours}小时${minutes % 60}分钟`;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 构建列表虚拟滚动 - 用于处理大量数据(5000+ 条)
|
||||
const buildListRef = useRef<HTMLDivElement>(null);
|
||||
const buildVirtualizer = useVirtualizer({
|
||||
count: filteredBuilds.length,
|
||||
getScrollElement: () => buildListRef.current,
|
||||
estimateSize: () => 120, // 预估每个构建卡片的高度
|
||||
overscan: 5, // 预渲染额外的项目数量
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
@ -381,6 +423,18 @@ const JenkinsManager: React.FC = () => {
|
||||
<Layers className="h-4 w-4" />
|
||||
视图 ({filteredViews.length})
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={loadViews}
|
||||
disabled={loading.views || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading.views ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@ -388,11 +442,12 @@ const JenkinsManager: React.FC = () => {
|
||||
onClick={handleSyncViews}
|
||||
disabled={syncing.views || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${syncing.views ? 'animate-spin' : ''}`}
|
||||
<Download
|
||||
className={`h-4 w-4 ${syncing.views ? 'animate-bounce' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
@ -423,7 +478,7 @@ const JenkinsManager: React.FC = () => {
|
||||
? 'bg-accent border-primary'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => setSelectedView(view)}
|
||||
onClick={() => setSelectedView(selectedView?.id === view.id ? undefined : view)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="flex-1 truncate font-medium">
|
||||
@ -472,8 +527,20 @@ const JenkinsManager: React.FC = () => {
|
||||
<CardHeader className="border-b flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">
|
||||
任务 {selectedView && `(${filteredJobs.length})`}
|
||||
任务 ({filteredJobs.length}){selectedView && ` - ${selectedView.viewName}`}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => loadJobs(selectedView?.id)}
|
||||
disabled={loading.jobs || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading.jobs ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@ -481,11 +548,12 @@ const JenkinsManager: React.FC = () => {
|
||||
onClick={handleSyncJobs}
|
||||
disabled={syncing.jobs || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${syncing.jobs ? 'animate-spin' : ''}`}
|
||||
<Download
|
||||
className={`h-4 w-4 ${syncing.jobs ? 'animate-bounce' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
@ -501,15 +569,10 @@ const JenkinsManager: React.FC = () => {
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : !selectedView ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<Box className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">请先选择视图</p>
|
||||
</div>
|
||||
) : filteredJobs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<Box className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">暂无任务</p>
|
||||
<p className="text-sm">{selectedView ? '当前视图暂无任务' : '暂无任务'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2 space-y-2">
|
||||
@ -521,7 +584,7 @@ const JenkinsManager: React.FC = () => {
|
||||
? 'bg-accent border-primary'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => setSelectedJob(job)}
|
||||
onClick={() => setSelectedJob(selectedJob?.id === job.id ? undefined : job)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="flex-1 truncate font-medium">{job.jobName}</span>
|
||||
@ -599,8 +662,20 @@ const JenkinsManager: React.FC = () => {
|
||||
<CardHeader className="border-b flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">
|
||||
构建 {selectedJob && `(${filteredBuilds.length})`}
|
||||
构建 ({filteredBuilds.length}){selectedJob && ` - ${selectedJob.jobName}`}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => loadBuilds(selectedJob?.id)}
|
||||
disabled={loading.builds || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${loading.builds ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@ -608,10 +683,11 @@ const JenkinsManager: React.FC = () => {
|
||||
onClick={handleSyncBuilds}
|
||||
disabled={syncing.builds || !selectedInstanceId}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${syncing.builds ? 'animate-spin' : ''}`}
|
||||
<Download
|
||||
className={`h-4 w-4 ${syncing.builds ? 'animate-bounce' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
@ -623,27 +699,42 @@ const JenkinsManager: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div
|
||||
ref={buildListRef}
|
||||
className="flex-1 overflow-auto"
|
||||
>
|
||||
{loading.builds ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : !selectedJob ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">请先选择任务</p>
|
||||
</div>
|
||||
) : filteredBuilds.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
|
||||
<p className="text-sm">暂无构建</p>
|
||||
<p className="text-sm">{selectedJob ? '当前任务暂无构建' : '暂无构建'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{filteredBuilds.map((build) => (
|
||||
<div
|
||||
style={{
|
||||
height: `${buildVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{buildVirtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const build = filteredBuilds[virtualItem.index];
|
||||
return (
|
||||
<div
|
||||
key={build.id}
|
||||
className="group p-3 hover:bg-accent transition-colors"
|
||||
data-index={virtualItem.index}
|
||||
ref={buildVirtualizer.measureElement}
|
||||
className="group p-3 hover:bg-accent transition-colors border-b"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@ -687,7 +778,8 @@ const JenkinsManager: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -28,7 +28,7 @@ export const syncJenkinsViews = (externalSystemId: number) =>
|
||||
|
||||
// 获取任务列表
|
||||
export const getJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) =>
|
||||
request.get<JenkinsJobDTO[]>(`/api/v1/jenkins-job`, { params });
|
||||
request.get<JenkinsJobDTO[]>(`/api/v1/jenkins-job/list`, { params });
|
||||
|
||||
// 同步任务
|
||||
export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) =>
|
||||
@ -38,7 +38,7 @@ export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: num
|
||||
|
||||
// 获取构建列表
|
||||
export const getJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) =>
|
||||
request.get<JenkinsBuildDTO[]>(`/api/v1/jenkins-build`, { params });
|
||||
request.get<JenkinsBuildDTO[]>(`/api/v1/jenkins-build/list`, { params });
|
||||
|
||||
// 同步构建(根据jobId)
|
||||
export const syncJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) =>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user