Skip to content

Commit df1bd58

Browse files
committed
poc
1 parent a0595cc commit df1bd58

File tree

23 files changed

+740
-119
lines changed

23 files changed

+740
-119
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
{
168168
"path": "lib/components/internal/widget-exports.js",
169169
"brotli": false,
170-
"limit": "810 kB",
170+
"limit": "840 kB",
171171
"ignore": "react-dom"
172172
}
173173
],

pages/app/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function isAppLayoutPage(pageId?: string) {
4242
'prompt-input/simple',
4343
'funnel-analytics/static-single-page-flow',
4444
'funnel-analytics/static-multi-page-flow',
45+
'error-boundary',
4546
];
4647
return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match));
4748
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { Suspense } from 'react';
5+
6+
import { AppLayout, Spinner } from '~components';
7+
import { ErrorBoundariesProvider } from '~components/error-boundary/context';
8+
import I18nProvider from '~components/i18n';
9+
import messages from '~components/i18n/messages/all.en';
10+
11+
import ScreenshotArea from '../utils/screenshot-area';
12+
13+
function createDelayedResource(ms: number, error: Error) {
14+
let done = false;
15+
const promise = new Promise<void>(resolve =>
16+
setTimeout(() => {
17+
done = true;
18+
resolve();
19+
}, ms)
20+
);
21+
return {
22+
read() {
23+
if (!done) {
24+
throw promise;
25+
}
26+
throw error;
27+
},
28+
};
29+
}
30+
31+
const resource = createDelayedResource(2000, new Error('Async page load failed'));
32+
33+
function AsyncFailingPage() {
34+
resource.read();
35+
return <div>Loaded page</div>;
36+
}
37+
38+
export default function ErrorBoundaryAsyncDemo() {
39+
return (
40+
<ScreenshotArea gutters={false}>
41+
<I18nProvider messages={[messages]} locale="en">
42+
<ErrorBoundariesProvider value={{ feedbackLink: '/#' }}>
43+
{/* AppLayout remains synchronous */}
44+
<AppLayout
45+
navigationHide={true}
46+
toolsHide={true}
47+
content={
48+
<Suspense fallback={<Fallback />}>
49+
<AsyncFailingPage />
50+
</Suspense>
51+
}
52+
/>
53+
</ErrorBoundariesProvider>
54+
</I18nProvider>
55+
</ScreenshotArea>
56+
);
57+
}
58+
59+
function Fallback() {
60+
return (
61+
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
62+
<Spinner size="large" />
63+
</div>
64+
);
65+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { useContext, useState } from 'react';
5+
6+
import {
7+
AppLayout,
8+
Box,
9+
Button,
10+
Checkbox,
11+
Container,
12+
Drawer,
13+
ExpandableSection,
14+
Header,
15+
Link,
16+
Modal,
17+
Popover,
18+
SpaceBetween,
19+
SplitPanel,
20+
Table,
21+
} from '~components';
22+
import { ErrorBoundariesProvider } from '~components/error-boundary/context';
23+
import I18nProvider from '~components/i18n';
24+
import messages from '~components/i18n/messages/all.en';
25+
26+
import AppContext, { AppContextType } from '../app/app-context';
27+
import ScreenshotArea from '../utils/screenshot-area';
28+
29+
type PageContext = React.Context<AppContextType<{ errorBoundariesActive: boolean }>>;
30+
31+
export default function () {
32+
const {
33+
urlParams: { errorBoundariesActive = true },
34+
setUrlParams,
35+
} = useContext(AppContext as PageContext);
36+
const [splitPanelOpen, setSplitPanelOpen] = useState(true);
37+
const [activeDrawerId, setActiveDrawerId] = useState<null | string>('d1');
38+
const [modalOpen, setModalOpen] = useState(false);
39+
return (
40+
<ScreenshotArea gutters={false}>
41+
<I18nProvider messages={[messages]} locale="en">
42+
<ErrorBoundariesProvider active={errorBoundariesActive} value={{ feedbackLink: '/#' }}>
43+
<AppLayout
44+
navigationHide={true}
45+
activeDrawerId={activeDrawerId}
46+
onDrawerChange={({ detail }) => setActiveDrawerId(detail.activeDrawerId)}
47+
splitPanel={
48+
<SplitPanel header="Split panel">
49+
<BrokenButton />
50+
</SplitPanel>
51+
}
52+
splitPanelOpen={splitPanelOpen}
53+
onSplitPanelToggle={({ detail }) => setSplitPanelOpen(detail.open)}
54+
drawers={[
55+
{
56+
id: 'd1',
57+
content: (
58+
<Drawer header={<Header variant="h2">Drawer 1</Header>}>
59+
<BrokenButton />
60+
</Drawer>
61+
),
62+
trigger: { iconName: 'bug' },
63+
ariaLabels: { drawerName: 'Drawer 1', triggerButton: 'Open drawer 1', closeButton: 'Close drawer 1' },
64+
},
65+
{
66+
id: 'd2',
67+
content: (
68+
<Drawer header={<Header variant="h2">Drawer 2</Header>}>
69+
<BrokenButton />
70+
</Drawer>
71+
),
72+
trigger: { iconName: 'call' },
73+
ariaLabels: { drawerName: 'Drawer 2', triggerButton: 'Open drawer 2', closeButton: 'Close drawer 2' },
74+
},
75+
]}
76+
content={
77+
<Box>
78+
<h1>Error boundary demo: components</h1>
79+
<Box margin={{ bottom: 'm' }}>
80+
<Checkbox
81+
checked={errorBoundariesActive}
82+
onChange={({ detail }) => setUrlParams({ errorBoundariesActive: detail.checked })}
83+
>
84+
Error boundaries on
85+
</Checkbox>
86+
</Box>
87+
88+
<SpaceBetween size="m">
89+
<SpaceBetween size="m" direction="horizontal">
90+
<BrokenButton />
91+
92+
<Box>
93+
<Button onClick={() => setModalOpen(true)}>Show modal</Button>
94+
<Modal visible={modalOpen} header="Modal" onDismiss={() => setModalOpen(false)}>
95+
<BrokenButton />
96+
</Modal>
97+
</Box>
98+
99+
<Popover header="Header" content={<BrokenButton />} triggerType="custom">
100+
<Button>Show popover</Button>
101+
</Popover>
102+
</SpaceBetween>
103+
104+
<Container header={<Header>Container 1</Header>}>
105+
<BrokenButton />
106+
</Container>
107+
108+
<Container header={<Header>Container 2</Header>}>
109+
<BrokenButton />
110+
</Container>
111+
112+
<ExpandableSection
113+
variant="container"
114+
headerText="Expandable section"
115+
headerActions={<BrokenButton />}
116+
defaultExpanded={true}
117+
>
118+
<BrokenButton />
119+
</ExpandableSection>
120+
121+
<Table
122+
header={<Header>Table</Header>}
123+
columnDefinitions={[
124+
{
125+
header: 'Column 1',
126+
cell: item => <Link href="/#/light/error-boundary/demo-async-load">{item}</Link>,
127+
},
128+
{ header: 'Column 2', cell: item => `Content 2:${item}` },
129+
{ header: 'Column 3', cell: item => `Content 3:${item}` },
130+
{ header: 'Actions', cell: () => <BrokenButton /> },
131+
]}
132+
items={[1, 2, 3]}
133+
></Table>
134+
</SpaceBetween>
135+
</Box>
136+
}
137+
/>
138+
</ErrorBoundariesProvider>
139+
</I18nProvider>
140+
</ScreenshotArea>
141+
);
142+
}
143+
144+
function BrokenButton() {
145+
const [errorState, setErrorState] = useState(false);
146+
return <Button onClick={() => setErrorState(true)}>Broken button {errorState ? {} : ''}</Button>;
147+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { useState } from 'react';
5+
6+
import { AppLayout, Box, Button, Container, ExpandableSection, Form, Header, Popover, SpaceBetween } from '~components';
7+
import { ErrorBoundariesProvider } from '~components/error-boundary/context';
8+
import I18nProvider from '~components/i18n';
9+
import messages from '~components/i18n/messages/all.en';
10+
11+
import ScreenshotArea from '../utils/screenshot-area';
12+
13+
export default function () {
14+
return (
15+
<ScreenshotArea gutters={false}>
16+
<I18nProvider messages={[messages]} locale="en">
17+
<ErrorBoundariesProvider value={{ feedbackLink: '/#' }}>
18+
<AppLayout
19+
contentType="form"
20+
navigationHide={true}
21+
content={
22+
<Box>
23+
<h1>Error boundary demo: form</h1>
24+
<p>When an unexpected error occurs inside a form, it is no longer scoped to sections.</p>
25+
26+
<Form
27+
actions={
28+
<SpaceBetween direction="horizontal" size="m">
29+
<BrokenButton />
30+
</SpaceBetween>
31+
}
32+
>
33+
<SpaceBetween size="m">
34+
<Container header={<Header>Container 1</Header>}>
35+
<BrokenButton />
36+
</Container>
37+
38+
<Container header={<Header>Container 2</Header>}>
39+
<Popover header="Header" content={<BrokenButton />} triggerType="custom">
40+
<Button>Show popover</Button>
41+
</Popover>
42+
</Container>
43+
44+
<ExpandableSection
45+
variant="container"
46+
headerText="Expandable section"
47+
headerActions={<BrokenButton />}
48+
defaultExpanded={true}
49+
>
50+
<BrokenButton />
51+
</ExpandableSection>
52+
</SpaceBetween>
53+
</Form>
54+
</Box>
55+
}
56+
/>
57+
</ErrorBoundariesProvider>
58+
</I18nProvider>
59+
</ScreenshotArea>
60+
);
61+
}
62+
63+
function BrokenButton() {
64+
const [errorState, setErrorState] = useState(false);
65+
return <Button onClick={() => setErrorState(true)}>Broken button {errorState ? {} : ''}</Button>;
66+
}

src/app-layout/error-boundary.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React from 'react';
5+
6+
import InternalErrorBoundary from '../error-boundary/internal';
7+
8+
import styles from './styles.css.js';
9+
10+
export function ErrorBoundaryMain({ children }: { children: React.ReactNode }) {
11+
return (
12+
<InternalErrorBoundary wrapper={content => <div className={styles['error-boundary-wrapper']}>{content}</div>}>
13+
{children}
14+
</InternalErrorBoundary>
15+
);
16+
}

src/app-layout/styles.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
min-inline-size: 0;
3636
background-color: awsui.$color-background-layout-main;
3737
position: relative;
38+
3839
&-scrollable {
3940
overflow: auto;
4041
}
@@ -68,3 +69,10 @@
6869
// applied to content or content header, whatever comes first
6970
padding-block-start: awsui.$space-scaled-m;
7071
}
72+
73+
.error-boundary-wrapper {
74+
block-size: 100%;
75+
display: flex;
76+
align-items: center;
77+
justify-content: center;
78+
}

src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD
128128
/>
129129
</div>
130130
<div
131+
key={TOOLS_DRAWER_ID}
131132
className={clsx(
132133
styles['drawer-content'],
133134
activeDrawerId !== TOOLS_DRAWER_ID && styles['drawer-content-hidden']
@@ -137,7 +138,7 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD
137138
{toolsContent}
138139
</div>
139140
{activeDrawerId !== TOOLS_DRAWER_ID && (
140-
<div className={styles['drawer-content']} style={{ blockSize: drawerHeight }}>
141+
<div key={activeDrawerId} className={styles['drawer-content']} style={{ blockSize: drawerHeight }}>
141142
{activeDrawer?.content}
142143
</div>
143144
)}

src/app-layout/visual-refresh-toolbar/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useMobile } from '../../internal/hooks/use-mobile';
1313
import { useGetGlobalBreadcrumbs } from '../../internal/plugins/helpers/use-global-breadcrumbs';
1414
import globalVars from '../../internal/styles/global-vars';
1515
import { getSplitPanelDefaultSize } from '../../split-panel/utils/size-utils';
16+
import { ErrorBoundaryMain } from '../error-boundary';
1617
import { AppLayoutProps } from '../interfaces';
1718
import { SplitPanelProviderProps } from '../split-panel';
1819
import { MIN_DRAWER_SIZE, OnChangeParams, useDrawers } from '../utils/use-drawers';
@@ -517,7 +518,7 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef<AppLayoutProps.Ref, AppLa
517518
headerVariant={headerVariant}
518519
contentHeader={contentHeader}
519520
// delay rendering the content until registration of this instance is complete
520-
content={registered ? content : null}
521+
content={registered ? <ErrorBoundaryMain>{content}</ErrorBoundaryMain> : null}
521522
navigation={resolvedNavigation && <AppLayoutNavigation appLayoutInternals={appLayoutInternals} />}
522523
navigationOpen={resolvedNavigationOpen}
523524
navigationWidth={navigationWidth}

src/app-layout/visual-refresh/drawers.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ function ActiveDrawer() {
143143
</div>
144144
{toolsContent && (
145145
<div
146+
key={TOOLS_DRAWER_ID}
146147
className={clsx(
147148
styles['drawer-content'],
148149
activeDrawerId !== TOOLS_DRAWER_ID && styles['drawer-content-hidden']
@@ -152,7 +153,9 @@ function ActiveDrawer() {
152153
</div>
153154
)}
154155
{activeDrawerId !== TOOLS_DRAWER_ID && (
155-
<div className={styles['drawer-content']}>{activeDrawerId && activeDrawer?.content}</div>
156+
<div key={activeDrawerId} className={styles['drawer-content']}>
157+
{activeDrawerId && activeDrawer?.content}
158+
</div>
156159
)}
157160
</div>
158161
</aside>

0 commit comments

Comments
 (0)