Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13300-fixed-1768996885991.md
Original file line number Diff line number Diff line change
@@ -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))
161 changes: 161 additions & 0 deletions packages/manager/src/features/IAM/Delegations/UpdateDelegationForm.tsx
Original file line number Diff line number Diff line change
@@ -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 = ({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much cleaner now πŸ‘

delegation,
formattedCurrentUsers,
isLoading,
onClose,
userOptions,
}: DelegationsFormProps) => {
const theme = useTheme();

const { data: permissions } = usePermissions('account', [
'update_delegate_users',
]);

const { mutateAsync: updateDelegates } =
useUpdateChildAccountDelegatesQuery();

const form = useForm<UpdateDelegationsFormValues>({
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 && (
<Notice text={errors.root?.message} variant="error" />
)}
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<Typography sx={{ marginBottom: theme.tokens.spacing.S16 }}>
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&apos;t be visible in the user list on the
child account.
</Typography>

<Typography
sx={{
marginBottom: theme.tokens.spacing.S8,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
Update delegation for <strong>{delegation.company}:</strong>
</Typography>

<Controller
control={control}
name="users"
render={({ field, fieldState }) => (
<Autocomplete
data-testid="delegates-autocomplete"
errorText={fieldState.error?.message}
isOptionEqualToValue={(option, value) =>
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}
/>
)}
/>

<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
label: 'Update',
loading: isSubmitting,
type: 'submit',
disabled: !permissions?.update_delegate_users,
tooltipText: !permissions?.update_delegate_users
? 'You do not have permission to update delegations.'
: undefined,
}}
secondaryButtonProps={{
'data-testid': 'cancel',
label: 'Cancel',
onClick: handleClose,
}}
/>
</form>
</FormProvider>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

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';
Expand Down Expand Up @@ -118,27 +117,32 @@
});
});

it('should show error when no users are selected', async () => {
const emptyDelegation = {
...mockChildAccountWithDelegates,
users: [],
};
it('allows sending an empty payload', async () => {
renderWithTheme(<UpdateDelegationsDrawer {...defaultProps} />);

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();

Check warning on line 131 in packages/manager/src/features/IAM/Delegations/UpdateDelegationsDrawer.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found","line":131,"column":14,"nodeType":"MemberExpression","messageId":"preferImplicitAssert","endLine":131,"endColumn":30}
});

renderWithTheme(
<UpdateDelegationsDrawer
delegation={emptyDelegation}
onClose={vi.fn()}
open={true}
/>
);
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: [],
});
});
});
});
Loading