Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
{
"path": "lib/components/internal/widget-exports.js",
"brotli": false,
"limit": "810 kB",
"limit": "840 kB",
"ignore": "react-dom"
}
],
Expand Down
1 change: 1 addition & 0 deletions pages/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function isAppLayoutPage(pageId?: string) {
'prompt-input/simple',
'funnel-analytics/static-single-page-flow',
'funnel-analytics/static-multi-page-flow',
'error-boundary',
];
return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match));
}
Expand Down
65 changes: 65 additions & 0 deletions pages/error-boundary/demo-async-load.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { Suspense } from 'react';

import { AppLayout, Spinner } from '~components';
import { ErrorBoundariesProvider } from '~components/error-boundary/context';
import I18nProvider from '~components/i18n';
import messages from '~components/i18n/messages/all.en';

import ScreenshotArea from '../utils/screenshot-area';

function createDelayedResource(ms: number, error: Error) {
let done = false;
const promise = new Promise<void>(resolve =>
setTimeout(() => {
done = true;
resolve();
}, ms)
);
return {
read() {
if (!done) {
throw promise;
}
throw error;
},
};
}

const resource = createDelayedResource(2000, new Error('Async page load failed'));

function AsyncFailingPage() {
resource.read();
return <div>Loaded page</div>;
}

export default function ErrorBoundaryAsyncDemo() {
return (
<ScreenshotArea gutters={false}>
<I18nProvider messages={[messages]} locale="en">
<ErrorBoundariesProvider value={{ feedbackLink: '/#' }}>
{/* AppLayout remains synchronous */}
<AppLayout
navigationHide={true}
toolsHide={true}
content={
<Suspense fallback={<Fallback />}>
<AsyncFailingPage />
</Suspense>
}
/>
</ErrorBoundariesProvider>
</I18nProvider>
</ScreenshotArea>
);
}

function Fallback() {
return (
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<Spinner size="large" />
</div>
);
}
147 changes: 147 additions & 0 deletions pages/error-boundary/demo-components.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useContext, useState } from 'react';

import {
AppLayout,
Box,
Button,
Checkbox,
Container,
Drawer,
ExpandableSection,
Header,
Link,
Modal,
Popover,
SpaceBetween,
SplitPanel,
Table,
} from '~components';
import { ErrorBoundariesProvider } from '~components/error-boundary/context';
import I18nProvider from '~components/i18n';
import messages from '~components/i18n/messages/all.en';

import AppContext, { AppContextType } from '../app/app-context';
import ScreenshotArea from '../utils/screenshot-area';

type PageContext = React.Context<AppContextType<{ errorBoundariesActive: boolean }>>;

