Skip to content

Commit fb4d915

Browse files
aaleksee-akamaicpathipaConal Ryan
authored
feat: [UIE-8603] - IAM RBAC: User Entities - Remove Assignment (linode#12027)
* feat: [UIE-8603] - IAM RBAC: User Entities - Remove Assignment * changesets * add the confirmation dialog for removing chip * resolved conflicts * Update packages/manager/src/features/IAM/Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog.tsx * Update packages/manager/.changeset/pr-12027-upcoming-features-1744711021887.md * Fix test --------- Co-authored-by: cpathipa <[email protected]> Co-authored-by: Conal Ryan <[email protected]>
1 parent ea6a661 commit fb4d915

File tree

10 files changed

+495
-63
lines changed

10 files changed

+495
-63
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Upcoming Features
3+
---
4+
5+
fix the api to the right one for iam ([#12027](https://github.com/linode/manager/pull/12027))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
IAM: Add a new confirmation dialog for removing entity for the role ([#12027](https://github.com/linode/manager/pull/12027))

packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424

2525
import { AssignedEntities } from '../../Users/UserRoles/AssignedEntities';
2626
import { Permissions } from '../Permissions/Permissions';
27+
import { RemoveAssignmentConfirmationDialog } from '../RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog';
2728
import {
2829
addEntitiesNamesToRoles,
2930
combineRoles,
@@ -38,12 +39,17 @@ import { UnassignRoleConfirmationDialog } from './UnassignRoleConfirmationDialog
3839
import { UpdateEntitiesDrawer } from './UpdateEntitiesDrawer';
3940

4041
import type {
42+
CombinedEntity,
4143
DrawerModes,
4244
EntitiesType,
4345
ExtendedRoleMap,
4446
RoleMap,
4547
} from '../utilities';
46-
import type { AccountAccessRole, EntityAccessRole } from '@linode/api-v4';
48+
import type {
49+
AccountAccessRole,
50+
EntityAccessRole,
51+
EntityTypePermissions,
52+
} from '@linode/api-v4';
4753
import type { TableItem } from 'src/components/CollapsibleTable/CollapsibleTable';
4854

4955
export const AssignedRolesTable = () => {
@@ -55,13 +61,16 @@ export const AssignedRolesTable = () => {
5561
const [isChangeRoleDrawerOpen, setIsChangeRoleDrawerOpen] =
5662
React.useState<boolean>(false);
5763
const [selectedRole, setSelectedRole] = React.useState<ExtendedRoleMap>();
64+
const [selectedEntity, setSelectedEntity] = React.useState<CombinedEntity>();
5865
const [isUnassignRoleDialogOpen, setIsUnassignRoleDialogOpen] =
5966
React.useState<boolean>(false);
6067
const [isUpdateEntitiesDrawerOpen, setIsUpdateEntitiesDrawerOpen] =
6168
React.useState<boolean>(false);
6269

6370
const [drawerMode, setDrawerMode] =
6471
React.useState<DrawerModes>('assign-role');
72+
const [isRemoveAssignmentDialogOpen, setIsRemoveAssignmentDialogOpen] =
73+
React.useState<boolean>(false);
6574

6675
const handleChangeRole = (role: ExtendedRoleMap) => {
6776
setIsChangeRoleDrawerOpen(true);
@@ -79,6 +88,15 @@ export const AssignedRolesTable = () => {
7988
setSelectedRole(role);
8089
};
8190

91+
const handleRemoveAssignment = (
92+
entity: CombinedEntity,
93+
role: ExtendedRoleMap
94+
) => {
95+
setIsRemoveAssignmentDialogOpen(true);
96+
setSelectedEntity(entity);
97+
setSelectedRole(role);
98+
};
99+
82100
const { data: accountPermissions, isLoading: accountPermissionsLoading } =
83101
useAccountPermissions();
84102
const { data: entities, isLoading: entitiesLoading } = useAccountEntities();
@@ -142,9 +160,9 @@ export const AssignedRolesTable = () => {
142160
) : (
143161
<TableCell sx={{ display: { sm: 'table-cell', xs: 'none' } }}>
144162
<AssignedEntities
145-
entities={role.entity_names!}
146163
onButtonClick={handleViewEntities}
147-
roleName={role.name}
164+
onRemoveAssignment={handleRemoveAssignment}
165+
role={role}
148166
/>
149167
</TableCell>
150168
)}
@@ -301,6 +319,18 @@ export const AssignedRolesTable = () => {
301319
open={isUpdateEntitiesDrawerOpen}
302320
role={selectedRole}
303321
/>
322+
<RemoveAssignmentConfirmationDialog
323+
onClose={() => setIsRemoveAssignmentDialogOpen(false)}
324+
open={isRemoveAssignmentDialogOpen}
325+
role={{
326+
entity_type: selectedRole?.entity_type as EntityTypePermissions,
327+
id: selectedRole?.id as EntityAccessRole,
328+
entity_id: selectedEntity?.id as number,
329+
entity_name: selectedEntity?.name as string,
330+
role_name: selectedRole?.name as EntityAccessRole,
331+
access: 'entity_access',
332+
}}
333+
/>
304334
</Grid>
305335
);
306336
};
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
import { MemoryRouter } from 'react-router-dom';
5+
6+
import { accountPermissionsFactory } from 'src/factories/accountPermissions';
7+
import { renderWithTheme } from 'src/utilities/testHelpers';
8+
9+
import { RemoveAssignmentConfirmationDialog } from './RemoveAssignmentConfirmationDialog';
10+
11+
import type { EntitiesRole } from '../utilities';
12+
13+
const mockRole: EntitiesRole = {
14+
role_name: 'firewall_admin',
15+
id: 'firewall_admin-1',
16+
entity_id: 1,
17+
entity_name: 'Test',
18+
entity_type: 'firewall',
19+
access: 'entity_access',
20+
};
21+
22+
const props = {
23+
onClose: vi.fn(),
24+
onSuccess: vi.fn(),
25+
open: true,
26+
role: mockRole,
27+
};
28+
29+
vi.mock('react-router-dom', async () => {
30+
const actual = await vi.importActual('react-router-dom');
31+
return {
32+
...actual,
33+
useParams: () => ({ username: 'test_user' }),
34+
};
35+
});
36+
37+
const queryMocks = vi.hoisted(() => ({
38+
useAccountPermissions: vi.fn().mockReturnValue({}),
39+
useAccountUserPermissions: vi.fn().mockReturnValue({}),
40+
}));
41+
42+
vi.mock('src/queries/iam/iam', async () => {
43+
const actual = await vi.importActual<any>('src/queries/iam/iam');
44+
return {
45+
...actual,
46+
useAccountPermissions: queryMocks.useAccountPermissions,
47+
useAccountUserPermissions: queryMocks.useAccountUserPermissions,
48+
};
49+
});
50+
51+
const mockDeleteUserRole = vi.fn();
52+
vi.mock('@linode/api-v4', async () => {
53+
return {
54+
...(await vi.importActual<any>('@linode/api-v4')),
55+
updateUserPermissions: (username: string, data: any) => {
56+
mockDeleteUserRole(data);
57+
return Promise.resolve(props);
58+
},
59+
};
60+
});
61+
62+
describe('RemoveAssignmentConfirmationDialog', () => {
63+
it('should render', () => {
64+
renderWithTheme(
65+
<MemoryRouter>
66+
<RemoveAssignmentConfirmationDialog {...props} />{' '}
67+
</MemoryRouter>
68+
);
69+
70+
const headerText = screen.getByText(
71+
'Remove the Test entity from the firewall_admin role assignment?'
72+
);
73+
expect(headerText).toBeVisible();
74+
75+
const paragraph = screen.getByText(/Youre about to remove the/i);
76+
77+
expect(paragraph).toBeVisible();
78+
expect(paragraph).toHaveTextContent(mockRole.entity_name);
79+
expect(paragraph).toHaveTextContent(mockRole.role_name);
80+
expect(paragraph).toHaveTextContent(/test_user/i);
81+
82+
expect(
83+
screen.getByText(/This change will be applied immediately./i)
84+
).toBeVisible();
85+
86+
const buttons = screen.getAllByRole('button');
87+
expect(buttons?.length).toBe(3);
88+
});
89+
90+
it('calls onClose when the cancel button is clicked', async () => {
91+
renderWithTheme(<RemoveAssignmentConfirmationDialog {...props} />);
92+
93+
const cancelButton = screen.getByText('Cancel');
94+
expect(cancelButton).toBeVisible();
95+
96+
await userEvent.click(cancelButton);
97+
expect(props.onClose).toHaveBeenCalledTimes(1);
98+
});
99+
100+
it('should allow remove the assignment', async () => {
101+
queryMocks.useAccountUserPermissions.mockReturnValue({
102+
data: {
103+
account_access: ['account_linode_admin', 'account_admin'],
104+
entity_access: [
105+
{
106+
id: 1,
107+
type: 'firewall',
108+
roles: ['firewall_admin'],
109+
},
110+
],
111+
},
112+
});
113+
114+
queryMocks.useAccountPermissions.mockReturnValue({
115+
data: accountPermissionsFactory.build(),
116+
});
117+
118+
renderWithTheme(<RemoveAssignmentConfirmationDialog {...props} />);
119+
120+
const removeButton = screen.getByText('Remove');
121+
expect(removeButton).toBeVisible();
122+
123+
await userEvent.click(removeButton);
124+
125+
await waitFor(() => {
126+
expect(mockDeleteUserRole).toHaveBeenCalledWith({
127+
account_access: ['account_linode_admin', 'account_admin'],
128+
entity_access: [],
129+
});
130+
});
131+
});
132+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { ActionsPanel, Notice, Typography } from '@linode/ui';
2+
import { useSnackbar } from 'notistack';
3+
import React from 'react';
4+
import { useParams } from 'react-router-dom';
5+
6+
import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
7+
import {
8+
useAccountUserPermissions,
9+
useAccountUserPermissionsMutation,
10+
} from 'src/queries/iam/iam';
11+
12+
import { deleteUserEntity, type EntitiesRole } from '../utilities';
13+
14+
interface Props {
15+
onClose: () => void;
16+
onSuccess?: () => void;
17+
open: boolean;
18+
role: EntitiesRole | undefined;
19+
}
20+
21+
export const RemoveAssignmentConfirmationDialog = (props: Props) => {
22+
const { onClose: _onClose, onSuccess, open, role } = props;
23+
const { username } = useParams<{ username: string }>();
24+
25+
const { enqueueSnackbar } = useSnackbar();
26+
27+
const {
28+
error,
29+
isPending,
30+
mutateAsync: updateUserPermissions,
31+
reset,
32+
} = useAccountUserPermissionsMutation(username);
33+
34+
const { data: assignedRoles } = useAccountUserPermissions(username ?? '');
35+
36+
const onClose = () => {
37+
reset(); // resets the error state of the useMutation
38+
_onClose();
39+
};
40+
41+
const onDelete = async () => {
42+
const roleName = role!.role_name;
43+
const entityId = role!.entity_id;
44+
const entityType = role!.entity_type;
45+
46+
const updatedUserEntityRoles = deleteUserEntity(
47+
assignedRoles!.entity_access,
48+
roleName,
49+
entityId,
50+
entityType
51+
);
52+
53+
await updateUserPermissions({
54+
...assignedRoles!,
55+
entity_access: updatedUserEntityRoles,
56+
});
57+
58+
enqueueSnackbar(`Entity removed`, {
59+
variant: 'success',
60+
});
61+
if (onSuccess) {
62+
onSuccess();
63+
}
64+
onClose();
65+
};
66+
67+
return (
68+
<ConfirmationDialog
69+
actions={
70+
<ActionsPanel
71+
primaryButtonProps={{
72+
label: 'Remove',
73+
loading: isPending,
74+
onClick: onDelete,
75+
}}
76+
secondaryButtonProps={{
77+
label: 'Cancel',
78+
onClick: onClose,
79+
}}
80+
style={{ padding: 0 }}
81+
/>
82+
}
83+
error={error?.[0].reason}
84+
onClose={onClose}
85+
open={open}
86+
title={`Remove the ${role?.entity_name} entity from the ${role?.role_name} role assignment?`}
87+
>
88+
<Notice variant="warning">
89+
<Typography>
90+
You’re about to remove the <strong>{role?.entity_name}</strong> entity
91+
from the <strong>{role?.role_name}</strong> role for{' '}
92+
<strong>{username}</strong>. This change will be applied immediately.
93+
</Typography>
94+
</Notice>
95+
</ConfirmationDialog>
96+
);
97+
};

0 commit comments

Comments
 (0)