diff --git a/src/frontend/platform/src/App.tsx b/src/frontend/platform/src/App.tsx
index 4afc71f64..61951da96 100644
--- a/src/frontend/platform/src/App.tsx
+++ b/src/frontend/platform/src/App.tsx
@@ -170,7 +170,7 @@ export default function App() {
const noAuthPages = ['chat', 'resouce']
const path = location.pathname.replace(__APP_ENV__.BASE_URL, '').split('/')?.[1] || ''
- // 动态路由根据权限:部门管理员补 create_app、子租户管理员补 sys(详见 resolveRoutePermissions)
+ // Resolve route permissions with fallbacks for department and child tenant admins.
const router = useMemo(() => {
if (user && user.role === 'admin') return getAdminRouter()
if (!user?.user_id) return null
diff --git a/src/frontend/platform/src/layout/HeaderMenu.tsx b/src/frontend/platform/src/layout/HeaderMenu.tsx
index 87983903f..025424d0e 100644
--- a/src/frontend/platform/src/layout/HeaderMenu.tsx
+++ b/src/frontend/platform/src/layout/HeaderMenu.tsx
@@ -15,6 +15,7 @@ export default function HeaderMenu({ }) {
return user.role === 'admin'
}, [user])
const canOpenBuild = isAdmin
+ || Boolean(user.is_child_admin)
|| Boolean(user.web_menu?.includes('build'))
|| Boolean(user.web_menu?.includes('create_app'))
const canManageWorkbench = canManageWorkbenchConfig(user)
diff --git a/src/frontend/platform/src/layout/MainLayout.tsx b/src/frontend/platform/src/layout/MainLayout.tsx
index d8ddab3a5..75f8b6bc5 100755
--- a/src/frontend/platform/src/layout/MainLayout.tsx
+++ b/src/frontend/platform/src/layout/MainLayout.tsx
@@ -69,16 +69,16 @@ export default function MainLayout() {
navigator('/reset')
}
- // 系统超管(租户超管仍走自定义角色 web_menu;部门管理员由服务端合并全量菜单)
+ // Super admins and child tenant admins receive the full admin menu.
+ // Tenant management remains super-admin-only; department admins receive menus from the backend.
const isSuperAdmin = useMemo(() => user.role === "admin", [user])
const isDeptAdmin = Boolean(user.is_department_admin)
const isChildAdmin = Boolean(user.is_child_admin)
const canManageWorkbenchConfig = isSuperAdmin || isChildAdmin
- // 侧栏:数据集 / 日志 — 超管与部门管理员
- const isFullAdminShell = isSuperAdmin || isDeptAdmin
- // 侧栏:系统管理 — 超管 / 部门管理员 / Child Admin。SystemPage 内部按
- // PRD §3.3 已为 Child Admin 分了 Tab 视角(组织 + 角色)
- const showSystemNav = isFullAdminShell || isChildAdmin
+ // Covers admin entries such as datasets that have no independent web_menu route key.
+ const isFullAdminShell = isSuperAdmin || isDeptAdmin || isChildAdmin
+ // SystemPage limits each admin type to the tabs it can manage.
+ const showSystemNav = isFullAdminShell
// 审批管理 — 仅超管 / Child Admin(部门管理员不可见)
const showApprovalNav = isSuperAdmin || isChildAdmin
// Admin-area approval scope (falls back to the legacy global flag for
@@ -96,7 +96,7 @@ export default function MainLayout() {
|| user.web_menu?.includes('frontend')
|| canManageWorkbenchConfig
}
- return user.web_menu?.includes(menu) || isSuperAdmin
+ return user.web_menu?.includes(menu) || isSuperAdmin || isChildAdmin
}
const u = user as User
diff --git a/src/frontend/platform/src/routes/index.tsx b/src/frontend/platform/src/routes/index.tsx
index d5d728907..0f87943b1 100755
--- a/src/frontend/platform/src/routes/index.tsx
+++ b/src/frontend/platform/src/routes/index.tsx
@@ -173,13 +173,25 @@ function hasRoutePermission(permissions: string[], key: string) {
}
/**
- * 把后端下发的 web_menu 转成路由层用的 permissions 数组。
+ * Convert the backend web_menu value into route permissions.
*
- * - 部门管理员补 `create_app`(后端来不及下发时的兜底,原有行为)。
- * - 子租户管理员(Child Admin)补 `sys` / `model`:后端 web_menu 不下发 sys/system_config 给非
- * 超管/非部门管理员,且默认不下发 `model` / `workstation` 资源。子租户管理员需要在自己
- * 租户内管理模型和工作台配置,路由层放行后由后端各自的 tenant admin 校验实际权限。
+ * - Department admins receive a create_app fallback when the backend omits it.
+ * - Child tenant admins receive every admin route except tenant management. Their backend web_menu
+ * still comes from a regular role, while business APIs retain tenant and resource authorization.
*/
+const CHILD_ADMIN_ROUTE_PERMISSIONS = [
+ "board",
+ "build",
+ "create_app",
+ "knowledge",
+ "model",
+ "evaluation",
+ "mark_task",
+ "log",
+ "sys",
+ "workstation",
+] as const
+
export function resolveRoutePermissions(user: {
web_menu?: string[]
is_department_admin?: boolean | null
@@ -189,14 +201,9 @@ export function resolveRoutePermissions(user: {
if (user.is_department_admin && !perms.includes("create_app")) {
perms = [...perms, "create_app"]
}
- if (user.is_child_admin && !perms.includes("sys")) {
- perms = [...perms, "sys"]
- }
- if (user.is_child_admin && !perms.includes("model")) {
- perms = [...perms, "model"]
- }
- if (user.is_child_admin && !perms.includes("workstation")) {
- perms = [...perms, "workstation"]
+ if (user.is_child_admin) {
+ const missing = CHILD_ADMIN_ROUTE_PERMISSIONS.filter((permission) => !perms.includes(permission))
+ if (missing.length) perms = [...perms, ...missing]
}
return perms
}
diff --git a/src/frontend/platform/src/test/headerMenuChildAdmin.test.tsx b/src/frontend/platform/src/test/headerMenuChildAdmin.test.tsx
new file mode 100644
index 000000000..00f58fc6c
--- /dev/null
+++ b/src/frontend/platform/src/test/headerMenuChildAdmin.test.tsx
@@ -0,0 +1,38 @@
+import { render, screen } from "@testing-library/react"
+import { MemoryRouter } from "react-router-dom"
+import { describe, expect, it, vi } from "vitest"
+
+import { userContext } from "@/contexts/userContext"
+import HeaderMenu from "@/layout/HeaderMenu"
+
+vi.hoisted(() => {
+ ;(globalThis as any).__APP_ENV__ = { BASE_URL: "" }
+})
+
+vi.mock("@/components/bs-icons", () => ({
+ TabIcon: () => ,
+}))
+
+describe("HeaderMenu for Child Admin", () => {
+ it("shows application, tool, and workbench tabs without role web_menu grants", () => {
+ const user = {
+ user_id: 1,
+ user_name: "child-admin",
+ role: "user",
+ web_menu: [],
+ is_child_admin: true,
+ }
+
+ render(
+
+
+
+
+ ,
+ )
+
+ expect(screen.getByText("build.app")).toBeInTheDocument()
+ expect(screen.getByText("build.tools")).toBeInTheDocument()
+ expect(screen.getByText("build.workbench")).toBeInTheDocument()
+ })
+})
diff --git a/src/frontend/platform/src/test/mainLayoutSystemNavChildAdmin.test.tsx b/src/frontend/platform/src/test/mainLayoutSystemNavChildAdmin.test.tsx
index da10992db..ce6ea660f 100644
--- a/src/frontend/platform/src/test/mainLayoutSystemNavChildAdmin.test.tsx
+++ b/src/frontend/platform/src/test/mainLayoutSystemNavChildAdmin.test.tsx
@@ -145,7 +145,7 @@ function renderLayout(userOverrides: Record = {}) {
);
}
-describe("MainLayout system / tenant nav for Child Admin (PRD §3.3)", () => {
+describe("MainLayout admin nav for Child Admin", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(window, "localStorage", {
@@ -158,26 +158,25 @@ describe("MainLayout system / tenant nav for Child Admin (PRD §3.3)", () => {
});
});
- it("Child Admin sees the system menu entry", () => {
- renderLayout({ is_child_admin: true });
- expect(screen.getByText("menu.system")).toBeInTheDocument();
- });
-
- it("Child Admin sees the build entry for workstation config", () => {
- renderLayout({ is_child_admin: true });
- expect(screen.getByText("menu.skills")).toBeInTheDocument();
- });
-
- it("Child Admin does NOT see the tenant management entry (super-only until backend is opened up)", () => {
+ it("Child Admin sees every admin menu except tenant management", () => {
renderLayout({ is_child_admin: true });
+ for (const label of [
+ "menu.dashboard",
+ "menu.skills",
+ "menu.knowledge",
+ "menu.dataset",
+ "menu.models",
+ "menu.evaluation",
+ "menu.annotation",
+ "menu.log",
+ "menu.approval",
+ "menu.system",
+ ]) {
+ expect(screen.getByText(label)).toBeInTheDocument();
+ }
expect(screen.queryByText("tenant.management")).toBeNull();
});
- it("Child Admin does NOT see the dataset entry (super / dept admin only)", () => {
- renderLayout({ is_child_admin: true });
- expect(screen.queryByText("menu.dataset")).toBeNull();
- });
-
it("plain user without any admin flag sees neither system nor tenant management", () => {
renderLayout();
expect(screen.queryByText("menu.system")).toBeNull();
diff --git a/src/frontend/platform/src/test/resolveRoutePermissions.test.ts b/src/frontend/platform/src/test/resolveRoutePermissions.test.ts
index 5fb7928fa..1988cc530 100644
--- a/src/frontend/platform/src/test/resolveRoutePermissions.test.ts
+++ b/src/frontend/platform/src/test/resolveRoutePermissions.test.ts
@@ -96,11 +96,18 @@ describe("resolveRoutePermissions", () => {
expect(perms.filter((p) => p === "model")).toHaveLength(1)
})
- it("handles missing web_menu gracefully", () => {
+ it("grants Child Admin every admin route permission except tenant management", () => {
expect(resolveRoutePermissions({})).toEqual([])
expect(resolveRoutePermissions({ is_child_admin: true })).toEqual([
- "sys",
+ "board",
+ "build",
+ "create_app",
+ "knowledge",
"model",
+ "evaluation",
+ "mark_task",
+ "log",
+ "sys",
"workstation",
])
})
@@ -116,6 +123,6 @@ describe("resolveRoutePermissions", () => {
})
expect(perms).toContain("sys")
expect(perms).toContain("workstation")
- expect(perms).not.toContain("create_app")
+ expect(perms).toContain("create_app")
})
})