Skip to content

Commit a421736

Browse files
refactor: [M3-9764] - Application Error Boundaries (#12024)
* Initial commit - save progress * Add zero-state icon * match design * testing * cleanup * Added changeset: Application Error Boundaries * feedback @bnussman-akamai @coliu-akamai @carrillo-erik * small responsive fix * oops test
1 parent 6ae014b commit a421736

13 files changed

Lines changed: 266 additions & 89 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Changed
3+
---
4+
5+
Application Error Boundaries ([#12024](https://github.com/linode/manager/pull/12024))

packages/manager/eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ export const baseConfig = [
333333
],
334334
];
335335
}
336+
if (rule === 'prefer-explicit-assert') {
337+
return [`testing-library/${rule}`, 'off'];
338+
}
336339
// All other rules just get set to warn
337340
return [`testing-library/${rule}`, 'warn'];
338341
})

packages/manager/src/App.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import '@reach/tabs/styles.css';
2-
import { ErrorBoundary } from '@sentry/react';
32
import * as React from 'react';
43

54
import {
65
DocumentTitleSegment,
76
withDocumentTitleProvider,
87
} from 'src/components/DocumentTitle';
98
import withFeatureFlagProvider from 'src/containers/withFeatureFlagProvider.container';
10-
import TheApplicationIsOnFire from 'src/features/TheApplicationIsOnFire';
9+
import { ErrorBoundaryFallback } from 'src/features/ErrorBoundary/ErrorBoundaryFallback';
1110

1211
import { SplashScreen } from './components/SplashScreen';
1312
import { GoTo } from './GoTo';
@@ -34,7 +33,7 @@ const BaseApp = withDocumentTitleProvider(
3433
}
3534

3635
return (
37-
<ErrorBoundary fallback={<TheApplicationIsOnFire />}>
36+
<ErrorBoundaryFallback>
3837
{/** Accessibility helper */}
3938
<a className="skip-link" href="#main-content">
4039
Skip to main content
@@ -54,7 +53,7 @@ const BaseApp = withDocumentTitleProvider(
5453
*/}
5554
<MainContent />
5655
<GlobalListeners />
57-
</ErrorBoundary>
56+
</ErrorBoundaryFallback>
5857
);
5958
})
6059
);

