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
}
},
"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

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 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> = {}): 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(<QueryClientProvider client={queryClient}>{modal}</QueryClientProvider>)
}
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(
<PermissionSetModal
open
mode="create"
@ -86,7 +102,7 @@ describe('PermissionSetModal', () => {
).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(
<PermissionSetModal
open
mode="create"
@ -109,7 +125,7 @@ describe('PermissionSetModal', () => {
})
it('should render dataset permission catalog when resource type is dataset', () => {
render(
renderModal(
<PermissionSetModal
open
mode="create"
@ -122,7 +138,9 @@ describe('PermissionSetModal', () => {
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(
<PermissionSetModal
open
mode="create"
@ -168,7 +186,7 @@ describe('PermissionSetModal', () => {
const user = userEvent.setup()
const handleSubmit = vi.fn()
render(
renderModal(
<PermissionSetModal
open
mode="edit"
@ -200,7 +218,7 @@ describe('PermissionSetModal', () => {
// 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(
<PermissionSetModal
open
mode="view"

View File

@ -1,18 +1,21 @@
import type { SelectorKey } from 'i18next'
import type { AccessPolicyResourceType } from '@/models/access-control'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
useAppPermissionCatalog,
useDatasetPermissionCatalog,
} from '@/service/access-control/use-permission-catalog'
import { consoleQuery } from '@/service/client'
export const usePermissionsGroups = (resourceType: AccessPolicyResourceType) => {
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]))

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 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> = {}): 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(<QueryClientProvider client={queryClient}>{modal}</QueryClientProvider>)
}
const createRole = (overrides: Partial<Role> = {}): Role => ({
id: 'role-1',
@ -43,13 +59,12 @@ const createRole = (overrides: Partial<Role> = {}): 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(
<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(
'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(<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()
})
@ -79,14 +93,14 @@ describe('RoleModal', () => {
const handleClose = 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.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(
<RoleModal
open
mode="edit"
@ -132,7 +146,7 @@ describe('RoleModal', () => {
// 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(
<RoleModal open mode="view" role={createRole()} onClose={vi.fn()} onSubmit={vi.fn()} />,
)

View File

@ -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]))

View File

@ -61,10 +61,6 @@ export type PermissionGroup = {
permissions: Permission[]
}
export type PermissionGroups = {
groups: PermissionGroup[]
}
export type PermissionKey = string
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,
})
}