Skip to content

Commit f01ef1a

Browse files
feat: [UIE-10166] - IAM: url params for tables (#13397)
* feat: [UIE-10166] - IAM: url params for roles table * entities table + tests * cleanup * Added changeset: IAM: adds the URL params to Assigned Roles and Assigned Entities tables * small cleanup
1 parent c68e4c6 commit f01ef1a

File tree

7 files changed

+197
-75
lines changed

7 files changed

+197
-75
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Added
3+
---
4+
5+
IAM: adds the URL params to Assigned Roles and Assigned Entities tables ([#13397](https://github.com/linode/manager/pull/13397))

packages/manager/src/features/IAM/Roles/Defaults/DefaultRoles.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const loadingTestId = 'circle-progress';
1414
const queryMocks = vi.hoisted(() => ({
1515
useGetDefaultDelegationAccessQuery: vi.fn().mockReturnValue({}),
1616
useLocation: vi.fn().mockReturnValue({}),
17+
useSearch: vi.fn().mockReturnValue({}),
18+
useNavigate: vi.fn(() => vi.fn()),
1719
useIsDefaultDelegationRolesForChildAccount: vi
1820
.fn()
1921
.mockReturnValue({ isDefaultDelegationRolesForChildAccount: true }),
@@ -25,6 +27,8 @@ vi.mock('@tanstack/react-router', async () => {
2527
return {
2628
...actual,
2729
useLocation: queryMocks.useLocation,
30+
useSearch: queryMocks.useSearch,
31+
useNavigate: queryMocks.useNavigate,
2832
};
2933
});
3034

@@ -71,7 +75,6 @@ describe('DefaultRoles', () => {
7175
isLoading: false,
7276
});
7377
const { queryByTestId } = renderWithTheme(<DefaultRoles />);
74-
7578
await waitForElementToBeRemoved(queryByTestId(loadingTestId));
7679
expect(screen.getByText('Default Roles for Delegate Users')).toBeVisible();
7780
expect(screen.getByRole('table')).toBeVisible();

packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/AssignedEntitiesTable.test.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const queryMocks = vi.hoisted(() => ({
1212
useAllAccountEntities: vi.fn().mockReturnValue({}),
1313
useParams: vi.fn().mockReturnValue({}),
1414
useSearch: vi.fn().mockReturnValue({}),
15+
useNavigate: vi.fn(() => vi.fn()),
1516
useUserRoles: vi.fn().mockReturnValue({}),
1617
}));
1718

@@ -37,6 +38,7 @@ vi.mock('@tanstack/react-router', async () => {
3738
...actual,
3839
useParams: queryMocks.useParams,
3940
useSearch: queryMocks.useSearch,
41+
useNavigate: queryMocks.useNavigate,
4042
};
4143
});
4244

@@ -102,14 +104,11 @@ describe('AssignedEntitiesTable', () => {
102104
data: mockEntities,
103105
});
104106

105-
renderWithTheme(<AssignedEntitiesTable />);
107+
queryMocks.useSearch.mockReturnValue({ query: 'NonExistentRole' });
106108

107-
const searchInput = screen.getByPlaceholderText('Search');
108-
await userEvent.type(searchInput, 'NonExistentRole');
109+
renderWithTheme(<AssignedEntitiesTable />);
109110

110-
await waitFor(() => {
111-
expect(screen.getByText('No items to display.')).toBeVisible();
112-
});
111+
expect(screen.getByText('No items to display.')).toBeVisible();
113112
});
114113

115114
it('should filter roles based on search query', async () => {

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

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
} from '@linode/queries';
55
import { Select, Typography, useTheme } from '@linode/ui';
66
import Grid from '@mui/material/Grid';
7-
import { useSearch } from '@tanstack/react-router';
7+
import { useNavigate, useSearch } from '@tanstack/react-router';
88
import React from 'react';
99

1010
import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
@@ -55,38 +55,62 @@ interface Props {
5555
username?: string;
5656
}
5757

58+
const DEFAULTS_ENTITIES_URL = '/iam/roles/defaults/entity-access';
59+
const USER_ENTITIES_URL = '/iam/users/$username/entities';
60+
5861
export const AssignedEntitiesTable = ({ username }: Props) => {
5962
const theme = useTheme();
6063
const { data: permissions } = usePermissions('account', [
6164
'is_account_admin',
6265
'update_default_delegate_access',
6366
'list_entities',
6467
]);
68+
const navigate = useNavigate();
6569

6670
const { isDefaultDelegationRolesForChildAccount } =
6771
useIsDefaultDelegationRolesForChildAccount();
6872

69-
const { selectedRole: selectedRoleSearchParam } = useSearch({
70-
strict: false,
73+
const {
74+
query: queryParam,
75+
entityType: entityTypeParam,
76+
order: orderParam,
77+
selectedRole: selectedRoleSearchParam,
78+
orderBy: orderByParam,
79+
} = useSearch({
80+
from: isDefaultDelegationRolesForChildAccount
81+
? DEFAULTS_ENTITIES_URL
82+
: USER_ENTITIES_URL,
7183
});
7284

73-
const [order, setOrder] = React.useState<'asc' | 'desc'>('asc');
74-
const [orderBy, setOrderBy] = React.useState<OrderByKeys>('entity_name');
85+
const order: 'asc' | 'desc' = orderParam ?? 'asc';
86+
87+
const ORDERABLE_KEYS = ['entity_name', 'entity_type', 'role_name'] as const;
88+
const isValidOrderBy = (v: unknown): v is OrderByKeys =>
89+
ORDERABLE_KEYS.includes(v as OrderByKeys);
90+
const orderBy: OrderByKeys = isValidOrderBy(orderByParam)
91+
? orderByParam
92+
: 'entity_name';
7593

7694
const handleOrderChange = (newOrderBy: OrderByKeys) => {
77-
if (orderBy === newOrderBy) {
78-
setOrder(order === 'asc' ? 'desc' : 'asc');
79-
} else {
80-
setOrderBy(newOrderBy);
81-
setOrder('asc');
82-
}
95+
const nextOrder: 'asc' | 'desc' =
96+
orderBy === newOrderBy ? (order === 'asc' ? 'desc' : 'asc') : 'asc';
97+
navigate({
98+
to: isDefaultDelegationRolesForChildAccount
99+
? DEFAULTS_ENTITIES_URL
100+
: USER_ENTITIES_URL,
101+
params: isDefaultDelegationRolesForChildAccount
102+
? undefined
103+
: { username: username || '' },
104+
search: (prev) => ({
105+
...prev,
106+
order: nextOrder,
107+
orderBy: newOrderBy,
108+
}),
109+
});
83110
};
84111

85-
const [query, setQuery] = React.useState(selectedRoleSearchParam ?? '');
86-
87-
const [entityType, setEntityType] = React.useState<null | SelectOption>(
88-
ALL_ENTITIES_OPTION
89-
);
112+
// Use the router `query` param, falling back to `selectedRole` for initial value
113+
const appliedQuery = queryParam ?? selectedRoleSearchParam ?? '';
90114

91115
const [drawerMode, setDrawerMode] =
92116
React.useState<DrawerModes>('assign-role');
@@ -149,6 +173,14 @@ export const AssignedEntitiesTable = ({ username }: Props) => {
149173
return { filterableOptions, roles };
150174
}, [assignedRoles, entities]);
151175

176+
const selectedEntityTypeOption = React.useMemo<null | SelectOption>(() => {
177+
const value = entityTypeParam ?? ALL_ENTITIES_OPTION.value;
178+
return (
179+
filterableOptions.find((opt) => opt.value === value) ||
180+
ALL_ENTITIES_OPTION
181+
);
182+
}, [filterableOptions, entityTypeParam]);
183+
152184
const handleChangeRole = (role: EntitiesRole, mode: DrawerModes) => {
153185
setIsChangeRoleForEntityDrawerOpen(true);
154186
setSelectedRole(role);
@@ -176,9 +208,9 @@ export const AssignedEntitiesTable = ({ username }: Props) => {
176208
};
177209

178210
const filteredRoles = getFilteredRoles({
179-
entityType: entityType?.value as 'all' | EntityType,
211+
entityType: entityTypeParam ?? 'all',
180212
getSearchableFields,
181-
query,
213+
query: appliedQuery,
182214
roles,
183215
}) as EntitiesRole[];
184216

@@ -197,8 +229,8 @@ export const AssignedEntitiesTable = ({ username }: Props) => {
197229

198230
const pagination = usePaginationV2({
199231
currentRoute: isDefaultDelegationRolesForChildAccount
200-
? '/iam/roles/defaults/entity-access'
201-
: `/iam/users/$username/entities`,
232+
? DEFAULTS_ENTITIES_URL
233+
: USER_ENTITIES_URL,
202234
initialPage: 1,
203235
preferenceKey: ENTITIES_TABLE_PREFERENCE_KEY,
204236
clientSidePaginationData: filteredAndSortedRoles,
@@ -309,24 +341,50 @@ export const AssignedEntitiesTable = ({ username }: Props) => {
309341
hideLabel
310342
label="Filter"
311343
onSearch={(value) => {
312-
pagination.handlePageChange(1);
313-
setQuery(value);
344+
navigate({
345+
to: isDefaultDelegationRolesForChildAccount
346+
? DEFAULTS_ENTITIES_URL
347+
: USER_ENTITIES_URL,
348+
params:
349+
isDefaultDelegationRolesForChildAccount && !username
350+
? undefined
351+
: username,
352+
search: (prev) => ({
353+
...prev,
354+
page: 1,
355+
query: value !== '' ? value : undefined,
356+
}),
357+
});
314358
}}
315359
placeholder="Search"
316360
sx={{ height: 34 }}
317-
value={query}
361+
value={appliedQuery}
318362
/>
319363
<Select
320364
hideLabel
321365
label="Select type"
322366
onChange={(_, selected) => {
323-
pagination.handlePageChange(1);
324-
setEntityType(selected ?? null);
367+
const nextEntityType = (selected?.value ??
368+
ALL_ENTITIES_OPTION.value) as 'all' | EntityType;
369+
navigate({
370+
to: isDefaultDelegationRolesForChildAccount
371+
? DEFAULTS_ENTITIES_URL
372+
: USER_ENTITIES_URL,
373+
params:
374+
isDefaultDelegationRolesForChildAccount && !username
375+
? undefined
376+
: username,
377+
search: (prev) => ({
378+
...prev,
379+
page: 1,
380+
entityType: nextEntityType,
381+
}),
382+
});
325383
}}
326384
options={filterableOptions}
327385
placeholder="All Entities"
328386
sx={{ minWidth: 250 }}
329-
value={entityType}
387+
value={selectedEntityTypeOption}
330388
/>
331389
</Grid>
332390
<Table aria-label="Assigned Entities">

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { AssignedRolesTable } from './AssignedRolesTable';
1212
const queryMocks = vi.hoisted(() => ({
1313
useAllAccountEntities: vi.fn().mockReturnValue({}),
1414
useParams: vi.fn().mockReturnValue({}),
15+
useNavigate: vi.fn(() => vi.fn()),
16+
useSearch: vi.fn().mockReturnValue({}),
1517
useAccountRoles: vi.fn().mockReturnValue({}),
1618
useUserRoles: vi.fn().mockReturnValue({}),
1719
useGetDefaultDelegationAccessQuery: vi.fn().mockReturnValue({}),
@@ -44,6 +46,8 @@ vi.mock('@tanstack/react-router', async () => {
4446
return {
4547
...actual,
4648
useParams: queryMocks.useParams,
49+
useNavigate: queryMocks.useNavigate,
50+
useSearch: queryMocks.useSearch,
4751
};
4852
});
4953

@@ -125,14 +129,11 @@ describe('AssignedRolesTable', () => {
125129
data: mockEntities,
126130
});
127131

128-
renderWithTheme(<AssignedRolesTable />);
132+
queryMocks.useSearch.mockReturnValue({ query: 'NonExistentRole' });
129133

130-
const searchInput = screen.getByPlaceholderText('Search');
131-
await userEvent.type(searchInput, 'NonExistentRole');
134+
renderWithTheme(<AssignedRolesTable />);
132135

133-
await waitFor(() => {
134-
expect(screen.getByText('No items to display.')).toBeVisible();
135-
});
136+
expect(screen.getByText('No items to display.')).toBeVisible();
136137
});
137138

138139
it('should filter roles based on search query', async () => {
@@ -150,8 +151,7 @@ describe('AssignedRolesTable', () => {
150151

151152
renderWithTheme(<AssignedRolesTable />);
152153

153-
const searchInput = screen.getByPlaceholderText('Search');
154-
await userEvent.type(searchInput, 'account_linode_admin');
154+
queryMocks.useSearch.mockReturnValue({ query: 'account_linode_admin' });
155155

156156
await waitFor(() => {
157157
expect(screen.queryByText('account_linode_admin')).toBeVisible();
@@ -173,9 +173,7 @@ describe('AssignedRolesTable', () => {
173173

174174
renderWithTheme(<AssignedRolesTable />);
175175

176-
const autocomplete = screen.getByPlaceholderText('All Assigned Roles');
177-
await userEvent.type(autocomplete, 'Firewall Roles');
178-
176+
queryMocks.useSearch.mockReturnValue({ roleType: 'firewall' });
179177
await waitFor(() => {
180178
expect(screen.queryByText('account_firewall_creator')).toBeVisible();
181179
});

0 commit comments

Comments
 (0)