Skip to content

Commit 7ffb24a

Browse files
feat: [UIE-10051] - IAM Delegation: allow sending an empty array (linode#13300)
* feat: [UIE-10051] - IAM Delegation: allow sending an empty array * Added changeset: IAM DElegation: remove restriction to update user delegation with empty array, update the delegations after reopening a drawer * small drawer refactor --------- Co-authored-by: Alban Bailly <abailly@akamai.com>
1 parent ac0fdf1 commit 7ffb24a

File tree

5 files changed

+205
-184
lines changed

5 files changed

+205
-184
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Fixed
3+
---
4+
5+
IAM DElegation: remove restriction to update user delegation with empty array, update the delegations after reopening a drawer ([#13300](https://github.com/linode/manager/pull/13300))
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { useUpdateChildAccountDelegatesQuery } from '@linode/queries';
2+
import { ActionsPanel, Autocomplete, Notice, Typography } from '@linode/ui';
3+
import { useTheme } from '@mui/material';
4+
import { enqueueSnackbar } from 'notistack';
5+
import * as React from 'react';
6+
import { Controller, FormProvider, useForm } from 'react-hook-form';
7+
8+
import { usePermissions } from '../hooks/usePermissions';
9+
import { INTERNAL_ERROR_NO_CHANGES_SAVED } from '../Shared/constants';
10+
import { getPlaceholder } from '../Shared/Entities/utils';
11+
12+
import type { ChildAccount, ChildAccountWithDelegates } from '@linode/api-v4';
13+
14+
interface UpdateDelegationsFormValues {
15+
users: UserOption[];
16+
}
17+
18+
interface UserOption {
19+
label: string;
20+
value: string;
21+
}
22+
23+
interface DelegationsFormProps {
24+
delegation: ChildAccount | ChildAccountWithDelegates;
25+
formattedCurrentUsers: UserOption[];
26+
isLoading: boolean;
27+
onClose: () => void;
28+
userOptions: UserOption[];
29+
}
30+
export const UpdateDelegationForm = ({
31+
delegation,
32+
formattedCurrentUsers,
33+
isLoading,
34+
onClose,
35+
userOptions,
36+
}: DelegationsFormProps) => {
37+
const theme = useTheme();
38+
39+
const { data: permissions } = usePermissions('account', [
40+
'update_delegate_users',
41+
]);
42+
43+
const { mutateAsync: updateDelegates } =
44+
useUpdateChildAccountDelegatesQuery();
45+
46+
const form = useForm<UpdateDelegationsFormValues>({
47+
defaultValues: {
48+
users: formattedCurrentUsers,
49+
},
50+
});
51+
52+
const {
53+
control,
54+
formState: { errors, isSubmitting },
55+
handleSubmit,
56+
reset,
57+
setError,
58+
} = form;
59+
60+
const onSubmit = async (values: UpdateDelegationsFormValues) => {
61+
const usersList = values.users.map((user) => user.value);
62+
63+
try {
64+
await updateDelegates({
65+
euuid: delegation.euuid,
66+
users: usersList,
67+
});
68+
enqueueSnackbar(`Delegation updated`, { variant: 'success' });
69+
handleClose();
70+
} catch (errors) {
71+
for (const error of errors) {
72+
setError('root', {
73+
message: error.reason ?? INTERNAL_ERROR_NO_CHANGES_SAVED,
74+
});
75+
}
76+
}
77+
};
78+
79+
const handleClose = () => {
80+
reset();
81+
onClose();
82+
};
83+
84+
return (
85+
<>
86+
{errors.root?.message && (
87+
<Notice text={errors.root?.message} variant="error" />
88+
)}
89+
<FormProvider {...form}>
90+
<form onSubmit={handleSubmit(onSubmit)}>
91+
<Typography sx={{ marginBottom: theme.tokens.spacing.S16 }}>
92+
Add or remove users who should have access to the child account.
93+
Users removed from this list will lose the role assignment on the
94+
child account and they won&apos;t be visible in the user list on the
95+
child account.
96+
</Typography>
97+
98+
<Typography
99+
sx={{
100+
marginBottom: theme.tokens.spacing.S8,
101+
overflow: 'hidden',
102+
textOverflow: 'ellipsis',
103+
whiteSpace: 'nowrap',
104+
}}
105+
>
106+
Update delegation for <strong>{delegation.company}:</strong>
107+
</Typography>
108+
109+
<Controller
110+
control={control}
111+
name="users"
112+
render={({ field, fieldState }) => (
113+
<Autocomplete
114+
data-testid="delegates-autocomplete"
115+
errorText={fieldState.error?.message}
116+
isOptionEqualToValue={(option, value) =>
117+
option.value === value.value
118+
}
119+
label={'Delegate Users'}
120+
loading={isLoading}
121+
multiple
122+
noMarginTop
123+
onChange={(_, newValue) => {
124+
field.onChange(newValue || []);
125+
}}
126+
options={userOptions}
127+
placeholder={getPlaceholder(
128+
'delegates',
129+
field.value.length,
130+
userOptions.length
131+
)}
132+
textFieldProps={{
133+
hideLabel: true,
134+
}}
135+
value={field.value}
136+
/>
137+
)}
138+
/>
139+
140+
<ActionsPanel
141+
primaryButtonProps={{
142+
'data-testid': 'submit',
143+
label: 'Update',
144+
loading: isSubmitting,
145+
type: 'submit',
146+
disabled: !permissions?.update_delegate_users,
147+
tooltipText: !permissions?.update_delegate_users
148+
? 'You do not have permission to update delegations.'
149+
: undefined,
150+
}}
151+
secondaryButtonProps={{
152+
'data-testid': 'cancel',
153+
label: 'Cancel',
154+
onClick: handleClose,
155+
}}
156+
/>
157+
</form>
158+
</FormProvider>
159+
</>
160+
);
161+
};

packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.test.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { vi } from 'vitest';
55

66
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';
77

8-
import { DELEGATION_VALIDATION_ERROR } from '../Shared/constants';
98
import { UpdateDelegationsDrawer } from './UpdateDelegationsDrawer';
109

1110
import type { ChildAccountWithDelegates, User } from '@linode/api-v4';
@@ -118,27 +117,32 @@ describe('UpdateDelegationsDrawer', () => {
118117
});
119118
});
120119

121-
it('should show error when no users are selected', async () => {
122-
const emptyDelegation = {
123-
...mockChildAccountWithDelegates,
124-
users: [],
125-
};
120+
it('allows sending an empty payload', async () => {
121+
renderWithTheme(<UpdateDelegationsDrawer {...defaultProps} />);
122+
123+
const user = userEvent.setup();
124+
125+
// Open the autocomplete and deselect the preselected user (user1)
126+
const autocompleteInput = screen.getByRole('combobox');
127+
await user.click(autocompleteInput);
128+
129+
await waitFor(() => {
130+
// Ensure options are rendered
131+
expect(screen.getByRole('option', { name: 'user1' })).toBeInTheDocument();
132+
});
126133

127-
renderWithTheme(
128-
<UpdateDelegationsDrawer
129-
delegation={emptyDelegation}
130-
onClose={vi.fn()}
131-
open={true}
132-
/>
133-
);
134+
const user1Option = screen.getByRole('option', { name: 'user1' });
135+
await user.click(user1Option); // toggles off the selected user
134136

135-
// Try to submit without selecting any users
136-
const submitButton = screen.getByRole('button', { name: 'Update' });
137-
await userEvent.click(submitButton);
137+
// Submit with no users selected
138+
const submitButton = screen.getByRole('button', { name: /update/i });
139+
await user.click(submitButton);
138140

139141
await waitFor(() => {
140-
const errorElement = screen.getByText(DELEGATION_VALIDATION_ERROR);
141-
expect(errorElement).toBeVisible();
142+
expect(mocks.mockMutateAsync).toHaveBeenCalledWith({
143+
euuid: mockChildAccountWithDelegates.euuid,
144+
users: [],
145+
});
142146
});
143147
});
144148
});

0 commit comments

Comments
 (0)