feat: add permission management modal and permission picker for access rules

This commit is contained in:
twwu 2026-04-27 16:41:48 +08:00
parent 339e4c8a1f
commit 5f4b086e39
5 changed files with 635 additions and 10 deletions

View File

@ -15,6 +15,7 @@ export type AccessRule = {
name: string name: string
description: string description: string
assignedRoles: AssignedRole[] assignedRoles: AssignedRole[]
permissions: string[]
} }
export type AccessRuleRowProps = { export type AccessRuleRowProps = {

View File

@ -1,11 +1,13 @@
'use client' 'use client'
import type { AccessRule } from './access-rule-row' 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 { useCallback, useState } from 'react'
import AccessRuleSection from './access-rule-section' import AccessRuleSection from './access-rule-section'
import AddRuleTargetsModal from './add-rule-targets-modal' 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[] = [ const APP_ACCESS_RULES: AccessRule[] = [
{ {
id: 'app-full-access', id: 'app-full-access',
@ -17,6 +19,17 @@ const APP_ACCESS_RULES: AccessRule[] = [
{ id: 'app-admin', name: 'App Admin' }, { id: 'app-admin', name: 'App Admin' },
{ id: 'executive', name: 'Executive' }, { 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', id: 'app-can-edit',
@ -26,6 +39,11 @@ const APP_ACCESS_RULES: AccessRule[] = [
{ id: 'app-editor', name: 'App Editor' }, { id: 'app-editor', name: 'App Editor' },
{ id: 'it-staff', name: 'IT Staff' }, { 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', id: 'app-can-view-and-use',
@ -36,6 +54,9 @@ const APP_ACCESS_RULES: AccessRule[] = [
{ id: 'ops-staff', name: 'Ops Staff' }, { id: 'ops-staff', name: 'Ops Staff' },
{ id: 'member', name: 'Member' }, { id: 'member', name: 'Member' },
], ],
permissions: [
'app.test_and_debug',
],
}, },
{ {
id: 'app-can-preview', id: 'app-can-preview',
@ -44,10 +65,10 @@ const APP_ACCESS_RULES: AccessRule[] = [
assignedRoles: [ assignedRoles: [
{ id: 'partner', name: 'Partner' }, { id: 'partner', name: 'Partner' },
], ],
permissions: [],
}, },
] ]
// todo: replace with API data when backend is ready
const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [
{ {
id: 'kb-full-access', id: 'kb-full-access',
@ -59,6 +80,16 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [
{ id: 'kb-admin', name: 'KB Admin' }, { id: 'kb-admin', name: 'KB Admin' },
{ id: 'executive', name: 'Executive' }, { 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', id: 'kb-can-edit',
@ -69,6 +100,12 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [
{ id: 'ops-staff', name: 'Ops Staff' }, { id: 'ops-staff', name: 'Ops Staff' },
{ id: 'it-staff', name: 'IT 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', id: 'kb-can-view',
@ -77,6 +114,7 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [
assignedRoles: [ assignedRoles: [
{ id: 'member', name: 'Member' }, { id: 'member', name: 'Member' },
], ],
permissions: ['kb.view'],
}, },
{ {
id: 'kb-can-preview', id: 'kb-can-preview',
@ -85,6 +123,7 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [
assignedRoles: [ assignedRoles: [
{ id: 'partner', name: 'Partner' }, { id: 'partner', name: 'Partner' },
], ],
permissions: [],
}, },
{ {
id: 'kb-can-test', id: 'kb-can-test',
@ -93,20 +132,33 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [
assignedRoles: [ assignedRoles: [
{ id: 'tester', name: 'Tester' }, { id: 'tester', name: 'Tester' },
], ],
permissions: ['kb.view'],
}, },
] ]
type PermissionSetModalState = {
mode: PermissionSetModalMode
resourceType: ResourceType
initialValues?: PermissionSetFormValues
}
const AccessRulesPage = () => { const AccessRulesPage = () => {
const [addingRule, setAddingRule] = useState<AccessRule | null>(null) const [addingRule, setAddingRule] = useState<AccessRule | null>(null)
const [permissionSetModalState, setPermissionSetModalState]
const handleAddRole = useCallback((rule: AccessRule) => { = useState<PermissionSetModalState | null>(null)
setAddingRule(rule)
}, [])
const closeAddModal = useCallback(() => { const closeAddModal = useCallback(() => {
setAddingRule(null) setAddingRule(null)
}, []) }, [])
const closePermissionSetModal = useCallback(() => {
setPermissionSetModalState(null)
}, [])
const handleAddRole = useCallback((rule: AccessRule) => {
setAddingRule(rule)
}, [])
const handleAddSubmit = useCallback( const handleAddSubmit = useCallback(
(_selection: { roleIds: string[], memberIds: string[] }) => { (_selection: { roleIds: string[], memberIds: string[] }) => {
// TODO: wire up to API when backend is ready. // 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(() => { const noop = useCallback(() => {
// TODO: wire up to API when backend is ready. // 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 ( return (
<> <>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
@ -125,8 +214,8 @@ const AccessRulesPage = () => {
title="App Access Rules" title="App Access Rules"
rules={APP_ACCESS_RULES} rules={APP_ACCESS_RULES}
createButtonLabel="Create App permission set" createButtonLabel="Create App permission set"
onCreate={noop} onCreate={createApp}
onEditRule={noop} onEditRule={editApp}
onCopyRule={noop} onCopyRule={noop}
onDeleteRule={noop} onDeleteRule={noop}
onAddRole={handleAddRole} onAddRole={handleAddRole}
@ -136,8 +225,8 @@ const AccessRulesPage = () => {
title="Knowledge Base Access Rules" title="Knowledge Base Access Rules"
rules={KNOWLEDGE_BASE_ACCESS_RULES} rules={KNOWLEDGE_BASE_ACCESS_RULES}
createButtonLabel="Create KB permission set" createButtonLabel="Create KB permission set"
onCreate={noop} onCreate={createKb}
onEditRule={noop} onEditRule={editKb}
onCopyRule={noop} onCopyRule={noop}
onDeleteRule={noop} onDeleteRule={noop}
onAddRole={handleAddRole} onAddRole={handleAddRole}
@ -154,6 +243,16 @@ const AccessRulesPage = () => {
onSubmit={handleAddSubmit} onSubmit={handleAddSubmit}
/> />
)} )}
{permissionSetModalState && (
<PermissionSetModal
open
mode={permissionSetModalState.mode}
resourceType={permissionSetModalState.resourceType}
initialValues={permissionSetModalState.initialValues}
onClose={closePermissionSetModal}
onSubmit={handlePermissionSetSubmit}
/>
)}
</> </>
) )
} }

View File

@ -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<PermissionSetFormValues>
onClose: () => void
onSubmit: (values: PermissionSetFormValues) => void
}
const RESOURCE_LABEL: Record<ResourceType, string> = {
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<PermissionSetModalProps, 'open'>
const PermissionSetModalBody = ({
mode,
resourceType,
initialValues,
onClose,
onSubmit,
}: PermissionSetModalBodyProps) => {
const [name, setName] = useState(initialValues?.name ?? '')
const [description, setDescription] = useState(initialValues?.description ?? '')
const [permissions, setPermissions] = useState<string[]>(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 (
<DialogContent
className="max-h-[85vh] w-[560px] overflow-visible p-0"
backdropProps={{ forceRender: true }}
>
<div className="relative px-6 pt-6 pb-4">
<DialogCloseButton />
<div className="pr-8">
<DialogTitle className="system-xl-semibold text-text-primary">
{buildTitle(mode, resourceType)}
</DialogTitle>
<DialogDescription className="mt-1 system-sm-regular text-text-tertiary">
{buildDescription(mode, resourceType)}
</DialogDescription>
</div>
</div>
<div className="border-t border-divider-subtle" />
<div className="flex flex-col gap-5 px-6 py-5">
<div className="flex flex-col gap-1">
<label htmlFor="permission-set-name" className="system-sm-medium text-text-secondary">
permission set name
<span aria-hidden className="ml-0.5 text-text-destructive">*</span>
</label>
<Input
id="permission-set-name"
value={name}
onChange={e => setName(e.target.value)}
placeholder="e.g. Can export DSL"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="permission-set-description" className="system-sm-medium text-text-secondary">
Description
</label>
<Textarea
id="permission-set-description"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Describe what this permission set grants"
className="min-h-20 resize-none"
/>
</div>
<div className="flex flex-col gap-2">
<div className="system-sm-medium text-text-secondary">Permissions</div>
{permissions.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{permissions.map((id) => {
const p = permissionMap[id]
if (!p)
return null
return (
<span
key={id}
className={cn(
'inline-flex items-center gap-1 rounded-md bg-util-colors-indigo-indigo-50 px-1.5 py-0.5 system-xs-medium text-text-accent',
'border-[0.5px] border-components-panel-border',
)}
>
<span>{p.name}</span>
<button
type="button"
className="flex h-3.5 w-3.5 items-center justify-center rounded hover:bg-state-base-hover"
aria-label={`Remove ${p.name}`}
onClick={() => handleRemovePermission(id)}
>
<span aria-hidden className="i-ri-close-line h-3 w-3" />
</button>
</span>
)
})}
</div>
)}
<PermissionPicker
resourceType={resourceType}
value={permissions}
onChange={setPermissions}
/>
</div>
</div>
<div className="flex items-center justify-between gap-3 border-t border-divider-subtle px-6 py-4">
<a
href="https://docs.dify.ai/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 system-xs-medium text-text-accent hover:underline"
>
<span>Learn more about permissions</span>
<span aria-hidden className="i-ri-external-link-line h-3.5 w-3.5" />
</a>
<div className="flex items-center gap-2">
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
<Button
variant="primary"
disabled={!canSubmit}
onClick={handleConfirm}
>
Confirm
</Button>
</div>
</div>
</DialogContent>
)
}
const PermissionSetModal = ({
open,
mode,
resourceType,
initialValues,
onClose,
onSubmit,
}: PermissionSetModalProps) => {
return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen)
onClose()
}}
>
<PermissionSetModalBody
mode={mode}
resourceType={resourceType}
initialValues={initialValues}
onClose={onClose}
onSubmit={onSubmit}
/>
</Dialog>
)
}
export default PermissionSetModal

View File

@ -0,0 +1,197 @@
'use client'
import type { PermissionGroup, ResourceType } from './permissions-data'
import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
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'
type PermissionPickerProps = {
resourceType: ResourceType
value: string[]
onChange: (next: string[]) => void
className?: string
}
const PermissionPicker = ({
resourceType,
value,
onChange,
className,
}: PermissionPickerProps) => {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
// Re-focus the search input after the dropdown takes over focus, so the user
// can keep typing to filter permissions.
useEffect(() => {
if (!open)
return
const timer = setTimeout(() => {
inputRef.current?.focus({ preventScroll: true })
}, 0)
return () => clearTimeout(timer)
}, [open])
const nodes = PERMISSION_NODES_BY_RESOURCE[resourceType]
const filtered = useMemo(
() => filterPermissionNodes(nodes, search),
[nodes, search],
)
const selectedSet = useMemo(() => new Set(value), [value])
const togglePermission = (id: string) => {
if (selectedSet.has(id))
onChange(value.filter(v => v !== id))
else
onChange([...value, id])
}
const getGroupState = (group: PermissionGroup) => {
const checkedCount = group.items.reduce(
(acc, i) => acc + (selectedSet.has(i.id) ? 1 : 0),
0,
)
return {
allChecked: checkedCount > 0 && checkedCount === group.items.length,
indeterminate: checkedCount > 0 && checkedCount < group.items.length,
}
}
const toggleGroup = (group: PermissionGroup) => {
const { allChecked, indeterminate } = getGroupState(group)
const ids = group.items.map(i => i.id)
if (allChecked || indeterminate) {
const idSet = new Set(ids)
onChange(value.filter(v => !idSet.has(v)))
}
else {
const next = new Set(value)
ids.forEach(id => next.add(id))
onChange(Array.from(next))
}
}
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger>
<div
className={cn(
'flex cursor-text items-center gap-2 rounded-lg bg-components-input-bg-normal px-3 py-2 hover:bg-components-input-bg-hover',
open && 'bg-components-input-bg-active shadow-xs ring-[0.5px] ring-components-input-border-active',
className,
)}
>
<span aria-hidden className="i-ri-search-line h-4 w-4 shrink-0 text-text-tertiary" />
<input
ref={inputRef}
className="min-w-0 grow appearance-none bg-transparent system-sm-regular text-text-primary caret-primary-600 outline-hidden placeholder:text-text-tertiary"
placeholder="Search permissions..."
value={search}
onChange={e => setSearch(e.target.value)}
onFocus={() => setOpen(true)}
onMouseDown={e => e.stopPropagation()}
onKeyDown={(e) => {
e.stopPropagation()
if (e.key === 'Escape')
setOpen(false)
}}
/>
<span
aria-hidden
className={cn(
'i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-tertiary transition-transform',
open && 'rotate-180',
)}
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
placement="bottom-start"
sideOffset={4}
popupClassName="max-h-80 w-[var(--anchor-width)] py-1"
>
{filtered.length === 0 && (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
No permissions found
</div>
)}
{filtered.map((node) => {
if (node.kind === 'leaf') {
const checked = selectedSet.has(node.leaf.id)
return (
<button
key={node.leaf.id}
type="button"
role="menuitemcheckbox"
aria-checked={checked}
onClick={() => togglePermission(node.leaf.id)}
className="mx-1 flex h-7 w-[calc(100%-0.5rem)] items-center gap-2 rounded-lg px-2 text-left outline-hidden hover:bg-state-base-hover"
>
<Checkbox checked={checked} className="pointer-events-none" />
<span className="system-sm-regular text-text-secondary">
{node.leaf.name}
</span>
</button>
)
}
const { allChecked, indeterminate } = getGroupState(node.group)
return (
<div key={node.group.id} className="flex flex-col">
<button
type="button"
role="menuitemcheckbox"
aria-checked={allChecked ? true : indeterminate ? 'mixed' : false}
onClick={() => toggleGroup(node.group)}
className="mx-1 flex h-7 w-[calc(100%-0.5rem)] items-center gap-2 rounded-lg px-2 text-left outline-hidden hover:bg-state-base-hover"
>
<Checkbox
checked={allChecked}
indeterminate={indeterminate}
className="pointer-events-none"
/>
<span className="system-sm-regular text-text-secondary">
{node.group.label}
</span>
</button>
{node.group.items.map((item) => {
const checked = selectedSet.has(item.id)
return (
<button
key={item.id}
type="button"
role="menuitemcheckbox"
aria-checked={checked}
onClick={() => togglePermission(item.id)}
className={cn(
'mx-1 flex h-7 w-[calc(100%-0.5rem)] items-center gap-2 rounded-lg pr-2 pl-7 text-left outline-hidden hover:bg-state-base-hover',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
)}
>
<Checkbox checked={checked} className="pointer-events-none" />
<span className="system-sm-regular text-text-secondary">
{item.name}
</span>
</button>
)
})}
</div>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)
}
export default PermissionPicker

View File

@ -0,0 +1,103 @@
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<ResourceType, PermissionNode[]> = {
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<string, PermissionLeaf> => {
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
}