feat: refactor access rule management to use updated role and account structures, enhance loading states, and improve role assignment functionality

This commit is contained in:
twwu 2026-05-12 10:05:30 +08:00
parent a3b00a2f83
commit 9609f003c6
18 changed files with 255 additions and 169 deletions

View File

@ -26,7 +26,7 @@ const AccessRuleRow = ({
onEdit, onEdit,
onAddRole, onAddRole,
}: AccessRuleRowProps) => { }: AccessRuleRowProps) => {
const { policy, role_ids, account_ids } = rule const { policy, roles, accounts } = rule
const { id: policyId, resource_type } = policy const { id: policyId, resource_type } = policy
const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule]) const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule])
@ -38,8 +38,8 @@ const AccessRuleRow = ({
const handleRemoveRole = useCallback((id: string, type: BindingType) => { const handleRemoveRole = useCallback((id: string, type: BindingType) => {
const payload = { const payload = {
id: policyId, id: policyId,
role_ids: role_ids.map(role => role.id), role_ids: roles.map(role => role.role_id),
account_ids: account_ids.map(account => account.id), account_ids: accounts.map(account => account.account_id),
} }
if (type === 'role') { if (type === 'role') {
payload.role_ids = payload.role_ids.filter(roleId => roleId !== id) payload.role_ids = payload.role_ids.filter(roleId => roleId !== id)
@ -61,7 +61,7 @@ const AccessRuleRow = ({
}, },
}) })
} }
}, [account_ids, policyId, resource_type, role_ids, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings]) }, [accounts, policyId, resource_type, roles, updateAppAccessRuleBindings, updateDatasetAccessRuleBindings])
return ( return (
<div className={cn('flex items-start gap-2 py-3.5', className)}> <div className={cn('flex items-start gap-2 py-3.5', className)}>
@ -73,20 +73,20 @@ const AccessRuleRow = ({
{policy.description} {policy.description}
</p> </p>
<div className="mt-2 flex flex-wrap items-center gap-1.5"> <div className="mt-2 flex flex-wrap items-center gap-1.5">
{role_ids.map(role => ( {roles.map(role => (
<RoleTag <RoleTag
key={role.id} key={role.role_id}
id={role.id} id={role.role_id}
label={role.name} label={role.role_name}
type="role" type="role"
onRemove={handleRemoveRole} onRemove={handleRemoveRole}
/> />
))} ))}
{account_ids.map(account => ( {accounts.map(account => (
<RoleTag <RoleTag
key={account.id} key={account.account_id}
id={account.id} id={account.account_id}
label={account.name} label={account.account_name}
type="account" type="account"
onRemove={handleRemoveRole} onRemove={handleRemoveRole}
/> />

View File

@ -9,6 +9,7 @@ import AccessRuleRow from './access-rule-row'
type AccessRuleSectionProps = { type AccessRuleSectionProps = {
title: string title: string
rules: AccessPolicyWithBindings[] rules: AccessPolicyWithBindings[]
isLoadingRules: boolean
createButtonLabel: string createButtonLabel: string
onCreate?: () => void onCreate?: () => void
onEditRule?: (rule: AccessPolicyWithBindings) => void onEditRule?: (rule: AccessPolicyWithBindings) => void
@ -19,6 +20,7 @@ type AccessRuleSectionProps = {
const AccessRuleSection = ({ const AccessRuleSection = ({
title, title,
rules, rules,
isLoadingRules,
createButtonLabel, createButtonLabel,
onCreate, onCreate,
onEditRule, onEditRule,
@ -31,7 +33,12 @@ const AccessRuleSection = ({
<h3 className="pr-3 system-xs-medium-uppercase tracking-wide text-text-tertiary"> <h3 className="pr-3 system-xs-medium-uppercase tracking-wide text-text-tertiary">
{title} {title}
</h3> </h3>
<Button variant="secondary" size="medium" onClick={onCreate}> <Button
variant="secondary"
size="medium"
onClick={onCreate}
disabled={isLoadingRules}
>
{createButtonLabel} {createButtonLabel}
</Button> </Button>
</div> </div>

View File

@ -15,7 +15,7 @@ const AppAccessRuleSection = ({
onEditRule, onEditRule,
onAddRole, onAddRole,
}: AppAccessRuleSectionProps) => { }: AppAccessRuleSectionProps) => {
const { data: appAccessRulesResponse } = useWorkspaceAppAccessRules({ const { data: appAccessRulesResponse, isLoading } = useWorkspaceAppAccessRules({
page: 1, page: 1,
limit: 20, limit: 20,
}) })
@ -26,6 +26,7 @@ const AppAccessRuleSection = ({
<AccessRuleSection <AccessRuleSection
title="App Access Rules" title="App Access Rules"
rules={appAccessRules} rules={appAccessRules}
isLoadingRules={isLoading}
createButtonLabel="Create App permission set" createButtonLabel="Create App permission set"
onCreate={onCreate} onCreate={onCreate}
onEditRule={onEditRule} onEditRule={onEditRule}

View File

@ -15,7 +15,7 @@ const DatasetAccessRuleSection = ({
onEditRule, onEditRule,
onAddRole, onAddRole,
}: DatasetAccessRuleSectionProps) => { }: DatasetAccessRuleSectionProps) => {
const { data: datasetAccessRulesResponse } = useWorkspaceDatasetAccessRules({ const { data: datasetAccessRulesResponse, isLoading } = useWorkspaceDatasetAccessRules({
page: 1, page: 1,
limit: 20, limit: 20,
}) })
@ -26,6 +26,7 @@ const DatasetAccessRuleSection = ({
<AccessRuleSection <AccessRuleSection
title="Knowledge Base Access Rules" title="Knowledge Base Access Rules"
rules={datasetAccessRules} rules={datasetAccessRules}
isLoadingRules={isLoading}
createButtonLabel="Create KB permission set" createButtonLabel="Create KB permission set"
onCreate={onCreate} onCreate={onCreate}
onEditRule={onEditRule} onEditRule={onEditRule}

View File

@ -4,7 +4,7 @@ import type { PermissionSetFormValues, PermissionSetModalMode } from './permissi
import type { AccessPolicyResourceType, AccessPolicyWithBindings } from '@/models/access-control' import type { AccessPolicyResourceType, AccessPolicyWithBindings } from '@/models/access-control'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useCreateAccessRule, useUpdateAppAccessRuleBindings, useUpdateDatasetAccessRuleBindings } from '@/service/access-control/use-workspace-access-rules' import { useCreateAccessRule, useUpdateAccessRule, useUpdateAppAccessRuleBindings, useUpdateDatasetAccessRuleBindings } from '@/service/access-control/use-workspace-access-rules'
import AddRuleTargetsModal from './add-rule-targets-modal' import AddRuleTargetsModal from './add-rule-targets-modal'
import AppAccessRuleSection from './app-access-rule-section' import AppAccessRuleSection from './app-access-rule-section'
import DatasetAccessRuleSection from './dataset-access-rule-section' import DatasetAccessRuleSection from './dataset-access-rule-section'
@ -13,6 +13,7 @@ import PermissionSetModal from './permission-set-modal'
type PermissionSetModalState = { type PermissionSetModalState = {
mode: PermissionSetModalMode mode: PermissionSetModalMode
resourceType: AccessPolicyResourceType resourceType: AccessPolicyResourceType
ruleId?: string
initialValues?: PermissionSetFormValues initialValues?: PermissionSetFormValues
} }
@ -74,6 +75,7 @@ const AccessRulesPage = () => {
setPermissionSetModalState({ setPermissionSetModalState({
mode: 'edit', mode: 'edit',
resourceType, resourceType,
ruleId: policy.id,
initialValues: { initialValues: {
name: policy.name, name: policy.name,
description: policy.description, description: policy.description,
@ -84,23 +86,43 @@ const AccessRulesPage = () => {
[], [],
) )
const { mutateAsync: createAccessRule } = useCreateAccessRule(permissionSetModalState?.resourceType) const { mutateAsync: createAccessRule } = useCreateAccessRule()
const { mutateAsync: updateAccessRule } = useUpdateAccessRule()
const handlePermissionSetSubmit = useCallback( const handlePermissionSetSubmit = useCallback(
(values: PermissionSetFormValues) => { (values: PermissionSetFormValues) => {
const mode = permissionSetModalState?.mode || ''
const id = permissionSetModalState?.ruleId || ''
const { name, description, permissionKeys } = values const { name, description, permissionKeys } = values
createAccessRule({ if (mode === 'create') {
name, createAccessRule({
description, name,
permission_keys: permissionKeys, description,
}, { permission_keys: permissionKeys,
onSuccess: () => { resourceType: permissionSetModalState!.resourceType,
toast.success('Access rule created successfully') }, {
closePermissionSetModal() onSuccess: () => {
}, toast.success('Access rule created successfully')
}) closePermissionSetModal()
},
})
}
else if (mode === 'edit') {
updateAccessRule({
id: id!,
name,
description,
permission_keys: permissionKeys,
resourceType: permissionSetModalState!.resourceType,
}, {
onSuccess: () => {
toast.success('Access rule updated successfully')
closePermissionSetModal()
},
})
}
}, },
[closePermissionSetModal, createAccessRule], [closePermissionSetModal, createAccessRule, updateAccessRule, permissionSetModalState],
) )
const createApp = useCallback(() => handleCreate('app'), [handleCreate]) const createApp = useCallback(() => handleCreate('app'), [handleCreate])
@ -131,8 +153,8 @@ const AccessRulesPage = () => {
{addingRule && ( {addingRule && (
<AddRuleTargetsModal <AddRuleTargetsModal
ruleName={addingRule.policy.name} ruleName={addingRule.policy.name}
initialRoleIds={addingRule.role_ids.map(role => role.id)} initialRoleIds={addingRule.roles.map(role => role.role_id)}
initialMemberIds={addingRule.account_ids.map(account => account.id)} initialMemberIds={addingRule.accounts.map(account => account.account_id)}
onClose={closeAddModal} onClose={closeAddModal}
onSubmit={handleAddSubmit} onSubmit={handleAddSubmit}
/> />

View File

@ -15,6 +15,7 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox' import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles'
export type AssignableRole = { export type AssignableRole = {
id: string id: string
@ -23,38 +24,31 @@ export type AssignableRole = {
} }
export type AssignRolesModalProps = { export type AssignRolesModalProps = {
open: boolean
member: Member member: Member
onClose: () => void onClose: () => void
onSubmit: (roleIds: string[]) => void onSubmit: (roleIds: string[]) => void
} }
type AssignRolesModalBodyProps = { type AssignRolesModalBodyProps = AssignRolesModalProps
roles: AssignableRole[]
} & Omit<AssignRolesModalProps, 'open'>
// TODO: replace with roles fetched from the permissions API once available.
const MOCK_ASSIGNABLE_ROLES: AssignableRole[] = [
{ id: 'admin', name: 'Admin', description: 'Full access to workspace management and settings' },
{ id: 'editor', name: 'Editor', description: 'Create and edit resources without settings access' },
{ id: 'member', name: 'Member', description: 'Basic workspace access' },
{ id: 'auditor', name: 'Auditor', description: 'View application logs and audit trails' },
{ id: 'tester', name: 'Tester', description: 'Test applications in sandbox environments' },
]
const AssignRolesModalBody = ({ const AssignRolesModalBody = ({
roles,
member, member,
onClose, onClose,
onSubmit, onSubmit,
}: AssignRolesModalBodyProps) => { }: AssignRolesModalBodyProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [selected, setSelected] = useState<string[]>(() => { const [selected, setSelected] = useState<string[]>(() => {
const match = MOCK_ASSIGNABLE_ROLES.find(r => r.id === member.role) return member.roles?.map(role => role.id) || []
return match ? [match.id] : []
}) })
const [keyword, setKeyword] = useState('') const [keyword, setKeyword] = useState('')
const { data: rolesData, isLoading: rolesLoading } = useWorkspaceRoleList({
page: 1,
limit: 20,
})
const roles = useMemo(() => rolesData?.data ?? [], [rolesData])
const filteredRoles = useMemo(() => { const filteredRoles = useMemo(() => {
const trimmed = keyword.trim().toLowerCase() const trimmed = keyword.trim().toLowerCase()
if (!trimmed) if (!trimmed)
@ -116,58 +110,62 @@ const AssignRolesModalBody = ({
className="mt-2 min-h-0 flex-1" className="mt-2 min-h-0 flex-1"
slotClassNames={{ viewport: 'px-3 overscroll-contain' }} slotClassNames={{ viewport: 'px-3 overscroll-contain' }}
> >
{filteredRoles.length === 0 {rolesLoading
? ( ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary"> <div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t('members.assignRolesModal.empty', { Loading roles...
ns: 'common',
defaultValue: 'No matching roles',
})}
</div> </div>
) )
: ( : filteredRoles.length === 0
<ul className="flex flex-col gap-0.5"> ? (
{filteredRoles.map((role) => { <div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
const checked = selected.includes(role.id) {t('members.assignRolesModal.empty', {
const handleToggle = () => toggle(role.id) ns: 'common',
return ( defaultValue: 'No matching roles',
<li key={role.id}> })}
<div </div>
role="checkbox" )
aria-checked={checked} : (
tabIndex={0} <ul className="flex flex-col gap-0.5">
className={cn( {filteredRoles.map((role) => {
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active', const checked = selected.includes(role.id)
checked && 'bg-state-accent-hover hover:bg-state-accent-hover', const handleToggle = () => toggle(role.id)
)} return (
onClick={handleToggle} <li key={role.id}>
onKeyDown={(e) => { <div
if (e.key === ' ' || e.key === 'Enter') { role="checkbox"
e.preventDefault() aria-checked={checked}
handleToggle() tabIndex={0}
} className={cn(
}} 'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
> checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
<Checkbox
checked={checked}
className="pointer-events-none mt-0.5"
/>
<div className="min-w-0 flex-1">
<div className="system-sm-semibold text-text-secondary">
{role.name}
</div>
{role.description && (
<div className="mt-0.5 system-xs-regular text-text-tertiary">
{role.description}
</div>
)} )}
onClick={handleToggle}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
handleToggle()
}
}}
>
<Checkbox
checked={checked}
className="pointer-events-none mt-0.5"
/>
<div className="min-w-0 flex-1">
<div className="system-sm-semibold text-text-secondary">
{role.name}
</div>
<div className="mt-0.5 system-xs-regular text-text-tertiary">
{role.description || 'No description'}
</div>
</div>
</div> </div>
</div> </li>
</li> )
) })}
})} </ul>
</ul> )}
)}
</ScrollArea> </ScrollArea>
<div className="flex shrink-0 items-center justify-between gap-3 border-t border-divider-subtle px-6 py-4"> <div className="flex shrink-0 items-center justify-between gap-3 border-t border-divider-subtle px-6 py-4">
@ -175,7 +173,6 @@ const AssignRolesModalBody = ({
{t('members.assignRolesModal.selectedCount', { {t('members.assignRolesModal.selectedCount', {
ns: 'common', ns: 'common',
count: selected.length, count: selected.length,
defaultValue: '{{count}} selected',
})} })}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -192,21 +189,19 @@ const AssignRolesModalBody = ({
} }
const AssignRolesModal = ({ const AssignRolesModal = ({
open,
member, member,
onClose, onClose,
onSubmit, onSubmit,
}: AssignRolesModalProps) => { }: AssignRolesModalProps) => {
return ( return (
<Dialog <Dialog
open={open} open
onOpenChange={(nextOpen) => { onOpenChange={(nextOpen) => {
if (!nextOpen) if (!nextOpen)
onClose() onClose()
}} }}
> >
<AssignRolesModalBody <AssignRolesModalBody
roles={MOCK_ASSIGNABLE_ROLES}
member={member} member={member}
onClose={onClose} onClose={onClose}
onSubmit={onSubmit} onSubmit={onSubmit}

View File

@ -24,13 +24,6 @@ import TransferOwnershipModal from './transfer-ownership-modal'
const MembersPage = () => { const MembersPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
const RoleMap = {
owner: t('members.owner', { ns: 'common' }),
admin: t('members.admin', { ns: 'common' }),
editor: t('members.editor', { ns: 'common' }),
dataset_operator: t('members.datasetOperator', { ns: 'common' }),
normal: t('members.normal', { ns: 'common' }),
}
const locale = useLocale() const locale = useLocale()
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext() const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
@ -141,7 +134,7 @@ const MembersPage = () => {
<MemberRow <MemberRow
key={account.id} key={account.id}
member={account} member={account}
roleLabel={RoleMap[account.role] || RoleMap.normal} roles={account.roles}
isCurrentUser={userProfile.email === account.email} isCurrentUser={userProfile.email === account.email}
canManage={isCurrentWorkspaceManager} canManage={isCurrentWorkspaceManager}
operatorRole={currentWorkspace.role} operatorRole={currentWorkspace.role}
@ -192,7 +185,6 @@ const MembersPage = () => {
<MemberDetailsModal <MemberDetailsModal
open={!!detailsMember} open={!!detailsMember}
member={detailsMember} member={detailsMember}
roleLabel={RoleMap[detailsMember.role] || RoleMap.normal}
canAssignRoles={ canAssignRoles={
isCurrentWorkspaceManager isCurrentWorkspaceManager
&& detailsMember.role !== 'owner' && detailsMember.role !== 'owner'

View File

@ -9,15 +9,15 @@ import {
DialogContent, DialogContent,
DialogTitle, DialogTitle,
} from '@langgenius/dify-ui/dialog' } from '@langgenius/dify-ui/dialog'
import { memo, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useRolesOfMember } from '@/service/access-control/use-member-roles'
import AssignRolesModal from '../assign-roles-modal' import AssignRolesModal from '../assign-roles-modal'
import PermissionRoleChip from './permission-role-chip' import PermissionRoleChip from './permission-role-chip'
export type MemberDetailsModalProps = { export type MemberDetailsModalProps = {
open: boolean open: boolean
member: Member member: Member
roleLabel: string
canAssignRoles?: boolean canAssignRoles?: boolean
onClose: () => void onClose: () => void
onAssignSubmit?: (roleIds: string[]) => void onAssignSubmit?: (roleIds: string[]) => void
@ -26,7 +26,6 @@ export type MemberDetailsModalProps = {
const MemberDetailsModal = ({ const MemberDetailsModal = ({
open, open,
member, member,
roleLabel,
canAssignRoles = false, canAssignRoles = false,
onClose, onClose,
onAssignSubmit, onAssignSubmit,
@ -34,12 +33,21 @@ const MemberDetailsModal = ({
const { t } = useTranslation() const { t } = useTranslation()
const [assignOpen, setAssignOpen] = useState(false) const [assignOpen, setAssignOpen] = useState(false)
const assignedRoles = [{ key: member.role, label: roleLabel }] const { data: rolesOfMember } = useRolesOfMember(member.id)
const handleAssignSubmit = (ids: string[]) => { const roles = rolesOfMember?.roles || []
const builtinRoles = roles.filter(role => role.is_builtin)
const customRoles = roles.filter(role => !role.is_builtin)
const handleClose = useCallback(() => {
setAssignOpen(false)
}, [])
const handleAssignSubmit = useCallback((ids: string[]) => {
onAssignSubmit?.(ids) onAssignSubmit?.(ids)
setAssignOpen(false) setAssignOpen(false)
} }, [onAssignSubmit])
return ( return (
<> <>
@ -87,7 +95,7 @@ const MemberDetailsModal = ({
})} })}
</span> </span>
<span className="system-xs-medium text-text-tertiary"> <span className="system-xs-medium text-text-tertiary">
{assignedRoles.length} {roles.length}
</span> </span>
</div> </div>
{canAssignRoles && ( {canAssignRoles && (
@ -108,33 +116,50 @@ const MemberDetailsModal = ({
)} )}
</div> </div>
<div className="mt-4"> {builtinRoles.length > 0 && (
<div className="mb-2 system-2xs-medium-uppercase text-text-tertiary"> <div className="mt-4">
{t('members.memberDetails.generalGroup', { <div className="mb-2 system-2xs-medium-uppercase text-text-tertiary">
ns: 'common', {t('members.memberDetails.generalGroup', {
defaultValue: 'GENERAL', ns: 'common',
})} })}
</div>
<div className="flex flex-wrap gap-1.5">
{builtinRoles.map(role => (
<PermissionRoleChip
key={role.id}
roleKey={role.id}
label={role.name}
/>
))}
</div>
</div> </div>
<div className="flex flex-wrap gap-1.5"> )}
{assignedRoles.map(role => ( {customRoles.length > 0 && (
<PermissionRoleChip <div className="mt-4">
key={role.key} <div className="mb-2 system-2xs-medium-uppercase text-text-tertiary">
roleKey={role.key} {t('members.memberDetails.customGroup', {
label={role.label} ns: 'common',
highlighted })}
/> </div>
))} <div className="flex flex-wrap gap-1.5">
{customRoles.map(role => (
<PermissionRoleChip
key={role.id}
roleKey={role.id}
label={role.name}
/>
))}
</div>
</div> </div>
</div> )}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{assignOpen && ( {assignOpen && (
<AssignRolesModal <AssignRolesModal
open={assignOpen}
member={member} member={member}
onClose={() => setAssignOpen(false)} onClose={handleClose}
onSubmit={handleAssignSubmit} onSubmit={handleAssignSubmit}
/> />
)} )}

View File

@ -13,7 +13,6 @@ import { getRolePermissionKeys } from './role-permissions'
export type PermissionRoleChipProps = { export type PermissionRoleChipProps = {
roleKey: string roleKey: string
label: string label: string
highlighted?: boolean
onRemove?: () => void onRemove?: () => void
className?: string className?: string
} }
@ -21,7 +20,6 @@ export type PermissionRoleChipProps = {
const PermissionRoleChip = ({ const PermissionRoleChip = ({
roleKey, roleKey,
label, label,
highlighted = false,
onRemove, onRemove,
className, className,
}: PermissionRoleChipProps) => { }: PermissionRoleChipProps) => {
@ -32,10 +30,8 @@ const PermissionRoleChip = ({
const chip = ( const chip = (
<span <span
className={cn( className={cn(
'inline-flex h-6 max-w-full cursor-default items-center gap-1 rounded-md px-1.5 system-xs-medium shadow-xs', 'group inline-flex h-6 max-w-full cursor-default items-center gap-1 rounded-md px-1.5 system-xs-medium shadow-xs',
highlighted 'bg-background-body text-text-secondary group-hover:bg-state-accent-hover group-hover:text-text-accent',
? 'bg-state-accent-hover text-text-accent'
: 'bg-background-body text-text-secondary',
className, className,
)} )}
data-testid="permission-role-chip" data-testid="permission-role-chip"
@ -56,7 +52,7 @@ const PermissionRoleChip = ({
}} }}
className={cn( className={cn(
'flex h-4 w-4 items-center justify-center rounded hover:bg-black/5', 'flex h-4 w-4 items-center justify-center rounded hover:bg-black/5',
highlighted ? 'text-text-accent' : 'text-text-tertiary', 'text-text-tertiary',
)} )}
> >
<span aria-hidden className="i-ri-close-line h-3 w-3" /> <span aria-hidden className="i-ri-close-line h-3 w-3" />

View File

@ -10,7 +10,10 @@ import RoleBadges from './role-badges'
type MemberRowProps = { type MemberRowProps = {
member: Member member: Member
roleLabel: string roles: Array<{
id: string
name: string
}>
isCurrentUser: boolean isCurrentUser: boolean
canManage: boolean canManage: boolean
operatorRole: string operatorRole: string
@ -22,7 +25,7 @@ type MemberRowProps = {
const MemberRow = ({ const MemberRow = ({
member, member,
roleLabel, roles,
isCurrentUser, isCurrentUser,
canManage, canManage,
operatorRole, operatorRole,
@ -34,6 +37,8 @@ const MemberRow = ({
const { t } = useTranslation() const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow() const { formatTimeFromNow } = useFormatTimeFromNow()
const roleNames = roles.map(role => role.name)
const openDetails = useCallback(() => { const openDetails = useCallback(() => {
onOpenDetails(member) onOpenDetails(member)
}, [member, onOpenDetails]) }, [member, onOpenDetails])
@ -98,7 +103,7 @@ const MemberRow = ({
> >
<RoleBadges <RoleBadges
className="grow" className="grow"
roles={[roleLabel]} roleNames={roleNames}
/> />
{canManage && ( {canManage && (
<MemberMenu <MemberMenu

View File

@ -22,17 +22,17 @@ const RoleBadge = ({ label, className }: RoleBadgeProps) => {
} }
export type RoleBadgesProps = { export type RoleBadgesProps = {
roles: string[] roleNames: string[]
max?: number max?: number
className?: string className?: string
} }
const RoleBadges = ({ roles, max = 2, className }: RoleBadgesProps) => { const RoleBadges = ({ roleNames, max = 2, className }: RoleBadgesProps) => {
if (!roles.length) if (!roleNames.length)
return null return null
const visible = roles.slice(0, max) const visible = roleNames.slice(0, max)
const overflow = roles.slice(max) const overflow = roleNames.slice(max)
return ( return (
<div className={cn('flex min-w-0 flex-wrap items-center gap-1', className)}> <div className={cn('flex min-w-0 flex-wrap items-center gap-1', className)}>

View File

@ -246,6 +246,7 @@
"members.memberActions": "Member actions", "members.memberActions": "Member actions",
"members.memberDetails.assign": "Assign", "members.memberDetails.assign": "Assign",
"members.memberDetails.assignedRoles": "Assigned Roles", "members.memberDetails.assignedRoles": "Assigned Roles",
"members.memberDetails.customGroup": "CUSTOMIZED",
"members.memberDetails.generalGroup": "GENERAL", "members.memberDetails.generalGroup": "GENERAL",
"members.memberDetails.openAria": "Open member details for {{name}}", "members.memberDetails.openAria": "Open member details for {{name}}",
"members.memberDetails.permissions.assignRoles": "Assign roles", "members.memberDetails.permissions.assignRoles": "Assign roles",

View File

@ -246,7 +246,8 @@
"members.memberActions": "成员操作", "members.memberActions": "成员操作",
"members.memberDetails.assign": "分配", "members.memberDetails.assign": "分配",
"members.memberDetails.assignedRoles": "已分配角色", "members.memberDetails.assignedRoles": "已分配角色",
"members.memberDetails.generalGroup": "通用角色", "members.memberDetails.customGroup": "自定义",
"members.memberDetails.generalGroup": "通用",
"members.memberDetails.openAria": "打开 {{name}} 的成员详情", "members.memberDetails.openAria": "打开 {{name}} 的成员详情",
"members.memberDetails.permissions.assignRoles": "分配角色", "members.memberDetails.permissions.assignRoles": "分配角色",
"members.memberDetails.permissions.createApps": "创建应用", "members.memberDetails.permissions.createApps": "创建应用",

View File

@ -131,13 +131,13 @@ export type UpdateAccessPolicyRequest = {
export type BindingType = 'role' | 'account' export type BindingType = 'role' | 'account'
export type Bindings = { export type Bindings = {
role_ids: Array<{ roles: Array<{
id: string role_id: string
name: string role_name: string
}> }>
account_ids: Array<{ accounts: Array<{
id: string account_id: string
name: string account_name: string
}> }>
} }
@ -170,12 +170,12 @@ export type GetDatasetAccessPoliciesResponse = {
pagination: Pagination pagination: Pagination
} }
export type RolesOfMemberResponse = {
account_id: string
roles: Role[]
}
export type UpdateRolesOfMemberRequest = { export type UpdateRolesOfMemberRequest = {
member_id: string member_id: string
role_ids: string[] role_ids: string[]
} }
export type UpdateRolesOfMemberResponse = {
account_id: string
roles: Role[]
}

View File

@ -54,6 +54,10 @@ export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_l
avatar: string avatar: string
status: 'pending' | 'active' | 'banned' | 'closed' status: 'pending' | 'active' | 'banned' | 'closed'
role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator' role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator'
roles: Array<{
id: string
name: string
}>
} }
enum ProviderName { enum ProviderName {

View File

@ -0,0 +1,27 @@
import type { RolesOfMemberResponse } from '@/models/access-control'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { get, put } from '../base'
const NAME_SPACE = 'rbac-member-roles'
export const useRolesOfMember = (memberId: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'member-roles', memberId],
queryFn: () => get<RolesOfMemberResponse>(`/workspaces/current/rbac/members/${memberId}/rbac-roles`),
})
}
export const useUpdateRolesOfMember = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'update-member-roles'],
mutationFn: ({ memberId, roleIds }: { memberId: string, roleIds: string[] }) =>
put(`/workspaces/current/rbac/members/${memberId}/rbac-roles`, {
body: { role_ids: roleIds },
}),
onSuccess: (_, { memberId }) => {
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'member-roles', memberId] })
},
})
}

View File

@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { get } from '../base'
const NAME_SPACE = 'workspace-permission-keys'
export const useWorkspacePermissionKeys = () => {
return useQuery({
queryKey: [NAME_SPACE],
queryFn: () => get('/workspaces/current/rbac/my-permissions'),
})
}

View File

@ -29,13 +29,13 @@ export const useWorkspaceDatasetAccessRules = (params?: PaginationParameters) =>
}) })
} }
export const useCreateAccessRule = (resourceType?: AccessPolicyResourceType) => { export const useCreateAccessRule = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationKey: [NAME_SPACE, 'create', resourceType], mutationKey: [NAME_SPACE, 'create'],
mutationFn: (data: CreateAccessPolicyRequest) => { mutationFn: (data: CreateAccessPolicyRequest & { resourceType: AccessPolicyResourceType }) => {
const { name, description, permission_keys } = data const { name, description, permission_keys, resourceType } = data
return post<AccessPolicy>('/workspaces/current/rbac/access-policies', { return post<AccessPolicy>('/workspaces/current/rbac/access-policies', {
body: { body: {
resource_type: resourceType, resource_type: resourceType,
@ -45,20 +45,18 @@ export const useCreateAccessRule = (resourceType?: AccessPolicyResourceType) =>
}, },
}) })
}, },
onSuccess: () => { onSuccess: (_, { resourceType }) => {
if (resourceType) { queryClient.invalidateQueries({ queryKey: [NAME_SPACE, resourceType] })
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, resourceType] })
}
}, },
}) })
} }
export const useUpdateAccessRule = (resourceType: AccessPolicyResourceType) => { export const useUpdateAccessRule = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationKey: [NAME_SPACE, 'update', resourceType], mutationKey: [NAME_SPACE, 'update'],
mutationFn: (data: UpdateAccessPolicyRequest) => { mutationFn: (data: UpdateAccessPolicyRequest & { resourceType: AccessPolicyResourceType }) => {
const { id, name, description, permission_keys } = data const { id, name, description, permission_keys } = data
return put<AccessPolicy>(`/workspaces/current/rbac/access-policies/${id}`, { return put<AccessPolicy>(`/workspaces/current/rbac/access-policies/${id}`, {
body: { body: {
@ -69,7 +67,7 @@ export const useUpdateAccessRule = (resourceType: AccessPolicyResourceType) => {
}, },
}) })
}, },
onSuccess: () => { onSuccess: (_, { resourceType }) => {
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, resourceType] }) queryClient.invalidateQueries({ queryKey: [NAME_SPACE, resourceType] })
}, },
}) })