Skip to content

Commit 87d5cc1

Browse files
committed
💄 优化系统项目管理导航
1 parent 2573b86 commit 87d5cc1

8 files changed

Lines changed: 170 additions & 80 deletions

File tree

web/src/App.test.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,35 @@ test("shows project list and handles switching", async () => {
307307
render(<App />);
308308

309309
// Should render active project Sider details
310-
expect(await screen.findByText("Project One (当前)")).toBeTruthy();
310+
expect(await screen.findByText("当前项目【Project One")).toBeTruthy();
311311
// Should render projects table with unique link roles
312312
expect((await screen.findAllByText("Project One")).length).toBeGreaterThan(0);
313313
expect(await screen.findByText("Project Two")).toBeTruthy();
314314
});
315+
316+
test("hides the system project and exposes its clients to admins", async () => {
317+
mockApi({
318+
isAdmin: true,
319+
projects: [
320+
project({ projectId: "system", name: "System", role: null }),
321+
project({ projectId: "p1", name: "Project One" }),
322+
],
323+
});
324+
window.history.pushState({}, "", "/manage/projects");
325+
render(<App />);
326+
327+
expect(await screen.findByText("系统客户端")).toBeTruthy();
328+
expect(screen.queryByText("[系统项目]")).toBeNull();
329+
expect(screen.queryByRole("link", { name: "System" })).toBeNull();
330+
331+
fireEvent.click(screen.getByText("系统客户端"));
332+
await waitFor(() => {
333+
expect(window.location.pathname).toBe("/manage/projects/system/overview");
334+
});
335+
expect(screen.getByText("当前项目【系统客户端】")).toBeTruthy();
336+
expect(screen.getAllByText("项目概览").length).toBeGreaterThan(0);
337+
expect(screen.getByText("审计日志")).toBeTruthy();
338+
expect(screen.queryByText("OIDC 客户端")).toBeNull();
339+
expect(screen.queryByText("当前项目【Project One】")).toBeNull();
340+
expect(screen.queryByRole("button", { name: "创建客户端" })).toBeNull();
341+
});

