From 6a2bb145e38946d8ed63a075062d02c918727ea2 Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 23 Apr 2026 16:16:29 +0800 Subject: [PATCH] feat: implement role management UI in permissions page --- .../header/account-setting/index.tsx | 2 +- .../permissions-page/index.tsx | 156 ++++++++++++++++ .../permissions-page/role-list/index.tsx | 70 +++++++ .../permissions-page/role-list/row-menu.tsx | 74 ++++++++ .../permissions-page/role-list/row.tsx | 53 ++++++ .../permissions-page/role-modal/index.tsx | 151 +++++++++++++++ .../role-modal/permission-field.tsx | 62 +++++++ .../role-modal/permission-picker.tsx | 173 ++++++++++++++++++ .../role-modal/permissions-data.ts | 66 +++++++ 9 files changed, 806 insertions(+), 1 deletion(-) create mode 100644 web/app/components/header/account-setting/permissions-page/role-list/index.tsx create mode 100644 web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx create mode 100644 web/app/components/header/account-setting/permissions-page/role-list/row.tsx create mode 100644 web/app/components/header/account-setting/permissions-page/role-modal/index.tsx create mode 100644 web/app/components/header/account-setting/permissions-page/role-modal/permission-field.tsx create mode 100644 web/app/components/header/account-setting/permissions-page/role-modal/permission-picker.tsx create mode 100644 web/app/components/header/account-setting/permissions-page/role-modal/permissions-data.ts diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index f53098660b..3d552c84de 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -78,7 +78,7 @@ export default function AccountSetting({ key: ACCOUNT_SETTING_TAB.PERMISSIONS, name: t('settings.permissions', { ns: 'common' }), icon: , - activeIcon: , + activeIcon: , }, { key: ACCOUNT_SETTING_TAB.ACCESS_RULES, 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 54272a03ed..7cf90f7541 100644 --- a/web/app/components/header/account-setting/permissions-page/index.tsx +++ b/web/app/components/header/account-setting/permissions-page/index.tsx @@ -1,6 +1,147 @@ +'use client' + +import type { Role, RoleListGroup } from './role-list' +import type { RoleModalMode } from './role-modal' import { Button } from '@langgenius/dify-ui/button' +import { useCallback, useState } from 'react' +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 +} | null const PermissionsPage = () => { + const [modalState, setModalState] = useState(null) + + const openCreate = useCallback(() => { + setModalState({ mode: 'create' }) + }, []) + + const handleView = useCallback((role: Role) => { + setModalState({ mode: 'view', role }) + }, []) + + const handleEdit = useCallback((role: Role) => { + setModalState({ mode: 'edit', role }) + }, []) + + const closeModal = useCallback(() => setModalState(null), []) + + const handleSubmit = useCallback( + (_data: { name: string, description: string, permissions: string[] }) => { + // TODO: wire up to API when backend is ready + }, + [], + ) + return ( <>
@@ -17,12 +158,27 @@ const PermissionsPage = () => {
+ + {modalState && ( + + )} ) } 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 new file mode 100644 index 0000000000..48fccb64b9 --- /dev/null +++ b/web/app/components/header/account-setting/permissions-page/role-list/index.tsx @@ -0,0 +1,70 @@ +'use client' + +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 + title: string + items: Role[] +} + +export type RoleListProps = { + groups: RoleListGroup[] + className?: string + onView?: (role: Role) => void + onEdit?: (role: Role) => void + onDelete?: (role: Role) => void +} + +const RoleList = ({ + groups, + className, + onView, + onEdit, + onDelete, +}: RoleListProps) => { + return ( +
+ {groups.map((group, groupIndex) => ( +
0 && 'mt-6')} + > +

+ {group.title} +

+
+ {group.items.map((row, rowIndex) => ( + 0 && 'border-t border-divider-subtle', + )} + name={row.name} + description={row.description} + roleType={group.type} + role={row} + onView={onView} + onEdit={onEdit} + onDelete={onDelete} + /> + ))} +
+
+ ))} +
+ ) +} + +export default RoleList 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 new file mode 100644 index 0000000000..2a4c4cc6c7 --- /dev/null +++ b/web/app/components/header/account-setting/permissions-page/role-list/row-menu.tsx @@ -0,0 +1,74 @@ +'use client' +import type { Role, RoleType } from '.' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@langgenius/dify-ui/dropdown-menu' +import { useCallback, useState } from 'react' +import ActionButton from '@/app/components/base/action-button' + +type RowMenuProps = { + roleType: RoleType + role: Role + onView?: (role: Role) => void + onEdit?: (role: Role) => void + onDelete?: (role: Role) => void +} + +const RowMenu = ({ + roleType, + role, + onView, + onEdit, + onDelete, +}: RowMenuProps) => { + const [open, setOpen] = useState(false) + + const handleView = useCallback(() => onView?.(role), [onView, role]) + + const handleEdit = useCallback(() => onEdit?.(role), [onEdit, role]) + + const handleDuplicate = useCallback(() => { + // TODO: wire up to API when backend is ready + }, []) + + const handleDelete = useCallback(() => onDelete?.(role), [onDelete, role]) + + return ( + + }> + + + + { + roleType === 'system' && ( + + View + + ) + } + { + roleType === 'custom' && ( + <> + + Edit + + + Duplicate + + + + Delete + + + ) + } + + + ) +} + +export default RowMenu 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 new file mode 100644 index 0000000000..d4f67cd725 --- /dev/null +++ b/web/app/components/header/account-setting/permissions-page/role-list/row.tsx @@ -0,0 +1,53 @@ +import type { Role, RoleType } from '.' +import { cn } from '@langgenius/dify-ui/cn' +import { memo } from 'react' +import RowMenu from './row-menu' + +type RowProps = { + className?: string + name: string + description: string + roleType: RoleType + role: Role + onView?: (role: Role) => void + onEdit?: (role: Role) => void + onDelete?: (role: Role) => void +} + +const Row = ({ + className, + name, + description, + roleType, + role, + onView, + onEdit, + onDelete, +}: RowProps) => { + return ( +
+
+
+ {name} +
+

