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[] = [
{