Skip to content

Commit fdbaeb9

Browse files
Tech Stories - [M3-10021]: Reroute IAM (linode#12312)
* intitial commit - save progress * replace utils * fix units * missing usePagination instances * missing unit * another missing unit * fix user roles filtering * Added changeset: Reroute IAM * some redirect feedback * feedback @kwojtowiakamai * feedback @kwojtowiakamai * feedback @kwojtowiakamai
1 parent 7b03972 commit fdbaeb9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+650
-333
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Tech Stories
3+
---
4+
5+
Reroute IAM ([#12312](https://github.com/linode/manager/pull/12312))

packages/manager/eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ export const baseConfig = [
413413
'src/features/Events/**/*',
414414
'src/features/Firewalls/**/*',
415415
'src/features/Help/**/*',
416+
'src/features/IAM/**/*',
416417
'src/features/Images/**/*',
417418
'src/features/Kubernetes/**/*',
418419
'src/features/Longview/**/*',

packages/manager/src/MainContent.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import { ENABLE_MAINTENANCE_MODE } from './constants';
3636
import { complianceUpdateContext } from './context/complianceUpdateContext';
3737
import { sessionExpirationContext } from './context/sessionExpirationContext';
3838
import { switchAccountSessionContext } from './context/switchAccountSessionContext';
39-
import { useIsIAMEnabled } from './features/IAM/hooks/useIsIAMEnabled';
4039
import { TOPMENU_HEIGHT } from './features/TopMenu/constants';
4140
import { useGlobalErrors } from './hooks/useGlobalErrors';
4241
import { migrationRouter } from './routes';
@@ -112,12 +111,6 @@ const LinodesRoutes = React.lazy(() =>
112111
}))
113112
);
114113

115-
const IAM = React.lazy(() =>
116-
import('src/features/IAM').then((module) => ({
117-
default: module.IdentityAccessManagement,
118-
}))
119-
);
120-
121114
export const MainContent = () => {
122115
const contentRef = React.useRef<HTMLDivElement>(null);
123116
const { classes, cx } = useStyles();
@@ -153,8 +146,6 @@ export const MainContent = () => {
153146
const { data: accountSettings } = useAccountSettings();
154147
const defaultRoot = accountSettings?.managed ? '/managed' : '/linodes';
155148

156-
const { isIAMEnabled } = useIsIAMEnabled();
157-
158149
const isNarrowViewport = useMediaQuery((theme: Theme) =>
159150
theme.breakpoints.down(960)
160151
);
@@ -286,9 +277,6 @@ export const MainContent = () => {
286277
component={LinodesRoutes}
287278
path="/linodes"
288279
/>
289-
{isIAMEnabled && (
290-
<Route component={IAM} path="/iam" />
291-
)}
292280
<Redirect exact from="/" to={defaultRoot} />
293281
{/** We don't want to break any bookmarks. This can probably be removed eventually. */}
294282
<Redirect from="/dashboard" to={defaultRoot} />

packages/manager/src/dev-tools/ServiceWorkerTool.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export const ServiceWorkerTool = () => {
139139
JSON.stringify(currentNotificationsData) !==
140140
JSON.stringify(customNotificationsData);
141141

142-
const hasCustomUserAccountPermissionsChanges =
142+
const hasCustomUserAccountPermissionsChanges =
143143
JSON.stringify(currentUserAccountPermissionsData) !==
144144
JSON.stringify(customUserAccountPermissionsData);
145145
const hasCustomUserEntityPermissionsChanges =

packages/manager/src/features/IAM/IAMLanding.tsx

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import * as React from 'react';
2-
import { matchPath, useHistory, useLocation } from 'react-router-dom';
32

4-
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
53
import { LandingHeader } from 'src/components/LandingHeader';
64
import { SuspenseLoader } from 'src/components/SuspenseLoader';
75
import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
8-
import { TabLinkList } from 'src/components/Tabs/TabLinkList';
96
import { TabPanels } from 'src/components/Tabs/TabPanels';
107
import { Tabs } from 'src/components/Tabs/Tabs';
8+
import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList';
9+
import { useTabs } from 'src/hooks/useTabs';
1110

1211
import { IAM_DOCS_LINK } from './Shared/constants';
1312

@@ -24,31 +23,16 @@ const Roles = React.lazy(() =>
2423
);
2524

2625
export const IdentityAccessLanding = React.memo(() => {
27-
const history = useHistory();
28-
const location = useLocation();
29-
30-
const tabs = [
26+
const { tabs, tabIndex, handleTabChange } = useTabs([
3127
{
32-
routeName: `/iam/users`,
28+
to: `/iam/users`,
3329
title: 'Users',
3430
},
3531
{
36-
routeName: `/iam/roles`,
32+
to: `/iam/roles`,
3733
title: 'Roles',
3834
},
39-
];
40-
41-
const navToURL = (index: number) => {
42-
history.push(tabs[index].routeName);
43-
};
44-
45-
const getDefaultTabIndex = () => {
46-
return (
47-
tabs.findIndex((tab) =>
48-
Boolean(matchPath(tab.routeName, { path: location.pathname }))
49-
) || 0
50-
);
51-
};
35+
]);
5236

5337
const landingHeaderProps = {
5438
breadcrumbProps: {
@@ -63,11 +47,9 @@ export const IdentityAccessLanding = React.memo(() => {
6347

6448
return (
6549
<>
66-
<DocumentTitleSegment segment="Identity and Access" />
6750
<LandingHeader {...landingHeaderProps} spacingBottom={4} />
68-
69-
<Tabs index={getDefaultTabIndex()} onChange={navToURL}>
70-
<TabLinkList tabs={tabs} />
51+
<Tabs index={tabIndex} onChange={handleTabChange}>
52+
<TanStackTabLinkList tabs={tabs} />
7153

7254
<React.Suspense fallback={<SuspenseLoader />}>
7355
<TabPanels>

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { screen } from '@testing-library/react';
22
import React from 'react';
33

44
import { accountRolesFactory } from 'src/factories/accountRoles';
5-
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
import { renderWithThemeAndRouter } from 'src/utilities/testHelpers';
66

77
import { RolesLanding } from './Roles';
88

@@ -33,25 +33,25 @@ beforeEach(() => {
3333
});
3434

3535
describe('RolesLanding', () => {
36-
it('renders loading state when permissions are loading', () => {
36+
it('renders loading state when permissions are loading', async () => {
3737
queryMocks.useAccountRoles.mockReturnValue({
3838
data: null,
3939
isLoading: true,
4040
});
4141

42-
renderWithTheme(<RolesLanding />);
42+
await renderWithThemeAndRouter(<RolesLanding />);
4343

4444
expect(screen.getByRole('progressbar')).toBeInTheDocument();
4545
});
4646

47-
it('renders roles table when permissions are loaded', () => {
47+
it('renders roles table when permissions are loaded', async () => {
4848
const mockPermissions = accountRolesFactory.build();
4949
queryMocks.useAccountRoles.mockReturnValue({
5050
data: mockPermissions,
5151
isLoading: false,
5252
});
5353

54-
renderWithTheme(<RolesLanding />);
54+
await renderWithThemeAndRouter(<RolesLanding />);
5555
// RolesTable has a textbox at the top
5656
expect(screen.getByRole('textbox')).toBeInTheDocument();
5757
});

packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.test.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fireEvent, screen, waitFor } from '@testing-library/react';
22
import React from 'react';
33

4-
import { renderWithTheme } from 'src/utilities/testHelpers';
4+
import { renderWithThemeAndRouter } from 'src/utilities/testHelpers';
55

66
import { RolesTable } from './RolesTable';
77

@@ -43,27 +43,26 @@ beforeEach(() => {
4343
});
4444

4545
describe('RolesTable', () => {
46-
it('renders no roles when roles array is empty', () => {
47-
const { getByText, getByTestId } = renderWithTheme(
46+
it('renders no roles when roles array is empty', async () => {
47+
const { getByText, getByTestId } = await renderWithThemeAndRouter(
4848
<RolesTable roles={[]} />
4949
);
5050

5151
expect(getByTestId('roles-table')).toBeInTheDocument();
5252
expect(getByText('No items to display.')).toBeInTheDocument();
5353
});
5454

55-
it('renders roles correctly when roles array is provided', () => {
56-
const { getByText, getByTestId, getAllByRole } = renderWithTheme(
57-
<RolesTable roles={mockRoles} />
58-
);
55+
it('renders roles correctly when roles array is provided', async () => {
56+
const { getByText, getByTestId, getAllByRole } =
57+
await renderWithThemeAndRouter(<RolesTable roles={mockRoles} />);
5958

6059
expect(getByTestId('roles-table')).toBeInTheDocument();
6160
expect(getAllByRole('combobox').length).toEqual(1);
6261
expect(getByText('Account volume admin')).toBeInTheDocument();
6362
});
6463

6564
it('filters roles to warranted results based on search input', async () => {
66-
renderWithTheme(<RolesTable roles={mockRoles} />);
65+
await renderWithThemeAndRouter(<RolesTable roles={mockRoles} />);
6766
const searchInput: HTMLInputElement = screen.getByPlaceholderText('Search');
6867
fireEvent.change(searchInput, { target: { value: 'Account' } });
6968

@@ -78,7 +77,7 @@ describe('RolesTable', () => {
7877
});
7978

8079
it('filters roles to no results based on search input if warranted', async () => {
81-
renderWithTheme(<RolesTable roles={mockRoles} />);
80+
await renderWithThemeAndRouter(<RolesTable roles={mockRoles} />);
8281

8382
const searchInput: HTMLInputElement = screen.getByPlaceholderText('Search');
8483
fireEvent.change(searchInput, {

packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
getFacadeRoleDescription,
2525
mapEntityTypesForSelect,
2626
} from 'src/features/IAM/Shared/utilities';
27-
import { usePagination } from 'src/hooks/usePagination';
27+
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
2828

2929
import { ROLES_TABLE_PREFERENCE_KEY } from '../../Shared/constants';
3030

@@ -52,7 +52,11 @@ export const RolesTable = ({ roles = [] }: Props) => {
5252
const [selectedRows, setSelectedRows] = useState<RoleView[]>([]);
5353
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
5454

55-
const pagination = usePagination(1, ROLES_TABLE_PREFERENCE_KEY);
55+
const pagination = usePaginationV2({
56+
currentRoute: '/iam/roles',
57+
initialPage: 1,
58+
preferenceKey: ROLES_TABLE_PREFERENCE_KEY,
59+
});
5660

5761
// Filtering
5862
const getFilteredRows = (

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { accountEntityFactory } from 'src/factories/accountEntities';
66
import { accountRolesFactory } from 'src/factories/accountRoles';
77
import { userRolesFactory } from 'src/factories/userRoles';
88
import { makeResourcePage } from 'src/mocks/serverHandlers';
9-
import { renderWithTheme } from 'src/utilities/testHelpers';
9+
import { renderWithThemeAndRouter } from 'src/utilities/testHelpers';
1010

1111
import { AssignedRolesTable } from './AssignedRolesTable';
1212

1313
const queryMocks = vi.hoisted(() => ({
1414
useAccountEntities: vi.fn().mockReturnValue({}),
15+
useParams: vi.fn().mockReturnValue({}),
1516
useAccountRoles: vi.fn().mockReturnValue({}),
1617
useUserRoles: vi.fn().mockReturnValue({}),
1718
}));
@@ -33,6 +34,14 @@ vi.mock('src/queries/entities/entities', async () => {
3334
};
3435
});
3536

37+
vi.mock('@tanstack/react-router', async () => {
38+
const actual = await vi.importActual('@tanstack/react-router');
39+
return {
40+
...actual,
41+
useParams: queryMocks.useParams,
42+
};
43+
});
44+
3645
const mockEntities = [
3746
accountEntityFactory.build({
3847
id: 7,
@@ -46,12 +55,18 @@ const mockEntities = [
4655
];
4756

4857
describe('AssignedRolesTable', () => {
58+
beforeEach(() => {
59+
queryMocks.useParams.mockReturnValue({
60+
username: 'test_user',
61+
});
62+
});
63+
4964
it('should display no roles text if there are no roles assigned to user', async () => {
5065
queryMocks.useUserRoles.mockReturnValue({
5166
data: {},
5267
});
5368

54-
renderWithTheme(<AssignedRolesTable />);
69+
await renderWithThemeAndRouter(<AssignedRolesTable />);
5570

5671
expect(screen.getByText('No items to display.')).toBeVisible();
5772
});
@@ -69,7 +84,7 @@ describe('AssignedRolesTable', () => {
6984
data: makeResourcePage(mockEntities),
7085
});
7186

72-
renderWithTheme(<AssignedRolesTable />);
87+
await renderWithThemeAndRouter(<AssignedRolesTable />);
7388

7489
expect(screen.getByText('account_linode_admin')).toBeVisible();
7590
expect(screen.getAllByText('All Linodes')[0]).toBeVisible();
@@ -97,7 +112,7 @@ describe('AssignedRolesTable', () => {
97112
data: makeResourcePage(mockEntities),
98113
});
99114

100-
renderWithTheme(<AssignedRolesTable />);
115+
await renderWithThemeAndRouter(<AssignedRolesTable />);
101116

102117
const searchInput = screen.getByPlaceholderText('Search');
103118
await userEvent.type(searchInput, 'NonExistentRole');
@@ -120,7 +135,7 @@ describe('AssignedRolesTable', () => {
120135
data: makeResourcePage(mockEntities),
121136
});
122137

123-
renderWithTheme(<AssignedRolesTable />);
138+
await renderWithThemeAndRouter(<AssignedRolesTable />);
124139

125140
const searchInput = screen.getByPlaceholderText('Search');
126141
await userEvent.type(searchInput, 'account_linode_admin');
@@ -143,7 +158,7 @@ describe('AssignedRolesTable', () => {
143158
data: makeResourcePage(mockEntities),
144159
});
145160

146-
renderWithTheme(<AssignedRolesTable />);
161+
await renderWithThemeAndRouter(<AssignedRolesTable />);
147162

148163
const autocomplete = screen.getByPlaceholderText('All Assigned Roles');
149164
await userEvent.type(autocomplete, 'Firewall Roles');

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Button, CircleProgress, Select, Typography } from '@linode/ui';
22
import { useTheme } from '@mui/material';
33
import Grid from '@mui/material/Grid';
4+
import { useNavigate, useParams } from '@tanstack/react-router';
45
import React from 'react';
5-
import { useHistory, useParams } from 'react-router-dom';
66

77
import { CollapsibleTable } from 'src/components/CollapsibleTable/CollapsibleTable';
88
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
@@ -13,7 +13,7 @@ import { TableCell } from 'src/components/TableCell';
1313
import { TableRow } from 'src/components/TableRow';
1414
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
1515
import { TableSortCell } from 'src/components/TableSortCell/TableSortCell';
16-
import { usePagination } from 'src/hooks/usePagination';
16+
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
1717
import { useAccountEntities } from 'src/queries/entities/entities';
1818
import { useAccountRoles, useUserRoles } from 'src/queries/iam/iam';
1919

@@ -63,15 +63,19 @@ const ALL_ROLES_OPTION: SelectOption = {
6363
};
6464

6565
export const AssignedRolesTable = () => {
66-
const { username } = useParams<{ username: string }>();
67-
const history = useHistory();
66+
const { username } = useParams({ from: '/iam/users/$username' });
67+
const navigate = useNavigate();
6868
const theme = useTheme();
6969

7070
const [order, setOrder] = React.useState<'asc' | 'desc'>('asc');
7171
const [orderBy, setOrderBy] = React.useState<OrderByKeys>('name');
7272
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
7373

74-
const pagination = usePagination(1, ASSIGNED_ROLES_TABLE_PREFERENCE_KEY);
74+
const pagination = usePaginationV2({
75+
currentRoute: '/iam/users/$username/roles',
76+
initialPage: 1,
77+
preferenceKey: ASSIGNED_ROLES_TABLE_PREFERENCE_KEY,
78+
});
7579

7680
const handleOrderChange = (newOrderBy: OrderByKeys) => {
7781
if (orderBy === newOrderBy) {
@@ -163,9 +167,10 @@ export const AssignedRolesTable = () => {
163167
roleName: AccountAccessRole | EntityAccessRole
164168
) => {
165169
const selectedRole = roleName;
166-
history.push({
167-
pathname: `/iam/users/${username}/entities`,
168-
state: { selectedRole },
170+
navigate({
171+
to: '/iam/users/$username/entities',
172+
params: { username },
173+
search: { selectedRole },
169174
});
170175
};
171176

0 commit comments

Comments
 (0)