diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx index ace93a9d92..56a3842178 100644 --- a/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx +++ b/web/app/components/header/account-setting/access-rules-page/access-rule-row-menu.tsx @@ -1,5 +1,6 @@ 'use client' +import type { AccessPolicy } from '@/models/access-control' import { DropdownMenu, DropdownMenuContent, @@ -7,22 +8,43 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' -import { useState } from 'react' +import { toast } from '@langgenius/dify-ui/toast' +import { useCallback, useState } from 'react' import ActionButton from '@/app/components/base/action-button' +import { useCopyAccessRule, useDeleteAccessRule } from '@/service/access-control/use-workspace-access-rules' export type AccessRuleRowMenuProps = { + rule: AccessPolicy onEdit?: () => void - onCopy?: () => void - onDelete?: () => void } const AccessRuleRowMenu = ({ + rule, onEdit, - onCopy, - onDelete, }: AccessRuleRowMenuProps) => { const [open, setOpen] = useState(false) + const { mutateAsync: copyAccessRule } = useCopyAccessRule(rule.resource_type) + const { mutateAsync: deleteAccessRule } = useDeleteAccessRule(rule.resource_type) + + const handleCopyRules = useCallback(() => { + copyAccessRule(rule.id, { + onSuccess: () => { + toast.success('Access rule copied successfully') + setOpen(false) + }, + }) + }, [copyAccessRule, rule.id]) + + const handleDelete = useCallback(() => { + deleteAccessRule(rule.id, { + onSuccess: () => { + toast.success('Access rule deleted successfully') + setOpen(false) + }, + }) + }, [deleteAccessRule, rule.id]) + return ( Copy @@ -57,7 +79,7 @@ const AccessRuleRowMenu = ({ Delete 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 bbd2201bd9..1f93f09177 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 @@ -1,32 +1,19 @@ 'use client' +import type { AccessPolicyWithBindings } from '@/models/access-control' import { cn } from '@langgenius/dify-ui/cn' +import { toast } from '@langgenius/dify-ui/toast' import { memo, useCallback } from 'react' +import { useUpdateAppAccessRuleBindings, useUpdateDatasetAccessRuleBindings } from '@/service/access-control/use-workspace-access-rules' import AccessRuleRowMenu from './access-rule-row-menu' import RoleTag from './role-tag' -export type AssignedRole = { - id: string - name: string -} - -export type AccessRule = { - id: string - name: string - description: string - assignedRoles: AssignedRole[] - permissions: string[] -} - export type AccessRuleRowProps = { - rule: AccessRule + rule: AccessPolicyWithBindings className?: string showMenu?: boolean - onEdit?: (rule: AccessRule) => void - onCopy?: (rule: AccessRule) => void - onDelete?: (rule: AccessRule) => void - onAddRole?: (rule: AccessRule) => void - onRemoveRole?: (rule: AccessRule, role: AssignedRole) => void + onEdit?: (rule: AccessPolicyWithBindings) => void + onAddRole?: (rule: AccessPolicyWithBindings) => void } const AccessRuleRow = ({ @@ -34,38 +21,61 @@ const AccessRuleRow = ({ className, showMenu = true, onEdit, - onCopy, - onDelete, onAddRole, - onRemoveRole, }: AccessRuleRowProps) => { + const { policy, role_ids } = rule + const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule]) - const handleCopy = useCallback(() => onCopy?.(rule), [onCopy, rule]) - const handleDelete = useCallback(() => onDelete?.(rule), [onDelete, rule]) const handleAddRole = useCallback(() => onAddRole?.(rule), [onAddRole, rule]) + const { mutateAsync: updateAppAccessRuleBindings } = useUpdateAppAccessRuleBindings() + const { mutateAsync: updateDatasetAccessRuleBindings } = useUpdateDatasetAccessRuleBindings() + + const handleRemoveRole = useCallback((roleId: string) => { + const payload = { + id: policy.id, + role_ids: role_ids.filter(id => id !== roleId), + account_ids: [], + } + if (policy.resource_type === 'app') { + updateAppAccessRuleBindings(payload, { + onSuccess: () => { + toast.success('Access rule updated successfully') + }, + }) + } + else if (policy.resource_type === 'dataset') { + updateDatasetAccessRuleBindings(payload, { + onSuccess: () => { + toast.success('Access rule updated successfully') + }, + }) + } + }, [policy.id, policy.resource_type, role_ids, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings]) + return (
- {rule.name} + {policy.name}

- {rule.description} + {policy.description}

- {rule.assignedRoles.map(role => ( + {role_ids.map(role => ( onRemoveRole(rule, role) : undefined} + key={role} + id={role} + label={role} + onRemove={handleRemoveRole} /> ))}
diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx index 68664bff77..0631b3ef96 100644 --- a/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx +++ b/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx @@ -1,21 +1,18 @@ 'use client' -import type { AccessRule, AssignedRole } from './access-rule-row' +import type { AccessPolicyWithBindings } from '@/models/access-control' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { memo } from 'react' import AccessRuleRow from './access-rule-row' -export type AccessRuleSectionProps = { +type AccessRuleSectionProps = { title: string - rules: AccessRule[] + rules: AccessPolicyWithBindings[] createButtonLabel: string onCreate?: () => void - onEditRule?: (rule: AccessRule) => void - onCopyRule?: (rule: AccessRule) => void - onDeleteRule?: (rule: AccessRule) => void - onAddRole?: (rule: AccessRule) => void - onRemoveRole?: (rule: AccessRule, role: AssignedRole) => void + onEditRule?: (rule: AccessPolicyWithBindings) => void + onAddRole?: (rule: AccessPolicyWithBindings) => void className?: string } @@ -25,10 +22,7 @@ const AccessRuleSection = ({ createButtonLabel, onCreate, onEditRule, - onCopyRule, - onDeleteRule, onAddRole, - onRemoveRole, className, }: AccessRuleSectionProps) => { return ( @@ -44,14 +38,11 @@ const AccessRuleSection = ({
{rules.map((rule, index) => ( 0 && 'border-t border-divider-subtle')} onEdit={onEditRule} - onCopy={onCopyRule} - onDelete={onDeleteRule} onAddRole={onAddRole} - onRemoveRole={onRemoveRole} /> ))}
diff --git a/web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx b/web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx index 357bd698b6..3a5e3abd4a 100644 --- a/web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx +++ b/web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx @@ -15,14 +15,9 @@ import { ScrollArea } from '@langgenius/dify-ui/scroll-area' import { useCallback, useMemo, useState } from 'react' import Checkbox from '@/app/components/base/checkbox' import Input from '@/app/components/base/input' +import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' import { useMembers } from '@/service/use-common' -export type AssignableRoleOption = { - id: string - name: string - description?: string -} - export type AssignableMemberOption = { id: string name: string @@ -49,15 +44,6 @@ const TABS: Array<{ key: TabKey, label: string }> = [ { key: 'members', label: 'MEMBERS' }, ] -// TODO: replace with roles fetched from the permissions API once available. -const MOCK_ROLE_OPTIONS: AssignableRoleOption[] = [ - { id: 'admin', name: 'Admin', description: 'Full workspace management' }, - { id: 'editor', name: 'Editor', description: 'Create and edit resources' }, - { id: 'member', name: 'Member', description: 'Basic access' }, - { id: 'auditor', name: 'Auditor', description: 'View logs and audit trails' }, - { id: 'tester', name: 'Tester', description: 'Test in sandbox' }, -] - const toMemberOption = (member: Member): AssignableMemberOption => ({ id: member.id, name: member.name, @@ -72,9 +58,19 @@ const AddRuleTargetsModalBody = ({ onClose, onSubmit, }: AddRuleTargetsModalBaseProps) => { - const { data: membersData, isLoading: membersLoading } = useMembers() + const [activeTab, setActiveTab] = useState('roles') + const [keyword, setKeyword] = useState('') + const [selectedRoleIds, setSelectedRoleIds] = useState(initialRoleIds) + const [selectedMemberIds, setSelectedMemberIds] = useState(initialMemberIds) - const roles = MOCK_ROLE_OPTIONS + const { data: rolesData, isLoading: rolesLoading } = useWorkspaceRoleList({ + page: 1, + limit: 20, + }) + + const roles = useMemo(() => rolesData?.data ?? [], [rolesData]) + + const { data: membersData, isLoading: membersLoading } = useMembers() const members = useMemo(() => { const accounts = membersData?.accounts ?? [] @@ -82,10 +78,6 @@ const AddRuleTargetsModalBody = ({ .filter(account => account.status !== 'banned' && account.status !== 'closed') .map(toMemberOption) }, [membersData]) - const [activeTab, setActiveTab] = useState('roles') - const [keyword, setKeyword] = useState('') - const [selectedRoleIds, setSelectedRoleIds] = useState(initialRoleIds) - const [selectedMemberIds, setSelectedMemberIds] = useState(initialMemberIds) const trimmed = keyword.trim().toLowerCase() @@ -202,55 +194,61 @@ const AddRuleTargetsModalBody = ({ slotClassNames={{ viewport: 'px-3 overscroll-contain' }} > {activeTab === 'roles' && ( - filteredRoles.length === 0 + rolesLoading ? (
- No matching roles + Loading roles...
) - : ( -
    - {filteredRoles.map((role) => { - const checked = selectedRoleIds.includes(role.id) - const handleToggle = () => toggleRole(role.id) - return ( -
  • -
    { - if (e.key === ' ' || e.key === 'Enter') { - e.preventDefault() - handleToggle() - } - }} - > - -
    -
    - {role.name} -
    - {role.description && ( -
    - {role.description} -
    + : filteredRoles.length === 0 + ? ( +
    + No matching roles +
    + ) + : ( +
      + {filteredRoles.map((role) => { + const checked = selectedRoleIds.includes(role.id) + const handleToggle = () => toggleRole(role.id) + return ( +
    • +
      { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault() + handleToggle() + } + }} + > + +
      +
      + {role.name} +
      + {role.description && ( +
      + {role.description} +
      + )} +
      -
    -
  • - ) - })} -
- ) + + ) + })} + + ) )} {activeTab === 'members' && ( membersLoading diff --git a/web/app/components/header/account-setting/access-rules-page/app-access-rule-section.tsx b/web/app/components/header/account-setting/access-rules-page/app-access-rule-section.tsx new file mode 100644 index 0000000000..29279013fa --- /dev/null +++ b/web/app/components/header/account-setting/access-rules-page/app-access-rule-section.tsx @@ -0,0 +1,38 @@ +import type { AccessPolicyWithBindings } from '@/models/access-control' +import { useWorkspaceAppAccessRules } from '@/service/access-control/use-workspace-access-rules' +import AccessRuleSection from './access-rule-section' + +type AppAccessRuleSectionProps = { + className?: string + onCreate?: () => void + onEditRule?: (rule: AccessPolicyWithBindings) => void + onAddRole?: (rule: AccessPolicyWithBindings) => void +} + +const AppAccessRuleSection = ({ + className, + onCreate, + onEditRule, + onAddRole, +}: AppAccessRuleSectionProps) => { + const { data: appAccessRulesResponse } = useWorkspaceAppAccessRules({ + page: 1, + limit: 20, + }) + + const appAccessRules = appAccessRulesResponse?.items || [] + + return ( + + ) +} + +export default AppAccessRuleSection diff --git a/web/app/components/header/account-setting/access-rules-page/dataset-access-rule-section.tsx b/web/app/components/header/account-setting/access-rules-page/dataset-access-rule-section.tsx new file mode 100644 index 0000000000..5687393f91 --- /dev/null +++ b/web/app/components/header/account-setting/access-rules-page/dataset-access-rule-section.tsx @@ -0,0 +1,38 @@ +import type { AccessPolicyWithBindings } from '@/models/access-control' +import { useWorkspaceDatasetAccessRules } from '@/service/access-control/use-workspace-access-rules' +import AccessRuleSection from './access-rule-section' + +type DatasetAccessRuleSectionProps = { + className?: string + onCreate?: () => void + onEditRule?: (rule: AccessPolicyWithBindings) => void + onAddRole?: (rule: AccessPolicyWithBindings) => void +} + +const DatasetAccessRuleSection = ({ + className, + onCreate, + onEditRule, + onAddRole, +}: DatasetAccessRuleSectionProps) => { + const { data: datasetAccessRulesResponse } = useWorkspaceDatasetAccessRules({ + page: 1, + limit: 20, + }) + + const datasetAccessRules = datasetAccessRulesResponse?.items || [] + + return ( + + ) +} + +export default DatasetAccessRuleSection 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 5f7371f3be..0ce129e7dd 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,149 +1,23 @@ '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 type { AccessPolicyResourceType, AccessPolicyWithBindings } from '@/models/access-control' +import { toast } from '@langgenius/dify-ui/toast' import { useCallback, useState } from 'react' -import AccessRuleSection from './access-rule-section' +import { useCreateAccessRule, useUpdateAppAccessRuleBindings, useUpdateDatasetAccessRuleBindings } from '@/service/access-control/use-workspace-access-rules' import AddRuleTargetsModal from './add-rule-targets-modal' +import AppAccessRuleSection from './app-access-rule-section' +import DatasetAccessRuleSection from './dataset-access-rule-section' import PermissionSetModal from './permission-set-modal' -const APP_ACCESS_RULES: AccessRule[] = [ - { - id: 'app-full-access', - name: 'Full access', - description: 'Highest level. Can edit, publish, delete apps, and manage access for this app.', - assignedRoles: [ - { id: 'owner', name: 'Owner' }, - { id: 'admin', name: 'Admin' }, - { 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', - name: 'Can edit', - description: 'Modify Prompts, adjust workflows, change variables. Test and publish updates.', - assignedRoles: [ - { 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', - name: 'Can view & use', - description: 'View and use the app. Access Prompt and workflow logs. Cannot modify.', - assignedRoles: [ - { id: 'tester', name: 'Tester' }, - { id: 'ops-staff', name: 'Ops Staff' }, - { id: 'member', name: 'Member' }, - ], - permissions: [ - 'app.test_and_debug', - ], - }, - { - id: 'app-can-preview', - name: 'Can preview', - description: 'View the app in the list only. Cannot open the editor or use the app.', - assignedRoles: [ - { id: 'partner', name: 'Partner' }, - ], - permissions: [], - }, -] - -const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ - { - id: 'kb-full-access', - name: 'Full access', - description: 'Highest level. Can edit, publish, delete apps, and manage access for this knowledge base.', - assignedRoles: [ - { id: 'owner', name: 'Owner' }, - { id: 'admin', name: 'Admin' }, - { 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', - name: 'Can edit', - description: 'Edit knowledge base content, modify settings, and run tests.', - assignedRoles: [ - { id: 'kb-editor', name: 'KB Editor' }, - { 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', - name: 'Can view', - description: 'View knowledge base sources and logs. Cannot modify content.', - assignedRoles: [ - { id: 'member', name: 'Member' }, - ], - permissions: ['kb.view'], - }, - { - id: 'kb-can-preview', - name: 'Can preview', - description: 'View in the list only. Cannot access the detail page.', - assignedRoles: [ - { id: 'partner', name: 'Partner' }, - ], - permissions: [], - }, - { - id: 'kb-can-test', - name: 'Can test', - description: 'Test knowledge base retrieval efficiency in sandbox.', - assignedRoles: [ - { id: 'tester', name: 'Tester' }, - ], - permissions: ['kb.view'], - }, -] - type PermissionSetModalState = { mode: PermissionSetModalMode - resourceType: ResourceType + resourceType: AccessPolicyResourceType initialValues?: PermissionSetFormValues } const AccessRulesPage = () => { - const [addingRule, setAddingRule] = useState(null) + const [addingRule, setAddingRule] = useState(null) const [permissionSetModalState, setPermissionSetModalState] = useState(null) @@ -155,89 +29,110 @@ const AccessRulesPage = () => { setPermissionSetModalState(null) }, []) - const handleAddRole = useCallback((rule: AccessRule) => { + const handleAddRole = useCallback((rule: AccessPolicyWithBindings) => { setAddingRule(rule) }, []) + const { mutateAsync: updateAppAccessRuleBindings } = useUpdateAppAccessRuleBindings() + const { mutateAsync: updateDatasetAccessRuleBindings } = useUpdateDatasetAccessRuleBindings() + const handleAddSubmit = useCallback( - (_selection: { roleIds: string[], memberIds: string[] }) => { - // TODO: wire up to API when backend is ready. + (selection: { roleIds: string[], memberIds: string[] }) => { + const { id, resource_type } = addingRule!.policy + const payload = { + id, + role_ids: selection.roleIds, + account_ids: selection.memberIds, + } + if (resource_type === 'app') { + updateAppAccessRuleBindings(payload, { + onSuccess: () => { + toast.success('Access rule updated successfully') + closeAddModal() + }, + }) + } + else if (resource_type === 'dataset') { + updateDatasetAccessRuleBindings(payload, { + onSuccess: () => { + toast.success('Access rule updated successfully') + closeAddModal() + }, + }) + } }, - [], + [addingRule, closeAddModal, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings], ) - const handleCreate = useCallback((resourceType: ResourceType) => { + const handleCreate = useCallback((resourceType: AccessPolicyResourceType) => { setPermissionSetModalState({ mode: 'create', resourceType }) }, []) const handleEdit = useCallback( - (resourceType: ResourceType, rule: AccessRule) => { + (resourceType: AccessPolicyResourceType, rule: AccessPolicyWithBindings) => { + const { policy } = rule setPermissionSetModalState({ mode: 'edit', resourceType, initialValues: { - name: rule.name, - description: rule.description, - permissions: rule.permissions, + name: policy.name, + description: policy.description, + permissionKeys: policy.permission_keys, }, }) }, [], ) + const { mutateAsync: createAccessRule } = useCreateAccessRule(permissionSetModalState?.resourceType) + const handlePermissionSetSubmit = useCallback( - (_values: PermissionSetFormValues) => { - // TODO: wire up to API when backend is ready. + (values: PermissionSetFormValues) => { + const { name, description, permissionKeys } = values + createAccessRule({ + name, + description, + permission_keys: permissionKeys, + }, { + onSuccess: () => { + toast.success('Access rule created successfully') + closePermissionSetModal() + }, + }) }, - [], + [closePermissionSetModal, createAccessRule], ) - 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 createKb = useCallback(() => handleCreate('dataset'), [handleCreate]) const editApp = useCallback( - (rule: AccessRule) => handleEdit('app', rule), + (rule: AccessPolicyWithBindings) => handleEdit('app', rule), [handleEdit], ) const editKb = useCallback( - (rule: AccessRule) => handleEdit('knowledge_base', rule), + (rule: AccessPolicyWithBindings) => handleEdit('dataset', rule), [handleEdit], ) return ( <>
- -
{addingRule && ( role.id)} + ruleName={addingRule.policy.name} + initialRoleIds={addingRule.role_ids} initialMemberIds={[]} onClose={closeAddModal} onSubmit={handleAddSubmit} diff --git a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/hooks.ts b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/hooks.ts new file mode 100644 index 0000000000..ef41d2c4c9 --- /dev/null +++ b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/hooks.ts @@ -0,0 +1,22 @@ +import type { AccessPolicyResourceType } from '@/models/access-control' +import { useAppPermissionCatalog, useDatasetPermissionCatalog } from '@/service/access-control/use-permission-catalog' + +export const usePermissionsGroups = (resourceType: AccessPolicyResourceType) => { + const { data: appPermissionCatalog } = useAppPermissionCatalog(resourceType === 'app') + const { data: datasetPermissionCatalog } = useDatasetPermissionCatalog(resourceType === 'dataset') + + const permissionCatalog = resourceType === 'app' ? appPermissionCatalog : datasetPermissionCatalog + + const groups = permissionCatalog?.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/access-rules-page/permission-set-modal/index.tsx b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/index.tsx index 04059c060e..012388be21 100644 --- 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 @@ -1,6 +1,6 @@ 'use client' -import type { ResourceType } from './permissions-data' +import type { AccessPolicyResourceType } from '@/models/access-control' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { @@ -10,40 +10,40 @@ import { DialogDescription, DialogTitle, } from '@langgenius/dify-ui/dialog' -import { useMemo, useState } from 'react' +import { useState } from 'react' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' +import { usePermissionsGroups } from './hooks' import PermissionPicker from './permission-picker' -import { getPermissionMap } from './permissions-data' export type PermissionSetModalMode = 'create' | 'edit' export type PermissionSetFormValues = { name: string description: string - permissions: string[] + permissionKeys: string[] } export type PermissionSetModalProps = { open: boolean mode: PermissionSetModalMode - resourceType: ResourceType + resourceType: AccessPolicyResourceType initialValues?: Partial onClose: () => void onSubmit: (values: PermissionSetFormValues) => void } -const RESOURCE_LABEL: Record = { +const RESOURCE_LABEL: Record = { app: 'App', - knowledge_base: 'Knowledge Base', + dataset: 'Knowledge Base', } -const buildTitle = (mode: PermissionSetModalMode, resource: ResourceType): string => { +const buildTitle = (mode: PermissionSetModalMode, resource: AccessPolicyResourceType): string => { const verb = mode === 'create' ? 'Create' : 'Edit' return `${verb} ${RESOURCE_LABEL[resource]} permission set` } -const buildDescription = (mode: PermissionSetModalMode, resource: ResourceType): string => { +const buildDescription = (mode: PermissionSetModalMode, resource: AccessPolicyResourceType): string => { if (mode === 'edit') return 'Modify the name, description, and permissions granted for this permission set.' if (resource === 'app') @@ -62,9 +62,9 @@ const PermissionSetModalBody = ({ }: PermissionSetModalBodyProps) => { const [name, setName] = useState(initialValues?.name ?? '') const [description, setDescription] = useState(initialValues?.description ?? '') - const [permissions, setPermissions] = useState(initialValues?.permissions ?? []) + const [permissionKeys, setPermissionKeys] = useState(initialValues?.permissionKeys ?? []) - const permissionMap = useMemo(() => getPermissionMap(resourceType), [resourceType]) + const { permissionMap } = usePermissionsGroups(resourceType) const trimmedName = name.trim() const canSubmit = trimmedName.length > 0 @@ -75,13 +75,13 @@ const PermissionSetModalBody = ({ onSubmit({ name: trimmedName, description: description.trim(), - permissions, + permissionKeys, }) onClose() } - const handleRemovePermission = (id: string) => { - setPermissions(prev => prev.filter(p => p !== id)) + const handleRemovePermission = (key: string) => { + setPermissionKeys(prev => prev.filter(p => p !== key)) } return ( @@ -132,15 +132,15 @@ const PermissionSetModalBody = ({
Permissions
- {permissions.length > 0 && ( + {permissionKeys.length > 0 && (
- {permissions.map((id) => { - const p = permissionMap[id] + {permissionKeys.map((key) => { + const p = permissionMap[key] if (!p) return null return ( handleRemovePermission(id)} + onClick={() => handleRemovePermission(key)} > @@ -162,8 +162,8 @@ const PermissionSetModalBody = ({ )}
diff --git a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/permission-picker.tsx b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/permission-picker.tsx index 626c224df8..942364e68c 100644 --- a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/permission-picker.tsx +++ b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/permission-picker.tsx @@ -1,21 +1,20 @@ 'use client' -import type { PermissionGroup, ResourceType } from './permissions-data' +import type { AccessPolicyResourceType, PermissionGroup } from '@/models/access-control' import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { useEffect, useMemo, useRef, useState } from 'react' import Checkbox from '@/app/components/base/checkbox' -import { - filterPermissionNodes, - PERMISSION_NODES_BY_RESOURCE, -} from './permissions-data' +import { usePermissionsGroups } from './hooks' type PermissionPickerProps = { - resourceType: ResourceType + resourceType: AccessPolicyResourceType value: string[] onChange: (next: string[]) => void className?: string @@ -42,12 +41,19 @@ const PermissionPicker = ({ return () => clearTimeout(timer) }, [open]) - const nodes = PERMISSION_NODES_BY_RESOURCE[resourceType] + const { groups } = usePermissionsGroups(resourceType) - const filtered = useMemo( - () => filterPermissionNodes(nodes, search), - [nodes, search], - ) + const filteredGroups = useMemo(() => { + const q = search.trim().toLowerCase() + if (!q) + return groups + return groups + .map(group => ({ + ...group, + permissions: group.permissions.filter(i => i.name.toLowerCase().includes(q)), + })) + .filter(group => group.permissions.length > 0) + }, [search, groups]) const selectedSet = useMemo(() => new Set(value), [value]) @@ -59,19 +65,19 @@ const PermissionPicker = ({ } const getGroupState = (group: PermissionGroup) => { - const checkedCount = group.items.reduce( - (acc, i) => acc + (selectedSet.has(i.id) ? 1 : 0), + const checkedCount = group.permissions.reduce( + (acc, i) => acc + (selectedSet.has(i.key) ? 1 : 0), 0, ) return { - allChecked: checkedCount > 0 && checkedCount === group.items.length, - indeterminate: checkedCount > 0 && checkedCount < group.items.length, + allChecked: checkedCount > 0 && checkedCount === group.permissions.length, + indeterminate: checkedCount > 0 && checkedCount < group.permissions.length, } } const toggleGroup = (group: PermissionGroup) => { const { allChecked, indeterminate } = getGroupState(group) - const ids = group.items.map(i => i.id) + const ids = group.permissions.map(i => i.key) if (allChecked || indeterminate) { const idSet = new Set(ids) onChange(value.filter(v => !idSet.has(v))) @@ -122,71 +128,46 @@ const PermissionPicker = ({ sideOffset={4} popupClassName="max-h-80 w-[var(--anchor-width)] py-1" > - {filtered.length === 0 && ( + {filteredGroups.length === 0 && (
No permissions found
)} - {filtered.map((node) => { - if (node.kind === 'leaf') { - const checked = selectedSet.has(node.leaf.id) - return ( - - ) - } - const { allChecked, indeterminate } = getGroupState(node.group) + {filteredGroups.map((group) => { + const { allChecked, indeterminate } = getGroupState(group) return ( -
+ - {node.group.items.map((item) => { - const checked = selectedSet.has(item.id) + {group.permissions.map((item) => { + const checked = selectedSet.has(item.key) return ( - + ) })} -
+ ) })} diff --git a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/permissions-data.ts b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/permissions-data.ts deleted file mode 100644 index c9634a6024..0000000000 --- a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/permissions-data.ts +++ /dev/null @@ -1,103 +0,0 @@ -export type PermissionLeaf = { - id: string - name: string -} - -export type PermissionGroup = { - id: string - label: string - items: PermissionLeaf[] -} - -export type PermissionNode - = | { kind: 'leaf', leaf: PermissionLeaf } - | { kind: 'group', group: PermissionGroup } - -export type ResourceType = 'app' | 'knowledge_base' - -const APP_PERMISSION_NODES: PermissionNode[] = [ - { kind: 'leaf', leaf: { id: 'app.editing_and_layout', name: 'Editing and layout app' } }, - { kind: 'leaf', leaf: { id: 'app.test_and_debug', name: 'Test and debug app' } }, - { kind: 'leaf', leaf: { id: 'app.delete', name: 'Delete app' } }, - { kind: 'leaf', leaf: { id: 'app.import_export_dsl', name: 'Import and Export DSL' } }, - { kind: 'leaf', leaf: { id: 'app.release_version_management', name: 'Application Release and Version Management' } }, - { kind: 'leaf', leaf: { id: 'app.annotation_management', name: 'Annotation Management' } }, - { - kind: 'group', - group: { - id: 'app.api_management', - label: 'API Management', - items: [ - { id: 'app.api_management.toggle', name: 'Enable/Disable API Access' }, - { id: 'app.api_management.create_key', name: 'Create App API Key' }, - { id: 'app.api_management.delete_key', name: 'Delete App API Key' }, - ], - }, - }, -] - -const KNOWLEDGE_BASE_PERMISSION_NODES: PermissionNode[] = [ - { kind: 'leaf', leaf: { id: 'kb.view', name: 'View Knowledge Base' } }, - { kind: 'leaf', leaf: { id: 'kb.edit_configuration', name: 'Edit Knowledge Base Configuration' } }, - { - kind: 'group', - group: { - id: 'kb.manage_documents', - label: 'Managing Knowledge Base Documents', - items: [ - { id: 'kb.manage_documents.add', name: 'Add Document' }, - { id: 'kb.manage_documents.delete', name: 'Delete Document' }, - { id: 'kb.manage_documents.download', name: 'Download Document' }, - ], - }, - }, - { kind: 'leaf', leaf: { id: 'kb.import_export_pipeline', name: 'Import Pipeline from DSL / Export Knowledge Pipeline DSL' } }, - { kind: 'leaf', leaf: { id: 'kb.pipeline_publishing_versioning', name: 'Knowledge Base Pipeline Publishing and Version Management' } }, - { kind: 'leaf', leaf: { id: 'kb.delete', name: 'Delete Knowledge Base' } }, -] - -export const PERMISSION_NODES_BY_RESOURCE: Record = { - app: APP_PERMISSION_NODES, - knowledge_base: KNOWLEDGE_BASE_PERMISSION_NODES, -} - -export const flattenPermissionNodes = (nodes: PermissionNode[]): PermissionLeaf[] => { - const out: PermissionLeaf[] = [] - for (const node of nodes) { - if (node.kind === 'leaf') - out.push(node.leaf) - else - out.push(...node.group.items) - } - return out -} - -export const getPermissionMap = (resourceType: ResourceType): Record => { - const flat = flattenPermissionNodes(PERMISSION_NODES_BY_RESOURCE[resourceType]) - return Object.fromEntries(flat.map(p => [p.id, p])) -} - -export const filterPermissionNodes = ( - nodes: PermissionNode[], - keyword: string, -): PermissionNode[] => { - const q = keyword.trim().toLowerCase() - if (!q) - return nodes - const out: PermissionNode[] = [] - for (const node of nodes) { - if (node.kind === 'leaf') { - if (node.leaf.name.toLowerCase().includes(q)) - out.push(node) - } - else { - const matchedItems = node.group.items.filter(i => i.name.toLowerCase().includes(q)) - const groupMatch = node.group.label.toLowerCase().includes(q) - if (groupMatch) - out.push(node) - else if (matchedItems.length > 0) - out.push({ kind: 'group', group: { ...node.group, items: matchedItems } }) - } - } - return out -} diff --git a/web/app/components/header/account-setting/access-rules-page/role-tag.tsx b/web/app/components/header/account-setting/access-rules-page/role-tag.tsx index c5a9dd6871..5a78abad55 100644 --- a/web/app/components/header/account-setting/access-rules-page/role-tag.tsx +++ b/web/app/components/header/account-setting/access-rules-page/role-tag.tsx @@ -4,12 +4,18 @@ import { cn } from '@langgenius/dify-ui/cn' import { memo } from 'react' export type RoleTagProps = { + id: string label: string - onRemove?: () => void + onRemove?: (id: string) => void className?: string } -const RoleTag = ({ label, onRemove, className }: RoleTagProps) => { +const RoleTag = ({ + id, + label, + onRemove, + className, +}: RoleTagProps) => { return ( { aria-label={`Remove ${label}`} onClick={(e) => { e.stopPropagation() - onRemove() + onRemove(id) }} className="flex h-4 w-4 items-center justify-center rounded text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary" > diff --git a/web/app/components/header/account-setting/permissions-page/role-modal/permission-picker.tsx b/web/app/components/header/account-setting/permissions-page/role-modal/permission-picker.tsx index f0ce36d489..44b8fd9c50 100644 --- a/web/app/components/header/account-setting/permissions-page/role-modal/permission-picker.tsx +++ b/web/app/components/header/account-setting/permissions-page/role-modal/permission-picker.tsx @@ -7,7 +7,6 @@ import { DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { useEffect, useMemo, useRef, useState } from 'react' @@ -20,7 +19,11 @@ type PermissionPickerProps = { className?: string } -const PermissionPicker = ({ value, onChange, className }: PermissionPickerProps) => { +const PermissionPicker = ({ + value, + onChange, + className, +}: PermissionPickerProps) => { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const inputRef = useRef(null) @@ -129,11 +132,10 @@ const PermissionPicker = ({ value, onChange, className }: PermissionPickerProps) No permissions found
)} - {filteredGroups.map((group, groupIndex) => { + {filteredGroups.map((group) => { const { allChecked, indeterminate } = getGroupState(group) return ( - {groupIndex > 0 && }