mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 18:24:32 +08:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: Charles Yao <chongbinyao33@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: yunlu.wen <yunlu.wen@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Jingyi <jingyi.qi@dify.ai> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: gigglewang <gigglewang@dify.ai> Co-authored-by: chariri <w@chariri.moe> Co-authored-by: Evan <2869018789@qq.com> Co-authored-by: zyssyz123 <916125788@qq.com>
191 lines
8.0 KiB
TypeScript
191 lines
8.0 KiB
TypeScript
'use client'
|
|
|
|
import type { PermissionGroup } from '@/models/access-control'
|
|
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
|
import { useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
type PermissionGroupListProps = {
|
|
groups: PermissionGroup[]
|
|
value: string[]
|
|
onChange: (next: string[]) => void
|
|
className?: string
|
|
readonly?: boolean
|
|
}
|
|
|
|
const PermissionGroupList = ({
|
|
groups,
|
|
value,
|
|
onChange,
|
|
className,
|
|
readonly = false,
|
|
}: PermissionGroupListProps) => {
|
|
const { t } = useTranslation()
|
|
const [expandedGroupKeys, setExpandedGroupKeys] = useState<Set<string> | null>(null)
|
|
|
|
const selectedSet = useMemo(() => new Set(value), [value])
|
|
|
|
const defaultExpandedGroupKeys = useMemo(() => {
|
|
if (groups.length === 0)
|
|
return new Set<string>()
|
|
const firstSelectedGroup = groups.find(group =>
|
|
group.permissions.some(permission => selectedSet.has(permission.key)),
|
|
)
|
|
return new Set([(firstSelectedGroup ?? groups[0]!).group_key])
|
|
}, [groups, selectedSet])
|
|
|
|
const expandedGroupKeysMerged = expandedGroupKeys ?? defaultExpandedGroupKeys
|
|
|
|
const toggleGroupExpanded = (groupKey: string) => {
|
|
setExpandedGroupKeys((prev) => {
|
|
const next = new Set(prev ?? expandedGroupKeysMerged)
|
|
if (next.has(groupKey))
|
|
next.delete(groupKey)
|
|
else
|
|
next.add(groupKey)
|
|
return next
|
|
})
|
|
}
|
|
|
|
const togglePermission = (permissionKey: string) => {
|
|
if (readonly)
|
|
return
|
|
|
|
if (selectedSet.has(permissionKey))
|
|
onChange(value.filter(key => key !== permissionKey))
|
|
else
|
|
onChange([...value, permissionKey])
|
|
}
|
|
|
|
const toggleGroupSelection = (group: PermissionGroup, selectedCount: number) => {
|
|
if (readonly)
|
|
return
|
|
|
|
const permissionKeys = group.permissions.map(permission => permission.key)
|
|
if (selectedCount === permissionKeys.length) {
|
|
const groupPermissionKeySet = new Set(permissionKeys)
|
|
onChange(value.filter(key => !groupPermissionKeySet.has(key)))
|
|
return
|
|
}
|
|
|
|
const next = new Set(value)
|
|
permissionKeys.forEach(key => next.add(key))
|
|
onChange(Array.from(next))
|
|
}
|
|
|
|
return (
|
|
<div className={cn('min-h-0 flex-1 rounded-xl border border-components-panel-border bg-components-panel-bg shadow-xs', className)}>
|
|
<ScrollArea
|
|
className="h-full overflow-hidden rounded-xl"
|
|
slotClassNames={{
|
|
viewport: 'overscroll-contain',
|
|
content: groups.length === 0 ? 'h-full' : undefined,
|
|
}}
|
|
>
|
|
{groups.length === 0
|
|
? (
|
|
<div className="flex h-full items-center justify-center px-3 py-6 text-center system-sm-regular text-text-tertiary">
|
|
{t('permissionList.noPermissionsFound', { ns: 'permission' })}
|
|
</div>
|
|
)
|
|
: (
|
|
<div className="flex flex-col">
|
|
{groups.map((group) => {
|
|
const expanded = expandedGroupKeysMerged.has(group.group_key)
|
|
const selectedCount = group.permissions.reduce(
|
|
(count, permission) => count + (selectedSet.has(permission.key) ? 1 : 0),
|
|
0,
|
|
)
|
|
const totalCount = group.permissions.length
|
|
const allSelected = totalCount > 0 && selectedCount === totalCount
|
|
|
|
return (
|
|
<div key={group.group_key} className="border-b border-b-divider-subtle">
|
|
<div className="group/permission-category flex h-12 w-full items-center gap-3 px-3 hover:bg-state-base-hover">
|
|
<button
|
|
type="button"
|
|
className="min-w-0 flex-1 truncate text-left system-md-medium text-text-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
|
aria-expanded={expanded}
|
|
onClick={() => toggleGroupExpanded(group.group_key)}
|
|
>
|
|
{group.group_name}
|
|
</button>
|
|
{!readonly && totalCount > 0 && (
|
|
<button
|
|
type="button"
|
|
className="shrink-0 rounded-md px-1.5 py-0.5 system-sm-medium text-text-accent opacity-0 group-hover/permission-category:opacity-100 hover:bg-state-accent-hover focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
|
onClick={() => toggleGroupSelection(group, selectedCount)}
|
|
>
|
|
{allSelected
|
|
? t('permissionList.clearAll', { ns: 'permission' })
|
|
: t('permissionList.selectAll', { ns: 'permission' })}
|
|
</button>
|
|
)}
|
|
{selectedCount > 0 && (
|
|
<span className="shrink-0 rounded-md bg-util-colors-blue-blue-100 px-2 py-0.5 system-sm-medium text-text-accent">
|
|
{selectedCount}
|
|
/
|
|
{totalCount}
|
|
</span>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="flex h-6 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
|
aria-label={expanded ? t('permissionList.collapseGroup', { ns: 'permission' }) : t('permissionList.expandGroup', { ns: 'permission' })}
|
|
onClick={() => toggleGroupExpanded(group.group_key)}
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className={cn(
|
|
'i-ri-arrow-right-s-fill h-4 w-4 text-text-tertiary transition-transform',
|
|
expanded && 'rotate-90',
|
|
)}
|
|
/>
|
|
</button>
|
|
</div>
|
|
{expanded && (
|
|
<div className="flex flex-col">
|
|
{group.permissions.map((permission) => {
|
|
const checked = selectedSet.has(permission.key)
|
|
|
|
return (
|
|
<div
|
|
key={permission.key}
|
|
className={cn(
|
|
'flex min-h-9 items-center gap-3 px-3 py-1.5',
|
|
readonly ? 'cursor-default' : 'cursor-pointer hover:bg-state-base-hover',
|
|
checked && 'bg-state-accent-hover',
|
|
checked && !readonly && 'hover:bg-state-accent-hover',
|
|
)}
|
|
onClick={() => togglePermission(permission.key)}
|
|
>
|
|
<Checkbox
|
|
checked={checked}
|
|
disabled={readonly}
|
|
className="shrink-0"
|
|
onCheckedChange={() => {
|
|
togglePermission(permission.key)
|
|
}}
|
|
/>
|
|
<span className="min-w-0 flex-1 truncate system-md-regular text-text-secondary">
|
|
{permission.name}
|
|
</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default PermissionGroupList
|