Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [UIE-8140] - IAM RBAC - Assign New Roles drawer update #11834

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from

Conversation

rodonnel-akamai
Copy link
Contributor

Description 📝

Adding in the functionality behind the Assign New Roles drawer for a single user

Changes 🔄

List any change(s) relevant to the reviewer.

  • Added Autocomplete for multiple roles to be selected
  • Allow user to add and remove roles within the drawer

Does not include (will be introduced in a future PR - TODO are listed in code):

  • Actual backend call - merely logs the results of the user selection when Assign is clicked
  • Link for help text
  • "Hide details" link, whose behavior is still not completely defined

Target release date 🗓️

(dev)

Preview 📷

Include a screenshot or screen recording of the change.

🔒 Use the Mask Sensitive Data setting for security.

💡 Use <video src="" /> tag when including recordings in table.

Before (Figma) After
Screenshot 2025-03-12 at 11 50 58 AM Screenshot 2025-03-12 at 11 42 33 AM

How to test 🧪

Prerequisites

(How to setup test environment)

  • Ensure the Identity and Access Beta flag is enabled in dev tools
  • Ensure the MSW is enabled in dev tools
  • Click on the any username in the users table and go to the tab Assigned Roles or click on the user's menu View User Roles
  • Click on Assign New Role

Verification steps

(How to verify changes)

  • Confirm drawer opens
  • Multiple roles can be selected and removed
Author Checklists

As an Author, to speed up the review process, I considered 🤔

👀 Doing a self review
❔ Our contribution guidelines
🤏 Splitting feature into small PRs
➕ Adding a changeset
🧪 Providing/improving test coverage
🔐 Removing all sensitive information from the code and PR description
🚩 Using a feature flag to protect the release
👣 Providing comprehensive reproduction steps
📑 Providing or updating our documentation
🕛 Scheduling a pair reviewing session
📱 Providing mobile support
♿ Providing accessibility support


  • I have read and considered all applicable items listed above.

As an Author, before moving this PR from Draft to Open, I confirmed ✅

  • All unit tests are passing
  • TypeScript compilation succeeded without errors
  • Code passes all linting rules

@rodonnel-akamai rodonnel-akamai requested a review from a team as a code owner March 12, 2025 15:54
@rodonnel-akamai rodonnel-akamai requested review from dwiley-akamai and hana-akamai and removed request for a team March 12, 2025 15:54
@cpathipa cpathipa requested review from cpathipa and removed request for hana-akamai March 12, 2025 16:42
Copy link
Contributor

@jaalah-akamai jaalah-akamai left a comment

Choose a reason for hiding this comment

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

@rodonnel-akamai I tried to explain things as thoroughly as I could this time around since you've asked. There may be some gaps in what I suggested, but read our docs and look at some other examples already in IAM. Happy to clarify any questions! 👍

