From 5f4b086e39c19991b1494de09e888724193eca9b Mon Sep 17 00:00:00 2001 From: twwu Date: Mon, 27 Apr 2026 16:41:48 +0800 Subject: [PATCH] feat: add permission management modal and permission picker for access rules --- .../access-rules-page/access-rule-row.tsx | 1 + .../access-rules-page/index.tsx | 119 ++++++++- .../permission-set-modal/index.tsx | 225 ++++++++++++++++++ .../permission-picker.tsx | 197 +++++++++++++++ .../permission-set-modal/permissions-data.ts | 103 ++++++++ 5 files changed, 635 insertions(+), 10 deletions(-) create mode 100644 web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx create mode 100644 web/app/components/header/account-setting/access-rules-page/permission-set-modal/permission-picker.tsx create mode 100644 web/app/components/header/account-setting/access-rules-page/permission-set-modal/permissions-data.ts diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx index e16704e0e6..a652c38b57 100644 --- a/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx +++ b/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx @@ -15,6 +15,7 @@ export type AccessRule = { name: string description: string assignedRoles: AssignedRole[] + permissions: string[] } export type AccessRuleRowProps = { diff --git a/web/app/components/header/account-setting/access-rules-page/index.tsx b/web/app/components/header/account-setting/access-rules-page/index.tsx index 67a3dd05cd..5f7371f3be 100644 --- a/web/app/components/header/account-setting/access-rules-page/index.tsx +++ b/web/app/components/header/account-setting/access-rules-page/index.tsx @@ -1,11 +1,13 @@ 'use client' import type { AccessRule } from './access-rule-row' +import type { PermissionSetFormValues, PermissionSetModalMode } from './permission-set-modal' +import type { ResourceType } from './permission-set-modal/permissions-data' import { useCallback, useState } from 'react' import AccessRuleSection from './access-rule-section' import AddRuleTargetsModal from './add-rule-targets-modal' +import PermissionSetModal from './permission-set-modal' -// todo: replace with API data when backend is ready const APP_ACCESS_RULES: AccessRule[] = [ { id: 'app-full-access', @@ -17,6 +19,17 @@ const APP_ACCESS_RULES: AccessRule[] = [ { id: 'app-admin', name: 'App Admin' }, { id: 'executive', name: 'Executive' }, ], + permissions: [ + 'app.editing_and_layout', + 'app.test_and_debug', + 'app.delete', + 'app.import_export_dsl', + 'app.release_version_management', + 'app.annotation_management', + 'app.api_management.toggle', + 'app.api_management.create_key', + 'app.api_management.delete_key', + ], }, { id: 'app-can-edit', @@ -26,6 +39,11 @@ const APP_ACCESS_RULES: AccessRule[] = [ { id: 'app-editor', name: 'App Editor' }, { id: 'it-staff', name: 'IT Staff' }, ], + permissions: [ + 'app.editing_and_layout', + 'app.test_and_debug', + 'app.release_version_management', + ], }, { id: 'app-can-view-and-use', @@ -36,6 +54,9 @@ const APP_ACCESS_RULES: AccessRule[] = [ { id: 'ops-staff', name: 'Ops Staff' }, { id: 'member', name: 'Member' }, ], + permissions: [ + 'app.test_and_debug', + ], }, { id: 'app-can-preview', @@ -44,10 +65,10 @@ const APP_ACCESS_RULES: AccessRule[] = [ assignedRoles: [ { id: 'partner', name: 'Partner' }, ], + permissions: [], }, ] -// todo: replace with API data when backend is ready const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ { id: 'kb-full-access', @@ -59,6 +80,16 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ { id: 'kb-admin', name: 'KB Admin' }, { id: 'executive', name: 'Executive' }, ], + permissions: [ + 'kb.view', + 'kb.edit_configuration', + 'kb.manage_documents.add', + 'kb.manage_documents.delete', + 'kb.manage_documents.download', + 'kb.import_export_pipeline', + 'kb.pipeline_publishing_versioning', + 'kb.delete', + ], }, { id: 'kb-can-edit', @@ -69,6 +100,12 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ { id: 'ops-staff', name: 'Ops Staff' }, { id: 'it-staff', name: 'IT Staff' }, ], + permissions: [ + 'kb.edit_configuration', + 'kb.manage_documents.add', + 'kb.manage_documents.delete', + 'kb.manage_documents.download', + ], }, { id: 'kb-can-view', @@ -77,6 +114,7 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ assignedRoles: [ { id: 'member', name: 'Member' }, ], + permissions: ['kb.view'], }, { id: 'kb-can-preview', @@ -85,6 +123,7 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ assignedRoles: [ { id: 'partner', name: 'Partner' }, ], + permissions: [], }, { id: 'kb-can-test', @@ -93,20 +132,33 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ assignedRoles: [ { id: 'tester', name: 'Tester' }, ], + permissions: ['kb.view'], }, ] +type PermissionSetModalState = { + mode: PermissionSetModalMode + resourceType: ResourceType + initialValues?: PermissionSetFormValues +} + const AccessRulesPage = () => { const [addingRule, setAddingRule] = useState(null) - - const handleAddRole = useCallback((rule: AccessRule) => { - setAddingRule(rule) - }, []) + const [permissionSetModalState, setPermissionSetModalState] + = useState(null) const closeAddModal = useCallback(() => { setAddingRule(null) }, []) + const closePermissionSetModal = useCallback(() => { + setPermissionSetModalState(null) + }, []) + + const handleAddRole = useCallback((rule: AccessRule) => { + setAddingRule(rule) + }, []) + const handleAddSubmit = useCallback( (_selection: { roleIds: string[], memberIds: string[] }) => { // TODO: wire up to API when backend is ready. @@ -114,10 +166,47 @@ const AccessRulesPage = () => { [], ) + const handleCreate = useCallback((resourceType: ResourceType) => { + setPermissionSetModalState({ mode: 'create', resourceType }) + }, []) + + const handleEdit = useCallback( + (resourceType: ResourceType, rule: AccessRule) => { + setPermissionSetModalState({ + mode: 'edit', + resourceType, + initialValues: { + name: rule.name, + description: rule.description, + permissions: rule.permissions, + }, + }) + }, + [], + ) + + const handlePermissionSetSubmit = useCallback( + (_values: PermissionSetFormValues) => { + // TODO: wire up to API when backend is ready. + }, + [], + ) + const noop = useCallback(() => { // TODO: wire up to API when backend is ready. }, []) + const createApp = useCallback(() => handleCreate('app'), [handleCreate]) + const createKb = useCallback(() => handleCreate('knowledge_base'), [handleCreate]) + const editApp = useCallback( + (rule: AccessRule) => handleEdit('app', rule), + [handleEdit], + ) + const editKb = useCallback( + (rule: AccessRule) => handleEdit('knowledge_base', rule), + [handleEdit], + ) + return ( <>
@@ -125,8 +214,8 @@ const AccessRulesPage = () => { title="App Access Rules" rules={APP_ACCESS_RULES} createButtonLabel="Create App permission set" - onCreate={noop} - onEditRule={noop} + onCreate={createApp} + onEditRule={editApp} onCopyRule={noop} onDeleteRule={noop} onAddRole={handleAddRole} @@ -136,8 +225,8 @@ const AccessRulesPage = () => { title="Knowledge Base Access Rules" rules={KNOWLEDGE_BASE_ACCESS_RULES} createButtonLabel="Create KB permission set" - onCreate={noop} - onEditRule={noop} + onCreate={createKb} + onEditRule={editKb} onCopyRule={noop} onDeleteRule={noop} onAddRole={handleAddRole} @@ -154,6 +243,16 @@ const AccessRulesPage = () => { onSubmit={handleAddSubmit} /> )} + {permissionSetModalState && ( + + )} ) } diff --git a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx new file mode 100644 index 0000000000..04059c060e --- /dev/null +++ b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx @@ -0,0 +1,225 @@ +'use client' + +import type { ResourceType } from './permissions-data' +import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' +import { + Dialog, + DialogCloseButton, + DialogContent, + DialogDescription, + DialogTitle, +} from '@langgenius/dify-ui/dialog' +import { useMemo, useState } from 'react' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import PermissionPicker from './permission-picker' +import { getPermissionMap } from './permissions-data' + +export type PermissionSetModalMode = 'create' | 'edit' + +export type PermissionSetFormValues = { + name: string + description: string + permissions: string[] +} + +export type PermissionSetModalProps = { + open: boolean + mode: PermissionSetModalMode + resourceType: ResourceType + initialValues?: Partial + onClose: () => void + onSubmit: (values: PermissionSetFormValues) => void +} + +const RESOURCE_LABEL: Record = { + app: 'App', + knowledge_base: 'Knowledge Base', +} + +const buildTitle = (mode: PermissionSetModalMode, resource: ResourceType): string => { + const verb = mode === 'create' ? 'Create' : 'Edit' + return `${verb} ${RESOURCE_LABEL[resource]} permission set` +} + +const buildDescription = (mode: PermissionSetModalMode, resource: ResourceType): string => { + if (mode === 'edit') + return 'Modify the name, description, and permissions granted for this permission set.' + if (resource === 'app') + return 'Create an app permission set that can be referenced in access rules for quick authorization.' + return 'Create a knowledge base permission set that can be referenced in access rules for quick authorization.' +} + +type PermissionSetModalBodyProps = Omit + +const PermissionSetModalBody = ({ + mode, + resourceType, + initialValues, + onClose, + onSubmit, +}: PermissionSetModalBodyProps) => { + const [name, setName] = useState(initialValues?.name ?? '') + const [description, setDescription] = useState(initialValues?.description ?? '') + const [permissions, setPermissions] = useState(initialValues?.permissions ?? []) + + const permissionMap = useMemo(() => getPermissionMap(resourceType), [resourceType]) + + const trimmedName = name.trim() + const canSubmit = trimmedName.length > 0 + + const handleConfirm = () => { + if (!canSubmit) + return + onSubmit({ + name: trimmedName, + description: description.trim(), + permissions, + }) + onClose() + } + + const handleRemovePermission = (id: string) => { + setPermissions(prev => prev.filter(p => p !== id)) + } + + return ( + +
+ +
+ + {buildTitle(mode, resourceType)} + + + {buildDescription(mode, resourceType)} + +
+
+ +
+ +
+
+ + setName(e.target.value)} + placeholder="e.g. Can export DSL" + /> +
+ +
+ +