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 ( { if (!nextOpen) onClose() }} > { 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 { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext() @@ -141,7 +134,7 @@ const MembersPage = () => { { void onAssignSubmit?: (roleIds: string[]) => void @@ -26,7 +26,6 @@ export type MemberDetailsModalProps = { const MemberDetailsModal = ({ open, member, - roleLabel, canAssignRoles = false, onClose, onAssignSubmit, @@ -34,12 +33,21 @@ const MemberDetailsModal = ({ const { t } = useTranslation() 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) setAssignOpen(false) - } + }, [onAssignSubmit]) return ( <> @@ -87,7 +95,7 @@ const MemberDetailsModal = ({ })} - {assignedRoles.length} + {roles.length}
{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] }) }, })