Skip to content

Commit 18ba87a

Browse files
feat: [UIE-9358] - IAM Parent/Child: Child Account - Default Entity Access (linode#12993)
* feat: [UIE-9358] - IAM Parent/Child: Child Account - Default Entity Access * move logic to the hook * fix assigned default roles * test and filter out assigned roles * remove useMemo
1 parent 38e5b76 commit 18ba87a

File tree

13 files changed

+294
-74
lines changed

13 files changed

+294
-74
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { screen } from '@testing-library/react';
2+
import React from 'react';
3+
4+
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
6+
import { DefaultEntityAccess } from './DefaultEntityAccess';
7+
8+
const queryMocks = vi.hoisted(() => ({
9+
useAllAccountEntities: vi.fn().mockReturnValue({}),
10+
useParams: vi.fn().mockReturnValue({}),
11+
useSearch: vi.fn().mockReturnValue({}),
12+
useGetDefaultDelegationAccessQuery: vi.fn().mockReturnValue({}),
13+
useIsDefaultDelegationRolesForChildAccount: vi
14+
.fn()
15+
.mockReturnValue({ isDefaultDelegationRolesForChildAccount: true }),
16+
}));
17+
18+
vi.mock('src/features/IAM/hooks/useDelegationRole', () => ({
19+
useIsDefaultDelegationRolesForChildAccount:
20+
queryMocks.useIsDefaultDelegationRolesForChildAccount,
21+
}));
22+
23+
vi.mock('@linode/queries', async () => {
24+
const actual = await vi.importActual<any>('@linode/queries');
25+
return {
26+
...actual,
27+
useGetDefaultDelegationAccessQuery:
28+
queryMocks.useGetDefaultDelegationAccessQuery,
29+
};
30+
});
31+
32+
vi.mock('src/queries/entities/entities', async () => {
33+
const actual = await vi.importActual('src/queries/entities/entities');
34+
return {
35+
...actual,
36+
useAllAccountEntities: queryMocks.useAllAccountEntities,
37+
};
38+
});
39+
40+
vi.mock('@tanstack/react-router', async () => {
41+
const actual = await vi.importActual('@tanstack/react-router');
42+
return {
43+
...actual,
44+
useParams: queryMocks.useParams,
45+
useSearch: queryMocks.useSearch,
46+
};
47+
});
48+
49+
describe('DefaultEntityAccess', () => {
50+
it('should render', async () => {
51+
renderWithTheme(<DefaultEntityAccess />);
52+
53+
expect(
54+
screen.getByText('Default Entity Access for Delegate Users')
55+
).toBeVisible();
56+
expect(screen.getByPlaceholderText('Search')).toBeVisible();
57+
expect(screen.getByPlaceholderText('All Entities')).toBeVisible();
58+
expect(screen.getByRole('table')).toBeVisible();
59+
});
60+
});

packages/manager/src/features/IAM/Roles/Defaults/DefaultEntityAccess.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Paper, Stack, Typography } from '@linode/ui';
22
import * as React from 'react';
33