export default function () {
const {
urlParams: { errorBoundariesActive = true },
setUrlParams,
} = useContext(AppContext as PageContext);
const [splitPanelOpen, setSplitPanelOpen] = useState(true);
const [activeDrawerId, setActiveDrawerId] = useState<null | string>('d1');
const [modalOpen, setModalOpen] = useState(false);
return (
<ScreenshotArea gutters={false}>
<I18nProvider messages={[messages]} locale="en">
<ErrorBoundariesProvider active={errorBoundariesActive} value={{ feedbackLink: '/#' }}>
<AppLayout
navigationHide={true}
activeDrawerId={activeDrawerId}
onDrawerChange={({ detail }) => setActiveDrawerId(detail.activeDrawerId)}
splitPanel={
<SplitPanel header="Split panel">
<BrokenButton />
</SplitPanel>
}
splitPanelOpen={splitPanelOpen}
onSplitPanelToggle={({ detail }) => setSplitPanelOpen(detail.open)}
drawers={[
{
id: 'd1',
content: (
<Drawer header={<Header variant="h2">Drawer 1</Header>}>
<BrokenButton />
</Drawer>
),
trigger: { iconName: 'bug' },
ariaLabels: { drawerName: 'Drawer 1', triggerButton: 'Open drawer 1', closeButton: 'Close drawer 1' },
},
{
id: 'd2',
content: (
<Drawer header={<Header variant="h2">Drawer 2</Header>}>
<BrokenButton />
</Drawer>
),
trigger: { iconName: 'call' },
ariaLabels: { drawerName: 'Drawer 2', triggerButton: 'Open drawer 2', closeButton: 'Close drawer 2' },
},
]}
content={
<Box>
<h1>Error boundary demo: components</h1>
<Box margin={{ bottom: 'm' }}>
<Checkbox
checked={errorBoundariesActive}
onChange={({ detail }) => setUrlParams({ errorBoundariesActive: detail.checked })}
>
Error boundaries on
</Checkbox>
</Box>

<SpaceBetween size="m">
<SpaceBetween size="m" direction="horizontal">
<BrokenButton />

<Box>
<Button onClick={() => setModalOpen(true)}>Show modal</Button>
<Modal visible={modalOpen} header="Modal" onDismiss={() => setModalOpen(false)}>
<BrokenButton />
</Modal>
</Box>

<Popover header="Header" content={<BrokenButton />} triggerType="custom">
<Button>Show popover</Button>
</Popover>
</SpaceBetween>

<Container header={<Header>Container 1</Header>}>
<BrokenButton />
</Container>

<Container header={<Header>Container 2</Header>}>
<BrokenButton />
</Container>

<ExpandableSection
variant="container"
headerText="Expandable section"
headerActions={<BrokenButton />}
defaultExpanded={true}
>
<BrokenButton />
</ExpandableSection>

<Table
header={<Header>Table</Header>}
columnDefinitions={[
{
header: 'Column 1',
cell: item => <Link href="/#/light/error-boundary/demo-async-load">{item}</Link>,
},
{ header: 'Column 2', cell: item => `Content 2:${item}` },
{ header: 'Column 3', cell: item => `Content 3:${item}` },
{ header: 'Actions', cell: () => <BrokenButton /> },
]}
items={[1, 2, 3]}
></Table>
</SpaceBetween>
</Box>
}
/>
</ErrorBoundariesProvider>
</I18nProvider>
</ScreenshotArea>
);
}

function BrokenButton() {
const [errorState, setErrorState] = useState(false);
return <Button onClick={() => setErrorState(true)}>Broken button {errorState ? {} : ''}</Button>;
}
66 changes: 66 additions & 0 deletions pages/error-boundary/demo-form.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useState } from 'react';

import { AppLayout, Box, Button, Container, ExpandableSection, Form, Header, Popover, SpaceBetween } from '~components';
import { ErrorBoundariesProvider } from '~components/error-boundary/context';
import I18nProvider from '~components/i18n';
import messages from '~components/i18n/messages/all.en';

import ScreenshotArea from '../utils/screenshot-area';

export default function () {
return (
<ScreenshotArea gutters={false}>
<I18nProvider messages={[messages]} locale="en">
<ErrorBoundariesProvider value={{ feedbackLink: '/#' }}>
<AppLayout
contentType="form"
navigationHide={true}
content={
<Box>
<h1>Error boundary demo: form</h1>
<p>When an unexpected error occurs inside a form, it is no longer scoped to sections.</p>

<Form
actions={
<SpaceBetween direction="horizontal" size="m">
<BrokenButton />
</SpaceBetween>
}
>
<SpaceBetween size="m">
<Container header={<Header>Container 1</Header>}>
<BrokenButton />
</Container>

<Container header={<Header>Container 2</Header>}>
<Popover header="Header" content={<BrokenButton />} triggerType="custom">
<Button>Show popover</Button>
</Popover>
</Container>

<ExpandableSection
variant="container"
headerText="Expandable section"
headerActions={<BrokenButton />}
defaultExpanded={true}
>
<BrokenButton />
</ExpandableSection>
</SpaceBetween>
</Form>
</Box>
}
/>
</ErrorBoundariesProvider>
</I18nProvider>
</ScreenshotArea>
);
}

