Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add functionality to support the 'Assign New Roles' drawer for a single user in IAM ([#11834](https://github.com/linode/manager/pull/11834))
6 changes: 6 additions & 0 deletions packages/manager/src/features/IAM/Shared/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,9 @@ export const updateUserRoles = ({
}
);
};

export interface AssignNewRoleFormValues {
roles: {
role: RolesType | null;
}[];
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,63 @@
import { Autocomplete, Drawer, Typography } from '@linode/ui';
import { ActionsPanel, Drawer, Typography } from '@linode/ui';
import { useTheme } from '@mui/material';
import React from 'react';
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';

import { Link } from 'src/components/Link';
import { LinkButton } from 'src/components/LinkButton';
import { NotFound } from 'src/components/NotFound';
import { StyledLinkButtonBox } from 'src/components/SelectFirewallPanel/SelectFirewallPanel';
import { AssignSingleRole } from 'src/features/IAM/Users/UserRoles/AssignSingleRole';
import { useAccountPermissions } from 'src/queries/iam/iam';

import { AssignedPermissionsPanel } from '../../Shared/AssignedPermissionsPanel/AssignedPermissionsPanel';
import { getAllRoles, getRoleByName } from '../../Shared/utilities';
import { getAllRoles } from '../../Shared/utilities';

import type { RolesType } from '../../Shared/utilities';
import type { AssignNewRoleFormValues } from '../../Shared/utilities';

interface Props {
onClose: () => void;
open: boolean;
}

export const AssignNewRoleDrawer = ({ onClose, open }: Props) => {
const [
selectedOptions,
setSelectedOptions,
] = React.useState<RolesType | null>(null);
const theme = useTheme();

const {
data: accountPermissions,
isLoading: accountPermissionsLoading,
} = useAccountPermissions();
const { data: accountPermissions } = useAccountPermissions();

const form = useForm<AssignNewRoleFormValues>({
defaultValues: {
roles: [{ role: null }],
},
});

const { control, handleSubmit, reset, watch } = form;
const { append, fields, remove } = useFieldArray({
control,
name: 'roles',
});

// to watch changes to this value since we're conditionally rendering "Add another role"
const roles = watch('roles');

const allRoles = React.useMemo(() => {
if (!accountPermissions) {
return [];
}

return getAllRoles(accountPermissions);
}, [accountPermissions]);

// Get the selected role based on the `selectedOptions`
const selectedRole = React.useMemo(() => {
if (!selectedOptions || !accountPermissions) {
return null;
}
return getRoleByName(accountPermissions, selectedOptions.value);
}, [selectedOptions, accountPermissions]);
const onSubmit = handleSubmit(async (values: AssignNewRoleFormValues) => {
// TODO - make this really do something apart from console logging - UIE-8590

// const selectedRoles = values.roles.map((r) => r.role).filter(Boolean);
handleClose();
});

const handleClose = () => {
reset();

onClose();
};

// TODO - add a link 'Learn more" - UIE-8534
return (
Copy link
Contributor

Choose a reason for hiding this comment

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

The entire drawer will need to wrapped in a FormProvider: <FormProvider {...form}>

Expand All @@ -50,31 +67,49 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => {
open={open}
title="Assign New Roles"
>
<Typography sx={{ marginBottom: 2.5 }}>
Select a role you want to assign to a user. Some roles require selecting
resources they should apply to. Configure the first role and continue
adding roles or save the assignment.
<Link to=""> Learn more about roles and permissions.</Link>
</Typography>

<Autocomplete
renderOption={(props, option) => (
<li {...props} key={option.label}>
{option.label}
</li>
)}
label="Assign New Roles"
loading={accountPermissionsLoading}
onChange={(_, value) => setSelectedOptions(value)}
options={allRoles}
placeholder="Select a Role"
textFieldProps={{ hideLabel: true, noMarginTop: true }}
value={selectedOptions}
/>

{selectedRole && (
<AssignedPermissionsPanel key={selectedRole.name} role={selectedRole} />
)}
{' '}
<FormProvider {...form}>
<form onSubmit={onSubmit}>
<Typography sx={{ marginBottom: 2.5 }}>
Select a role you want to assign to a user. Some roles require
selecting resources they should apply to. Configure the first role
and continue adding roles or save the assignment.
<Link to=""> Learn more about roles and permissions.</Link>
</Typography>

{!!accountPermissions &&
fields.map((field, index) => (
<AssignSingleRole
index={index}
key={field.id}
onRemove={() => remove(index)}
options={allRoles}
permissions={accountPermissions}
/>
))}

{/* If all roles are filled, allow them to add another */}
{roles.length > 0 && roles.every((field) => field.role) && (
<StyledLinkButtonBox sx={{ marginTop: theme.tokens.spacing.S12 }}>
<LinkButton onClick={() => append({ role: null })}>
Add another role
</LinkButton>
</StyledLinkButtonBox>
)}
<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
label: 'Assign',
type: 'submit',
}}
secondaryButtonProps={{
'data-testid': 'cancel',
label: 'Cancel',
onClick: handleClose,
}}
/>
</form>
</FormProvider>
</Drawer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Autocomplete, Button } from '@linode/ui';
import Close from '@mui/icons-material/Close';
import { Divider, useTheme } from '@mui/material';
import Box from '@mui/material/Box';
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';

import { AssignedPermissionsPanel } from 'src/features/IAM/Shared/AssignedPermissionsPanel/AssignedPermissionsPanel';
import { getRoleByName } from 'src/features/IAM/Shared/utilities';

import type { IamAccountPermissions } from '@linode/api-v4';
import type {
AssignNewRoleFormValues,
RolesType,
} from 'src/features/IAM/Shared/utilities';

interface Props {
index: number;
onRemove: (idx: number) => void;
options: RolesType[];
permissions: IamAccountPermissions;
}

export const AssignSingleRole = ({
index,
onRemove,
options,
permissions,
}: Props) => {
const theme = useTheme();

const { control } = useFormContext<AssignNewRoleFormValues>();

return (
<Box display="flex">
<Box display="flex" flexDirection="column" sx={{ flex: '5 1 auto' }}>
{index !== 0 && (
<Divider
sx={{
marginBottom: theme.tokens.spacing.S12,
}}
/>
)}

<Controller
render={({ field: { onChange, value } }) => (
<>
<Autocomplete
onChange={(event, newValue) => {
onChange(newValue);
}}
label="Assign New Roles"
options={options}
placeholder="Select a Role"
textFieldProps={{ hideLabel: true }}
value={value || null}
/>
{value && (
<AssignedPermissionsPanel
role={getRoleByName(permissions, value.value)!}
/>
)}
</>
)}
control={control}
name={`roles.${index}.role`}
/>
</Box>
<Box
sx={{
flex: '0 1 auto',
marginTop:
index === 0 ? -theme.tokens.spacing.S4 : theme.tokens.spacing.S16,
verticalAlign: 'top',
}}
>
<Button disabled={index === 0} onClick={() => onRemove(index)}>
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd petition UX to revisit having an X button that's always disabled rather than just not showing it. Less code, cleaner, and better accessibility.

<Close />
</Button>
</Box>
</Box>
);
};