Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

IAM: Default Roles Table ([#12990](https://github.com/linode/manager/pull/12990))
23 changes: 13 additions & 10 deletions packages/manager/src/features/IAM/Roles/Defaults/DefaultRoles.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Paper, Stack, Typography } from '@linode/ui';
import { Paper, Typography } from '@linode/ui';
import * as React from 'react';

import { AssignedRolesTable } from '../../Shared/AssignedRolesTable/AssignedRolesTable';

export const DefaultRoles = () => {
return (
<Paper>
<Stack>
<Typography variant="h2">Default Roles for Delegate Users</Typography>
<Typography marginTop={2}>
View and manage roles to be assigned to delegate users by default.
Note that changes implemented here will apply to only new delegate
users. For existing delegate users, use their Assigned Roles page to
update the assignment.
</Typography>
</Stack>
<Typography variant="h2">Default Roles for Delegate Users</Typography>
<Typography mt={2}>
View and manage roles to be assigned to delegate users by default. Note
that changes implemented here will apply to only new delegate users.
</Typography>
<Typography mb={2}>
For existing delegate users, use their Assigned Roles page to update the
assignment.
</Typography>
<AssignedRolesTable />
</Paper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
}),
];

const mockUserRoles = userRolesFactory.build();
const mockAccountRoles = accountRolesFactory.build();

