refactor(web): remove permission catalog service hooks (#39342)

This commit is contained in:
yyh 2026-07-21 14:01:00 +08:00 committed by GitHub
parent aa718add24
commit 7da6b2ce36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 138 additions and 136 deletions

View File

@ -6336,11 +6336,6 @@
"count": 1 "count": 1
} }
}, },
"web/service/access-control/use-permission-catalog.ts": {
"no-restricted-imports": {
"count": 1
}
},
"web/service/access-control/use-workspace-access-rules.ts": { "web/service/access-control/use-workspace-access-rules.ts": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 1 "count": 1

View File

@ -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 { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { consoleQuery } from '@/service/client'
import { createTestQueryClient } from '@/test/query-client'
import PermissionSetModal from '../index' import PermissionSetModal from '../index'
const expectedAppACLPermissionKeys = [ const expectedAppACLPermissionKeys = [
@ -19,59 +23,71 @@ const expectedAppACLPermissionKeys = [
const getPermissionKeyMatcher = (permissionKey: string) => const getPermissionKeyMatcher = (permissionKey: string) =>
new RegExp(permissionKey.replaceAll('.', '\\.')) new RegExp(permissionKey.replaceAll('.', '\\.'))
const mockCatalogs = vi.hoisted(() => ({ vi.mock('react-i18next', async () => {
app: { const { createReactI18nextMock } = await import('@/test/i18n-mock')
groups: [] as PermissionGroup[], return createReactI18nextMock({
}, 'permission.group.app_acl': 'Translated app permissions',
dataset: { 'permission.group.dataset_acl': 'Translated dataset permissions',
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> = {}): PermissionGroup => ({
group_key: 'app_management',
group_name: 'App management',
description: '',
permissions: expectedAppACLPermissionKeys.map((permissionKey) => ({
key: permissionKey,
name: permissionKey,
description: '',
})),
...overrides,
}) })
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(<QueryClientProvider client={queryClient}>{modal}</QueryClientProvider>)
}
describe('PermissionSetModal', () => { describe('PermissionSetModal', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() 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. // Rendering keeps the form fields and permission picker available inside the modal.
describe('Rendering', () => { describe('Rendering', () => {
it('should render create mode with app permission catalog', () => { it('should render create mode with app permission catalog', () => {
render( renderModal(
<PermissionSetModal <PermissionSetModal
open open
mode="create" mode="create"
@ -86,7 +102,7 @@ describe('PermissionSetModal', () => {
).toBeInTheDocument() ).toBeInTheDocument()
expect(screen.getByLabelText(/permission\.permissionSet\.nameLabel/)).toBeInTheDocument() expect(screen.getByLabelText(/permission\.permissionSet\.nameLabel/)).toBeInTheDocument()
expect(screen.getByLabelText('permission.permissionSet.descriptionLabel')).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', 'aria-expanded',
'true', 'true',
) )
@ -94,7 +110,7 @@ describe('PermissionSetModal', () => {
}) })
it('should render the complete app ACL permission catalog', async () => { it('should render the complete app ACL permission catalog', async () => {
render( renderModal(
<PermissionSetModal <PermissionSetModal
open open
mode="create" mode="create"
@ -109,7 +125,7 @@ describe('PermissionSetModal', () => {
}) })
it('should render dataset permission catalog when resource type is dataset', () => { it('should render dataset permission catalog when resource type is dataset', () => {
render( renderModal(
<PermissionSetModal <PermissionSetModal
open open
mode="create" mode="create"
@ -122,7 +138,9 @@ describe('PermissionSetModal', () => {
expect( expect(
screen.getByText('permission.permissionSet.modal.create.dataset.title'), screen.getByText('permission.permissionSet.modal.create.dataset.title'),
).toBeInTheDocument() ).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() expect(screen.getByText(/dataset\.acl\.edit/)).toBeInTheDocument()
}) })
}) })
@ -134,7 +152,7 @@ describe('PermissionSetModal', () => {
const handleClose = vi.fn() const handleClose = vi.fn()
const handleSubmit = vi.fn() const handleSubmit = vi.fn()
render( renderModal(
<PermissionSetModal <PermissionSetModal
open open
mode="create" mode="create"
@ -168,7 +186,7 @@ describe('PermissionSetModal', () => {
const user = userEvent.setup() const user = userEvent.setup()
const handleSubmit = vi.fn() const handleSubmit = vi.fn()
render( renderModal(
<PermissionSetModal <PermissionSetModal
open open
mode="edit" mode="edit"
@ -200,7 +218,7 @@ describe('PermissionSetModal', () => {
// View mode is read-only and uses close-only footer actions. // View mode is read-only and uses close-only footer actions.
describe('Read-only Mode', () => { describe('Read-only Mode', () => {
it('should disable editing and hide confirm action in view mode', () => { it('should disable editing and hide confirm action in view mode', () => {
render( renderModal(
<PermissionSetModal <PermissionSetModal
open open
mode="view" mode="view"

View File

@ -1,18 +1,21 @@
import type { SelectorKey } from 'i18next' import type { SelectorKey } from 'i18next'
import type { AccessPolicyResourceType } from '@/models/access-control' import type { AccessPolicyResourceType } from '@/models/access-control'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import { consoleQuery } from '@/service/client'
useAppPermissionCatalog,
useDatasetPermissionCatalog,
} from '@/service/access-control/use-permission-catalog'
export const usePermissionsGroups = (resourceType: AccessPolicyResourceType) => { export const usePermissionsGroups = (resourceType: AccessPolicyResourceType) => {
const { t } = useTranslation() const { t } = useTranslation()
const { data: appPermissionCatalog } = useAppPermissionCatalog(resourceType === 'app') const permissionCatalogQueryOptions =
const { data: datasetPermissionCatalog } = useDatasetPermissionCatalog(resourceType === 'dataset') resourceType === 'app'
? consoleQuery.workspaces.current.rbac.rolePermissions.catalog.app.get.queryOptions({
const permissionCatalog = resourceType === 'app' ? appPermissionCatalog : datasetPermissionCatalog input: {},
})
: consoleQuery.workspaces.current.rbac.rolePermissions.catalog.dataset.get.queryOptions({
input: {},
})
const { data: permissionCatalog } = useQuery(permissionCatalogQueryOptions)
const groups = useMemo(() => { const groups = useMemo(() => {
// Permission keys come from the catalog API, so this is a reviewed open-key boundary with a server-provided fallback. // 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, defaultValue,
}) })
return (permissionCatalog?.groups || []).map((group) => ({ return (permissionCatalog?.groups ?? []).map((group) => ({
...group, ...group,
group_name: t(($) => $[`group.${resourceType}_acl`], { group_name: t(($) => $[`group.${resourceType}_acl`], {
ns: 'permission', ns: 'permission',
defaultValue: group.group_name, defaultValue: group.group_name,
}), }),
permissions: group.permissions.map((permission) => ({ permissions: (group.permissions ?? []).map((permission) => ({
...permission, ...permission,
name: translatePermissionName(permission.key, permission.name), name: translatePermissionName(permission.key, permission.name),
})), })),
})) }))
}, [permissionCatalog?.groups, resourceType, t]) }, [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])) const permissionMap = Object.fromEntries(allPermissions.map((p) => [p.key, p]))

View File

@ -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 { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { consoleQuery } from '@/service/client'
import { createTestQueryClient } from '@/test/query-client'
import RoleModal from '../index' import RoleModal from '../index'
const mockWorkspacePermissionCatalog = vi.hoisted(() => ({ vi.mock('react-i18next', async () => {
groups: [] as PermissionGroup[], 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', () => ({ const workspacePermissionCatalog = {
useWorkspacePermissionCatalog: () => ({ groups: [
data: { groups: mockWorkspacePermissionCatalog.groups },
}),
}))
const createPermissionGroup = (overrides: Partial<PermissionGroup> = {}): PermissionGroup => ({
group_key: 'workspace_management',
group_name: 'Workspace management',
description: '',
permissions: [
{ {
key: 'workspace.member.manage', group_key: 'workspace_management',
name: 'Manage members', group_name: 'Workspace management',
description: '', 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(<QueryClientProvider client={queryClient}>{modal}</QueryClientProvider>)
}
const createRole = (overrides: Partial<Role> = {}): Role => ({ const createRole = (overrides: Partial<Role> = {}): Role => ({
id: 'role-1', id: 'role-1',
@ -43,13 +59,12 @@ const createRole = (overrides: Partial<Role> = {}): Role => ({
describe('RoleModal', () => { describe('RoleModal', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
mockWorkspacePermissionCatalog.groups = [createPermissionGroup()]
}) })
// Rendering keeps role fields and workspace permissions in one modal form. // Rendering keeps role fields and workspace permissions in one modal form.
describe('Rendering', () => { describe('Rendering', () => {
it('should render edit mode with role values and selected permissions', () => { it('should render edit mode with role values and selected permissions', () => {
render( renderModal(
<RoleModal open mode="edit" role={createRole()} onClose={vi.fn()} onSubmit={vi.fn()} />, <RoleModal open mode="edit" role={createRole()} onClose={vi.fn()} onSubmit={vi.fn()} />,
) )
@ -58,15 +73,14 @@ describe('RoleModal', () => {
expect(screen.getByLabelText('permission.role.modal.descriptionLabel')).toHaveValue( expect(screen.getByLabelText('permission.role.modal.descriptionLabel')).toHaveValue(
'Can operate workspace', 'Can operate workspace',
) )
expect(screen.getByRole('button', { name: /Workspace management/ })).toHaveAttribute( expect(
'aria-expanded', screen.getByRole('button', { name: /Translated workspace permissions/ }),
'true', ).toHaveAttribute('aria-expanded', 'true')
) expect(screen.getByText('Manage workspace members')).toBeInTheDocument()
expect(screen.getByText(/workspace\.member\.manage/)).toBeInTheDocument()
}) })
it('should disable confirm action when role name is empty', () => { it('should disable confirm action when role name is empty', () => {
render(<RoleModal open mode="create" onClose={vi.fn()} onSubmit={vi.fn()} />) renderModal(<RoleModal open mode="create" onClose={vi.fn()} onSubmit={vi.fn()} />)
expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeDisabled() expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeDisabled()
}) })
@ -79,14 +93,14 @@ describe('RoleModal', () => {
const handleClose = vi.fn() const handleClose = vi.fn()
const handleSubmit = vi.fn() const handleSubmit = vi.fn()
render(<RoleModal open mode="create" onClose={handleClose} onSubmit={handleSubmit} />) renderModal(<RoleModal open mode="create" onClose={handleClose} onSubmit={handleSubmit} />)
await user.type(screen.getByLabelText('permission.role.modal.nameLabel'), ' Support role ') await user.type(screen.getByLabelText('permission.role.modal.nameLabel'), ' Support role ')
await user.type( await user.type(
screen.getByLabelText('permission.role.modal.descriptionLabel'), screen.getByLabelText('permission.role.modal.descriptionLabel'),
' Helps members ', ' 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' })) await user.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
expect(handleSubmit).toHaveBeenCalledTimes(1) expect(handleSubmit).toHaveBeenCalledTimes(1)
@ -102,7 +116,7 @@ describe('RoleModal', () => {
const user = userEvent.setup() const user = userEvent.setup()
const handleSubmit = vi.fn() const handleSubmit = vi.fn()
render( renderModal(
<RoleModal <RoleModal
open open
mode="edit" mode="edit"
@ -132,7 +146,7 @@ describe('RoleModal', () => {
// View mode preserves the permission display but blocks edits and confirmation. // View mode preserves the permission display but blocks edits and confirmation.
describe('Read-only Mode', () => { describe('Read-only Mode', () => {
it('should render role details as read-only in view mode', () => { it('should render role details as read-only in view mode', () => {
render( renderModal(
<RoleModal open mode="view" role={createRole()} onClose={vi.fn()} onSubmit={vi.fn()} />, <RoleModal open mode="view" role={createRole()} onClose={vi.fn()} onSubmit={vi.fn()} />,
) )

View File

@ -1,11 +1,16 @@
import type { SelectorKey } from 'i18next' import type { SelectorKey } from 'i18next'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useWorkspacePermissionCatalog } from '@/service/access-control/use-permission-catalog' import { consoleQuery } from '@/service/client'
export const useWorkspacePermissionGroups = () => { export const useWorkspacePermissionGroups = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { data: workspacePermissionCatalog } = useWorkspacePermissionCatalog() const { data: workspacePermissionCatalog } = useQuery(
consoleQuery.workspaces.current.rbac.rolePermissions.catalog.get.queryOptions({
input: {},
}),
)
const groups = useMemo(() => { const groups = useMemo(() => {
// Permission keys come from the catalog API, so these are reviewed open-key boundaries with server-provided fallbacks. // 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, defaultValue,
}) })
return (workspacePermissionCatalog?.groups || []).map((group) => ({ return (workspacePermissionCatalog?.groups ?? []).map((group) => ({
...group, ...group,
group_name: translatePermissionGroupName(group.group_key, group.group_name), group_name: translatePermissionGroupName(group.group_key, group.group_name),
permissions: group.permissions.map((permission) => ({ permissions: (group.permissions ?? []).map((permission) => ({
...permission, ...permission,
name: translatePermissionName(permission.key, permission.name), name: translatePermissionName(permission.key, permission.name),
})), })),
})) }))
}, [t, workspacePermissionCatalog?.groups]) }, [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])) const permissionMap = Object.fromEntries(allPermissions.map((p) => [p.key, p]))

View File

@ -61,10 +61,6 @@ export type PermissionGroup = {
permissions: Permission[] permissions: Permission[]
} }
export type PermissionGroups = {
groups: PermissionGroup[]
}
export type PermissionKey = string export type PermissionKey = string
type RoleType = 'workspace' | 'app' | 'dataset' type RoleType = 'workspace' | 'app' | 'dataset'

View File

@ -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<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog'),
})
}
export const useAppPermissionCatalog = (enabled?: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'app'],
queryFn: () => get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog/app'),
enabled: enabled ?? true,
})
}
export const useDatasetPermissionCatalog = (enabled?: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'dataset'],
queryFn: () =>
get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog/dataset'),
enabled: enabled ?? true,
})
}