From 35696c6b2e444c72b9ed1b2ed314e4862c1b5e01 Mon Sep 17 00:00:00 2001 From: twwu Date: Sat, 9 May 2026 14:47:38 +0800 Subject: [PATCH] feat: implement role management features with hooks and UI components --- .../permissions-page/helpers.ts | 27 +++ .../account-setting/permissions-page/hooks.ts | 14 ++ .../permissions-page/index.tsx | 144 ++++----------- .../permissions-page/role-list/index.tsx | 17 +- .../permissions-page/role-list/row-menu.tsx | 25 ++- .../permissions-page/role-list/row.tsx | 13 +- .../permissions-page/role-modal/hooks.ts | 18 ++ .../permissions-page/role-modal/index.tsx | 34 ++-- .../role-modal/permission-field.tsx | 19 +- .../role-modal/permission-picker.tsx | 38 ++-- .../role-modal/permissions-data.ts | 66 ------- web/models/access-control.ts | 168 ++++++++++++++++-- .../access-control/use-permission-catalog.ts | 28 +++ .../access-control/use-workspace-roles.ts | 59 ++++++ 14 files changed, 417 insertions(+), 253 deletions(-) create mode 100644 web/app/components/header/account-setting/permissions-page/helpers.ts create mode 100644 web/app/components/header/account-setting/permissions-page/hooks.ts create mode 100644 web/app/components/header/account-setting/permissions-page/role-modal/hooks.ts delete mode 100644 web/app/components/header/account-setting/permissions-page/role-modal/permissions-data.ts create mode 100644 web/service/access-control/use-permission-catalog.ts create mode 100644 web/service/access-control/use-workspace-roles.ts diff --git a/web/app/components/header/account-setting/permissions-page/helpers.ts b/web/app/components/header/account-setting/permissions-page/helpers.ts new file mode 100644 index 0000000000..e511dd46b4 --- /dev/null +++ b/web/app/components/header/account-setting/permissions-page/helpers.ts @@ -0,0 +1,27 @@ +import type { RoleListGroup } from './role-list' +import type { RoleListResponse } from '@/models/access-control' + +export const formatRoleGroups = (roleListResponse: RoleListResponse | undefined): RoleListGroup[] => { + if (!roleListResponse) + return [] + const result: RoleListGroup[] = [] + const builtinRoles = roleListResponse.data.filter(role => role.is_builtin) + const customRoles = roleListResponse.data.filter(role => !role.is_builtin) + if (builtinRoles.length > 0) { + result.push({ + id: 'builtin', + category: 'global_system_default', + title: 'System Roles', + items: builtinRoles, + }) + } + if (customRoles.length > 0) { + result.push({ + id: 'custom', + category: 'global_custom', + title: 'Custom Roles', + items: customRoles, + }) + } + return result +} diff --git a/web/app/components/header/account-setting/permissions-page/hooks.ts b/web/app/components/header/account-setting/permissions-page/hooks.ts new file mode 100644 index 0000000000..7aceefda2c --- /dev/null +++ b/web/app/components/header/account-setting/permissions-page/hooks.ts @@ -0,0 +1,14 @@ +import type { PaginationParameters } from '@/models/access-control' +import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' +import { formatRoleGroups } from './helpers' + +export const useRoleGroups = (params?: PaginationParameters) => { + const { data: roleList, isLoading } = useWorkspaceRoleList(params) + + const roleGroups = formatRoleGroups(roleList) + + return { + roleGroups, + isLoading, + } +} diff --git a/web/app/components/header/account-setting/permissions-page/index.tsx b/web/app/components/header/account-setting/permissions-page/index.tsx index 7cf90f7541..9793b5f4ca 100644 --- a/web/app/components/header/account-setting/permissions-page/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/index.tsx @@ -1,118 +1,15 @@ 'use client' -import type { Role, RoleListGroup } from './role-list' -import type { RoleModalMode } from './role-modal' +import type { RoleModalMode, submitRoleData } from './role-modal' +import type { Role } from '@/models/access-control' import { Button } from '@langgenius/dify-ui/button' +import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' +import { useCreateWorkspaceRole, useUpdateWorkspaceRole } from '@/service/access-control/use-workspace-roles' +import { useRoleGroups } from './hooks' import RoleList from './role-list' import RoleModal from './role-modal' -const MOCK_ROLE_GROUPS: RoleListGroup[] = [ - { - id: 'system', - type: 'system', - title: 'System roles', - items: [ - { - id: 'owner', - name: 'Owner', - description: 'Full access to all workspace features and settings.', - permissions: [ - 'manage_model_providers', - 'manage_members', - 'manage_roles_permissions', - 'manage_billing', - 'manage_data_sources', - 'manage_api_extensions', - 'create_apps', - 'view_all_apps', - 'delete_any_app', - 'create_knowledge_bases', - 'view_all_knowledge_bases', - 'delete_any_knowledge_base', - 'view_all_app_logs', - 'cross_app_log_access', - 'view_sensitive_fields', - 'install_plugins', - 'uninstall_plugins', - ], - }, - { - id: 'admin', - name: 'Admin', - description: 'Manage apps, update settings, manage members and permissions.', - permissions: [ - 'manage_members', - 'manage_roles_permissions', - 'manage_data_sources', - 'create_apps', - 'view_all_apps', - 'create_knowledge_bases', - 'view_all_knowledge_bases', - ], - }, - { - id: 'editor', - name: 'Editor', - description: 'Create and edit resources (knowledge bases, apps, plugins) without workspace settings access.', - permissions: [ - 'create_apps', - 'view_all_apps', - 'create_knowledge_bases', - 'view_all_knowledge_bases', - 'install_plugins', - ], - }, - { - id: 'member', - name: 'Member', - description: 'Limited permissions within the workspace.', - permissions: ['view_all_apps', 'view_all_knowledge_bases'], - }, - { - id: 'none', - name: 'No Permission', - description: 'Default role with no permissions assigned.', - permissions: [], - }, - ], - }, - { - id: 'custom', - type: 'custom', - title: 'Custom roles', - items: [ - { - id: 'executive', - name: 'Executive', - description: 'Unrestricted access to all workspace operations.', - permissions: [ - 'manage_model_providers', - 'manage_members', - 'manage_roles_permissions', - 'manage_billing', - 'create_apps', - 'view_all_apps', - 'create_knowledge_bases', - 'view_all_knowledge_bases', - ], - }, - { - id: 'employee', - name: 'Employee', - description: 'Access to payroll bot and internal project knowledge bases.', - permissions: ['view_all_apps', 'view_all_knowledge_bases'], - }, - { - id: 'partner', - name: 'Partner', - description: 'View external-facing apps: product info, feedback forms, and visitor registration.', - permissions: ['view_all_apps'], - }, - ], - }, -] - type ModalState = { mode: RoleModalMode role?: Role @@ -121,6 +18,11 @@ type ModalState = { const PermissionsPage = () => { const [modalState, setModalState] = useState(null) + const { roleGroups } = useRoleGroups() + + const { mutateAsync: createWorkspaceRole } = useCreateWorkspaceRole() + const { mutateAsync: updateWorkspaceRole } = useUpdateWorkspaceRole() + const openCreate = useCallback(() => { setModalState({ mode: 'create' }) }, []) @@ -136,10 +38,28 @@ const PermissionsPage = () => { const closeModal = useCallback(() => setModalState(null), []) const handleSubmit = useCallback( - (_data: { name: string, description: string, permissions: string[] }) => { - // TODO: wire up to API when backend is ready + (data: submitRoleData) => { + const { name, description, permissionKeys } = data + const mode = modalState?.mode ?? '' + const roleId = modalState?.role?.id ?? '' + if (mode === 'create') { + createWorkspaceRole({ name, description, permission_keys: permissionKeys }, { + onSuccess: () => { + toast.success('Role created successfully') + closeModal() + }, + }) + } + else if (mode === 'edit') { + updateWorkspaceRole({ id: roleId, name, description, permission_keys: permissionKeys }, { + onSuccess: () => { + toast.success('Role updated successfully') + closeModal() + }, + }) + } }, - [], + [createWorkspaceRole, updateWorkspaceRole, closeModal, modalState], ) return ( @@ -165,7 +85,7 @@ const PermissionsPage = () => { diff --git a/web/app/components/header/account-setting/permissions-page/role-list/index.tsx b/web/app/components/header/account-setting/permissions-page/role-list/index.tsx index 0b5c521eb8..ddf4417456 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/index.tsx @@ -1,20 +1,12 @@ 'use client' +import type { Role, RoleCategory } from '@/models/access-control' import { cn } from '@langgenius/dify-ui/cn' import Row from './row' -export type Role = { - id: string - name: string - description: string - permissions?: string[] -} - -export type RoleType = 'system' | 'custom' - export type RoleListGroup = { id: string - type: RoleType + category: RoleCategory title: string items: Role[] } @@ -24,7 +16,6 @@ export type RoleListProps = { className?: string onView?: (role: Role) => void onEdit?: (role: Role) => void - onDelete?: (role: Role) => void } const RoleList = ({ @@ -32,7 +23,6 @@ const RoleList = ({ className, onView, onEdit, - onDelete, }: RoleListProps) => { return (
@@ -53,11 +43,10 @@ const RoleList = ({ )} name={row.name} description={row.description} - roleType={group.type} + roleCategory={group.category} role={row} onView={onView} onEdit={onEdit} - onDelete={onDelete} /> ))}
diff --git a/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx b/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx index 2a4c4cc6c7..03e809f7dc 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx @@ -1,5 +1,5 @@ 'use client' -import type { Role, RoleType } from '.' +import type { Role, RoleCategory } from '@/models/access-control' import { DropdownMenu, DropdownMenuContent, @@ -7,26 +7,28 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' import ActionButton from '@/app/components/base/action-button' +import { useDeleteWorkspaceRole } from '@/service/access-control/use-workspace-roles' type RowMenuProps = { - roleType: RoleType + roleCategory: RoleCategory role: Role onView?: (role: Role) => void onEdit?: (role: Role) => void - onDelete?: (role: Role) => void } const RowMenu = ({ - roleType, + roleCategory, role, onView, onEdit, - onDelete, }: RowMenuProps) => { const [open, setOpen] = useState(false) + const { mutateAsync: deleteRole } = useDeleteWorkspaceRole() + const handleView = useCallback(() => onView?.(role), [onView, role]) const handleEdit = useCallback(() => onEdit?.(role), [onEdit, role]) @@ -35,7 +37,14 @@ const RowMenu = ({ // TODO: wire up to API when backend is ready }, []) - const handleDelete = useCallback(() => onDelete?.(role), [onDelete, role]) + const handleDelete = useCallback(() => { + deleteRole(role.id, { + onSuccess: () => { + toast.success('Role deleted successfully') + setOpen(false) + }, + }) + }, [deleteRole, role.id]) return ( @@ -44,14 +53,14 @@ const RowMenu = ({ { - roleType === 'system' && ( + roleCategory === 'global_system_default' && ( View ) } { - roleType === 'custom' && ( + roleCategory === 'global_custom' && ( <> Edit diff --git a/web/app/components/header/account-setting/permissions-page/role-list/row.tsx b/web/app/components/header/account-setting/permissions-page/role-list/row.tsx index 82b323de49..f8c9a1bfff 100644 --- a/web/app/components/header/account-setting/permissions-page/role-list/row.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-list/row.tsx @@ -1,4 +1,4 @@ -import type { Role, RoleType } from '.' +import type { Role, RoleCategory } from '@/models/access-control' import { cn } from '@langgenius/dify-ui/cn' import { memo } from 'react' import RowMenu from './row-menu' @@ -7,22 +7,20 @@ type RowProps = { className?: string name: string description: string - roleType: RoleType + roleCategory: RoleCategory role: Role onView?: (role: Role) => void onEdit?: (role: Role) => void - onDelete?: (role: Role) => void } const Row = ({ className, name, description, - roleType, + roleCategory, role, onView, onEdit, - onDelete, }: RowProps) => { return (

- {description} + {description || 'No description'}

) 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 new file mode 100644 index 0000000000..2d006a58e3 --- /dev/null +++ b/web/app/components/header/account-setting/permissions-page/role-modal/hooks.ts @@ -0,0 +1,18 @@ +import { useWorkspacePermissionCatalog } from '@/service/access-control/use-permission-catalog' + +export const useWorkspacePermissionGroups = () => { + const { data: workspacePermissionCatalog } = useWorkspacePermissionCatalog() + + const groups = workspacePermissionCatalog?.groups || [] + + const allPermissions = groups.flatMap(g => g.permissions) || [] + + const permissionMap = Object.fromEntries( + allPermissions.map(p => [p.key, p]), + ) + + return { + groups, + permissionMap, + } +} diff --git a/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx b/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx index 6698318785..609ec152da 100644 --- a/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx @@ -1,6 +1,6 @@ 'use client' -import type { Role } from '../role-list' +import type { Role } from '@/models/access-control' import { Button } from '@langgenius/dify-ui/button' import { Dialog, @@ -9,23 +9,25 @@ import { DialogDescription, DialogTitle, } from '@langgenius/dify-ui/dialog' -import { useState } from 'react' +import { useCallback, useState } from 'react' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' import PermissionField from './permission-field' export type RoleModalMode = 'create' | 'view' | 'edit' -export type RoleModalRole = Role & { - permissions?: string[] +export type submitRoleData = { + name: string + description?: string + permissionKeys?: string[] } export type RoleModalProps = { mode: RoleModalMode open: boolean - role?: RoleModalRole + role?: Role onClose: () => void - onSubmit?: (data: { name: string, description: string, permissions: string[] }) => void + onSubmit?: (data: submitRoleData) => void } const TITLES: Record = { @@ -52,13 +54,21 @@ const RoleModal = ({ }: RoleModalProps) => { const [name, setName] = useState(role?.name ?? '') const [desc, setDesc] = useState(role?.description ?? '') - const [permissions, setPermissions] = useState(role?.permissions ?? []) + const [permissionKeys, setPermissionKeys] = useState(role?.permission_keys ?? []) const readonly = mode === 'view' const { title, description } = TITLES[mode] + const onRoleNameChange = useCallback((e: React.ChangeEvent) => { + setName(e.target.value) + }, []) + + const onRoleDescChange = useCallback((e: React.ChangeEvent) => { + setDesc(e.target.value) + }, []) + const handleSubmit = () => { - onSubmit?.({ name: name.trim(), description: desc.trim(), permissions }) + onSubmit?.({ name: name.trim(), description: desc.trim(), permissionKeys }) onClose() } @@ -94,7 +104,7 @@ const RoleModal = ({ setName(e.target.value)} + onChange={onRoleNameChange} placeholder="e.g. Marketing Lead" disabled={readonly} /> @@ -106,15 +116,15 @@ const RoleModal = ({