+ {description} +

+
+ +
+ ) +} + +export default memo(Row) 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 new file mode 100644 index 0000000000..6698318785 --- /dev/null +++ b/web/app/components/header/account-setting/permissions-page/role-modal/index.tsx @@ -0,0 +1,151 @@ +'use client' + +import type { Role } from '../role-list' +import { Button } from '@langgenius/dify-ui/button' +import { + Dialog, + DialogCloseButton, + DialogContent, + DialogDescription, + DialogTitle, +} from '@langgenius/dify-ui/dialog' +import { 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 RoleModalProps = { + mode: RoleModalMode + open: boolean + role?: RoleModalRole + onClose: () => void + onSubmit?: (data: { name: string, description: string, permissions: string[] }) => void +} + +const TITLES: Record = { + create: { + title: 'Create Role', + description: 'Create a role and assign permissions', + }, + edit: { + title: 'Edit Role', + description: 'Edit role details and permissions', + }, + view: { + title: 'View Role', + description: 'View role details and permissions', + }, +} + +const RoleModal = ({ + mode, + open, + role, + onClose, + onSubmit, +}: RoleModalProps) => { + const [name, setName] = useState(role?.name ?? '') + const [desc, setDesc] = useState(role?.description ?? '') + const [permissions, setPermissions] = useState(role?.permissions ?? []) + + const readonly = mode === 'view' + const { title, description } = TITLES[mode] + + const handleSubmit = () => { + onSubmit?.({ name: name.trim(), description: desc.trim(), permissions }) + onClose() + } + + return ( + { + if (!nextOpen) + onClose() + }} + > + +
+ +
+ + {title} + + + {description} + +
+
+
+
+
+ + setName(e.target.value)} + placeholder="e.g. Marketing Lead" + disabled={readonly} + /> +
+
+ +