Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -11,7 +11,7 @@ import { ChangeRoleForEntityDrawer } from '../../Shared/AssignedEntitiesTable/Ch
import type { EntitiesRole } from '../../Shared/types';

const queryMocks = vi.hoisted(() => ({
useParams: vi.fn().mockReturnValue({}),
useParams: vi.fn().mockReturnValue({ username: 'test_user' }),
useAccountRoles: vi.fn().mockReturnValue({}),
useUserRoles: vi.fn().mockReturnValue({}),
}));
Expand Down Expand Up @@ -47,6 +47,7 @@ const props = {
onClose: vi.fn(),
open: true,
role: mockRole,
username: 'test_user',
};

const mockUpdateUserRole = vi.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@ const queryMocks = vi.hoisted(() => ({
useParams: vi.fn().mockReturnValue({}),
useAccountRoles: vi.fn().mockReturnValue({}),
useUserRoles: vi.fn().mockReturnValue({}),
useGetDefaultDelegationAccessQuery: vi.fn().mockReturnValue({}),
useIsDefaultDelegationRolesForChildAccount: vi.fn().mockReturnValue({
isDefaultDelegationRolesForChildAccount: false,
}),
}));

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual<any>('@linode/queries');
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useAccountRoles: queryMocks.useAccountRoles,
useUserRoles: queryMocks.useUserRoles,
useGetDefaultDelegationAccessQuery:
queryMocks.useGetDefaultDelegationAccessQuery,
};
});

vi.mock('src/queries/entities/entities', async () => {
const actual = await vi.importActual<any>('src/queries/entities/entities');
const actual = await vi.importActual('src/queries/entities/entities');
return {
...actual,
useAllAccountEntities: queryMocks.useAllAccountEntities,
Expand All @@ -41,6 +47,11 @@ vi.mock('@tanstack/react-router', async () => {
};
});

vi.mock('../../hooks/useDelegationRole', () => ({
useIsDefaultDelegationRolesForChildAccount:
queryMocks.useIsDefaultDelegationRolesForChildAccount,
}));

const mockEntities = [
accountEntityFactory.build({
id: 7,
Expand All @@ -53,6 +64,9 @@ const mockEntities = [
}),
];

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

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

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 +114,11 @@ describe('AssignedRolesTable', () => {

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 +137,11 @@ describe('AssignedRolesTable', () => {

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 +160,11 @@ describe('AssignedRolesTable', () => {

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 +180,27 @@ describe('AssignedRolesTable', () => {
expect(screen.queryByText('account_firewall_creator')).toBeVisible();
});
});

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

queryMocks.useGetDefaultDelegationAccessQuery.mockReturnValue({
data: mockUserRoles,
});

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

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

renderWithTheme(<AssignedRolesTable />);

expect(screen.getByText('Add New Default Roles')).toBeVisible();
expect(screen.queryByText('Assign New Roles')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
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';
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,8 @@ const ALL_ROLES_OPTION: SelectOption = {
label: 'All Assigned Roles',
value: 'all',
};

export const AssignedRolesTable = () => {
const { username } = useParams({ from: '/iam/users/$username' });
const { username } = useParams({ strict: false });
const navigate = useNavigate();
const theme = useTheme();

Expand All @@ -76,8 +80,30 @@ 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 { data: defaultRolesData, isLoading: defaultRolesLoading } =
useGetDefaultDelegationAccessQuery({
enabled: isDefaultDelegationRolesForChildAccount,
});

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

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 +165,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 +197,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 +411,9 @@ export const AssignedRolesTable = () => {
: undefined
}
>
Assign New Roles
{isDefaultDelegationRolesForChildAccount
? 'Add New Default Roles'
: 'Assign New Roles'}
</Button>
</Grid>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
useAccountRoles,
useGetDefaultDelegationAccessQuery,
useUpdateDefaultDelegationAccessQuery,
useUserRoles,
useUserRolesMutation,
} from '@linode/queries';
Expand All @@ -17,6 +19,7 @@ import { Controller, useForm } from 'react-hook-form';

import { Link } from 'src/components/Link';

import { useIsDefaultDelegationRolesForChildAccount } from '../../hooks/useDelegationRole';
import { AssignedPermissionsPanel } from '../AssignedPermissionsPanel/AssignedPermissionsPanel';
import { ROLES_LEARN_MORE_LINK } from '../constants';
import {
Expand All @@ -40,15 +43,35 @@ interface Props {

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

const { username } = useParams({ strict: false });
const { data: accountRoles, isLoading: accountPermissionsLoading } =
useAccountRoles();

const { data: assignedRoles } = useUserRoles(username ?? '');
const { isDefaultDelegationRolesForChildAccount } =
useIsDefaultDelegationRolesForChildAccount();
const { data: defaultRolesData } = useGetDefaultDelegationAccessQuery({
enabled: isDefaultDelegationRolesForChildAccount,
});

const { data: userRolesData } = useUserRoles(
username ?? '',
!isDefaultDelegationRolesForChildAccount
);

const assignedRoles = isDefaultDelegationRolesForChildAccount
? defaultRolesData
: userRolesData;
const { mutateAsync: updateUserRoles } = useUserRolesMutation(
username,
Boolean(username)
);

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

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

await updateUserRoles(updatedUserRoles);
await mutationFn(updatedUserRoles);

handleClose();
} catch (errors) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const props = {
};

const queryMocks = vi.hoisted(() => ({
useParams: vi.fn().mockReturnValue({}),
useParams: vi.fn().mockReturnValue({ username: 'test_user' }),
useAccountRoles: vi.fn().mockReturnValue({}),
useUserRoles: vi.fn().mockReturnValue({}),
}));
Expand Down
Loading