From 7da6b2ce36f9e2aac5a21ca942006f8fa4e09846 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:01:00 +0800 Subject: [PATCH] refactor(web): remove permission catalog service hooks (#39342) --- oxlint-suppressions.json | 5 - .../__tests__/index.spec.tsx | 120 ++++++++++-------- .../permission-set-modal/hooks.ts | 25 ++-- .../role-modal/__tests__/index.spec.tsx | 76 ++++++----- .../permissions-page/role-modal/hooks.ts | 15 ++- web/models/access-control.ts | 4 - .../access-control/use-permission-catalog.ts | 29 ----- 7 files changed, 138 insertions(+), 136 deletions(-) delete mode 100644 web/service/access-control/use-permission-catalog.ts diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 20df68ca17e..bdfb8b75bd2 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -6336,11 +6336,6 @@ "count": 1 } }, - "web/service/access-control/use-permission-catalog.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/access-control/use-workspace-access-rules.ts": { "no-restricted-imports": { "count": 1 diff --git a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx index 3e255fac2fd..ab2e1f0a805 100644 --- a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx @@ -1,6 +1,10 @@ -import type { PermissionGroup } from '@/models/access-control' +import type { PermissionCatalogResponse } from '@dify/contracts/api/console/workspaces/types.gen' +import type { ReactNode } from 'react' +import { QueryClientProvider } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { consoleQuery } from '@/service/client' +import { createTestQueryClient } from '@/test/query-client' import PermissionSetModal from '../index' const expectedAppACLPermissionKeys = [ @@ -19,59 +23,71 @@ const expectedAppACLPermissionKeys = [ const getPermissionKeyMatcher = (permissionKey: string) => new RegExp(permissionKey.replaceAll('.', '\\.')) -const mockCatalogs = vi.hoisted(() => ({ - app: { - groups: [] as PermissionGroup[], - }, - dataset: { - groups: [] as PermissionGroup[], - }, -})) - -vi.mock('@/service/access-control/use-permission-catalog', () => ({ - useAppPermissionCatalog: () => ({ - data: { groups: mockCatalogs.app.groups }, - }), - useDatasetPermissionCatalog: () => ({ - data: { groups: mockCatalogs.dataset.groups }, - }), -})) - -const createPermissionGroup = (overrides: Partial = {}): PermissionGroup => ({ - group_key: 'app_management', - group_name: 'App management', - description: '', - permissions: expectedAppACLPermissionKeys.map((permissionKey) => ({ - key: permissionKey, - name: permissionKey, - description: '', - })), - ...overrides, +vi.mock('react-i18next', async () => { + const { createReactI18nextMock } = await import('@/test/i18n-mock') + return createReactI18nextMock({ + 'permission.group.app_acl': 'Translated app permissions', + 'permission.group.dataset_acl': 'Translated dataset permissions', + }) }) +const appPermissionCatalog = { + groups: [ + { + group_key: 'app_management', + group_name: 'App management', + description: '', + permissions: expectedAppACLPermissionKeys.map((permissionKey) => ({ + key: permissionKey, + name: permissionKey, + description: '', + })), + }, + ], +} satisfies PermissionCatalogResponse + +const datasetPermissionCatalog = { + groups: [ + { + group_key: 'dataset_management', + group_name: 'Dataset management', + description: '', + permissions: [ + { + key: 'dataset.acl.edit', + name: 'Edit dataset', + description: '', + }, + ], + }, + ], +} satisfies PermissionCatalogResponse + +const renderModal = (modal: ReactNode) => { + const queryClient = createTestQueryClient() + queryClient.setQueryData( + consoleQuery.workspaces.current.rbac.rolePermissions.catalog.app.get.queryKey({ input: {} }), + appPermissionCatalog, + ) + queryClient.setQueryData( + consoleQuery.workspaces.current.rbac.rolePermissions.catalog.dataset.get.queryKey({ + input: {}, + }), + datasetPermissionCatalog, + ) + + return render({modal}) +} + describe('PermissionSetModal', () => { beforeEach(() => { vi.clearAllMocks() - mockCatalogs.app.groups = [createPermissionGroup()] - mockCatalogs.dataset.groups = [ - createPermissionGroup({ - group_key: 'dataset_management', - group_name: 'Dataset management', - permissions: [ - { - key: 'dataset.acl.edit', - name: 'Edit dataset', - description: '', - }, - ], - }), - ] }) // Rendering keeps the form fields and permission picker available inside the modal. describe('Rendering', () => { it('should render create mode with app permission catalog', () => { - render( + renderModal( { ).toBeInTheDocument() expect(screen.getByLabelText(/permission\.permissionSet\.nameLabel/)).toBeInTheDocument() expect(screen.getByLabelText('permission.permissionSet.descriptionLabel')).toBeInTheDocument() - expect(screen.getByRole('button', { name: /App management/ })).toHaveAttribute( + expect(screen.getByRole('button', { name: /Translated app permissions/ })).toHaveAttribute( 'aria-expanded', 'true', ) @@ -94,7 +110,7 @@ describe('PermissionSetModal', () => { }) it('should render the complete app ACL permission catalog', async () => { - render( + renderModal( { }) it('should render dataset permission catalog when resource type is dataset', () => { - render( + renderModal( { expect( screen.getByText('permission.permissionSet.modal.create.dataset.title'), ).toBeInTheDocument() - expect(screen.getByRole('button', { name: /Dataset management/ })).toBeInTheDocument() + expect( + screen.getByRole('button', { name: /Translated dataset permissions/ }), + ).toBeInTheDocument() expect(screen.getByText(/dataset\.acl\.edit/)).toBeInTheDocument() }) }) @@ -134,7 +152,7 @@ describe('PermissionSetModal', () => { const handleClose = vi.fn() const handleSubmit = vi.fn() - render( + renderModal( { const user = userEvent.setup() const handleSubmit = vi.fn() - render( + renderModal( { // View mode is read-only and uses close-only footer actions. describe('Read-only Mode', () => { it('should disable editing and hide confirm action in view mode', () => { - render( + renderModal( { const { t } = useTranslation() - const { data: appPermissionCatalog } = useAppPermissionCatalog(resourceType === 'app') - const { data: datasetPermissionCatalog } = useDatasetPermissionCatalog(resourceType === 'dataset') - - const permissionCatalog = resourceType === 'app' ? appPermissionCatalog : datasetPermissionCatalog + const permissionCatalogQueryOptions = + resourceType === 'app' + ? consoleQuery.workspaces.current.rbac.rolePermissions.catalog.app.get.queryOptions({ + input: {}, + }) + : consoleQuery.workspaces.current.rbac.rolePermissions.catalog.dataset.get.queryOptions({ + input: {}, + }) + const { data: permissionCatalog } = useQuery(permissionCatalogQueryOptions) const groups = useMemo(() => { // Permission keys come from the catalog API, so this is a reviewed open-key boundary with a server-provided fallback. @@ -22,20 +25,20 @@ export const usePermissionsGroups = (resourceType: AccessPolicyResourceType) => defaultValue, }) - return (permissionCatalog?.groups || []).map((group) => ({ + return (permissionCatalog?.groups ?? []).map((group) => ({ ...group, group_name: t(($) => $[`group.${resourceType}_acl`], { ns: 'permission', defaultValue: group.group_name, }), - permissions: group.permissions.map((permission) => ({ + permissions: (group.permissions ?? []).map((permission) => ({ ...permission, name: translatePermissionName(permission.key, permission.name), })), })) }, [permissionCatalog?.groups, resourceType, t]) - const allPermissions = groups.flatMap((g) => g.permissions) || [] + const allPermissions = groups.flatMap((g) => g.permissions) const permissionMap = Object.fromEntries(allPermissions.map((p) => [p.key, p])) diff --git a/web/app/components/header/account-setting/permissions-page/role-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/permissions-page/role-modal/__tests__/index.spec.tsx index 3fca3fa1a67..892de41ba34 100644 --- a/web/app/components/header/account-setting/permissions-page/role-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-modal/__tests__/index.spec.tsx @@ -1,31 +1,47 @@ -import type { PermissionGroup, Role } from '@/models/access-control' +import type { PermissionCatalogResponse } from '@dify/contracts/api/console/workspaces/types.gen' +import type { ReactNode } from 'react' +import type { Role } from '@/models/access-control' +import { QueryClientProvider } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { consoleQuery } from '@/service/client' +import { createTestQueryClient } from '@/test/query-client' import RoleModal from '../index' -const mockWorkspacePermissionCatalog = vi.hoisted(() => ({ - groups: [] as PermissionGroup[], -})) +vi.mock('react-i18next', async () => { + const { createReactI18nextMock } = await import('@/test/i18n-mock') + return createReactI18nextMock({ + 'permission.group.workspace_management': 'Translated workspace permissions', + 'permissionKeys.workspace.member.manage': 'Manage workspace members', + }) +}) -vi.mock('@/service/access-control/use-permission-catalog', () => ({ - useWorkspacePermissionCatalog: () => ({ - data: { groups: mockWorkspacePermissionCatalog.groups }, - }), -})) - -const createPermissionGroup = (overrides: Partial = {}): PermissionGroup => ({ - group_key: 'workspace_management', - group_name: 'Workspace management', - description: '', - permissions: [ +const workspacePermissionCatalog = { + groups: [ { - key: 'workspace.member.manage', - name: 'Manage members', + group_key: 'workspace_management', + group_name: 'Workspace management', description: '', + permissions: [ + { + key: 'workspace.member.manage', + name: 'Manage members', + description: '', + }, + ], }, ], - ...overrides, -}) +} satisfies PermissionCatalogResponse + +const renderModal = (modal: ReactNode) => { + const queryClient = createTestQueryClient() + queryClient.setQueryData( + consoleQuery.workspaces.current.rbac.rolePermissions.catalog.get.queryKey({ input: {} }), + workspacePermissionCatalog, + ) + + return render({modal}) +} const createRole = (overrides: Partial = {}): Role => ({ id: 'role-1', @@ -43,13 +59,12 @@ const createRole = (overrides: Partial = {}): Role => ({ describe('RoleModal', () => { beforeEach(() => { vi.clearAllMocks() - mockWorkspacePermissionCatalog.groups = [createPermissionGroup()] }) // Rendering keeps role fields and workspace permissions in one modal form. describe('Rendering', () => { it('should render edit mode with role values and selected permissions', () => { - render( + renderModal( , ) @@ -58,15 +73,14 @@ describe('RoleModal', () => { expect(screen.getByLabelText('permission.role.modal.descriptionLabel')).toHaveValue( 'Can operate workspace', ) - expect(screen.getByRole('button', { name: /Workspace management/ })).toHaveAttribute( - 'aria-expanded', - 'true', - ) - expect(screen.getByText(/workspace\.member\.manage/)).toBeInTheDocument() + expect( + screen.getByRole('button', { name: /Translated workspace permissions/ }), + ).toHaveAttribute('aria-expanded', 'true') + expect(screen.getByText('Manage workspace members')).toBeInTheDocument() }) it('should disable confirm action when role name is empty', () => { - render() + renderModal() expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeDisabled() }) @@ -79,14 +93,14 @@ describe('RoleModal', () => { const handleClose = vi.fn() const handleSubmit = vi.fn() - render() + renderModal() await user.type(screen.getByLabelText('permission.role.modal.nameLabel'), ' Support role ') await user.type( screen.getByLabelText('permission.role.modal.descriptionLabel'), ' Helps members ', ) - await user.click(screen.getByText(/workspace\.member\.manage/)) + await user.click(screen.getByText('Manage workspace members')) await user.click(screen.getByRole('button', { name: 'common.operation.confirm' })) expect(handleSubmit).toHaveBeenCalledTimes(1) @@ -102,7 +116,7 @@ describe('RoleModal', () => { const user = userEvent.setup() const handleSubmit = vi.fn() - render( + renderModal( { // View mode preserves the permission display but blocks edits and confirmation. describe('Read-only Mode', () => { it('should render role details as read-only in view mode', () => { - render( + renderModal( , ) diff --git a/web/app/components/header/account-setting/permissions-page/role-modal/hooks.ts b/web/app/components/header/account-setting/permissions-page/role-modal/hooks.ts index a5b2e587089..dc7c20b70e4 100644 --- a/web/app/components/header/account-setting/permissions-page/role-modal/hooks.ts +++ b/web/app/components/header/account-setting/permissions-page/role-modal/hooks.ts @@ -1,11 +1,16 @@ import type { SelectorKey } from 'i18next' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useWorkspacePermissionCatalog } from '@/service/access-control/use-permission-catalog' +import { consoleQuery } from '@/service/client' export const useWorkspacePermissionGroups = () => { const { t } = useTranslation() - const { data: workspacePermissionCatalog } = useWorkspacePermissionCatalog() + const { data: workspacePermissionCatalog } = useQuery( + consoleQuery.workspaces.current.rbac.rolePermissions.catalog.get.queryOptions({ + input: {}, + }), + ) const groups = useMemo(() => { // Permission keys come from the catalog API, so these are reviewed open-key boundaries with server-provided fallbacks. @@ -20,17 +25,17 @@ export const useWorkspacePermissionGroups = () => { defaultValue, }) - return (workspacePermissionCatalog?.groups || []).map((group) => ({ + return (workspacePermissionCatalog?.groups ?? []).map((group) => ({ ...group, group_name: translatePermissionGroupName(group.group_key, group.group_name), - permissions: group.permissions.map((permission) => ({ + permissions: (group.permissions ?? []).map((permission) => ({ ...permission, name: translatePermissionName(permission.key, permission.name), })), })) }, [t, workspacePermissionCatalog?.groups]) - const allPermissions = groups.flatMap((g) => g.permissions) || [] + const allPermissions = groups.flatMap((g) => g.permissions) const permissionMap = Object.fromEntries(allPermissions.map((p) => [p.key, p])) diff --git a/web/models/access-control.ts b/web/models/access-control.ts index a96d2eb9c90..46be7e87f82 100644 --- a/web/models/access-control.ts +++ b/web/models/access-control.ts @@ -61,10 +61,6 @@ export type PermissionGroup = { permissions: Permission[] } -export type PermissionGroups = { - groups: PermissionGroup[] -} - export type PermissionKey = string type RoleType = 'workspace' | 'app' | 'dataset' diff --git a/web/service/access-control/use-permission-catalog.ts b/web/service/access-control/use-permission-catalog.ts deleted file mode 100644 index 151eef55ab1..00000000000 --- a/web/service/access-control/use-permission-catalog.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { PermissionGroups } from '@/models/access-control' -import { useQuery } from '@tanstack/react-query' -import { get } from '../base' - -const NAME_SPACE = 'rbac-permission-catalog' - -export const useWorkspacePermissionCatalog = () => { - return useQuery({ - queryKey: [NAME_SPACE, 'workspace'], - queryFn: () => get('/workspaces/current/rbac/role-permissions/catalog'), - }) -} - -export const useAppPermissionCatalog = (enabled?: boolean) => { - return useQuery({ - queryKey: [NAME_SPACE, 'app'], - queryFn: () => get('/workspaces/current/rbac/role-permissions/catalog/app'), - enabled: enabled ?? true, - }) -} - -export const useDatasetPermissionCatalog = (enabled?: boolean) => { - return useQuery({ - queryKey: [NAME_SPACE, 'dataset'], - queryFn: () => - get('/workspaces/current/rbac/role-permissions/catalog/dataset'), - enabled: enabled ?? true, - }) -}