mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
feat: implement role management features with hooks and UI components
This commit is contained in:
parent
a4d12efbb6
commit
35696c6b2e
@ -0,0 +1,27 @@
|
||||
import type { RoleListGroup } from './role-list'
|
||||
import type { RoleListResponse } from '@/models/access-control'
|
||||
|
||||
export const formatRoleGroups = (roleListResponse: RoleListResponse | undefined): RoleListGroup[] => {
|
||||
if (!roleListResponse)
|
||||
return []
|
||||
const result: RoleListGroup[] = []
|
||||
const builtinRoles = roleListResponse.data.filter(role => role.is_builtin)
|
||||
const customRoles = roleListResponse.data.filter(role => !role.is_builtin)
|
||||
if (builtinRoles.length > 0) {
|
||||
result.push({
|
||||
id: 'builtin',
|
||||
category: 'global_system_default',
|
||||
title: 'System Roles',
|
||||
items: builtinRoles,
|
||||
})
|
||||
}
|
||||
if (customRoles.length > 0) {
|
||||
result.push({
|
||||
id: 'custom',
|
||||
category: 'global_custom',
|
||||
title: 'Custom Roles',
|
||||
items: customRoles,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import type { PaginationParameters } from '@/models/access-control'
|
||||
import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles'
|
||||
import { formatRoleGroups } from './helpers'
|
||||
|
||||
export const useRoleGroups = (params?: PaginationParameters) => {
|
||||
const { data: roleList, isLoading } = useWorkspaceRoleList(params)
|
||||
|
||||
const roleGroups = formatRoleGroups(roleList)
|
||||
|
||||
return {
|
||||
roleGroups,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
@ -1,118 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import type { Role, RoleListGroup } from './role-list'
|
||||
import type { RoleModalMode } from './role-modal'
|
||||
import type { RoleModalMode, submitRoleData } from './role-modal'
|
||||
import type { Role } from '@/models/access-control'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCreateWorkspaceRole, useUpdateWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
||||
import { useRoleGroups } from './hooks'
|
||||
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
|
||||
@ -121,6 +18,11 @@ type ModalState = {
|
||||
const PermissionsPage = () => {
|
||||
const [modalState, setModalState] = useState<ModalState>(null)
|
||||
|
||||
const { roleGroups } = useRoleGroups()
|
||||
|
||||
const { mutateAsync: createWorkspaceRole } = useCreateWorkspaceRole()
|
||||
const { mutateAsync: updateWorkspaceRole } = useUpdateWorkspaceRole()
|
||||
|
||||
const openCreate = useCallback(() => {
|
||||
setModalState({ mode: 'create' })
|
||||
}, [])
|
||||
@ -136,10 +38,28 @@ const PermissionsPage = () => {
|
||||
const closeModal = useCallback(() => setModalState(null), [])
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(_data: { name: string, description: string, permissions: string[] }) => {
|
||||
// TODO: wire up to API when backend is ready
|
||||
(data: submitRoleData) => {
|
||||
const { name, description, permissionKeys } = data
|
||||
const mode = modalState?.mode ?? ''
|
||||
const roleId = modalState?.role?.id ?? ''
|
||||
if (mode === 'create') {
|
||||
createWorkspaceRole({ name, description, permission_keys: permissionKeys }, {
|
||||
onSuccess: () => {
|
||||
toast.success('Role created successfully')
|
||||
closeModal()
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (mode === 'edit') {
|
||||
updateWorkspaceRole({ id: roleId, name, description, permission_keys: permissionKeys }, {
|
||||
onSuccess: () => {
|
||||
toast.success('Role updated successfully')
|
||||
closeModal()
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
[],
|
||||
[createWorkspaceRole, updateWorkspaceRole, closeModal, modalState],
|
||||
)
|
||||
|
||||
return (
|
||||
@ -165,7 +85,7 @@ const PermissionsPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<RoleList
|
||||
groups={MOCK_ROLE_GROUPS}
|
||||
groups={roleGroups}
|
||||
onView={handleView}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
|
||||
@ -1,20 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import type { Role, RoleCategory } from '@/models/access-control'
|
||||
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
|
||||
category: RoleCategory
|
||||
title: string
|
||||
items: Role[]
|
||||
}
|
||||
@ -24,7 +16,6 @@ export type RoleListProps = {
|
||||
className?: string
|
||||
onView?: (role: Role) => void
|
||||
onEdit?: (role: Role) => void
|
||||
onDelete?: (role: Role) => void
|
||||
}
|
||||
|
||||
const RoleList = ({
|
||||
@ -32,7 +23,6 @@ const RoleList = ({
|
||||
className,
|
||||
onView,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: RoleListProps) => {
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)}>
|
||||
@ -53,11 +43,10 @@ const RoleList = ({
|
||||
)}
|
||||
name={row.name}
|
||||
description={row.description}
|
||||
roleType={group.type}
|
||||
roleCategory={group.category}
|
||||
role={row}
|
||||
onView={onView}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { Role, RoleType } from '.'
|
||||
import type { Role, RoleCategory } from '@/models/access-control'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -7,26 +7,28 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useCallback, useState } from 'react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { useDeleteWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
||||
|
||||
type RowMenuProps = {
|
||||
roleType: RoleType
|
||||
roleCategory: RoleCategory
|
||||
role: Role
|
||||
onView?: (role: Role) => void
|
||||
onEdit?: (role: Role) => void
|
||||
onDelete?: (role: Role) => void
|
||||
}
|
||||
|
||||
const RowMenu = ({
|
||||
roleType,
|
||||
roleCategory,
|
||||
role,
|
||||
onView,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: RowMenuProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const { mutateAsync: deleteRole } = useDeleteWorkspaceRole()
|
||||
|
||||
const handleView = useCallback(() => onView?.(role), [onView, role])
|
||||
|
||||
const handleEdit = useCallback(() => onEdit?.(role), [onEdit, role])
|
||||
@ -35,7 +37,14 @@ const RowMenu = ({
|
||||
// TODO: wire up to API when backend is ready
|
||||
}, [])
|
||||
|
||||
const handleDelete = useCallback(() => onDelete?.(role), [onDelete, role])
|
||||
const handleDelete = useCallback(() => {
|
||||
deleteRole(role.id, {
|
||||
onSuccess: () => {
|
||||
toast.success('Role deleted successfully')
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
}, [deleteRole, role.id])
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
@ -44,14 +53,14 @@ const RowMenu = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-[160px]">
|
||||
{
|
||||
roleType === 'system' && (
|
||||
roleCategory === 'global_system_default' && (
|
||||
<DropdownMenuItem className="system-sm-semibold text-text-secondary" onClick={handleView}>
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
{
|
||||
roleType === 'custom' && (
|
||||
roleCategory === 'global_custom' && (
|
||||
<>
|
||||
<DropdownMenuItem className="system-sm-semibold text-text-secondary" onClick={handleEdit}>
|
||||
Edit
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Role, RoleType } from '.'
|
||||
import type { Role, RoleCategory } from '@/models/access-control'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo } from 'react'
|
||||
import RowMenu from './row-menu'
|
||||
@ -7,22 +7,20 @@ type RowProps = {
|
||||
className?: string
|
||||
name: string
|
||||
description: string
|
||||
roleType: RoleType
|
||||
roleCategory: RoleCategory
|
||||
role: Role
|
||||
onView?: (role: Role) => void
|
||||
onEdit?: (role: Role) => void
|
||||
onDelete?: (role: Role) => void
|
||||
}
|
||||
|
||||
const Row = ({
|
||||
className,
|
||||
name,
|
||||
description,
|
||||
roleType,
|
||||
roleCategory,
|
||||
role,
|
||||
onView,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: RowProps) => {
|
||||
return (
|
||||
<div
|
||||
@ -36,15 +34,14 @@ const Row = ({
|
||||
{name}
|
||||
</div>
|
||||
<p className="mt-1 system-sm-regular text-text-tertiary">
|
||||
{description}
|
||||
{description || 'No description'}
|
||||
</p>
|
||||
</div>
|
||||
<RowMenu
|
||||
roleType={roleType}
|
||||
roleCategory={roleCategory}
|
||||
role={role}
|
||||
onView={onView}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { useWorkspacePermissionCatalog } from '@/service/access-control/use-permission-catalog'
|
||||
|
||||
export const useWorkspacePermissionGroups = () => {
|
||||
const { data: workspacePermissionCatalog } = useWorkspacePermissionCatalog()
|
||||
|
||||
const groups = workspacePermissionCatalog?.groups || []
|
||||
|
||||
const allPermissions = groups.flatMap(g => g.permissions) || []
|
||||
|
||||
const permissionMap = Object.fromEntries(
|
||||
allPermissions.map(p => [p.key, p]),
|
||||
)
|
||||
|
||||
return {
|
||||
groups,
|
||||
permissionMap,
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { Role } from '../role-list'
|
||||
import type { Role } from '@/models/access-control'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
@ -9,23 +9,25 @@ import {
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, 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 submitRoleData = {
|
||||
name: string
|
||||
description?: string
|
||||
permissionKeys?: string[]
|
||||
}
|
||||
|
||||
export type RoleModalProps = {
|
||||
mode: RoleModalMode
|
||||
open: boolean
|
||||
role?: RoleModalRole
|
||||
role?: Role
|
||||
onClose: () => void
|
||||
onSubmit?: (data: { name: string, description: string, permissions: string[] }) => void
|
||||
onSubmit?: (data: submitRoleData) => void
|
||||
}
|
||||
|
||||
const TITLES: Record<RoleModalMode, { title: string, description: string }> = {
|
||||
@ -52,13 +54,21 @@ const RoleModal = ({
|
||||
}: RoleModalProps) => {
|
||||
const [name, setName] = useState(role?.name ?? '')
|
||||
const [desc, setDesc] = useState(role?.description ?? '')
|
||||
const [permissions, setPermissions] = useState<string[]>(role?.permissions ?? [])
|
||||
const [permissionKeys, setPermissionKeys] = useState<string[]>(role?.permission_keys ?? [])
|
||||
|
||||
const readonly = mode === 'view'
|
||||
const { title, description } = TITLES[mode]
|
||||
|
||||
const onRoleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value)
|
||||
}, [])
|
||||
|
||||
const onRoleDescChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDesc(e.target.value)
|
||||
}, [])
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.({ name: name.trim(), description: desc.trim(), permissions })
|
||||
onSubmit?.({ name: name.trim(), description: desc.trim(), permissionKeys })
|
||||
onClose()
|
||||
}
|
||||
|
||||
@ -94,7 +104,7 @@ const RoleModal = ({
|
||||
<Input
|
||||
id="role-name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
onChange={onRoleNameChange}
|
||||
placeholder="e.g. Marketing Lead"
|
||||
disabled={readonly}
|
||||
/>
|
||||
@ -106,15 +116,15 @@ const RoleModal = ({
|
||||
<Textarea
|
||||
id="role-description"
|
||||
value={desc}
|
||||
onChange={e => setDesc(e.target.value)}
|
||||
onChange={onRoleDescChange}
|
||||
placeholder="Describe what this role is responsible for"
|
||||
disabled={readonly}
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<PermissionField
|
||||
value={permissions}
|
||||
onChange={setPermissions}
|
||||
value={permissionKeys}
|
||||
onChange={setPermissionKeys}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useWorkspacePermissionGroups } from './hooks'
|
||||
import PermissionPicker from './permission-picker'
|
||||
import { PERMISSION_MAP } from './permissions-data'
|
||||
|
||||
export type PermissionFieldProps = {
|
||||
value: string[]
|
||||
@ -15,6 +15,8 @@ const PermissionField = ({
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: PermissionFieldProps) => {
|
||||
const { permissionMap } = useWorkspacePermissionGroups()
|
||||
|
||||
const handleRemove = (id: string) => {
|
||||
onChange(value.filter(p => p !== id))
|
||||
}
|
||||
@ -24,13 +26,13 @@ const PermissionField = ({
|
||||
<div className="system-sm-medium text-text-secondary">Permissions</div>
|
||||
{value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{value.map((id) => {
|
||||
const p = PERMISSION_MAP[id]
|
||||
{value.map((key) => {
|
||||
const p = permissionMap[key]
|
||||
if (!p)
|
||||
return null
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
key={key}
|
||||
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',
|
||||
@ -42,7 +44,7 @@ const PermissionField = ({
|
||||
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={() => handleRemove(id)}
|
||||
onClick={() => handleRemove(key)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-line h-3 w-3" />
|
||||
</button>
|
||||
@ -52,6 +54,13 @@ const PermissionField = ({
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
value.length === 0 && (
|
||||
<div className="system-sm-regular text-text-tertiary">
|
||||
No permissions assigned yet
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{!readonly && (
|
||||
<PermissionPicker value={value} onChange={onChange} />
|
||||
)}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { PermissionGroup } from './permissions-data'
|
||||
import type { PermissionGroup } from '@/models/access-control'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -12,7 +12,7 @@ import {
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { PERMISSION_GROUPS } from './permissions-data'
|
||||
import { useWorkspacePermissionGroups } from './hooks'
|
||||
|
||||
type PermissionPickerProps = {
|
||||
value: string[]
|
||||
@ -37,17 +37,19 @@ const PermissionPicker = ({ value, onChange, className }: PermissionPickerProps)
|
||||
return () => clearTimeout(timer)
|
||||
}, [open])
|
||||
|
||||
const { groups } = useWorkspacePermissionGroups()
|
||||
|
||||
const filteredGroups = useMemo<PermissionGroup[]>(() => {
|
||||
const q = search.trim().toLowerCase()
|
||||
if (!q)
|
||||
return PERMISSION_GROUPS
|
||||
return PERMISSION_GROUPS
|
||||
return groups
|
||||
return groups
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: group.items.filter(i => i.name.toLowerCase().includes(q)),
|
||||
permissions: group.permissions.filter(i => i.name.toLowerCase().includes(q)),
|
||||
}))
|
||||
.filter(group => group.items.length > 0)
|
||||
}, [search])
|
||||
.filter(group => group.permissions.length > 0)
|
||||
}, [search, groups])
|
||||
|
||||
const selectedSet = useMemo(() => new Set(value), [value])
|
||||
|
||||
@ -59,19 +61,19 @@ const PermissionPicker = ({ value, onChange, className }: PermissionPickerProps)
|
||||
}
|
||||
|
||||
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)))
|
||||
@ -130,7 +132,7 @@ const PermissionPicker = ({ value, onChange, className }: PermissionPickerProps)
|
||||
{filteredGroups.map((group, groupIndex) => {
|
||||
const { allChecked, indeterminate } = getGroupState(group)
|
||||
return (
|
||||
<DropdownMenuGroup key={group.id}>
|
||||
<DropdownMenuGroup key={group.group_key}>
|
||||
{groupIndex > 0 && <DropdownMenuSeparator />}
|
||||
<button
|
||||
type="button"
|
||||
@ -143,16 +145,16 @@ const PermissionPicker = ({ value, onChange, className }: PermissionPickerProps)
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span className="system-xs-medium-uppercase tracking-wide text-text-tertiary">
|
||||
{group.label}
|
||||
{group.group_name}
|
||||
</span>
|
||||
</button>
|
||||
{group.items.map((item) => {
|
||||
const checked = selectedSet.has(item.id)
|
||||
{group.permissions.map((item) => {
|
||||
const checked = selectedSet.has(item.key)
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={item.id}
|
||||
key={item.key}
|
||||
checked={checked}
|
||||
onCheckedChange={() => togglePermission(item.id)}
|
||||
onCheckedChange={() => togglePermission(item.key)}
|
||||
className="gap-2 pl-6"
|
||||
>
|
||||
<Checkbox checked={checked} className="pointer-events-none" />
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
export type Permission = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type PermissionGroup = {
|
||||
id: string
|
||||
label: string
|
||||
items: Permission[]
|
||||
}
|
||||
|
||||
export const PERMISSION_GROUPS: PermissionGroup[] = [
|
||||
{
|
||||
id: 'general',
|
||||
label: 'General',
|
||||
items: [
|
||||
{ id: 'manage_model_providers', name: 'Manage model providers' },
|
||||
{ id: 'manage_members', name: 'Manage members' },
|
||||
{ id: 'manage_roles_permissions', name: 'Manage roles & permissions' },
|
||||
{ id: 'manage_billing', name: 'Manage billing' },
|
||||
{ id: 'manage_data_sources', name: 'Manage data sources' },
|
||||
{ id: 'manage_api_extensions', name: 'Manage API extensions' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'apps',
|
||||
label: 'Apps',
|
||||
items: [
|
||||
{ id: 'create_apps', name: 'Create apps' },
|
||||
{ id: 'view_all_apps', name: 'View all apps' },
|
||||
{ id: 'delete_any_app', name: 'Delete any app' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'knowledge',
|
||||
label: 'Knowledge',
|
||||
items: [
|
||||
{ id: 'create_knowledge_bases', name: 'Create knowledge bases' },
|
||||
{ id: 'view_all_knowledge_bases', name: 'View all knowledge bases' },
|
||||
{ id: 'delete_any_knowledge_base', name: 'Delete any knowledge base' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'logs_audit',
|
||||
label: 'Logs & Audit',
|
||||
items: [
|
||||
{ id: 'view_all_app_logs', name: 'View all app logs' },
|
||||
{ id: 'cross_app_log_access', name: 'Cross-app log access' },
|
||||
{ id: 'view_sensitive_fields', name: 'View sensitive fields' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'plugins',
|
||||
label: 'Plugins',
|
||||
items: [
|
||||
{ id: 'install_plugins', name: 'Install plugins' },
|
||||
{ id: 'uninstall_plugins', name: 'Uninstall plugins' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const ALL_PERMISSIONS: Permission[] = PERMISSION_GROUPS.flatMap(g => g.items)
|
||||
|
||||
export const PERMISSION_MAP: Record<string, Permission> = Object.fromEntries(
|
||||
ALL_PERMISSIONS.map(p => [p.id, p]),
|
||||
)
|
||||
@ -1,14 +1,18 @@
|
||||
export enum SubjectType {
|
||||
GROUP = 'group',
|
||||
ACCOUNT = 'account',
|
||||
}
|
||||
export const SubjectType = {
|
||||
GROUP: 'group',
|
||||
ACCOUNT: 'account',
|
||||
} as const
|
||||
|
||||
export enum AccessMode {
|
||||
PUBLIC = 'public',
|
||||
SPECIFIC_GROUPS_MEMBERS = 'private',
|
||||
ORGANIZATION = 'private_all',
|
||||
EXTERNAL_MEMBERS = 'sso_verified',
|
||||
}
|
||||
export type SubjectType = typeof SubjectType[keyof typeof SubjectType]
|
||||
|
||||
export const AccessMode = {
|
||||
PUBLIC: 'public',
|
||||
SPECIFIC_GROUPS_MEMBERS: 'private',
|
||||
ORGANIZATION: 'private_all',
|
||||
EXTERNAL_MEMBERS: 'sso_verified',
|
||||
} as const
|
||||
|
||||
export type AccessMode = typeof AccessMode[keyof typeof AccessMode]
|
||||
|
||||
export type AccessControlGroup = {
|
||||
id: 'string'
|
||||
@ -28,3 +32,147 @@ export type SubjectGroup = { subjectId: string, subjectType: SubjectType, groupD
|
||||
export type SubjectAccount = { subjectId: string, subjectType: SubjectType, accountData: AccessControlAccount }
|
||||
|
||||
export type Subject = SubjectGroup | SubjectAccount
|
||||
|
||||
export type Permission = {
|
||||
key: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
export type PermissionGroup = {
|
||||
group_key: string
|
||||
group_name: string
|
||||
description: string
|
||||
permissions: Permission[]
|
||||
}
|
||||
|
||||
export type PermissionGroups = {
|
||||
groups: PermissionGroup[]
|
||||
}
|
||||
|
||||
export type PermissionKey = string
|
||||
|
||||
export type RoleType = 'workspace' | 'app' | 'dataset'
|
||||
|
||||
export type RoleCategory = 'global_system_default' | 'global_custom'
|
||||
|
||||
export type Role = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
type: RoleType
|
||||
category: RoleCategory
|
||||
name: string
|
||||
description: string
|
||||
is_builtin: boolean
|
||||
permission_keys: PermissionKey[]
|
||||
}
|
||||
|
||||
export type Pagination = {
|
||||
total_count: number
|
||||
per_page: number
|
||||
current_page: number
|
||||
total_pages: number
|
||||
}
|
||||
|
||||
export type PaginationParameters = {
|
||||
page?: number
|
||||
limit?: number
|
||||
reverse?: boolean
|
||||
}
|
||||
|
||||
export type RoleListResponse = {
|
||||
data: Role[]
|
||||
pagination: Pagination
|
||||
}
|
||||
|
||||
export type CreateRoleRequest = {
|
||||
name: string
|
||||
description?: string
|
||||
permission_keys?: PermissionKey[]
|
||||
}
|
||||
|
||||
export type UpdateRolesRequest = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
permission_keys?: PermissionKey[]
|
||||
}
|
||||
|
||||
export type AccessPolicyResourceType = 'app' | 'dataset'
|
||||
|
||||
export type AccessPolicyCategory = 'global_system_default' | 'global_custom'
|
||||
|
||||
export type AccessPolicy = {
|
||||
id: string
|
||||
tenant_id: string
|
||||
resource_type: AccessPolicyResourceType
|
||||
policy_key: string
|
||||
name: string
|
||||
description: string
|
||||
permission_keys: PermissionKey[]
|
||||
is_builtin: boolean
|
||||
category: AccessPolicyCategory
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type GetAccessPoliciesRequest = {
|
||||
resource_type?: AccessPolicyResourceType
|
||||
} & PaginationParameters
|
||||
|
||||
export type GetAccessPoliciesResponse = {
|
||||
data: AccessPolicy[]
|
||||
pagination: Pagination
|
||||
}
|
||||
|
||||
export type CreateAccessPolicyRequest = {
|
||||
resource_type: AccessPolicyResourceType
|
||||
name: string
|
||||
description?: string
|
||||
permission_keys?: PermissionKey[]
|
||||
}
|
||||
|
||||
export type UpdateAccessPolicyRequest = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
permission_keys?: PermissionKey[]
|
||||
}
|
||||
|
||||
export type Bindings = {
|
||||
role_ids: string[]
|
||||
account_ids: string[]
|
||||
}
|
||||
|
||||
export type AccessPolicyWithBindings = {
|
||||
policy: AccessPolicy
|
||||
} & Bindings
|
||||
|
||||
export type GetAppAccessPolicyByAppIdResponse = {
|
||||
app_id: string
|
||||
items: AccessPolicyWithBindings[]
|
||||
}
|
||||
|
||||
export type GetDatasetAccessPolicyByDatasetIdResponse = {
|
||||
dataset_id: string
|
||||
items: AccessPolicyWithBindings[]
|
||||
}
|
||||
|
||||
export type GetAppAccessPoliciesResponse = {
|
||||
items: AccessPolicyWithBindings[]
|
||||
pagination: Pagination
|
||||
}
|
||||
|
||||
export type GetDatasetAccessPoliciesResponse = {
|
||||
items: AccessPolicyWithBindings[]
|
||||
pagination: Pagination
|
||||
}
|
||||
|
||||
export type UpdateRolesOfMemberRequest = {
|
||||
member_id: string
|
||||
role_ids: string[]
|
||||
}
|
||||
|
||||
export type UpdateRolesOfMemberResponse = {
|
||||
account_id: string
|
||||
roles: Role[]
|
||||
}
|
||||
|
||||
28
web/service/access-control/use-permission-catalog.ts
Normal file
28
web/service/access-control/use-permission-catalog.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type {
|
||||
PermissionGroups,
|
||||
} from '@/models/access-control'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { get } from '../base'
|
||||
|
||||
const NAME_SPACE = 'rbac-permission-catalog'
|
||||
|
||||
export const useWorkspacePermissionCatalog = () => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'workspace'],
|
||||
queryFn: () => get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog'),
|
||||
})
|
||||
}
|
||||
|
||||
export const useAppPermissionCatalog = () => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'app'],
|
||||
queryFn: () => get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog/app'),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDatasetPermissionCatalog = () => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'dataset'],
|
||||
queryFn: () => get<PermissionGroups>('/workspaces/current/rbac/role-permissions/catalog/dataset'),
|
||||
})
|
||||
}
|
||||
59
web/service/access-control/use-workspace-roles.ts
Normal file
59
web/service/access-control/use-workspace-roles.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type {
|
||||
CreateRoleRequest,
|
||||
PaginationParameters,
|
||||
RoleListResponse,
|
||||
} from '@/models/access-control'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { del, get, post, put } from '../base'
|
||||
|
||||
const NAME_SPACE = 'rbac-role-management'
|
||||
|
||||
export const useWorkspaceRoleList = (params?: PaginationParameters) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'workspace-role-list', params],
|
||||
queryFn: () => get<RoleListResponse>('/workspaces/current/rbac/roles', { params }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateWorkspaceRole = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'create-workspace-role'],
|
||||
mutationFn: (data: CreateRoleRequest) =>
|
||||
post<RoleListResponse>('/workspaces/current/rbac/roles', {
|
||||
body: { ...data },
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'workspace-role-list'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateWorkspaceRole = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'update-workspace-role'],
|
||||
mutationFn: (data: CreateRoleRequest & { id: string }) =>
|
||||
put<RoleListResponse>(`/workspaces/current/rbac/roles/${data.id}`, {
|
||||
body: { ...data },
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'workspace-role-list'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteWorkspaceRole = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'delete-workspace-role'],
|
||||
mutationFn: (id: string) =>
|
||||
del(`/workspaces/current/rbac/roles/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'workspace-role-list'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user