Skip to content

Commit 4b87f39

Browse files
feat: [UIE-8140, UIE-8662, UIE-8769] - IAM RBAC: fix bugs for the drawer (#12227)
* feat: [UIE-8140, UIE-8662, UIE-8769] - IAM RBAC: fix bugs for the drawer * Added changeset: IAM RBAC: fix bugs in the assign new roles drawer * fix styles after updating to mui v7 --------- Co-authored-by: cpathipa <[email protected]>
1 parent 8ab8654 commit 4b87f39

File tree

8 files changed

+145
-81
lines changed

8 files changed

+145
-81
lines changed
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 RBAC: fix bugs in the assign new roles drawer ([#12227](https://github.com/linode/manager/pull/12227))

packages/manager/src/features/IAM/Roles/RolesTable/RolesTableExpandedRow.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ describe('RolesTableExpandedRow', () => {
1313
it('renders when used', () => {
1414
renderWithTheme(<RolesTableExpandedRow permissions={[]} />);
1515

16-
expect(screen.getByRole('button')).toBeInTheDocument();
16+
expect(screen.getByText('Permissions')).toBeVisible();
1717
});
1818
});

packages/manager/src/features/IAM/Shared/Entities/Entities.test.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fireEvent, screen } from '@testing-library/react';
1+
import { screen } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33
import React from 'react';
44

@@ -26,6 +26,7 @@ const mockEntities = [
2626
accountEntityFactory.build({
2727
id: 7,
2828
type: 'linode',
29+
label: 'linode',
2930
}),
3031
accountEntityFactory.build({
3132
id: 1,
@@ -102,6 +103,34 @@ describe('Entities', () => {
102103
expect(autocomplete).toHaveLength(1);
103104
expect(autocomplete[0]).toBeVisible();
104105
expect(autocomplete[0]).toHaveAttribute('placeholder', 'None');
106+
const link = screen.getByRole('link', { name: /Create an Image Entity/i });
107+
expect(link).toBeVisible();
108+
});
109+
110+
it('renders correct data when it is an entity access', () => {
111+
queryMocks.useAccountEntities.mockReturnValue({
112+
data: makeResourcePage(mockEntities),
113+
});
114+
115+
renderWithTheme(
116+
<Entities
117+
access="entity_access"
118+
mode="assign-role"
119+
onChange={mockOnChange}
120+
type="vpc"
121+
value={mockValue}
122+
/>
123+
);
124+
125+
expect(screen.getByText('Entities')).toBeVisible();
126+
127+
// Verify comboboxes exist
128+
const autocomplete = screen.getAllByRole('combobox');
129+
expect(autocomplete).toHaveLength(1);
130+
expect(autocomplete[0]).toBeVisible();
131+
expect(autocomplete[0]).toHaveAttribute('placeholder', 'None');
132+
const link = screen.getByRole('link', { name: /Create a VPC Entity/i });
133+
expect(link).toBeVisible();
105134
});
106135

107136
it('renders correct options in Autocomplete dropdown when it is an entity access', async () => {
@@ -126,7 +155,11 @@ describe('Entities', () => {
126155
expect(screen.getByText('firewall-1')).toBeVisible();
127156
});
128157

129-
it('updates selected options when Autocomplete value changes when it is an entity access', () => {
158+
it('updates selected options when Autocomplete value changes when it is an entity access', async () => {
159+
queryMocks.useAccountEntities.mockReturnValue({
160+
data: makeResourcePage(mockEntities),
161+
});
162+
130163
renderWithTheme(
131164
<Entities
132165
access="entity_access"
@@ -138,9 +171,8 @@ describe('Entities', () => {
138171
);
139172

140173
const autocomplete = screen.getAllByRole('combobox')[0];
141-
fireEvent.change(autocomplete, { target: { value: 'linode7' } });
142-
fireEvent.keyDown(autocomplete, { key: 'Enter' });
143-
expect(screen.getByText('test-1')).toBeVisible();
174+
await userEvent.click(autocomplete);
175+
expect(screen.getByText('linode')).toBeVisible();
144176
});
145177

146178
it('renders Autocomplete as readonly when mode is "change-role"', () => {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export const Entities = ({
116116
<Notice spacingBottom={0} spacingTop={8} variant="warning">
117117
<Typography fontSize="inherit">
118118
<Link to={getCreateLinkForEntityType(type)}>
119-
Create a {getFormattedEntityType(type)} Entity
119+
Create {type === 'image' ? `an` : `a`}{' '}
120+
{getFormattedEntityType(type)} Entity{' '}
120121
</Link>{' '}
121122
first or choose a different role to continue assignment.
122123
</Typography>

packages/manager/src/features/IAM/Shared/Permissions/Permissions.style.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import { Box, Typography } from '@linode/ui';
2-
import { GridLegacy } from '@mui/material';
32
import { styled } from '@mui/material/styles';
43

5-
export const sxTooltipIcon = {
6-
marginLeft: 1,
7-
padding: 0,
8-
};
9-
10-
export const StyledGrid = styled(GridLegacy, { label: 'StyledGrid' })(() => ({
11-
alignItems: 'center',
12-
marginBottom: 2,
13-
}));
4+
export const StyledTitle = styled(Typography, { label: 'StyledTitle' })(
5+
({ theme }) => ({
6+
font: theme.tokens.alias.Typography.Label.Bold.S,
7+
marginBottom: theme.tokens.spacing.S4,
8+
})
9+
);
1410

1511
export const StyledPermissionItem = styled(Typography, {
1612
label: 'StyledPermissionItem',
1713
})(({ theme }) => ({
1814
borderRight: `1px solid ${theme.tokens.alias.Border.Normal}`,
1915
display: 'inline-block',
20-
padding: `0px ${theme.spacing(0.75)} ${theme.spacing(0.25)}`,
16+
padding: `0px ${theme.tokens.spacing.S6} ${theme.tokens.spacing.S2}`,
2117
}));
2218

2319
export const StyledContainer = styled('div', {

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

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StyledLinkButton, TooltipIcon, Typography } from '@linode/ui';
1+
import { StyledLinkButton, Typography } from '@linode/ui';
22
import { debounce } from '@mui/material';
33
import { Grid } from '@mui/material';
44
import * as React from 'react';
@@ -8,9 +8,8 @@ import {
88
StyledBox,
99
StyledClampedContent,
1010
StyledContainer,
11-
StyledGrid,
1211
StyledPermissionItem,
13-
sxTooltipIcon,
12+
StyledTitle,
1413
} from './Permissions.style';
1514

1615
import type { PermissionType } from '@linode/api-v4/lib/iam/types';
@@ -43,24 +42,9 @@ export const Permissions = ({ permissions }: Props) => {
4342
};
4443
}, [calculateHiddenItems, handleResize]);
4544

46-
// TODO: update the link for TooltipIcon when it's ready - UIE-8534
4745
return (
4846
<Grid container data-testid="parent" direction="column">
49-
<StyledGrid container item md={1}>
50-
<Typography
51-
sx={(theme) => ({
52-
font: theme.tokens.alias.Typography.Label.Bold.S,
53-
})}
54-
>
55-
Permissions
56-
</Typography>
57-
58-
<TooltipIcon
59-
status="help"
60-
sxTooltipIcon={sxTooltipIcon}
61-
text="Link is coming..."
62-
/>
63-
</StyledGrid>
47+
<StyledTitle>Permissions</StyledTitle>
6448
{!permissions.length ? (
6549
<Typography>
6650
This role doesn’t include permissions. Refer to the role description

packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ActionsPanel, Drawer, Typography } from '@linode/ui';
1+
import { ActionsPanel, Drawer, Notice, Typography } from '@linode/ui';
22
import { useTheme } from '@mui/material';
33
import Grid from '@mui/material/Grid';
4+
import { enqueueSnackbar } from 'notistack';
45
import React, { useState } from 'react';
56
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
67
import { useParams } from 'react-router-dom';
@@ -46,7 +47,7 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => {
4647
},
4748
});
4849

49-
const { control, handleSubmit, reset, watch } = form;
50+
const { control, handleSubmit, reset, watch, formState, setError } = form;
5051
const { append, fields, remove } = useFieldArray({
5152
control,
5253
name: 'roles',
@@ -64,16 +65,24 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => {
6465
return getAllRoles(accountPermissions);
6566
}, [accountPermissions]);
6667

67-
const { mutateAsync: updateUserRolePermissions } =
68+
const { mutateAsync: updateUserRolePermissions, isPending } =
6869
useAccountUserPermissionsMutation(username);
6970

7071
const onSubmit = handleSubmit(async (values: AssignNewRoleFormValues) => {
71-
const mergedRoles = mergeAssignedRolesIntoExistingRoles(
72-
values,
73-
existingRoles
74-
);
75-
await updateUserRolePermissions(mergedRoles);
76-
handleClose();
72+
try {
73+
const mergedRoles = mergeAssignedRolesIntoExistingRoles(
74+
values,
75+
existingRoles
76+
);
77+
78+
await updateUserRolePermissions(mergedRoles);
79+
enqueueSnackbar(`Roles added.`, {
80+
variant: 'success',
81+
});
82+
handleClose();
83+
} catch (error) {
84+
setError(error.field ?? 'root', { message: error[0].reason });
85+
}
7786
});
7887

7988
const handleClose = () => {
@@ -84,10 +93,19 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => {
8493
// TODO - add a link 'Learn more" - UIE-8534
8594
return (
8695
<Drawer onClose={onClose} open={open} title="Assign New Roles">
87-
{' '}
8896
<FormProvider {...form}>
8997
<form onSubmit={onSubmit}>
90-
<Typography sx={{ marginBottom: 3 }}>
98+
{formState.errors.root?.message && (
99+
<Notice variant="error">
100+
<Typography>
101+
Internal Error - Issue with updating permissions.
102+
<br />
103+
No changes were saved.
104+
</Typography>
105+
</Notice>
106+
)}
107+
108+
<Typography sx={{ marginBottom: 2.5 }}>
91109
Select a role you want to assign to a user. Some roles require
92110
selecting entities they should apply to. Configure the first role
93111
and continue adding roles or save the assignment.
@@ -139,6 +157,7 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => {
139157
'data-testid': 'submit',
140158
label: 'Assign',
141159
type: 'submit',
160+
loading: formState.isSubmitting || isPending,
142161
}}
143162
secondaryButtonProps={{
144163
'data-testid': 'cancel',

packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export const AssignSingleRole = ({
3030
}: Props) => {
3131
const theme = useTheme();
3232

33-
const { control } = useFormContext<AssignNewRoleFormValues>();
33+
const { control, watch, setValue } =
34+
useFormContext<AssignNewRoleFormValues>();
35+
const role = watch(`roles.${index}.role`);
36+
const roles = watch('roles');
3437

3538
return (
3639
<Box display="flex">
@@ -46,39 +49,63 @@ export const AssignSingleRole = ({
4649

4750
<Controller
4851
control={control}
49-
name={`roles.${index}`}
50-
render={({ field: { onChange, value } }) => (
51-
<>
52-
<Autocomplete
53-
label="Assign New Roles"
54-
onChange={(event, newValue) => {
55-
onChange({
56-
...value,
57-
role: newValue,
58-
});
59-
}}
60-
options={options}
61-
placeholder="Select a Role"
62-
textFieldProps={{ hideLabel: true }}
63-
value={value?.role || null}
64-
/>
65-
{value?.role && (
66-
<AssignedPermissionsPanel
67-
hideDetails={hideDetails}
68-
mode="assign-role"
69-
onChange={(updatedEntities) => {
70-
onChange({
71-
...value,
72-
entities: updatedEntities,
73-
});
74-
}}
75-
role={getRoleByName(permissions, value.role?.value)!}
76-
value={value.entities || []}
77-
/>
78-
)}
79-
</>
52+
name={`roles.${index}.role`}
53+
render={({ field: { onChange, value }, fieldState }) => (
54+
<Autocomplete
55+
errorText={fieldState.error?.message}
56+
label="Assign New Roles"
57+
onChange={(event, newValue) => {
58+
onChange(newValue);
59+
setValue(`roles.${index}.entities`, null);
60+
}}
61+
options={options}
62+
placeholder="Select a Role"
63+
textFieldProps={{ hideLabel: true }}
64+
value={value || null}
65+
/>
8066
)}
67+
rules={{
68+
validate: (value) => {
69+
if (!value) {
70+
return roles.length === 1
71+
? 'Select a role.'
72+
: 'Select a role or remove this entry.';
73+
}
74+
return true;
75+
},
76+
}}
8177
/>
78+
79+
{role && (
80+
<Controller
81+
control={control}
82+
name={`roles.${index}.entities`}
83+
render={({ field: { onChange, value }, fieldState }) => (
84+
<AssignedPermissionsPanel
85+
errorText={fieldState.error?.message}
86+
hideDetails={hideDetails}
87+
mode="assign-role"
88+
onChange={(updatedEntities) => {
89+
onChange(updatedEntities);
90+
}}
91+
role={getRoleByName(permissions, role?.value)!}
92+
value={value || []}
93+
/>
94+
)}
95+
rules={{
96+
validate: (value) => {
97+
if (role.access === 'account_access') return true;
98+
if (
99+
role.access === 'entity_access' &&
100+
(!value || value.length === 0)
101+
) {
102+
return 'Select entities.';
103+
}
104+
return true;
105+
},
106+
}}
107+
/>
108+
)}
82109
</Box>
83110
<Box
84111
sx={{
@@ -91,7 +118,7 @@ export const AssignSingleRole = ({
91118
verticalAlign: 'top',
92119
}}
93120
>
94-
<Button disabled={index === 0} onClick={() => onRemove(index)}>
121+
<Button disabled={roles.length === 1} onClick={() => onRemove(index)}>
95122
<DeleteIcon />
96123
</Button>
97124
</Box>

0 commit comments

Comments
 (0)