4+
import { AssignedEntitiesTable } from '../../Shared/AssignedEntitiesTable/AssignedEntitiesTable';
5+
46
export const DefaultEntityAccess = () => {
57
return (
68
<Paper>
7-
<Stack>
9+
<Stack marginBottom={2.5}>
810
<Typography variant="h2">
911
Default Entity Access for Delegate Users
1012
</Typography>
@@ -15,6 +17,7 @@ export const DefaultEntityAccess = () => {
1517
the assignment.
1618
</Typography>
1719
</Stack>
20+
<AssignedEntitiesTable />
1821
</Paper>
1922
);
2023
};

packages/manager/src/features/IAM/Users/UserEntities/AssignedEntitiesTable.test.tsx renamed to packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/AssignedEntitiesTable.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { accountEntityFactory } from 'src/factories/accountEntities';
66
import { userRolesFactory } from 'src/factories/userRoles';
77
import { renderWithTheme } from 'src/utilities/testHelpers';
88

9-
import { AssignedEntitiesTable } from '../../Users/UserEntities/AssignedEntitiesTable';
9+
import { AssignedEntitiesTable } from '../../Shared/AssignedEntitiesTable/AssignedEntitiesTable';
1010

1111
const queryMocks = vi.hoisted(() => ({
1212
useAllAccountEntities: vi.fn().mockReturnValue({}),

packages/manager/src/features/IAM/Users/UserEntities/AssignedEntitiesTable.tsx renamed to packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/AssignedEntitiesTable.tsx

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { useUserRoles } from '@linode/queries';
1+
import {
2+
useGetDefaultDelegationAccessQuery,
3+
useUserRoles,
4+
} from '@linode/queries';
25
import { Select, Typography, useTheme } from '@linode/ui';
36
import Grid from '@mui/material/Grid';
4-
import { useParams, useSearch } from '@tanstack/react-router';
7+
import { useSearch } from '@tanstack/react-router';
58
import React from 'react';
69

710
import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
@@ -20,19 +23,23 @@ import { TableSortCell } from 'src/components/TableSortCell';
2023
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
2124
import { useAllAccountEntities } from 'src/queries/entities/entities';
2225

26+
import { useIsDefaultDelegationRolesForChildAccount } from '../../hooks/useDelegationRole';
2327
import { usePermissions } from '../../hooks/usePermissions';
24-
import { ENTITIES_TABLE_PREFERENCE_KEY } from '../../Shared/constants';
25-
import { RemoveAssignmentConfirmationDialog } from '../../Shared/RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog';
28+
import {
29+
addEntityNamesToRoles,
30+
getSearchableFields,
31+
} from '../../Users/UserEntities/utils';
32+
import { ENTITIES_TABLE_PREFERENCE_KEY } from '../constants';
33+
import { RemoveAssignmentConfirmationDialog } from '../RemoveAssignmentConfirmationDialog/RemoveAssignmentConfirmationDialog';
2634
import {
2735
getFilteredRoles,
2836
getFormattedEntityType,
2937
groupAccountEntitiesByType,
3038
mapEntityTypesForSelect,
31-
} from '../../Shared/utilities';
39+
} from '../utilities';
3240
import { ChangeRoleForEntityDrawer } from './ChangeRoleForEntityDrawer';
33-
import { addEntityNamesToRoles, getSearchableFields } from './utils';
3441

35-
import type { DrawerModes, EntitiesRole } from '../../Shared/types';
42+
import type { DrawerModes, EntitiesRole } from '../types';
3643
import type { EntityType } from '@linode/api-v4';
3744
import type { SelectOption } from '@linode/ui';
3845
import type { Action } from 'src/components/ActionMenu/ActionMenu';
@@ -44,13 +51,17 @@ const ALL_ENTITIES_OPTION: SelectOption = {
4451

4552
type OrderByKeys = 'entity_name' | 'entity_type' | 'role_name';
4653

47-
export const AssignedEntitiesTable = () => {
48-
const { username } = useParams({
49-
from: '/iam/users/$username',
50-
});
54+
interface Props {
55+
username?: string;
56+
}
57+
58+
export const AssignedEntitiesTable = ({ username }: Props) => {
5159
const theme = useTheme();
5260
const { data: permissions } = usePermissions('account', ['is_account_admin']);
5361

62+
const { isDefaultDelegationRolesForChildAccount } =
63+
useIsDefaultDelegationRolesForChildAccount();
64+
5465
const { selectedRole: selectedRoleSearchParam } = useSearch({
5566
strict: false,
5667
});
@@ -59,7 +70,9 @@ export const AssignedEntitiesTable = () => {
5970
const [orderBy, setOrderBy] = React.useState<OrderByKeys>('entity_name');
6071

6172
const pagination = usePaginationV2({
62-
currentRoute: '/iam/users/$username/entities',
73+
currentRoute: isDefaultDelegationRolesForChildAccount
74+
? '/iam/roles/defaults/entity-access'
75+
: `/iam/users/$username/entities`,
6376
initialPage: 1,
6477
preferenceKey: ENTITIES_TABLE_PREFERENCE_KEY,
6578
});
@@ -93,10 +106,30 @@ export const AssignedEntitiesTable = () => {
93106
} = useAllAccountEntities({});
94107

95108
const {
96-
data: assignedRoles,
97-
error: assignedRolesError,
98-
isLoading: assignedRolesLoading,
99-
} = useUserRoles(username ?? '');
109+
data: assignedUserRoles,
110+
error: assignedUserRolesError,
111+
isLoading: assignedUserRolesLoading,
112+
} = useUserRoles(username ?? '', !isDefaultDelegationRolesForChildAccount);
113+
114+
const {
115+
data: delegateDefaultRoles,
116+
error: delegateDefaultRolesError,
117+
isLoading: delegateDefaultRolesLoading,
118+
} = useGetDefaultDelegationAccessQuery({
119+
enabled: isDefaultDelegationRolesForChildAccount,
120+
});
121+
122+
const assignedRoles = isDefaultDelegationRolesForChildAccount
123+
? delegateDefaultRoles
124+
: assignedUserRoles;
125+
126+
const error = isDefaultDelegationRolesForChildAccount
127+
? delegateDefaultRolesError
128+
: assignedUserRolesError;
129+
130+
const loading = isDefaultDelegationRolesForChildAccount
131+
? delegateDefaultRolesLoading
132+
: assignedUserRolesLoading;
100133

101134
const { filterableOptions, roles } = React.useMemo(() => {
102135
if (!assignedRoles || !entities) {
@@ -158,11 +191,11 @@ export const AssignedEntitiesTable = () => {
158191
});
159192

160193
const renderTableBody = () => {
161-
if (entitiesLoading || assignedRolesLoading) {
194+
if (entitiesLoading || loading) {
162195
return <TableRowLoading columns={3} rows={1} />;
163196
}
164197

165-
if (entitiesError || assignedRolesError) {
198+
if (entitiesError || error) {
166199
return (
167200
<TableRowError
168201
colSpan={3}
@@ -321,11 +354,13 @@ export const AssignedEntitiesTable = () => {
321354
onClose={() => setIsChangeRoleForEntityDrawerOpen(false)}
322355
open={isChangeRoleForEntityDrawerOpen}
323356
role={selectedRole}
357+
username={username}
324358
/>
325359
<RemoveAssignmentConfirmationDialog
326360
onClose={() => handleRemoveAssignmentDialogClose()}
327361
open={isRemoveAssignmentDialogOpen}
328362
role={selectedRole}
363+
username={username}
329364
/>
330365
{filteredRoles.length > PAGE_SIZES[0] && (
331366
<PaginationFooter

packages/manager/src/features/IAM/Users/UserEntities/ChangeRoleForEntityDrawer.test.tsx renamed to packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { accountRolesFactory } from 'src/factories/accountRoles';
66
import { userRolesFactory } from 'src/factories/userRoles';
77
import { renderWithTheme } from 'src/utilities/testHelpers';
88

9-
import { ChangeRoleForEntityDrawer } from './ChangeRoleForEntityDrawer';
9+
import { ChangeRoleForEntityDrawer } from '../../Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer';
1010

1111
import type { EntitiesRole } from '../../Shared/types';
1212

packages/manager/src/features/IAM/Users/UserEntities/ChangeRoleForEntityDrawer.tsx renamed to packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer.tsx

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
useAccountRoles,
3+
useGetDefaultDelegationAccessQuery,
4+
useUpdateDefaultDelegationAccessQuery,
35
useUserRoles,
46
useUserRolesMutation,
57
} from '@linode/queries';
@@ -11,61 +13,99 @@ import {
1113
Typography,
1214
} from '@linode/ui';
1315
import { useTheme } from '@mui/material/styles';
14-
import { useParams } from '@tanstack/react-router';
1516
import React from 'react';
1617
import { Controller, useForm } from 'react-hook-form';
1718

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

20-
import { AssignedPermissionsPanel } from '../../Shared/AssignedPermissionsPanel/AssignedPermissionsPanel';
21+
import { useIsDefaultDelegationRolesForChildAccount } from '../../hooks/useDelegationRole';
22+
import { AssignedPermissionsPanel } from '../AssignedPermissionsPanel/AssignedPermissionsPanel';
2123
import {
2224
INTERNAL_ERROR_NO_CHANGES_SAVED,
2325
ROLES_LEARN_MORE_LINK,
24-
} from '../../Shared/constants';
26+
} from '../constants';
2527
import {
2628
changeRoleForEntity,
2729
getAllRoles,
2830
getRoleByName,
29-
} from '../../Shared/utilities';
31+
isAccountRole,
32+
isEntityRole,
33+
} from '../utilities';
3034

31-
import type { DrawerModes, EntitiesRole } from '../../Shared/types';
32-
import type { ExtendedEntityRole } from '../../Shared/utilities';
35+
import type { DrawerModes, EntitiesRole } from '../types';
36+
import type { ExtendedEntityRole } from '../utilities';
3337

3438
interface Props {
3539
mode: DrawerModes;
3640
onClose: () => void;
3741
open: boolean;
3842
role: EntitiesRole | undefined;
43+
username?: string;
3944
}
4045

4146
export const ChangeRoleForEntityDrawer = ({
4247
mode,
4348
onClose,
4449
open,
4550
role,
51+
username,
4652
}: Props) => {
4753
const theme = useTheme();
48-
const { username } = useParams({
49-
from: '/iam/users/$username',
50-
});
54+
55+
const { isDefaultDelegationRolesForChildAccount } =
56+
useIsDefaultDelegationRolesForChildAccount();
5157

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

55-
const { data: assignedRoles } = useUserRoles(username ?? '');
61+
const { data: assignedUserRoles } = useUserRoles(
62+
username ?? '',
63+
!isDefaultDelegationRolesForChildAccount
64+
);
65+
66+
const { data: delegateDefaultRoles } = useGetDefaultDelegationAccessQuery({
67+
enabled: isDefaultDelegationRolesForChildAccount,
68+
});
69+
70+
const assignedRoles = isDefaultDelegationRolesForChildAccount
71+
? delegateDefaultRoles
72+
: assignedUserRoles;
73+
74+
const { mutateAsync: updateUserRoles } = useUserRolesMutation(username ?? '');
5675

57-
const { mutateAsync: updateUserRoles } = useUserRolesMutation(username);
76+
const { mutateAsync: updateDefaultDelegationRoles } =
77+
useUpdateDefaultDelegationAccessQuery();
5878

5979
// filtered roles by entity_type and access
6080
const allRoles = React.useMemo(() => {
6181
if (!accountRoles) {
6282
return [];
6383
}
6484

65-
return getAllRoles(accountRoles).filter(
66-
(el) => el.entity_type === role?.entity_type && el.access === role?.access
67-
);
68-
}, [accountRoles, role]);
85+
return getAllRoles(accountRoles).filter((el) => {
86+
const matchesRoleContext =
87+
el.entity_type === role?.entity_type &&
88+
el.access === role?.access &&
89+
el.value !== role?.role_name;
90+
91+
// Exclude account roles already assigned to the user
92+
if (isAccountRole(el)) {
93+
return (
94+
!assignedRoles?.account_access.includes(el.value) &&
95+
matchesRoleContext
96+
);
97+
}
98+
// Exclude entity roles already assigned to the user
99+
if (isEntityRole(el)) {
100+
return (
101+
!assignedRoles?.entity_access.some((entity) =>
102+
entity.roles.includes(el.value)
103+
) && matchesRoleContext
104+
);
105+
}
106+
return true;
107+
});
108+
}, [accountRoles, role, assignedRoles]);
69109

70110
const {
71111
control,
@@ -93,6 +133,10 @@ export const ChangeRoleForEntityDrawer = ({
93133
return getRoleByName(accountRoles, selectedOptions.value);
94134
}, [selectedOptions, accountRoles]);
95135

136+
const mutationFn = isDefaultDelegationRolesForChildAccount
137+
? updateDefaultDelegationRoles
138+
: updateUserRoles;
139+
96140
const onSubmit = async (data: { roleName: ExtendedEntityRole }) => {
97141
if (role?.role_name === data.roleName.label) {
98142
handleClose();
@@ -112,7 +156,7 @@ export const ChangeRoleForEntityDrawer = ({
112156
newRole
113157
);
114158

115-
await updateUserRoles({
159+
await mutationFn({
116160
...assignedRoles!,
117161
entity_access: updatedEntityRoles,
118162
});

packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ export const AssignedRolesTable = () => {
424424
onClose={() => setIsRemoveAssignmentDialogOpen(false)}
425425
open={isRemoveAssignmentDialogOpen}
426426
role={selectedRoleDetails}
427+
username={username}
427428
/>
428429
{filteredAndSortedRolesCount > PAGE_SIZES[0] && (
429430
<PaginationFooter

0 commit comments

Comments
 (0)