diff --git a/packages/manager/.changeset/pr-13384-fixed-1770729292708.md b/packages/manager/.changeset/pr-13384-fixed-1770729292708.md new file mode 100644 index 00000000000..f305d26e392 --- /dev/null +++ b/packages/manager/.changeset/pr-13384-fixed-1770729292708.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +IAM Delegation: error handling in remove role/entity confirmation dialog, visible “View User Detail” and “Delete User” options for delegate user ([#13384](https://github.com/linode/manager/pull/13384)) diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx index a6dcfefdccd..32a11a470f4 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx @@ -1,10 +1,11 @@ -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { accountRolesFactory } from 'src/factories/accountRoles'; import { renderWithTheme } from 'src/utilities/testHelpers'; +import { INTERNAL_ERROR_NO_CHANGES_SAVED } from '../constants'; import { UnassignRoleConfirmationDialog } from './UnassignRoleConfirmationDialog'; import type { ExtendedRoleView } from '../types'; @@ -31,6 +32,15 @@ const queryMocks = vi.hoisted(() => ({ useParams: vi.fn().mockReturnValue({ username: 'test_user' }), useAccountRoles: vi.fn().mockReturnValue({}), useUserRoles: vi.fn().mockReturnValue({}), + useUpdateDefaultDelegationAccessQuery: vi.fn().mockReturnValue({}), + useIsDefaultDelegationRolesForChildAccount: vi + .fn() + .mockReturnValue({ isDefaultDelegationRolesForChildAccount: false }), +})); + +vi.mock('src/features/IAM/hooks/useDelegationRole', () => ({ + useIsDefaultDelegationRolesForChildAccount: + queryMocks.useIsDefaultDelegationRolesForChildAccount, })); vi.mock('@linode/queries', async () => { @@ -39,6 +49,8 @@ vi.mock('@linode/queries', async () => { ...actual, useAccountRoles: queryMocks.useAccountRoles, useUserRoles: queryMocks.useUserRoles, + useUpdateDefaultDelegationAccessQuery: + queryMocks.useUpdateDefaultDelegationAccessQuery, }; }); @@ -63,6 +75,7 @@ vi.mock('@linode/api-v4', async () => { describe('UnassignRoleConfirmationDialog', () => { beforeEach(() => { + vi.clearAllMocks(); queryMocks.useParams.mockReturnValue({ username: 'test_user', }); @@ -140,4 +153,27 @@ describe('UnassignRoleConfirmationDialog', () => { }); }); }); + + it('displays error message when there is an API error', async () => { + const apiError = [{ reason: 'Failed to load user roles' }]; + + queryMocks.useUpdateDefaultDelegationAccessQuery.mockReturnValue({ + mutateAsync: vi.fn().mockRejectedValue(apiError), + isPending: false, + error: apiError, + }); + queryMocks.useIsDefaultDelegationRolesForChildAccount.mockReturnValue({ + isDefaultDelegationRolesForChildAccount: true, + }); + + renderWithTheme(); + + const removeButton = screen.getByText('Remove'); + expect(removeButton).toBeVisible(); + + await userEvent.click(removeButton); + await expect( + screen.getByText(INTERNAL_ERROR_NO_CHANGES_SAVED) + ).toBeVisible(); + }); }); diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx index d40141fbd80..3fba173c925 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx @@ -42,14 +42,17 @@ export const UnassignRoleConfirmationDialog = (props: Props) => { ? defaultRolesData : userRolesData; const { - error, + error: userRolesError, isPending, mutateAsync: updateUserRoles, reset, } = useUserRolesMutation(username); - const { mutateAsync: updateDefaultRoles, isPending: isDefaultRolesPending } = - useUpdateDefaultDelegationAccessQuery(); + const { + mutateAsync: updateDefaultRoles, + isPending: isDefaultRolesPending, + error: defaultDelegationRolesError, + } = useUpdateDefaultDelegationAccessQuery(); const mutationFn = isDefaultDelegationRolesForChildAccount ? updateDefaultRoles @@ -69,18 +72,25 @@ export const UnassignRoleConfirmationDialog = (props: Props) => { assignedRoles, initialRole, }); + try { + await mutationFn(updatedUserRoles); - await mutationFn(updatedUserRoles); - - enqueueSnackbar(`Role ${role?.name} has been deleted successfully.`, { - variant: 'success', - }); - if (onSuccess) { - onSuccess(); + enqueueSnackbar(`Role ${role?.name} has been deleted successfully.`, { + variant: 'success', + }); + if (onSuccess) { + onSuccess(); + } + onClose(); + } catch { + // error is handled by react-query and shown via } - onClose(); }; + const error = isDefaultDelegationRolesForChildAccount + ? defaultDelegationRolesError + : userRolesError; + return ( { label: 'Remove', loading: isPending || isDefaultRolesPending, onClick: onDelete, + disabled: isPending || isDefaultRolesPending, }} secondaryButtonProps={{ label: 'Cancel', diff --git a/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.test.tsx b/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.test.tsx index ee0911fe1a6..659b6d4b4d6 100644 --- a/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.test.tsx +++ b/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.test.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { accountRolesFactory } from 'src/factories/accountRoles'; import { renderWithTheme } from 'src/utilities/testHelpers'; +import { INTERNAL_ERROR_NO_CHANGES_SAVED } from '../constants'; import { RemoveAssignmentConfirmationDialog } from './RemoveAssignmentConfirmationDialog'; import type { EntitiesRole } from '../types'; @@ -29,6 +30,7 @@ const props = { const queryMocks = vi.hoisted(() => ({ useAccountRoles: vi.fn().mockReturnValue({}), useUserRoles: vi.fn().mockReturnValue({}), + useUpdateDefaultDelegationAccessQuery: vi.fn().mockReturnValue({}), useIsDefaultDelegationRolesForChildAccount: vi .fn() .mockReturnValue({ isDefaultDelegationRolesForChildAccount: false }), @@ -45,6 +47,8 @@ vi.mock('@linode/queries', async () => { ...actual, useAccountRoles: queryMocks.useAccountRoles, useUserRoles: queryMocks.useUserRoles, + useUpdateDefaultDelegationAccessQuery: + queryMocks.useUpdateDefaultDelegationAccessQuery, }; }); @@ -60,6 +64,10 @@ vi.mock('@linode/api-v4', async () => { }); describe('RemoveAssignmentConfirmationDialog', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('should render', async () => { renderWithTheme( @@ -145,4 +153,27 @@ describe('RemoveAssignmentConfirmationDialog', () => { expect(paragraph).toHaveTextContent(mockRole.entity_name); expect(paragraph).toHaveTextContent(mockRole.role_name); }); + + it('displays error message when there is an API error', async () => { + const apiError = [{ reason: 'Failed to load user roles' }]; + + queryMocks.useUpdateDefaultDelegationAccessQuery.mockReturnValue({ + mutateAsync: vi.fn().mockRejectedValue(apiError), + isPending: false, + error: apiError, + }); + + queryMocks.useIsDefaultDelegationRolesForChildAccount.mockReturnValue({ + isDefaultDelegationRolesForChildAccount: true, + }); + + renderWithTheme(); + const removeButton = screen.getByText('Remove'); + expect(removeButton).toBeVisible(); + + await userEvent.click(removeButton); + await expect( + screen.getByText(INTERNAL_ERROR_NO_CHANGES_SAVED) + ).toBeVisible(); + }); }); diff --git a/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.tsx b/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.tsx index abe400e6680..b61b4288e0c 100644 --- a/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.tsx +++ b/packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.tsx @@ -32,7 +32,7 @@ export const RemoveAssignmentConfirmationDialog = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); const { - error, + error: userRolesError, isPending: isUserRolesPending, mutateAsync: updateUserRoles, reset, @@ -41,6 +41,7 @@ export const RemoveAssignmentConfirmationDialog = (props: Props) => { const { mutateAsync: updateDefaultDelegationRoles, isPending: isDefaultDelegationRolesPending, + error: defaultDelegationRolesError, } = useUpdateDefaultDelegationAccessQuery(); const isPending = isUserRolesPending || isDefaultDelegationRolesPending; @@ -78,19 +79,25 @@ export const RemoveAssignmentConfirmationDialog = (props: Props) => { entity_id, entity_type ); - - await mutationFn({ - ...assignedRoles, - entity_access: updatedUserEntityRoles, - }); - - enqueueSnackbar(`Entity access removed`, { - variant: 'success', - }); - - onSuccess?.(); - onClose(); + try { + await mutationFn({ + ...assignedRoles, + entity_access: updatedUserEntityRoles, + }); + + enqueueSnackbar(`Entity access removed`, { + variant: 'success', + }); + + onSuccess?.(); + onClose(); + } catch { + // error is handled by react-query and shown via + } }; + const error = isDefaultDelegationRolesForChildAccount + ? defaultDelegationRolesError + : userRolesError; return ( { const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); const navigate = useNavigate(); - const { isChildUserType, isParentUserType, profileUserName } = - useDelegationRole(); + const { + isChildUserType, + isParentUserType, + isDelegateUserType, + profileUserName, + } = useDelegationRole(); const isAccountAdmin = permissions.is_account_admin; const canViewUser = permissions.view_user; const canDeleteUser = isAccountAdmin || permissions.delete_user; const isDelegateUser = userType === 'delegate'; - // Determine if the current account is a child account with isIAMDelegationEnabled enabled + // Determine if the current account is a child or delegate account with isIAMDelegationEnabled enabled // If so, we need to hide 'View User Details', 'Delete User', 'View Account Delegations' in the menu const shouldHideForChildDelegate = - isIAMDelegationEnabled && isChildUserType && isDelegateUser; + isIAMDelegationEnabled && + (isChildUserType || isDelegateUserType) && + isDelegateUser; const actions: Action[] = [ {