From bfa299791d3a7a2d3c5e0803a2c5ea7d62178a3e Mon Sep 17 00:00:00 2001 From: Richard O'Donnell Date: Wed, 12 Mar 2025 11:30:45 -0400 Subject: [PATCH 1/4] UIE-8140: Assign New Roles drawer update --- .../Users/UserRoles/AssignNewRoleDrawer.tsx | 107 ++++++++++++------ .../IAM/Users/UserRoles/AssignSingleRole.tsx | 85 ++++++++++++++ 2 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx index 75d8db02052..e229b147d4a 100644 --- a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx @@ -1,12 +1,14 @@ -import { Autocomplete, Drawer, Typography } from '@linode/ui'; -import React from 'react'; +import { ActionsPanel, Drawer, Typography } from '@linode/ui'; +import React, { useState } from 'react'; 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'; @@ -16,31 +18,48 @@ interface Props { } export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { - const [ - selectedOptions, - setSelectedOptions, - ] = React.useState(null); - - const { - data: accountPermissions, - isLoading: accountPermissionsLoading, - } = useAccountPermissions(); + const { data: accountPermissions } = useAccountPermissions(); 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 [selectedRoles, setSelectedRoles] = useState<(RolesType | null)[]>([ + null, + ]); + + const handleChangeRole = (index: number, value: RolesType | null) => { + const updatedRoles = [...selectedRoles]; + updatedRoles[index] = value; + setSelectedRoles(updatedRoles); + }; + + const addRole = () => setSelectedRoles([...selectedRoles, null]); + + const handleRemoveRole = (index: number) => { + const updatedRoles = selectedRoles.filter((_, i) => i !== index); + setSelectedRoles(updatedRoles); + }; + + const removeAllRoles = () => setSelectedRoles([null]); + + const handleSubmit = () => { + // TODO - make this really do something apart from console logging - UIE-8590 + // eslint-disable-next-line no-console + console.log( + 'Selected Roles:', + selectedRoles.filter((role) => role) + ); + handleClose(); + }; + + const handleClose = () => { + removeAllRoles(); + onClose(); + }; // TODO - add a link 'Learn more" - UIE-8534 return ( @@ -57,24 +76,38 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { Learn more about roles and permissions. - ( -
  • - {option.label} -
  • - )} - label="Assign New Roles" - loading={accountPermissionsLoading} - onChange={(_, value) => setSelectedOptions(value)} - options={allRoles} - placeholder="Select a Role" - textFieldProps={{ hideLabel: true, noMarginTop: true }} - value={selectedOptions} - /> + {!!accountPermissions && + selectedRoles.map((role, index) => ( + + ))} - {selectedRole && ( - + {/* If all roles are filled, allow them to add another */} + {selectedRoles.every((role) => role !== null) && ( + ({ marginTop: theme.spacing(1.5) })} + > + Add another role + )} + + ); }; diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx new file mode 100644 index 00000000000..fb25ac69dc8 --- /dev/null +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx @@ -0,0 +1,85 @@ +import { Autocomplete, Button } from '@linode/ui'; +import React from 'react'; + +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 { RolesType } from 'src/features/IAM/Shared/utilities'; +import Close from '@mui/icons-material/Close'; +import Box from '@mui/material/Box'; +import { Divider } from '@mui/material'; + +interface Props { + options: RolesType[]; + index: number; + selectedOption: RolesType | null; + permissions: IamAccountPermissions; + onChange: (idx: number, role: RolesType | null) => void; + onRemove: (idx: number) => void; +} + +export const AssignSingleRole = ({ + options, + index, + selectedOption, + permissions, + onChange, + onRemove, +}: Props) => { + // Get the selected role based on the `selectedOptions` + const selectedRole = React.useMemo(() => { + if (!selectedOption || !permissions) { + return null; + } + return getRoleByName(permissions, selectedOption.value); + }, [selectedOption, permissions]); + + return ( + + ({ flex: '5 1 auto' })} + > + {index !== 0 && ( + ({ + marginBottom: theme.spacing(1.5), + })} + > + )} + ( +
  • + {option.label} +
  • + )} + label="Assign New Roles" + options={options} + value={selectedOption} + onChange={(_, opt) => onChange(index, opt)} + placeholder="Select a Role" + textFieldProps={{ hideLabel: true }} + /> + {selectedRole && ( + + )} +
    + ({ + flex: '0 1 auto', + verticalAlign: 'top', + marginTop: index === 0 ? theme.spacing(-0.5) : theme.spacing(2), + })} + > + + +
    + ); +}; From 51d1b60ab243cc1cdf6b6533b05a54ff7c094d73 Mon Sep 17 00:00:00 2001 From: Richard O'Donnell Date: Wed, 12 Mar 2025 11:56:51 -0400 Subject: [PATCH 2/4] Added changeset: Adding in the functionality behind the Assign New Roles drawer for a single user in IAM --- .../.changeset/pr-11834-upcoming-features-1741795011556.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md diff --git a/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md b/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md new file mode 100644 index 00000000000..c7749215c37 --- /dev/null +++ b/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Adding in the functionality behind the Assign New Roles drawer for a single user in IAM ([#11834](https://github.com/linode/manager/pull/11834)) From b85d012340612f2cf89c12c23801514079963213 Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Sat, 15 Mar 2025 20:38:31 +0100 Subject: [PATCH 3/4] fix with using FormProvider --- ...r-11834-upcoming-features-1741795011556.md | 2 +- .../src/features/IAM/Shared/utilities.ts | 6 + .../Users/UserRoles/AssignNewRoleDrawer.tsx | 136 +++++++++--------- .../IAM/Users/UserRoles/AssignSingleRole.tsx | 96 ++++++------- 4 files changed, 123 insertions(+), 117 deletions(-) diff --git a/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md b/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md index c7749215c37..992ca971826 100644 --- a/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md +++ b/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md @@ -2,4 +2,4 @@ "@linode/manager": Upcoming Features --- -Adding in the functionality behind the Assign New Roles drawer for a single user in IAM ([#11834](https://github.com/linode/manager/pull/11834)) +Add functionality to support the 'Assign New Roles' drawer for a single user in IAM ([#11834](https://github.com/linode/manager/pull/11834)) diff --git a/packages/manager/src/features/IAM/Shared/utilities.ts b/packages/manager/src/features/IAM/Shared/utilities.ts index 62ad79a97b4..171e8eaeca0 100644 --- a/packages/manager/src/features/IAM/Shared/utilities.ts +++ b/packages/manager/src/features/IAM/Shared/utilities.ts @@ -429,3 +429,9 @@ export const updateUserRoles = ({ } ); }; + +export interface AssignNewRoleFormValues { + roles: { + role: RolesType | null; + }[]; +} diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx index e229b147d4a..3bc9e68864e 100644 --- a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx @@ -1,5 +1,7 @@ import { ActionsPanel, Drawer, Typography } from '@linode/ui'; -import React, { useState } from 'react'; +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'; @@ -10,7 +12,7 @@ import { useAccountPermissions } from 'src/queries/iam/iam'; import { getAllRoles } from '../../Shared/utilities'; -import type { RolesType } from '../../Shared/utilities'; +import type { AssignNewRoleFormValues } from '../../Shared/utilities'; interface Props { onClose: () => void; @@ -18,8 +20,25 @@ interface Props { } export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { + const theme = useTheme(); + const { data: accountPermissions } = useAccountPermissions(); + const form = useForm({ + 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 []; @@ -27,37 +46,16 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { return getAllRoles(accountPermissions); }, [accountPermissions]); - const [selectedRoles, setSelectedRoles] = useState<(RolesType | null)[]>([ - null, - ]); - - const handleChangeRole = (index: number, value: RolesType | null) => { - const updatedRoles = [...selectedRoles]; - updatedRoles[index] = value; - setSelectedRoles(updatedRoles); - }; - - const addRole = () => setSelectedRoles([...selectedRoles, null]); - - const handleRemoveRole = (index: number) => { - const updatedRoles = selectedRoles.filter((_, i) => i !== index); - setSelectedRoles(updatedRoles); - }; - - const removeAllRoles = () => setSelectedRoles([null]); - - const handleSubmit = () => { + const onSubmit = handleSubmit(async (values: AssignNewRoleFormValues) => { // TODO - make this really do something apart from console logging - UIE-8590 - // eslint-disable-next-line no-console - console.log( - 'Selected Roles:', - selectedRoles.filter((role) => role) - ); + + // const selectedRoles = values.roles.map((r) => r.role).filter(Boolean); handleClose(); - }; + }); const handleClose = () => { - removeAllRoles(); + reset(); + onClose(); }; @@ -69,45 +67,49 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { open={open} title="Assign New Roles" > - - 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. - Learn more about roles and permissions. - - - {!!accountPermissions && - selectedRoles.map((role, index) => ( - +
    + + 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. + Learn more about roles and permissions. + + + {!!accountPermissions && + fields.map((field, index) => ( + remove(index)} + options={allRoles} + permissions={accountPermissions} + /> + ))} + + {/* If all roles are filled, allow them to add another */} + {roles.length > 0 && roles.every((field) => field.role) && ( + + append({ role: null })}> + Add another role + + + )} + - ))} - - {/* If all roles are filled, allow them to add another */} - {selectedRoles.every((role) => role !== null) && ( - ({ marginTop: theme.spacing(1.5) })} - > - Add another role - - )} - - + + ); }; diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx index fb25ac69dc8..efda5d871a1 100644 --- a/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx @@ -1,80 +1,78 @@ 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 { RolesType } from 'src/features/IAM/Shared/utilities'; -import Close from '@mui/icons-material/Close'; -import Box from '@mui/material/Box'; -import { Divider } from '@mui/material'; +import type { + AssignNewRoleFormValues, + RolesType, +} from 'src/features/IAM/Shared/utilities'; interface Props { - options: RolesType[]; index: number; - selectedOption: RolesType | null; - permissions: IamAccountPermissions; - onChange: (idx: number, role: RolesType | null) => void; onRemove: (idx: number) => void; + options: RolesType[]; + permissions: IamAccountPermissions; } export const AssignSingleRole = ({ - options, index, - selectedOption, - permissions, - onChange, onRemove, + options, + permissions, }: Props) => { - // Get the selected role based on the `selectedOptions` - const selectedRole = React.useMemo(() => { - if (!selectedOption || !permissions) { - return null; - } - return getRoleByName(permissions, selectedOption.value); - }, [selectedOption, permissions]); + const theme = useTheme(); + + const { control } = useFormContext(); return ( - - ({ flex: '5 1 auto' })} - > + + {index !== 0 && ( ({ - marginBottom: theme.spacing(1.5), - })} - > + sx={{ + marginBottom: theme.tokens.spacing.S12, + }} + /> )} - ( -
  • - {option.label} -
  • + + ( + <> + { + onChange(newValue); + }} + label="Assign New Roles" + options={options} + placeholder="Select a Role" + textFieldProps={{ hideLabel: true }} + value={value || null} + /> + {value && ( + + )} + )} - label="Assign New Roles" - options={options} - value={selectedOption} - onChange={(_, opt) => onChange(index, opt)} - placeholder="Select a Role" - textFieldProps={{ hideLabel: true }} + control={control} + name={`roles.${index}.role`} /> - {selectedRole && ( - - )}
    ({ + sx={{ flex: '0 1 auto', + marginTop: + index === 0 ? -theme.tokens.spacing.S4 : theme.tokens.spacing.S16, verticalAlign: 'top', - marginTop: index === 0 ? theme.spacing(-0.5) : theme.spacing(2), - })} + }} >