function BrokenButton() {
const [errorState, setErrorState] = useState(false);
return <Button onClick={() => setErrorState(true)}>Broken button {errorState ? {} : ''}</Button>;
}
16 changes: 16 additions & 0 deletions src/app-layout/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React from 'react';

import InternalErrorBoundary from '../error-boundary/internal';

import styles from './styles.css.js';

export function ErrorBoundaryMain({ children }: { children: React.ReactNode }) {
return (
<InternalErrorBoundary wrapper={content => <div className={styles['error-boundary-wrapper']}>{content}</div>}>
{children}
</InternalErrorBoundary>
);
}
8 changes: 8 additions & 0 deletions src/app-layout/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
min-inline-size: 0;
background-color: awsui.$color-background-layout-main;
position: relative;

&-scrollable {
overflow: auto;
}
Expand Down Expand Up @@ -68,3 +69,10 @@
// applied to content or content header, whatever comes first
padding-block-start: awsui.$space-scaled-m;
}

.error-boundary-wrapper {
block-size: 100%;
display: flex;
align-items: center;
justify-content: center;
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD
/>
</div>
<div
key={TOOLS_DRAWER_ID}
className={clsx(
styles['drawer-content'],
activeDrawerId !== TOOLS_DRAWER_ID && styles['drawer-content-hidden']
Expand All @@ -137,7 +138,7 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD
{toolsContent}
</div>
{activeDrawerId !== TOOLS_DRAWER_ID && (
<div className={styles['drawer-content']} style={{ blockSize: drawerHeight }}>
<div key={activeDrawerId} className={styles['drawer-content']} style={{ blockSize: drawerHeight }}>
{activeDrawer?.content}
</div>
)}
Expand Down
3 changes: 2 additions & 1 deletion src/app-layout/visual-refresh-toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useMobile } from '../../internal/hooks/use-mobile';
import { useGetGlobalBreadcrumbs } from '../../internal/plugins/helpers/use-global-breadcrumbs';
import globalVars from '../../internal/styles/global-vars';
import { getSplitPanelDefaultSize } from '../../split-panel/utils/size-utils';
import { ErrorBoundaryMain } from '../error-boundary';
import { AppLayoutProps } from '../interfaces';
import { SplitPanelProviderProps } from '../split-panel';
import { MIN_DRAWER_SIZE, OnChangeParams, useDrawers } from '../utils/use-drawers';
Expand Down Expand Up @@ -517,7 +518,7 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef<AppLayoutProps.Ref, AppLa
headerVariant={headerVariant}
contentHeader={contentHeader}
// delay rendering the content until registration of this instance is complete
content={registered ? content : null}
content={registered ? <ErrorBoundaryMain>{content}</ErrorBoundaryMain> : null}
navigation={resolvedNavigation && <AppLayoutNavigation appLayoutInternals={appLayoutInternals} />}
navigationOpen={resolvedNavigationOpen}
navigationWidth={navigationWidth}
Expand Down
5 changes: 4 additions & 1 deletion src/app-layout/visual-refresh/drawers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ function ActiveDrawer() {
</div>
{toolsContent && (
<div
key={TOOLS_DRAWER_ID}
className={clsx(
styles['drawer-content'],
activeDrawerId !== TOOLS_DRAWER_ID && styles['drawer-content-hidden']
Expand All @@ -152,7 +153,9 @@ function ActiveDrawer() {
</div>
)}
{activeDrawerId !== TOOLS_DRAWER_ID && (
<div className={styles['drawer-content']}>{activeDrawerId && activeDrawer?.content}</div>
<div key={activeDrawerId} className={styles['drawer-content']}>
{activeDrawerId && activeDrawer?.content}
</div>
)}
</div>
</aside>
Expand Down
Loading
Loading