mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
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:
parent
a3b00a2f83
commit
9609f003c6
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)}>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": "创建应用",
|
||||||
|
|||||||
@ -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[]
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
27
web/service/access-control/use-member-roles.ts
Normal file
27
web/service/access-control/use-member-roles.ts
Normal 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] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
11
web/service/access-control/use-permission-keys.ts
Normal file
11
web/service/access-control/use-permission-keys.ts
Normal 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'),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user