web/src/app/router.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export const AppRouter: React.FC = () => {
3333
>
3434
<Route index element={<Navigate to="/projects" replace />} />
3535
<Route path="/projects" element={<ProjectList />} />
36+
<Route
37+
path="/projects/system/members"
38+
element={<Navigate to="/projects/system/clients" replace />}
39+
/>
3640
<Route
3741
path="/projects/:projectId/overview"
3842
element={<ProjectOverview />}
@@ -42,6 +46,10 @@ export const AppRouter: React.FC = () => {
4246
element={<MemberManager />}
4347
/>
4448
<Route path="/projects/:projectId/clients" element={<ClientList />} />
49+
<Route
50+
path="/projects/system/clients/new"
51+
element={<Navigate to="/projects/system/clients" replace />}
52+
/>
4553
<Route
4654
path="/projects/:projectId/clients/new"
4755
element={<ClientCreate />}

web/src/components/layout/DashboardLayout.tsx

Lines changed: 91 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export const DashboardLayout: React.FC = () => {
4747

4848
const [collapsed, setCollapsed] = useState(false);
4949
const [mobileVisible, setMobileVisible] = useState(false);
50+
const visibleProjects = projects.filter(
51+
(project) => project.projectId !== "system",
52+
);
5053

5154
const handleProjectSelect = (value: string) => {
5255
selectProject(value);
@@ -61,17 +64,20 @@ export const DashboardLayout: React.FC = () => {
6164

6265
if (paths.includes("projects")) {
6366
breadcrumbItems.push({
64-
title: "项目列表",
67+
title: "我的项目",
6568
key: "projects",
6669
onClick: () => navigate("/projects"),
6770
});
6871
if (activeProject) {
72+
const isSystemProject = activeProject.projectId === "system";
6973
breadcrumbItems.push({
70-
title: activeProject.name,
74+
title: isSystemProject ? "系统客户端" : activeProject.name,
7175
key: activeProject.projectId,
7276
onClick: () =>
7377
navigate(
74-
`/projects/${encodeURIComponent(activeProject.projectId)}/overview`,
78+
isSystemProject
79+
? "/projects/system/clients"
80+
: `/projects/${encodeURIComponent(activeProject.projectId)}/overview`,
7581
),
7682
});
7783
}
@@ -93,56 +99,72 @@ export const DashboardLayout: React.FC = () => {
9399
{
94100
key: "projects-list",
95101
icon: <ProjectOutlined />,
96-
label: "项目列表",
102+
label: "我的项目",
97103
onClick: () => navigate("/projects"),
98104
},
99105
];
100106

101-
if (activeProject) {
102-
items.push({
103-
key: "project-group",
104-
label: `${activeProject.name} (当前)`,
105-
type: "group",
106-
children: [
107-
{
108-
key: "overview",
109-
icon: <DesktopOutlined />,
110-
label: "项目概览",
111-
onClick: () =>
112-
navigate(
113-
`/projects/${encodeURIComponent(activeProject.projectId)}/overview`,
114-
),
115-
},
116-
{
117-
key: "clients",
118-
icon: <DesktopOutlined />,
119-
label: "OIDC 客户端",
120-
onClick: () =>
121-
navigate(
122-
`/projects/${encodeURIComponent(activeProject.projectId)}/clients`,
123-
),
124-
},
125-
{
126-
key: "members",
127-
icon: <TeamOutlined />,
128-
label: "成员管理",
129-
onClick: () =>
130-
navigate(
131-
`/projects/${encodeURIComponent(activeProject.projectId)}/members`,
132-
),
133-
},
134-
{
135-
key: "audit",
136-
icon: <AuditOutlined />,
137-
label: "审计日志",
138-
onClick: () =>
139-
navigate(
140-
`/projects/${encodeURIComponent(activeProject.projectId)}/audit`,
141-
),
142-
},
143-
],
144-
});
145-
}
107+
const isSystemProject = activeProject?.projectId === "system";
108+
items.push({
109+
key: "project-group",
110+
label: `当前项目【${isSystemProject ? "系统客户端" : (activeProject?.name ?? "未选择")}】`,
111+
type: "group",
112+
children: isSystemProject
113+
? [
114+
{
115+
key: "current-system-overview",
116+
icon: <DesktopOutlined />,
117+
label: "项目概览",
118+
onClick: () => navigate("/projects/system/overview"),
119+
},
120+
{
121+
key: "current-system-audit",
122+
icon: <AuditOutlined />,
123+
label: "审计日志",
124+
onClick: () => navigate("/projects/system/audit"),
125+
},
126+
]
127+
: activeProject
128+
? [
129+
{
130+
key: "overview",
131+
icon: <DesktopOutlined />,
132+
label: "项目概览",
133+
onClick: () =>
134+
navigate(
135+
`/projects/${encodeURIComponent(activeProject.projectId)}/overview`,
136+
),
137+
},
138+
{
139+
key: "clients",
140+
icon: <DesktopOutlined />,
141+
label: "OIDC 客户端",
142+
onClick: () =>
143+
navigate(
144+
`/projects/${encodeURIComponent(activeProject.projectId)}/clients`,
145+
),
146+
},
147+
{
148+
key: "members",
149+
icon: <TeamOutlined />,
150+
label: "成员管理",
151+
onClick: () =>
152+
navigate(
153+
`/projects/${encodeURIComponent(activeProject.projectId)}/members`,
154+
),
155+
},
156+
{
157+
key: "audit",
158+
icon: <AuditOutlined />,
159+
label: "审计日志",
160+
onClick: () =>
161+
navigate(
162+
`/projects/${encodeURIComponent(activeProject.projectId)}/audit`,
163+
),
164+
},
165+
]
166+
: [],
167+
});
146168

147169
if (identity?.isAdmin) {
148170
items.push({
@@ -153,9 +175,18 @@ export const DashboardLayout: React.FC = () => {
153175
{
154176
key: "reviews",
155177
icon: <SafetyCertificateOutlined />,
156-
label: "全局待审核",
178+
label: "待审核配置",
157179
onClick: () => navigate("/admin/reviews"),
158180
},
181+
{
182+
key: "system-clients",
183+
icon: <DesktopOutlined />,
184+
label: "系统客户端",
185+
onClick: () => {
186+
selectProject("system");
187+
navigate("/projects/system/overview");
188+
},
189+
},
159190
{
160191
key: "system-settings",
161192
icon: <SettingOutlined />,
@@ -220,6 +251,9 @@ export const DashboardLayout: React.FC = () => {
220251
alignItems: "center",
221252
justifyContent: "space-between",
222253
borderBottom: `1px solid ${token.colorBorderSecondary}`,
254+
position: "sticky",
255+
top: 0,
256+
zIndex: 1,
223257
}}
224258
>
225259
<Space>
@@ -234,14 +268,18 @@ export const DashboardLayout: React.FC = () => {
234268
}
235269
}}
236270
/>
237-
{projects.length > 0 && (
271+
{visibleProjects.length > 0 && (
238272
<Select
239-
value={activeProject?.projectId}
273+
value={
274+
activeProject?.projectId === "system"
275+
? undefined
276+
: activeProject?.projectId
277+
}
240278
onChange={handleProjectSelect}
241279
style={{ width: 200 }}
242280
placeholder="切换项目"
243281
>
244-
{projects.map((p) => (
282+
{visibleProjects.map((p) => (
245283
<Select.Option key={p.projectId} value={p.projectId}>
246284
{p.name}
247285
</Select.Option>

web/src/contexts/project-context.tsx

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import React, { createContext, useContext, useState, useEffect } from "react";
22
import type { Project } from "../api/types";
33
import { request } from "../api/client";
44
import { setActiveProjectForAccessControl } from "../app/providers/access-control-provider";
5+
import { useLocation } from "react-router-dom";
6+
7+
const SYSTEM_PROJECT_ID = "system";
8+
9+
function projectIdFromPath(pathname: string) {
10+
const match = pathname.match(/^\/projects\/([^/]+)/);
11+
return match?.[1] ? decodeURIComponent(match[1]) : undefined;
12+
}
513

614
interface ProjectContextType {
715
projects: Project[];
@@ -19,32 +27,29 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({
1927
const [projects, setProjects] = useState<Project[]>([]);
2028
const [activeProject, setActiveProject] = useState<Project | null>(null);
2129
const [loading, setLoading] = useState(true);
30+
const location = useLocation();
31+
32+
const activate = (project: Project | null) => {
33+
setActiveProject(project);
34+
setActiveProjectForAccessControl(project);
35+
};
2236

2337
const refreshProjects = async () => {
2438
setLoading(true);
2539
try {
2640
const data = await request<{ projects: Project[] }>("/projects");
2741
setProjects(data.projects);
28-
29-
// Auto-select or refresh active project
30-
if (activeProject) {
31-
const updated = data.projects.find(
32-
(p) => p.projectId === activeProject.projectId,
33-
);
34-
if (updated) {
35-
setActiveProject(updated);
36-
setActiveProjectForAccessControl(updated);
37-
} else if (data.projects.length > 0) {
38-
setActiveProject(data.projects[0]!);
39-
setActiveProjectForAccessControl(data.projects[0]!);
40-
} else {
41-
setActiveProject(null);
42-
setActiveProjectForAccessControl(null);
43-
}
44-
} else if (data.projects.length > 0) {
45-
setActiveProject(data.projects[0]!);
46-
setActiveProjectForAccessControl(data.projects[0]!);
47-
}
42+
const routeProjectId = projectIdFromPath(location.pathname);
43+
activate(
44+
data.projects.find((project) => project.projectId === routeProjectId) ??
45+
data.projects.find(
46+
(project) => project.projectId === activeProject?.projectId,
47+
) ??
48+
data.projects.find(
49+
(project) => project.projectId !== SYSTEM_PROJECT_ID,
50+
) ??
51+
null,
52+
);
4853
} catch (error) {
4954
console.error("Failed to fetch projects:", error);
5055
} finally {
@@ -54,14 +59,20 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({
5459

5560
const selectProject = (projectId: string) => {
5661
const project = projects.find((p) => p.projectId === projectId) ?? null;
57-
setActiveProject(project);
58-
setActiveProjectForAccessControl(project);
62+
activate(project);
5963
};
6064

6165
useEffect(() => {
6266
refreshProjects();
6367
}, []);
6468

69+
useEffect(() => {
70+
const routeProjectId = projectIdFromPath(location.pathname);
71+
if (!routeProjectId || routeProjectId === activeProject?.projectId) return;
72+
const project = projects.find((item) => item.projectId === routeProjectId);
73+
if (project) activate(project);
74+
}, [location.pathname, projects]);
75+
6576
return (
6677
<ProjectContext.Provider
6778
value={{

web/src/pages/admin/AdminReviews.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export const AdminReviews: React.FC = () => {
167167
];
168168

169169
return (
170-
<Card title="全局待审核客户端配置">
170+
<Card title="待审核客户端配置">
171171
<Space direction="vertical" style={{ width: "100%" }} size="large">
172172
<Table
173173
dataSource={pendingClients}

web/src/pages/clients/ClientList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export const ClientList: React.FC = () => {
148148
title="OIDC 客户端"
149149
extra={
150150
canWriteClient &&
151+
activeProject.projectId !== "system" &&
151152
!isArchived && (
152153
<Button
153154
type="primary"

web/src/pages/projects/ProjectList.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export const ProjectList: React.FC = () => {
2828
const [createVisible, setCreateVisible] = useState(false);
2929
const [form] = Form.useForm();
3030
const navigate = useNavigate();
31+
const visibleProjects = projects.filter(
32+
(project) => project.projectId !== "system",
33+
);
3134

3235
const handleCreate = async (values: any) => {
3336
try {
@@ -107,7 +110,7 @@ export const ProjectList: React.FC = () => {
107110

108111
return (
109112
<Card
110-
title="项目列表"
113+
title="我的项目"
111114
extra={
112115
<Button
113116
type="primary"
@@ -119,7 +122,7 @@ export const ProjectList: React.FC = () => {
119122
}
120123
>
121124
<Table
122-
dataSource={projects}
125+
dataSource={visibleProjects}
123126
columns={columns}
124127
rowKey="projectId"
125128
loading={loading}

0 commit comments

Comments
 (0)