From 82a47be8141a04b18633955b656cd026e0fde192 Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Wed, 21 Jan 2026 12:18:32 +0100 Subject: [PATCH 1/3] feat: [UIE-10051] - IAM Delegation: allow sending an empty array --- .../UpdateDelegationsDrawer.test.tsx | 40 ++++++++++--------- .../Delegations/UpdateDelegationsDrawer.tsx | 28 ++++++------- .../src/features/IAM/Shared/constants.ts | 2 - 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.test.tsx b/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.test.tsx index 7877411075e..cbcf0404d21 100644 --- a/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.test.tsx +++ b/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.test.tsx @@ -5,7 +5,6 @@ import { vi } from 'vitest'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; -import { DELEGATION_VALIDATION_ERROR } from '../Shared/constants'; import { UpdateDelegationsDrawer } from './UpdateDelegationsDrawer'; import type { ChildAccountWithDelegates, User } from '@linode/api-v4'; @@ -118,27 +117,32 @@ describe('UpdateDelegationsDrawer', () => { }); }); - it('should show error when no users are selected', async () => { - const emptyDelegation = { - ...mockChildAccountWithDelegates, - users: [], - }; + it('allows sending an empty payload', async () => { + renderWithTheme(); + + const user = userEvent.setup(); + + // Open the autocomplete and deselect the preselected user (user1) + const autocompleteInput = screen.getByRole('combobox'); + await user.click(autocompleteInput); + + await waitFor(() => { + // Ensure options are rendered + expect(screen.getByRole('option', { name: 'user1' })).toBeInTheDocument(); + }); - renderWithTheme( - - ); + const user1Option = screen.getByRole('option', { name: 'user1' }); + await user.click(user1Option); // toggles off the selected user - // Try to submit without selecting any users - const submitButton = screen.getByRole('button', { name: 'Update' }); - await userEvent.click(submitButton); + // Submit with no users selected + const submitButton = screen.getByRole('button', { name: /update/i }); + await user.click(submitButton); await waitFor(() => { - const errorElement = screen.getByText(DELEGATION_VALIDATION_ERROR); - expect(errorElement).toBeVisible(); + expect(mocks.mockMutateAsync).toHaveBeenCalledWith({ + euuid: mockChildAccountWithDelegates.euuid, + users: [], + }); }); }); }); diff --git a/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx b/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx index f277ef4cc6b..51e60716e53 100644 --- a/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx +++ b/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx @@ -15,10 +15,7 @@ import React from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; import { usePermissions } from '../hooks/usePermissions'; -import { - DELEGATION_VALIDATION_ERROR, - INTERNAL_ERROR_NO_CHANGES_SAVED, -} from '../Shared/constants'; +import { INTERNAL_ERROR_NO_CHANGES_SAVED } from '../Shared/constants'; import { getPlaceholder } from '../Shared/Entities/utils'; import type { ChildAccount, ChildAccountWithDelegates } from '@linode/api-v4'; @@ -58,9 +55,12 @@ export const UpdateDelegationsDrawer = ({ const { mutateAsync: updateDelegates } = useUpdateChildAccountDelegatesQuery(); - const currentUsers = React.useMemo(() => { + const formattedCurrentUsers = React.useMemo(() => { if (delegation && 'users' in delegation && delegation.users) { - return delegation.users; + return delegation.users.map((username) => ({ + label: username, + value: username, + })); } return []; }, [delegation]); @@ -75,22 +75,23 @@ export const UpdateDelegationsDrawer = ({ const form = useForm({ defaultValues: { - users: currentUsers.map((username) => ({ - label: username, - value: username, - })), + users: [], }, }); + const { control, formState: { errors, isSubmitting }, handleSubmit, reset, setError, - watch, } = form; - watch('users'); + // Reinitialize form values when the drawer opens or delegated users change + React.useEffect(() => { + if (!open) return; + reset({ users: formattedCurrentUsers }, { keepDirtyValues: true }); + }, [open, formattedCurrentUsers, reset]); const onSubmit = async (values: UpdateDelegationsFormValues) => { if (!delegation) return; @@ -174,9 +175,6 @@ export const UpdateDelegationsDrawer = ({ value={field.value} /> )} - rules={{ - required: DELEGATION_VALIDATION_ERROR, - }} /> Date: Wed, 21 Jan 2026 13:01:26 +0100 Subject: [PATCH 2/3] Added changeset: IAM DElegation: remove restriction to update user delegation with empty array, update the delegations after reopening a drawer --- packages/manager/.changeset/pr-13300-fixed-1768996885991.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-13300-fixed-1768996885991.md diff --git a/packages/manager/.changeset/pr-13300-fixed-1768996885991.md b/packages/manager/.changeset/pr-13300-fixed-1768996885991.md new file mode 100644 index 00000000000..088cfd3a31a --- /dev/null +++ b/packages/manager/.changeset/pr-13300-fixed-1768996885991.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +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)) From c760b6b42bfad5bf49526b8e42b966cdb04a5b13 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 22 Jan 2026 10:38:30 +0100 Subject: [PATCH 3/3] small drawer refactor --- .../IAM/Delegations/UpdateDelegationForm.tsx | 161 +++++++++++++++++ .../Delegations/UpdateDelegationsDrawer.tsx | 169 ++---------------- 2 files changed, 173 insertions(+), 157 deletions(-) create mode 100644 packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx diff --git a/packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx b/packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx new file mode 100644 index 00000000000..f4f70cb7466 --- /dev/null +++ b/packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx @@ -0,0 +1,161 @@ +import { useUpdateChildAccountDelegatesQuery } from '@linode/queries'; +import { ActionsPanel, Autocomplete, Notice, Typography } from '@linode/ui'; +import { useTheme } from '@mui/material'; +import { enqueueSnackbar } from 'notistack'; +import * as React from 'react'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; + +import { usePermissions } from '../hooks/usePermissions'; +import { INTERNAL_ERROR_NO_CHANGES_SAVED } from '../Shared/constants'; +import { getPlaceholder } from '../Shared/Entities/utils'; + +import type { ChildAccount, ChildAccountWithDelegates } from '@linode/api-v4'; + +interface UpdateDelegationsFormValues { + users: UserOption[]; +} + +interface UserOption { + label: string; + value: string; +} + +interface DelegationsFormProps { + delegation: ChildAccount | ChildAccountWithDelegates; + formattedCurrentUsers: UserOption[]; + isLoading: boolean; + onClose: () => void; + userOptions: UserOption[]; +} +export const UpdateDelegationForm = ({ + delegation, + formattedCurrentUsers, + isLoading, + onClose, + userOptions, +}: DelegationsFormProps) => { + const theme = useTheme(); + + const { data: permissions } = usePermissions('account', [ + 'update_delegate_users', + ]); + + const { mutateAsync: updateDelegates } = + useUpdateChildAccountDelegatesQuery(); + + const form = useForm({ + defaultValues: { + users: formattedCurrentUsers, + }, + }); + + const { + control, + formState: { errors, isSubmitting }, + handleSubmit, + reset, + setError, + } = form; + + const onSubmit = async (values: UpdateDelegationsFormValues) => { + const usersList = values.users.map((user) => user.value); + + try { + await updateDelegates({ + euuid: delegation.euuid, + users: usersList, + }); + enqueueSnackbar(`Delegation updated`, { variant: 'success' }); + handleClose(); + } catch (errors) { + for (const error of errors) { + setError('root', { + message: error.reason ?? INTERNAL_ERROR_NO_CHANGES_SAVED, + }); + } + } + }; + + const handleClose = () => { + reset(); + onClose(); + }; + + return ( + <> + {errors.root?.message && ( + + )} + +
+ + Add or remove users who should have access to the child account. + Users removed from this list will lose the role assignment on the + child account and they won't be visible in the user list on the + child account. + + + + Update delegation for {delegation.company}: + + + ( + + option.value === value.value + } + label={'Delegate Users'} + loading={isLoading} + multiple + noMarginTop + onChange={(_, newValue) => { + field.onChange(newValue || []); + }} + options={userOptions} + placeholder={getPlaceholder( + 'delegates', + field.value.length, + userOptions.length + )} + textFieldProps={{ + hideLabel: true, + }} + value={field.value} + /> + )} + /> + + + +
+ + ); +}; diff --git a/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx b/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx index 51e60716e53..32c0633e121 100644 --- a/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx +++ b/packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.tsx @@ -1,34 +1,11 @@ -import { - useAccountUsers, - useUpdateChildAccountDelegatesQuery, -} from '@linode/queries'; -import { - ActionsPanel, - Autocomplete, - Drawer, - Notice, - Typography, - useTheme, -} from '@linode/ui'; -import { enqueueSnackbar } from 'notistack'; +import { useAccountUsers } from '@linode/queries'; +import { Drawer } from '@linode/ui'; import React from 'react'; -import { Controller, FormProvider, useForm } from 'react-hook-form'; -import { usePermissions } from '../hooks/usePermissions'; -import { INTERNAL_ERROR_NO_CHANGES_SAVED } from '../Shared/constants'; -import { getPlaceholder } from '../Shared/Entities/utils'; +import { UpdateDelegationForm } from './UpdateDelegationForm'; import type { ChildAccount, ChildAccountWithDelegates } from '@linode/api-v4'; -interface UserOption { - label: string; - value: string; -} - -interface UpdateDelegationsFormValues { - users: UserOption[]; -} - interface Props { delegation: ChildAccount | ChildAccountWithDelegates | undefined; onClose: () => void; @@ -40,21 +17,11 @@ export const UpdateDelegationsDrawer = ({ onClose, open, }: Props) => { - const theme = useTheme(); - - // Get all parent accounts as options for delegation const { data: allParentAccounts, isLoading } = useAccountUsers({ enabled: open, filters: { user_type: 'parent' }, }); - const { data: permissions } = usePermissions('account', [ - 'update_delegate_users', - ]); - - const { mutateAsync: updateDelegates } = - useUpdateChildAccountDelegatesQuery(); - const formattedCurrentUsers = React.useMemo(() => { if (delegation && 'users' in delegation && delegation.users) { return delegation.users.map((username) => ({ @@ -73,129 +40,17 @@ export const UpdateDelegationsDrawer = ({ })); }, [allParentAccounts]); - const form = useForm({ - defaultValues: { - users: [], - }, - }); - - const { - control, - formState: { errors, isSubmitting }, - handleSubmit, - reset, - setError, - } = form; - - // Reinitialize form values when the drawer opens or delegated users change - React.useEffect(() => { - if (!open) return; - reset({ users: formattedCurrentUsers }, { keepDirtyValues: true }); - }, [open, formattedCurrentUsers, reset]); - - const onSubmit = async (values: UpdateDelegationsFormValues) => { - if (!delegation) return; - - const usersList = values.users.map((user) => user.value); - - try { - await updateDelegates({ - euuid: delegation.euuid, - users: usersList, - }); - enqueueSnackbar(`Delegation updated`, { variant: 'success' }); - handleClose(); - } catch (errors) { - for (const error of errors) { - setError(error?.field ?? 'root', { - message: INTERNAL_ERROR_NO_CHANGES_SAVED, - }); - } - } - }; - - const handleClose = () => { - reset(); - onClose(); - }; - return ( - - {errors.root?.message && ( - + + {delegation && ( + )} - -
- - Add or remove users who should have access to the child account. - Users removed from this list will lose the role assignment on the - child account and they won’t be visible in the user list on the - child account. - - - {delegation && ( - - Update delegation for {delegation.company}: - - )} - - ( - - option.value === value.value - } - label={'Delegate Users'} - loading={isLoading} - multiple - noMarginTop - onChange={(_, newValue) => { - field.onChange(newValue || []); - }} - options={userOptions} - placeholder={getPlaceholder( - 'delegates', - field.value.length, - userOptions.length - )} - textFieldProps={{ - hideLabel: true, - }} - value={field.value} - /> - )} - /> - - - -
); };