const handleRemoveRole = (index: number) => {
const updatedRoles = selectedRoles.filter((_, i) => i !== index);
setSelectedRoles(updatedRoles);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

We're going to want to refactor this to use React Hook Form so a lot of this is going to change.

Essentially, you'll want to do something like:

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

// shared/utilities
export interface AssignNewRoleFormValues {
  roles: {
    role: RolesType | null;
  }[];
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Removing roles will be handled by remove handler provided by RHF

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

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

Comment on lines 75 to 85
selectedRoles.map((role, index) => (
<AssignSingleRole
index={index}
key={role ? role.label : `${index}`}
onChange={handleChangeRole}
onRemove={handleRemoveRole}
options={allRoles}
permissions={accountPermissions}
selectedOption={selectedRoles[index]}
/>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that we're getting the roles from our RHF fields, this will have to change:

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

// eslint-disable-next-line no-console
console.log(
'Selected Roles:',
selectedRoles.filter((role) => role)
Copy link
Contributor

Choose a reason for hiding this comment

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

Use values from RHF
values.roles.map((r) => r.role).filter(Boolean)

{selectedRole && (
<AssignedPermissionsPanel key={selectedRole.name} role={selectedRole} />
{/* If all roles are filled, allow them to add another */}
{selectedRoles.every((role) => role !== null) && (
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the roles that we're watching:

{roles.length > 0 && roles.every((field) => field.role) && (

<StyledLinkButtonBox
sx={(theme) => ({ marginTop: theme.spacing(1.5) })}
>
<LinkButton onClick={addRole}>Add another role</LinkButton>
Copy link
Contributor

Choose a reason for hiding this comment

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

We can use the built in append:

<LinkButton onClick={() => append({ role: null })}>

export const AssignSingleRole = ({
options,
index,
selectedOption,
Copy link
Contributor

Choose a reason for hiding this comment

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

There will no need for selectedOption anymore with RHF

Comment on lines 31 to 36
const selectedRole = React.useMemo(() => {
if (!selectedOption || !permissions) {
return null;
}
return getRoleByName(permissions, selectedOption.value);
}, [selectedOption, permissions]);
Copy link
Contributor

Choose a reason for hiding this comment

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

We can just inline this now that things are simpler:

<AssignedPermissionsPanel
  role={getRoleByName(permissions, value.value)}
/>

Copy link
Contributor

Choose a reason for hiding this comment

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

In addition, we'll need to pass down the control via context:

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

Comment on lines 52 to 70
<Autocomplete
renderOption={(props, option) => (
<li {...props} key={option.label}>
{option.label}
</li>
)}
label="Assign New Roles"
options={options}
value={selectedOption}
onChange={(_, opt) => onChange(index, opt)}
placeholder="Select a Role"
textFieldProps={{ hideLabel: true }}
/>
{selectedRole && (
<AssignedPermissionsPanel
key={selectedRole.name}
role={selectedRole}
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll need to wrap this in a Controller from RHF so that it can update the parent fields:

<Controller
  render={({ field: { onChange, value } }) => (
    <>
      <Autocomplete
        onChange={(event, newValue) => {
          onChange(newValue);
        }}
        renderOption={(props, option) => (
          <li {...props} key={option.label}>
            {option.label}
          </li>
        )}
        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`}
/>

sx={(theme) => ({
flex: '0 1 auto',
verticalAlign: 'top',
marginTop: index === 0 ? theme.spacing(-0.5) : theme.spacing(2),
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing with spacing tokens

marginTop: index === 0 ? theme.spacing(-0.5) : theme.spacing(2),
})}
>
<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.

Copy link
Contributor

@dwiley-akamai dwiley-akamai left a comment

Choose a reason for hiding this comment

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

Do you have the ESLint extension on the IDE you are using?

Comment on lines 41 to 42
display={'flex'}
flexDirection={'column'}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
display={'flex'}
flexDirection={'column'}
display="flex"
flexDirection="column"

Comment on lines 46 to 50
<Divider
sx={(theme) => ({
marginBottom: theme.spacing(1.5),
})}
></Divider>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<Divider
sx={(theme) => ({
marginBottom: theme.spacing(1.5),
})}
></Divider>
<Divider
sx={(theme) => ({
marginBottom: theme.spacing(1.5),
})}
/>

@aaleksee-akamai
Copy link
Contributor

@jaalah-akamai thanks a lot for such a detailed explanation - it helped a lot! I’ve updated this PR according to your comments.

Copy link
Contributor

@cpathipa cpathipa left a comment

Choose a reason for hiding this comment

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

@aaleksee-akamai This looks much better now. Thank you for addressing the feedback.

@cpathipa cpathipa added Add'tl Approval Needed Waiting on another approval! and removed Requires Changes labels Mar 17, 2025
Copy link

github-actions bot commented Mar 17, 2025

Coverage Report:
Base Coverage: 79.96%
Current Coverage: 79.96%

@aaleksee-akamai aaleksee-akamai force-pushed the UIE-8140-assign-roles-drawer branch from ba507a1 to 9066886 Compare March 18, 2025 16:12
@aaleksee-akamai
Copy link
Contributor

@cpathipa , I've resolved conflicts

@linode-gh-bot
Copy link
Collaborator

Cloud Manager UI test results

🎉 539 passing tests on test run #9 ↗︎

❌ Failing✅ Passing↪️ Skipped🕐 Duration
0 Failing539 Passing3 Skipped113m 50s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants