diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx
index 61e984b455..dce7e9af54 100644
--- a/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx
+++ b/web/app/components/header/account-setting/access-rules-page/access-rule-row.tsx
@@ -26,7 +26,7 @@ const AccessRuleRow = ({
onEdit,
onAddRole,
}: AccessRuleRowProps) => {
- const { policy, role_ids, account_ids } = rule
+ const { policy, roles, accounts } = rule
const { id: policyId, resource_type } = policy
const handleEdit = useCallback(() => onEdit?.(rule), [onEdit, rule])
@@ -38,8 +38,8 @@ const AccessRuleRow = ({
const handleRemoveRole = useCallback((id: string, type: BindingType) => {
const payload = {
id: policyId,
- role_ids: role_ids.map(role => role.id),
- account_ids: account_ids.map(account => account.id),
+ role_ids: roles.map(role => role.role_id),
+ account_ids: accounts.map(account => account.account_id),
}
if (type === 'role') {
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 (
@@ -73,20 +73,20 @@ const AccessRuleRow = ({
{policy.description}
- {role_ids.map(role => (
+ {roles.map(role => (
))}
- {account_ids.map(account => (
+ {accounts.map(account => (
diff --git a/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx b/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx
index 0631b3ef96..dde789a90f 100644
--- a/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx
+++ b/web/app/components/header/account-setting/access-rules-page/access-rule-section.tsx
@@ -9,6 +9,7 @@ import AccessRuleRow from './access-rule-row'
type AccessRuleSectionProps = {
title: string
rules: AccessPolicyWithBindings[]
+ isLoadingRules: boolean
createButtonLabel: string
onCreate?: () => void
onEditRule?: (rule: AccessPolicyWithBindings) => void
@@ -19,6 +20,7 @@ type AccessRuleSectionProps = {
const AccessRuleSection = ({
title,
rules,
+ isLoadingRules,
createButtonLabel,
onCreate,
onEditRule,
@@ -31,7 +33,12 @@ const AccessRuleSection = ({
{title}
-
diff --git a/web/app/components/header/account-setting/access-rules-page/app-access-rule-section.tsx b/web/app/components/header/account-setting/access-rules-page/app-access-rule-section.tsx
index 29279013fa..ad35fe67c9 100644
--- a/web/app/components/header/account-setting/access-rules-page/app-access-rule-section.tsx
+++ b/web/app/components/header/account-setting/access-rules-page/app-access-rule-section.tsx
@@ -15,7 +15,7 @@ const AppAccessRuleSection = ({
onEditRule,
onAddRole,
}: AppAccessRuleSectionProps) => {
- const { data: appAccessRulesResponse } = useWorkspaceAppAccessRules({
+ const { data: appAccessRulesResponse, isLoading } = useWorkspaceAppAccessRules({
page: 1,
limit: 20,
})
@@ -26,6 +26,7 @@ const AppAccessRuleSection = ({
{
- const { data: datasetAccessRulesResponse } = useWorkspaceDatasetAccessRules({
+ const { data: datasetAccessRulesResponse, isLoading } = useWorkspaceDatasetAccessRules({
page: 1,
limit: 20,
})
@@ -26,6 +26,7 @@ const DatasetAccessRuleSection = ({
{
setPermissionSetModalState({
mode: 'edit',
resourceType,
+ ruleId: policy.id,
initialValues: {
name: policy.name,
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(
(values: PermissionSetFormValues) => {
+ const mode = permissionSetModalState?.mode || ''
+ const id = permissionSetModalState?.ruleId || ''
const { name, description, permissionKeys } = values
- createAccessRule({
- name,
- description,
- permission_keys: permissionKeys,
- }, {
- onSuccess: () => {
- toast.success('Access rule created successfully')
- closePermissionSetModal()
- },
- })
+ if (mode === 'create') {
+ createAccessRule({
+ name,
+ description,
+ permission_keys: permissionKeys,
+ resourceType: permissionSetModalState!.resourceType,
+ }, {
+ 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])
@@ -131,8 +153,8 @@ const AccessRulesPage = () => {
{addingRule && (
role.id)}
- initialMemberIds={addingRule.account_ids.map(account => account.id)}
+ initialRoleIds={addingRule.roles.map(role => role.role_id)}
+ initialMemberIds={addingRule.accounts.map(account => account.account_id)}
onClose={closeAddModal}
onSubmit={handleAddSubmit}
/>
diff --git a/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx b/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx
index 96a5f31ca8..e19899e665 100644
--- a/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx
+++ b/web/app/components/header/account-setting/members-page/assign-roles-modal/index.tsx
@@ -15,6 +15,7 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
+import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles'
export type AssignableRole = {
id: string
@@ -23,38 +24,31 @@ export type AssignableRole = {
}
export type AssignRolesModalProps = {
- open: boolean
member: Member
onClose: () => void
onSubmit: (roleIds: string[]) => void
}
-type AssignRolesModalBodyProps = {
- roles: AssignableRole[]
-} & Omit
-
-// 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' },
-]
+type AssignRolesModalBodyProps = AssignRolesModalProps
const AssignRolesModalBody = ({
- roles,
member,
onClose,
onSubmit,
}: AssignRolesModalBodyProps) => {
const { t } = useTranslation()
const [selected, setSelected] = useState(() => {
- const match = MOCK_ASSIGNABLE_ROLES.find(r => r.id === member.role)
- return match ? [match.id] : []
+ return member.roles?.map(role => role.id) || []
})
const [keyword, setKeyword] = useState('')
+ const { data: rolesData, isLoading: rolesLoading } = useWorkspaceRoleList({
+ page: 1,
+ limit: 20,
+ })
+
+ const roles = useMemo(() => rolesData?.data ?? [], [rolesData])
+
const filteredRoles = useMemo(() => {
const trimmed = keyword.trim().toLowerCase()
if (!trimmed)
@@ -116,58 +110,62 @@ const AssignRolesModalBody = ({
className="mt-2 min-h-0 flex-1"
slotClassNames={{ viewport: 'px-3 overscroll-contain' }}
>
- {filteredRoles.length === 0
+ {rolesLoading
? (
- {t('members.assignRolesModal.empty', {
- ns: 'common',
- defaultValue: 'No matching roles',
- })}
+ Loading roles...
)
- : (
-
- {filteredRoles.map((role) => {
- const checked = selected.includes(role.id)
- const handleToggle = () => toggle(role.id)
- return (
- -
-
{
- if (e.key === ' ' || e.key === 'Enter') {
- e.preventDefault()
- handleToggle()
- }
- }}
- >
-
-
-
- {role.name}
-
- {role.description && (
-
- {role.description}
-
+ : filteredRoles.length === 0
+ ? (
+
+ {t('members.assignRolesModal.empty', {
+ ns: 'common',
+ defaultValue: 'No matching roles',
+ })}
+
+ )
+ : (
+
+ {filteredRoles.map((role) => {
+ const checked = selected.includes(role.id)
+ const handleToggle = () => toggle(role.id)
+ return (
+ -
+
{
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault()
+ handleToggle()
+ }
+ }}
+ >
+
+
+
+ {role.name}
+
+
+ {role.description || 'No description'}
+
+
-
-
- )
- })}
-
- )}
+
+ )
+ })}
+
+ )}
@@ -175,7 +173,6 @@ const AssignRolesModalBody = ({
{t('members.assignRolesModal.selectedCount', {
ns: 'common',
count: selected.length,
- defaultValue: '{{count}} selected',
})}
@@ -192,21 +189,19 @@ const AssignRolesModalBody = ({
}
const AssignRolesModal = ({
- open,
member,
onClose,
onSubmit,
}: AssignRolesModalProps) => {
return (
{canAssignRoles && (
@@ -108,33 +116,50 @@ const MemberDetailsModal = ({
)}
-
-
- {t('members.memberDetails.generalGroup', {
- ns: 'common',
- defaultValue: 'GENERAL',
- })}
+ {builtinRoles.length > 0 && (
+
+
+ {t('members.memberDetails.generalGroup', {
+ ns: 'common',
+ })}
+
+
+ {builtinRoles.map(role => (
+
+ ))}
+
-
- {assignedRoles.map(role => (
-
- ))}
+ )}
+ {customRoles.length > 0 && (
+
+
+ {t('members.memberDetails.customGroup', {
+ ns: 'common',
+ })}
+
+
+ {customRoles.map(role => (
+
+ ))}
+
-
+ )}
{assignOpen && (
setAssignOpen(false)}
+ onClose={handleClose}
onSubmit={handleAssignSubmit}
/>
)}
diff --git a/web/app/components/header/account-setting/members-page/member-details-modal/permission-role-chip.tsx b/web/app/components/header/account-setting/members-page/member-details-modal/permission-role-chip.tsx
index d6a17de6b7..b9b4989eb2 100644
--- a/web/app/components/header/account-setting/members-page/member-details-modal/permission-role-chip.tsx
+++ b/web/app/components/header/account-setting/members-page/member-details-modal/permission-role-chip.tsx
@@ -13,7 +13,6 @@ import { getRolePermissionKeys } from './role-permissions'
export type PermissionRoleChipProps = {
roleKey: string
label: string
- highlighted?: boolean
onRemove?: () => void
className?: string
}
@@ -21,7 +20,6 @@ export type PermissionRoleChipProps = {
const PermissionRoleChip = ({
roleKey,
label,
- highlighted = false,
onRemove,
className,
}: PermissionRoleChipProps) => {
@@ -32,10 +30,8 @@ const PermissionRoleChip = ({
const chip = (
diff --git a/web/app/components/header/account-setting/members-page/member-row.tsx b/web/app/components/header/account-setting/members-page/member-row.tsx
index 6297e66f21..a12c276b2e 100644
--- a/web/app/components/header/account-setting/members-page/member-row.tsx
+++ b/web/app/components/header/account-setting/members-page/member-row.tsx
@@ -10,7 +10,10 @@ import RoleBadges from './role-badges'
type MemberRowProps = {
member: Member
- roleLabel: string
+ roles: Array<{
+ id: string
+ name: string
+ }>
isCurrentUser: boolean
canManage: boolean
operatorRole: string
@@ -22,7 +25,7 @@ type MemberRowProps = {
const MemberRow = ({
member,
- roleLabel,
+ roles,
isCurrentUser,
canManage,
operatorRole,
@@ -34,6 +37,8 @@ const MemberRow = ({
const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow()
+ const roleNames = roles.map(role => role.name)
+
const openDetails = useCallback(() => {
onOpenDetails(member)
}, [member, onOpenDetails])
@@ -98,7 +103,7 @@ const MemberRow = ({
>
{canManage && (
{
}
export type RoleBadgesProps = {
- roles: string[]
+ roleNames: string[]
max?: number
className?: string
}
-const RoleBadges = ({ roles, max = 2, className }: RoleBadgesProps) => {
- if (!roles.length)
+const RoleBadges = ({ roleNames, max = 2, className }: RoleBadgesProps) => {
+ if (!roleNames.length)
return null
- const visible = roles.slice(0, max)
- const overflow = roles.slice(max)
+ const visible = roleNames.slice(0, max)
+ const overflow = roleNames.slice(max)
return (
diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json
index 5921348b9e..386486fb53 100644
--- a/web/i18n/en-US/common.json
+++ b/web/i18n/en-US/common.json
@@ -246,6 +246,7 @@
"members.memberActions": "Member actions",
"members.memberDetails.assign": "Assign",
"members.memberDetails.assignedRoles": "Assigned Roles",
+ "members.memberDetails.customGroup": "CUSTOMIZED",
"members.memberDetails.generalGroup": "GENERAL",
"members.memberDetails.openAria": "Open member details for {{name}}",
"members.memberDetails.permissions.assignRoles": "Assign roles",
diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json
index 12d9ae9ffa..7cfc407233 100644
--- a/web/i18n/zh-Hans/common.json
+++ b/web/i18n/zh-Hans/common.json
@@ -246,7 +246,8 @@
"members.memberActions": "成员操作",
"members.memberDetails.assign": "分配",
"members.memberDetails.assignedRoles": "已分配角色",
- "members.memberDetails.generalGroup": "通用角色",
+ "members.memberDetails.customGroup": "自定义",
+ "members.memberDetails.generalGroup": "通用",
"members.memberDetails.openAria": "打开 {{name}} 的成员详情",
"members.memberDetails.permissions.assignRoles": "分配角色",
"members.memberDetails.permissions.createApps": "创建应用",
diff --git a/web/models/access-control.ts b/web/models/access-control.ts
index 97f3e487e5..139b12cb9c 100644
--- a/web/models/access-control.ts
+++ b/web/models/access-control.ts
@@ -131,13 +131,13 @@ export type UpdateAccessPolicyRequest = {
export type BindingType = 'role' | 'account'
export type Bindings = {
- role_ids: Array<{
- id: string
- name: string
+ roles: Array<{
+ role_id: string
+ role_name: string
}>
- account_ids: Array<{
- id: string
- name: string
+ accounts: Array<{
+ account_id: string
+ account_name: string
}>
}
@@ -170,12 +170,12 @@ export type GetDatasetAccessPoliciesResponse = {
pagination: Pagination
}
+export type RolesOfMemberResponse = {
+ account_id: string
+ roles: Role[]
+}
+
export type UpdateRolesOfMemberRequest = {
member_id: string
role_ids: string[]
}
-
-export type UpdateRolesOfMemberResponse = {
- account_id: string
- roles: Role[]
-}
diff --git a/web/models/common.ts b/web/models/common.ts
index 505db0e348..f726105967 100644
--- a/web/models/common.ts
+++ b/web/models/common.ts
@@ -54,6 +54,10 @@ export type Member = Pick
}
enum ProviderName {
diff --git a/web/service/access-control/use-member-roles.ts b/web/service/access-control/use-member-roles.ts
new file mode 100644
index 0000000000..f094f0c34c
--- /dev/null
+++ b/web/service/access-control/use-member-roles.ts
@@ -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(`/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] })
+ },
+ })
+}
diff --git a/web/service/access-control/use-permission-keys.ts b/web/service/access-control/use-permission-keys.ts
new file mode 100644
index 0000000000..d8bab9a6eb
--- /dev/null
+++ b/web/service/access-control/use-permission-keys.ts
@@ -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'),
+ })
+}
diff --git a/web/service/access-control/use-workspace-access-rules.ts b/web/service/access-control/use-workspace-access-rules.ts
index f206090340..bab8dc37b0 100644
--- a/web/service/access-control/use-workspace-access-rules.ts
+++ b/web/service/access-control/use-workspace-access-rules.ts
@@ -29,13 +29,13 @@ export const useWorkspaceDatasetAccessRules = (params?: PaginationParameters) =>
})
}
-export const useCreateAccessRule = (resourceType?: AccessPolicyResourceType) => {
+export const useCreateAccessRule = () => {
const queryClient = useQueryClient()
return useMutation({
- mutationKey: [NAME_SPACE, 'create', resourceType],
- mutationFn: (data: CreateAccessPolicyRequest) => {
- const { name, description, permission_keys } = data
+ mutationKey: [NAME_SPACE, 'create'],
+ mutationFn: (data: CreateAccessPolicyRequest & { resourceType: AccessPolicyResourceType }) => {
+ const { name, description, permission_keys, resourceType } = data
return post('/workspaces/current/rbac/access-policies', {
body: {
resource_type: resourceType,
@@ -45,20 +45,18 @@ export const useCreateAccessRule = (resourceType?: AccessPolicyResourceType) =>
},
})
},
- onSuccess: () => {
- if (resourceType) {
- queryClient.invalidateQueries({ queryKey: [NAME_SPACE, resourceType] })
- }
+ onSuccess: (_, { resourceType }) => {
+ queryClient.invalidateQueries({ queryKey: [NAME_SPACE, resourceType] })
},
})
}
-export const useUpdateAccessRule = (resourceType: AccessPolicyResourceType) => {
+export const useUpdateAccessRule = () => {
const queryClient = useQueryClient()
return useMutation({
- mutationKey: [NAME_SPACE, 'update', resourceType],
- mutationFn: (data: UpdateAccessPolicyRequest) => {
+ mutationKey: [NAME_SPACE, 'update'],
+ mutationFn: (data: UpdateAccessPolicyRequest & { resourceType: AccessPolicyResourceType }) => {
const { id, name, description, permission_keys } = data
return put(`/workspaces/current/rbac/access-policies/${id}`, {
body: {
@@ -69,7 +67,7 @@ export const useUpdateAccessRule = (resourceType: AccessPolicyResourceType) => {
},
})
},
- onSuccess: () => {
+ onSuccess: (_, { resourceType }) => {
queryClient.invalidateQueries({ queryKey: [NAME_SPACE, resourceType] })
},
})