Skip to content

Commit f66b452

Browse files
feat(ui-react-storage): allow custom error boundary (#6408)
* feat(ui-react-storage): allow custom error boundary * feat: allow functional error boundary and null to disable --------- Co-authored-by: Danny Banks <djb@amazon.com>
1 parent 3859cc4 commit f66b452

File tree

9 files changed

+151
-13
lines changed

9 files changed

+151
-13
lines changed

examples/next/pages/ui/components/storage/storage-browser/custom-actions/index.page.tsx

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
import React from 'react';
2-
32
import { createStorageBrowser } from '@aws-amplify/ui-react-storage/browser';
4-
5-
import { Flex } from '@aws-amplify/ui-react';
3+
import { Flex, Message } from '@aws-amplify/ui-react';
4+
import './styles.css';
65

76
import '@aws-amplify/ui-react-storage/styles.css';
87

9-
const { StorageBrowser } = createStorageBrowser({
8+
class CustomErrorBoundary extends React.Component<React.PropsWithChildren> {
9+
state = {
10+
hasError: false,
11+
};
12+
13+
static getDerivedStateFromError() {
14+
return { hasError: true };
15+
}
16+
17+
render() {
18+
if (this.state.hasError) {
19+
return (
20+
<Message variation="outlined" colorTheme="error">
21+
Oops. An unexpected error has happened.
22+
</Message>
23+
);
24+
}
25+
26+
return this.props.children;
27+
}
28+
}
29+
30+
const { StorageBrowser, useView } = createStorageBrowser({
1031
actions: {
1132
default: {
1233
copy: {
@@ -83,7 +104,24 @@ const { StorageBrowser } = createStorageBrowser({
83104
nextToken: undefined,
84105
}),
85106
},
107+
custom: {
108+
mockRuntimeError: {
109+
actionListItem: {
110+
icon: 'error',
111+
label: 'Mock unexpected error',
112+
},
113+
// Not used but to keep ts happy
114+
handler: () => ({
115+
result: Promise.resolve({
116+
status: 'COMPLETE',
117+
value: { key: 'trigger-runtime-error' },
118+
}),
119+
}),
120+
viewName: 'LocationDetailView',
121+
},
122+
},
86123
},
124+
ErrorBoundary: CustomErrorBoundary,
87125
config: {
88126
getLocationCredentials: () =>
89127
Promise.resolve({
@@ -112,6 +150,16 @@ const { StorageBrowser } = createStorageBrowser({
112150
},
113151
});
114152

153+
function LocationDetailViewWithExpectedError() {
154+
const { actionType } = useView('LocationDetail');
155+
156+
if (actionType === 'mockRuntimeError') {
157+
throw new Error('Unexpected Error');
158+
}
159+
160+
return <StorageBrowser.LocationDetailView />;
161+
}
162+
115163
function Example() {
116164
return (
117165
<Flex
@@ -121,7 +169,11 @@ function Example() {
121169
overflow="hidden"
122170
padding="xl"
123171
>
124-
<StorageBrowser />
172+
<StorageBrowser
173+
views={{
174+
LocationDetailView: LocationDetailViewWithExpectedError,
175+
}}
176+
/>
125177
</Flex>
126178
);
127179
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/* Intentionally to test error boundary. No effect in prod mode. */
2+
nextjs-portal {
3+
display: none;
4+
}

packages/e2e/cypress/integration/common/shared.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ Given(
231231
}
232232
);
233233

234+
Given('I expect an exception', () => {
235+
Cypress.on('uncaught:exception', () => {
236+
return false;
237+
});
238+
});
239+
234240
When('Sign in was called with {string}', (username: string) => {
235241
let tempStub = stub.calledWith(username, Cypress.env('VALID_PASSWORD'));
236242
stub = null;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Feature: Monitoring errors
2+
3+
Background:
4+
Given I'm running the example "ui/components/storage/storage-browser/custom-actions"
5+
6+
@react
7+
Scenario: Shows custom error boundary when unexpected error happens
8+
Given I expect an exception
9+
When I click the first button containing "my-prefix"
10+
Then I see the "Menu Toggle" button
11+
When I click the "Menu Toggle" button
12+
Then I see the "Mock unexpected error" menuitem
13+
When I click the "Mock unexpected error" menuitem
14+
Then I see "An unexpected error has happened."

packages/react-storage/src/components/StorageBrowser/ErrorBoundary/ErrorBoundary.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import React from 'react';
22

33
import { STORAGE_BROWSER_BLOCK_TO_BE_UPDATED } from '../constants';
44

5-
interface ErrorBoundaryProps {
6-
children: React.ReactNode;
7-
}
8-
95
interface ErrorBoundaryState {
106
hasError: boolean;
117
}
@@ -18,11 +14,13 @@ const Fallback = (): React.JSX.Element => (
1814
</div>
1915
);
2016

17+
export type ErrorBoundaryType = React.ComponentType<React.PropsWithChildren>;
18+
2119
export class ErrorBoundary extends React.Component<
22-
ErrorBoundaryProps,
20+
React.PropsWithChildren,
2321
ErrorBoundaryState
2422
> {
25-
constructor(props: ErrorBoundaryProps) {
23+
constructor(props: React.PropsWithChildren) {
2624
super(props);
2725
this.state = { hasError: false };
2826
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { ErrorBoundary } from './ErrorBoundary';
1+
export { ErrorBoundary, ErrorBoundaryType } from './ErrorBoundary';

packages/react-storage/src/components/StorageBrowser/__tests__/createStorageBrowser.spec.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,57 @@ describe('createStorageBrowser', () => {
9999
version: expect.any(String),
100100
});
101101
});
102+
103+
it('should accept custom error boundary', async () => {
104+
class CustomErrorBoundary extends React.Component<React.PropsWithChildren> {
105+
constructor(props: React.PropsWithChildren) {
106+
super(props);
107+
}
108+
109+
render() {
110+
const { children } = this.props;
111+
return (
112+
<div>
113+
<p>Custom Error Boundary</p>
114+
{children}
115+
</div>
116+
);
117+
}
118+
}
119+
120+
const { StorageBrowser } = createStorageBrowser({
121+
config: input.config,
122+
ErrorBoundary: CustomErrorBoundary,
123+
});
124+
125+
await waitFor(() => {
126+
render(<StorageBrowser />);
127+
});
128+
129+
expect(screen.getByText('Custom Error Boundary')).toBeInTheDocument();
130+
});
131+
132+
it('should support disabling error boundary', () => {
133+
const { StorageBrowser } = createStorageBrowser({
134+
config: input.config,
135+
ErrorBoundary: null,
136+
});
137+
138+
const LocationsViewWithError = () => {
139+
React.useEffect(() => {
140+
throw new Error('Unexpected Error');
141+
}, []);
142+
return <StorageBrowser.LocationsView />;
143+
};
144+
145+
expect(() => {
146+
render(
147+
<StorageBrowser
148+
views={{
149+
LocationsView: LocationsViewWithError,
150+
}}
151+
/>
152+
);
153+
}).toThrow('Unexpected Error');
154+
});
102155
});

packages/react-storage/src/components/StorageBrowser/createStorageBrowser.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { elementsDefault } from './context/elements';
1515
import { ComponentsProvider } from './ComponentsProvider';
1616
import { componentsDefault } from './componentsDefault';
1717
import { DisplayTextProvider } from './displayText';
18-
import { ErrorBoundary } from './ErrorBoundary';
18+
import { ErrorBoundary as DefaultErrorBoundary } from './ErrorBoundary';
1919
import { createConfigurationProvider, StoreProvider } from './providers';
2020
import { StorageBrowserDefault } from './StorageBrowserDefault';
2121
import {
@@ -131,6 +131,11 @@ export function createStorageBrowser<
131131
);
132132
}
133133

134+
const ErrorBoundary =
135+
input.ErrorBoundary === null
136+
? React.Fragment
137+
: input.ErrorBoundary ?? DefaultErrorBoundary;
138+
134139
const StorageBrowser: StorageBrowserType<
135140
DerivedActionViewType<RInput>,
136141
DerivedActionViews<RInput>

packages/react-storage/src/components/StorageBrowser/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from './actions';
99
import { GetLocationCredentials } from './credentials/types';
1010
import { Components } from './ComponentsProvider';
11+
import { ErrorBoundaryType } from './ErrorBoundary';
1112
import { RegisterAuthListener, StoreProviderProps } from './providers';
1213

1314
import {
@@ -44,6 +45,11 @@ export interface CreateStorageBrowserInput {
4445
actions?: StorageBrowserActions;
4546
config: Config;
4647
components?: Components;
48+
/**
49+
* Custom ErrorBoundary class. If omitted, a default ErrorBoundary is provided.
50+
* To disable ErrorBoundary, set to `null`.
51+
*/
52+
ErrorBoundary?: ErrorBoundaryType | null;
4753
}
4854

4955
export interface StorageBrowserProps<K = string, V = {}> {

0 commit comments

Comments
 (0)