describe('AssignedRolesTable', () => {
beforeEach(() => {
queryMocks.useParams.mockReturnValue({
Expand All @@ -72,11 +75,11 @@

it('should display roles and menu when data is available', async () => {
queryMocks.useUserRoles.mockReturnValue({
data: userRolesFactory.build(),
data: mockUserRoles,
});

queryMocks.useAccountRoles.mockReturnValue({
data: accountRolesFactory.build(),
data: mockAccountRoles,
});

queryMocks.useAllAccountEntities.mockReturnValue({
Expand All @@ -100,11 +103,11 @@

it('should display empty state when no roles match filters', async () => {
queryMocks.useUserRoles.mockReturnValue({
data: userRolesFactory.build(),
data: mockUserRoles,
});

queryMocks.useAccountRoles.mockReturnValue({
data: accountRolesFactory.build(),
data: mockAccountRoles,
});

queryMocks.useAllAccountEntities.mockReturnValue({
Expand All @@ -123,11 +126,11 @@

it('should filter roles based on search query', async () => {
queryMocks.useUserRoles.mockReturnValue({
data: userRolesFactory.build(),
data: mockUserRoles,
});

queryMocks.useAccountRoles.mockReturnValue({
data: accountRolesFactory.build(),
data: mockAccountRoles,
});

queryMocks.useAllAccountEntities.mockReturnValue({
Expand All @@ -146,11 +149,11 @@

it('should filter roles based on selected resource type', async () => {
queryMocks.useUserRoles.mockReturnValue({
data: userRolesFactory.build(),
data: mockUserRoles,
});

queryMocks.useAccountRoles.mockReturnValue({
data: accountRolesFactory.build(),
data: mockAccountRoles,
});

queryMocks.useAllAccountEntities.mockReturnValue({
Expand All @@ -166,4 +169,19 @@
expect(screen.queryByText('account_firewall_creator')).toBeVisible();
});
});

it('should show different button text for default roles view', async () => {
queryMocks.useAccountRoles.mockReturnValue({
data: mockAccountRoles,
});

queryMocks.useAllAccountEntities.mockReturnValue({
data: mockEntities,
});

renderWithTheme(<AssignedRolesTable />);

expect(screen.getByText('Add New Default Roles')).toBeVisible();

Check failure on line 184 in packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.test.tsx

View workflow job for this annotation

GitHub Actions / test-manager

src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.test.tsx > AssignedRolesTable > should show different button text for default roles view

TestingLibraryElementError: Unable to find an element with the text: Add New Default Roles. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="MuiGrid-root MuiGrid-direction-xs-row css-17fpwt7-MuiGrid-root" > <div class="MuiGrid-root MuiGrid-container MuiGrid-direction-xs-row css-nzo78a-MuiGrid-root" > <div class="MuiGrid-root MuiGrid-container MuiGrid-direction-xs-row css-j2shku-MuiGrid-root" > <div class="MuiBox-root css-1nao28u" > <div class="visually-hidden MuiBox-root css-15ybjgl" data-testid="inputLabelWrapper" > <label class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-animated css-1urk01m-MuiFormLabel-root-MuiInputLabel-root" data-qa-textfield-label="Filter" for="filter" > Filter </label> </div> <div class="MuiBox-root css-8atqhb" > <div class="MuiFormControl-root MuiFormControl-fullWidth MuiTextField-root css-18cs7tx-MuiFormControl-root-MuiTextField-root" data-qa-debounced-search="true" > <div class="MuiInputBase-root MuiInput-root MuiInputBase-colorPrimary MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-adornedStart MuiInputBase-adornedEnd css-zk0un1-MuiInputBase-root-MuiInput-root" > <div class="MuiInputAdornment-root MuiInputAdornment-positionStart MuiInputAdornment-standard MuiInputAdornment-sizeMedium css-si7mhv-MuiInputAdornment-root" > <span aria-hidden="true" class="notranslate" > ​ </span> <svg data-testid="SearchIcon" fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg" > <path clip-rule="evenodd" d="M3.33341 9.16667C3.33341 5.94501 5.94509 3.33334 9.16675 3.33334C12.3884 3.33334 15.0001 5.94501 15.0001 9.16667C15.0001 10.7383 14.3786 12.1648 13.3679 13.2137C13.3394 13.2356 13.312 13.2596 13.2858 13.2857C13.2597 13.3119 13.2357 13.3393 13.2137 13.3678C12.1648 14.3785 10.7384 15 9.16675 15C5.94509 15 3.33341 12.3883 3.33341 9.16667ZM13.8482 15.0266C12.5651 16.0529 10.9376 16.6667 9.16675 16.6667C5.02461 16.6667 1.66675 13.3088 1.66675 9.16667C1.66675 5.02454 5.02461 1.66667 9.16675 1.66667C13.3089 1.66667 16.6667 5.02454 16.6667 9.16667C16.6667 10.9375 16.053 12.565 15.0267 13.8481L18.0893 16.9107C18.4148 17.2362 18.4148 17.7638 18.0893 18.0893C17.7639 18.4147 17.2363 18.4147 16.9108 18.0893L13.8482 15.0266Z" fill="currentColor" fill-rule="evenodd" /> </svg> </div> <input aria-invalid="false" class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedStart MuiInputBase-inputAdornedEnd css-1cmslh1-MuiInputBase-input-MuiInput-input" data-testid="textfield-input" id="filter" placeholder="Search" type="text" value="" /> <div class="MuiInputAdornment-root MuiInputAdornment-positionEnd MuiInputAdornment-standard MuiInputAdornment-sizeMedium css-1wcu8rr-MuiInputAdornmen
expect(screen.queryByText('Assign New Roles')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useAccountRoles, useUserRoles } from '@linode/queries';
import {
useAccountRoles,
useGetDefaultDelegationAccessQuery,
useUserRoles,
} from '@linode/queries';
import { Button, CircleProgress, Select, Typography } from '@linode/ui';
import { useTheme } from '@mui/material';
import Grid from '@mui/material/Grid';
import { useNavigate, useParams } from '@tanstack/react-router';
import { useNavigate } from '@tanstack/react-router';
import React from 'react';

import { CollapsibleTable } from 'src/components/CollapsibleTable/CollapsibleTable';
Expand All @@ -17,6 +21,7 @@ import { TableSortCell } from 'src/components/TableSortCell/TableSortCell';
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
import { useAllAccountEntities } from 'src/queries/entities/entities';

import { useIsDefaultDelegationRolesForChildAccount } from '../../hooks/useDelegationRole';
import { usePermissions } from '../../hooks/usePermissions';
import { AssignedEntities } from '../../Users/UserRoles/AssignedEntities';
import { AssignNewRoleDrawer } from '../../Users/UserRoles/AssignNewRoleDrawer';
Expand Down Expand Up @@ -65,9 +70,11 @@ const ALL_ROLES_OPTION: SelectOption = {
label: 'All Assigned Roles',
value: 'all',
};

export const AssignedRolesTable = () => {
const { username } = useParams({ from: '/iam/users/$username' });
interface Props {
username?: string;
Comment thread
mpolotsk-akamai marked this conversation as resolved.
Outdated
}
export const AssignedRolesTable = (props: Props) => {
const { username } = props;
const navigate = useNavigate();
const theme = useTheme();

Expand All @@ -76,8 +83,33 @@ export const AssignedRolesTable = () => {
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
const { data: permissions } = usePermissions('account', ['is_account_admin']);

// Determine if we're on the default roles view based on delegation role and path
const { isDefaultDelegationRolesForChildAccount } =
useIsDefaultDelegationRolesForChildAccount();

const shouldFetchDefaultRoles =
isDefaultDelegationRolesForChildAccount && !username;
const shouldFetchUserRoles =
!isDefaultDelegationRolesForChildAccount && permissions?.is_account_admin;
Comment thread
mpolotsk-akamai marked this conversation as resolved.
Outdated

const { data: defaultRolesData, isLoading: defaultRolesLoading } =
useGetDefaultDelegationAccessQuery({ enabled: shouldFetchDefaultRoles });
Comment thread
mpolotsk-akamai marked this conversation as resolved.
Outdated

const { data: userRolesData, isLoading: userRolesLoading } = useUserRoles(
username ?? '',
shouldFetchUserRoles
);

const assignedRoles = isDefaultDelegationRolesForChildAccount
? defaultRolesData
: userRolesData;
const assignedRolesLoading = isDefaultDelegationRolesForChildAccount
? defaultRolesLoading
: userRolesLoading;
const pagination = usePaginationV2({
currentRoute: '/iam/users/$username/roles',
currentRoute: isDefaultDelegationRolesForChildAccount
? '/iam/roles/defaults/roles'
: '/iam/users/$username/roles',
initialPage: 1,
preferenceKey: ASSIGNED_ROLES_TABLE_PREFERENCE_KEY,
});
Expand Down Expand Up @@ -139,9 +171,6 @@ export const AssignedRolesTable = () => {
{}
);

const { data: assignedRoles, isLoading: assignedRolesLoading } = useUserRoles(
username ?? ''
);
const { filterableOptions, roles } = React.useMemo(() => {
if (!assignedRoles || !accountRoles) {
return { filterableOptions: [], roles: [] };
Expand Down Expand Up @@ -174,7 +203,7 @@ export const AssignedRolesTable = () => {
const selectedRole = roleName;
navigate({
to: '/iam/users/$username/entities',
params: { username },
params: { username: username || '' },
search: { selectedRole },
});
};
Expand Down Expand Up @@ -388,7 +417,9 @@ export const AssignedRolesTable = () => {
: undefined
}
>
Assign New Roles
{isDefaultDelegationRolesForChildAccount
? 'Add New Default Roles'
: 'Assign New Roles'}
</Button>
</Grid>
</Grid>
Expand All @@ -401,24 +432,33 @@ export const AssignedRolesTable = () => {
/>
<AssignNewRoleDrawer
assignedRoles={assignedRoles}
isDefaultRolesView={isDefaultDelegationRolesForChildAccount}
Comment thread
mpolotsk-akamai marked this conversation as resolved.
Outdated
onClose={() => setIsAssignNewRoleDrawerOpen(false)}
open={isAssignNewRoleDrawerOpen}
username={username}
/>
<ChangeRoleDrawer
assignedRoles={assignedRoles}
Comment thread
mpolotsk-akamai marked this conversation as resolved.
Outdated
isDefaultRolesView={isDefaultDelegationRolesForChildAccount}
mode={drawerMode}
onClose={() => setIsChangeRoleDrawerOpen(false)}
open={isChangeRoleDrawerOpen}
role={selectedRole}
username={username}
/>
<UnassignRoleConfirmationDialog
assignedRoles={assignedRoles}
isDefaultRolesView={isDefaultDelegationRolesForChildAccount}
onClose={() => setIsUnassignRoleDialogOpen(false)}
open={isUnassignRoleDialogOpen}
role={selectedRole}
username={username}
/>
<UpdateEntitiesDrawer
onClose={() => setIsUpdateEntitiesDrawerOpen(false)}
open={isUpdateEntitiesDrawerOpen}
role={selectedRole}
username={username}
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.

same comment as above. This can all be cleaned up since the useIsDefaultDelegationRolesForChildAccount is used in all those modals

/>
<RemoveAssignmentConfirmationDialog
onClose={() => setIsRemoveAssignmentDialogOpen(false)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
useAccountRoles,
useUserRoles,
useUpdateDefaultDelegationAccessQuery,
useUserRolesMutation,
} from '@linode/queries';
import {
Expand All @@ -11,7 +11,6 @@ import {
Typography,
} from '@linode/ui';
import { useTheme } from '@mui/material/styles';
import { useParams } from '@tanstack/react-router';
import React from 'react';
import { Controller, useForm } from 'react-hook-form';

Expand All @@ -30,24 +29,36 @@ import {

import type { DrawerModes, EntitiesOption, ExtendedRoleView } from '../types';
import type { RolesType } from '../utilities';
import type { IamUserRoles } from '@linode/api-v4';

interface Props {
assignedRoles?: IamUserRoles;
isDefaultRolesView?: boolean;
mode: DrawerModes;
onClose: () => void;
open: boolean;
role: ExtendedRoleView | undefined;
username?: string;
}

export const ChangeRoleDrawer = ({ mode, onClose, open, role }: Props) => {
export const ChangeRoleDrawer = ({
mode,
onClose,
open,
role,
username,
assignedRoles,
isDefaultRolesView,
}: Props) => {
const theme = useTheme();
const { username } = useParams({ from: '/iam/users/$username' });

const { data: accountRoles, isLoading: accountPermissionsLoading } =
useAccountRoles();

const { data: assignedRoles } = useUserRoles(username ?? '');
Comment thread
mpolotsk-akamai marked this conversation as resolved.
const { mutateAsync: updateUserRoles } = useUserRolesMutation(username || '');
Comment thread
mpolotsk-akamai marked this conversation as resolved.
Outdated

const { mutateAsync: updateUserRoles } = useUserRolesMutation(username);
const { mutateAsync: updateDefaultRoles } =
useUpdateDefaultDelegationAccessQuery();

const formattedAssignedEntities: EntitiesOption[] = React.useMemo(() => {
if (!role || !role.entity_names || !role.entity_ids) {
Expand Down Expand Up @@ -132,7 +143,11 @@ export const ChangeRoleDrawer = ({ mode, onClose, open, role }: Props) => {
newRole,
});

await updateUserRoles(updatedUserRoles);
if (isDefaultRolesView) {
Comment thread
mpolotsk-akamai marked this conversation as resolved.
Outdated
await updateDefaultRoles(updatedUserRoles);
} else {
await updateUserRoles(updatedUserRoles);
}

handleClose();
} catch (errors) {
Expand Down
Loading
Loading