packages/manager/src/MainContent.tsx

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { SideMenu } from 'src/components/PrimaryNav/SideMenu';
2424
import { SuspenseLoader } from 'src/components/SuspenseLoader';
2525
import { useDialogContext } from 'src/context/useDialogContext';
26+
import { ErrorBoundaryFallback } from 'src/features/ErrorBoundary/ErrorBoundaryFallback';
2627
import { Footer } from 'src/features/Footer';
2728
import { GlobalNotifications } from 'src/features/GlobalNotifications/GlobalNotifications';
2829
import {
@@ -325,6 +326,9 @@ export const MainContent = () => {
325326
>
326327
<MainContentBanner />
327328
<Box
329+
component="main"
330+
id="main-content"
331+
role="main"
328332
sx={(theme) => ({
329333
flex: 1,
330334
margin: '0 auto',
@@ -347,9 +351,6 @@ export const MainContent = () => {
347351
: SIDEBAR_WIDTH
348352
}px)`,
349353
})}
350-
component="main"
351-
id="main-content"
352-
role="main"
353354
>
354355
<Grid
355356
className={classes.grid}
@@ -360,48 +361,59 @@ export const MainContent = () => {
360361
<Grid className={cx(classes.switchWrapper, 'p0')}>
361362
<GlobalNotifications />
362363
<React.Suspense fallback={<SuspenseLoader />}>
363-
<Switch>
364-
<Route component={LinodesRoutes} path="/linodes" />
365-
<Route component={Kubernetes} path="/kubernetes" />
366-
{isIAMEnabled && (
367-
<Route component={IAM} path="/iam" />
368-
)}
369-
<Route component={Account} path="/account" />
370-
<Route component={Profile} path="/profile" />
371-
<Route component={Help} path="/support" />
372-
<Route component={SearchLanding} path="/search" />
373-
<Route component={EventsLanding} path="/events" />
374-
{isDatabasesEnabled && (
375-
<Route component={Databases} path="/databases" />
376-
)}
377-
{isACLPEnabled && (
364+
<ErrorBoundaryFallback>
365+
<Switch>
378366
<Route
379-
component={CloudPulseMetrics}
380-
path="/metrics"
367+
component={LinodesRoutes}
368+
path="/linodes"
381369
/>
382-
)}
383-
{isACLPEnabled && (
384370
<Route
385-
component={CloudPulseAlerts}
386-
path="/alerts"
387-
/>
388-
)}
389-
<Redirect exact from="/" to={defaultRoot} />
390-
{/** We don't want to break any bookmarks. This can probably be removed eventually. */}
391-
<Redirect from="/dashboard" to={defaultRoot} />
392-
{/**
393-
* This is the catch all routes that allows TanStack Router to take over.
394-
* When a route is not found here, it will be handled by the migration router, which in turns handles the NotFound component.
395-
* It is currently set to the migration router in order to incrementally migrate the app to the new routing.
396-
* This is a temporary solution until we are ready to fully migrate to TanStack Router.
397-
*/}
398-
<Route path="*">
399-
<RouterProvider
400-
context={{ queryClient }}
401-
router={migrationRouter as AnyRouter}
371+
component={Kubernetes}
372+
path="/kubernetes"
402373
/>
403-
</Route>
404-
</Switch>
374+
{isIAMEnabled && (
375+
<Route component={IAM} path="/iam" />
376+
)}
377+
<Route component={Account} path="/account" />
378+
<Route component={Profile} path="/profile" />
379+
<Route component={Help} path="/support" />
380+
<Route component={SearchLanding} path="/search" />
381+
<Route component={EventsLanding} path="/events" />
382+
{isDatabasesEnabled && (
383+
<Route
384+
component={Databases}
385+
path="/databases"
386+
/>
387+
)}
388+
{isACLPEnabled && (
389+
<Route
390+
component={CloudPulseMetrics}
391+
path="/metrics"
392+
/>
393+
)}
394+
{isACLPEnabled && (
395+
<Route
396+
component={CloudPulseAlerts}
397+
path="/alerts"
398+
/>
399+
)}
400+
<Redirect exact from="/" to={defaultRoot} />
401+
{/** We don't want to break any bookmarks. This can probably be removed eventually. */}
402+
<Redirect from="/dashboard" to={defaultRoot} />
403+
{/**
404+
* This is the catch all routes that allows TanStack Router to take over.
405+
* When a route is not found here, it will be handled by the migration router, which in turns handles the NotFound component.
406+
* It is currently set to the migration router in order to incrementally migrate the app to the new routing.
407+
* This is a temporary solution until we are ready to fully migrate to TanStack Router.
408+
*/}
409+
<Route path="*">
410+
<RouterProvider
411+
context={{ queryClient }}
412+
router={migrationRouter as AnyRouter}
413+
/>
414+
</Route>
415+
</Switch>
416+
</ErrorBoundaryFallback>
405417
</React.Suspense>
406418
</Grid>
407419
</Grid>

packages/manager/src/Router.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useGlobalErrors } from 'src/hooks/useGlobalErrors';
77

88
import { useIsACLPEnabled } from './features/CloudPulse/Utils/utils';
99
import { useIsDatabasesEnabled } from './features/Databases/utilities';
10+
import { ErrorBoundaryFallback } from './features/ErrorBoundary/ErrorBoundaryFallback';
1011
import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils';
1112
import { router } from './routes';
1213

@@ -29,5 +30,9 @@ export const Router = () => {
2930
},
3031
});
3132

32-
return <RouterProvider router={router} />;
33+
return (
34+
<ErrorBoundaryFallback useTanStackRouterBoundary={true}>
35+
<RouterProvider router={router} />
36+
</ErrorBoundaryFallback>
37+
);
3338
};
Lines changed: 10 additions & 0 deletions
Loading

packages/manager/src/components/CodeBlock/CodeBlock.styles.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ export const useCodeBlockStyles = makeStyles()((theme) => ({
66
borderRadius: theme.spacing(0.5),
77
overflowX: 'auto',
88
padding: theme.spacing(1.5),
9+
paddingRight: '40px',
910
},
1011
position: 'relative',
1112
},
1213
copyIcon: {
1314
position: 'absolute',
14-
right: `${theme.spacing(1.5)}`,
15+
right: 0,
16+
paddingRight: `${theme.spacing(1)}`,
1517
top: `${theme.spacing(1)}`,
18+
backgroundColor: theme.tokens.alias.Background.Neutral,
1619
},
1720
lineNumbers: {
1821
code: {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import * as React from 'react';
4+
5+
import { renderWithTheme } from 'src/utilities/testHelpers';
6+
7+
import { ErrorBoundaryFallback } from './ErrorBoundaryFallback';
8+
9+
describe('ErrorBoundaryFallback', () => {
10+
it('should render the ErrorComponent when an error is thrown', async () => {
11+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
12+
13+
const { location } = window;
14+
window.location = { ...location, reload: vi.fn() };
15+
16+
const ErrorComponent = () => {
17+
throw new Error('Test error for error boundary');
18+
};
19+
20+
renderWithTheme(
21+
<ErrorBoundaryFallback>
22+
<ErrorComponent />
23+
</ErrorBoundaryFallback>
24+
);
25+
26+
screen.getByText('Something went wrong');
27+
screen.getByText(
28+
'Please try the following steps that may help resolve the issue:'
29+
);
30+
screen.getByText('Update your browser version');
31+
screen.getByText('Clear your cookies');
32+
screen.getByText('Check your internet connection');
33+
screen.getByText('Resources:');
34+
screen.getByText('Clearing cache and cookies in a browser');
35+
screen.getByText('Akamai Compute Support');
36+
37+
expect(consoleSpy).toHaveBeenCalledTimes(3);
38+
39+
const refreshButton = screen.getByText('Refresh application');
40+
const reloadButton = screen.getByText('Reload page');
41+
42+
expect(refreshButton).toBeInTheDocument();
43+
expect(reloadButton).toBeInTheDocument();
44+
45+
await userEvent.click(reloadButton);
46+
expect(window.location.reload).toHaveBeenCalled();
47+
48+
consoleSpy.mockRestore();
49+
window.location = location;
50+
});
51+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ErrorBoundary } from '@sentry/react';
2+
import { CatchBoundary } from '@tanstack/react-router';
3+
import * as React from 'react';
4+
5+
import { ErrorComponent } from './ErrorComponent';
6+
7+
export const ErrorBoundaryFallback: React.FC<{
8+
children?: React.ReactNode;
9+
useTanStackRouterBoundary?: boolean;
10+
}> = ({ children, useTanStackRouterBoundary = false }) => (
11+
<ErrorBoundary fallback={ErrorComponent}>
12+
{useTanStackRouterBoundary ? (
13+
<CatchBoundary getResetKey={() => 'error-boundary-fallback'}>
14+
{children}
15+
</CatchBoundary>
16+
) : (
17+
children
18+
)}
19+
</ErrorBoundary>
20+
);

0 commit comments

Comments
 (0)