Skip to content

Commit fc8e316

Browse files
committed
Add reset for initial state
1 parent 1266abe commit fc8e316

3 files changed

Lines changed: 73 additions & 18 deletions

File tree

plugins/ui/src/js/src/layout/ReactPanel.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
1+
import React, {
2+
useCallback,
3+
useEffect,
4+
useMemo,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import ReactDOM from 'react-dom';
39
import { nanoid } from 'nanoid';
410
import {
@@ -95,6 +101,12 @@ function ReactPanel({
95101
const portalManager = usePortalPanelManager();
96102
const portal = portalManager.get(panelId);
97103
const panelTitle = title ?? metadata?.name ?? '';
104+
const [initialData, setInitialData] = useState(getInitialData());
105+
const onErrorReset = useCallback(() => {
106+
// Not EMPTY_ARRAY, because we always want to trigger a re-render
107+
// in case a panel is reloaded and errors again
108+
setInitialData([]);
109+
}, []);
98110

99111
// Tracks whether the panel is open and that we have emitted the onOpen event
100112
const isPanelOpenRef = useRef(false);
@@ -236,13 +248,13 @@ function ReactPanel({
236248
rowGap={rowGap}
237249
columnGap={columnGap}
238250
>
239-
<ReactPanelErrorBoundary>
251+
<ReactPanelErrorBoundary onReset={onErrorReset}>
240252
{/**
241253
* Don't render the children if there's an error with the widget. If there's an error with the widget, we can assume the children won't render properly,
242254
* but we still want the panels to appear so things don't disappear/jump around.
243255
*/}
244256
<PersistentStateProvider
245-
initialState={getInitialData()}
257+
initialState={initialData}
246258
onChange={onDataChange}
247259
>
248260
{React.Children.map(renderedChildren, child =>

plugins/ui/src/js/src/layout/ReactPanelErrorBoundary.test.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@ import { TestUtils } from '@deephaven/test-utils';
33
import { render, screen } from '@testing-library/react';
44
import { ReactPanelErrorBoundary } from './ReactPanelErrorBoundary';
55

6-
// Mock the WidgetErrorView component
7-
jest.mock('../widget/WidgetErrorView', () => ({
8-
__esModule: true,
9-
default: function MockWidgetErrorView({ error }: { error: Error }) {
10-
return <div data-testid="mock-error-view">{error.message}</div>;
11-
},
12-
}));
13-
146
describe('ReactPanelErrorBoundary', () => {
157
// Suppress console.error for our intentional errors
168
beforeAll(() => {
@@ -44,10 +36,27 @@ describe('ReactPanelErrorBoundary', () => {
4436
</ReactPanelErrorBoundary>
4537
);
4638

47-
expect(screen.getByTestId('mock-error-view')).toBeInTheDocument();
4839
expect(screen.getByText('Test error message')).toBeInTheDocument();
4940
});
5041

42+
it('renders error view with reload button when using onReset', () => {
43+
const ErrorComponent = () => {
44+
throw new Error('Test error message');
45+
};
46+
47+
const onReset = jest.fn();
48+
49+
render(
50+
<ReactPanelErrorBoundary onReset={onReset}>
51+
<ErrorComponent />
52+
</ReactPanelErrorBoundary>
53+
);
54+
55+
expect(screen.getByText('Test error message')).toBeInTheDocument();
56+
screen.getByRole('button', { name: 'Reload' }).click();
57+
expect(onReset).toHaveBeenCalled();
58+
});
59+
5160
it('recovers when children are updated after error', () => {
5261
const ErrorComponent = () => {
5362
throw new Error('Test error message');
@@ -60,7 +69,6 @@ describe('ReactPanelErrorBoundary', () => {
6069
);
6170

6271
// Verify error state
63-
expect(screen.getByTestId('mock-error-view')).toBeInTheDocument();
6472
expect(screen.getByText('Test error message')).toBeInTheDocument();
6573

6674
// Update with working component
@@ -73,7 +81,7 @@ describe('ReactPanelErrorBoundary', () => {
7381
// Verify recovery
7482
expect(screen.getByTestId('working-component')).toBeInTheDocument();
7583
expect(screen.getByText('Working Content')).toBeInTheDocument();
76-
expect(screen.queryByTestId('mock-error-view')).not.toBeInTheDocument();
84+
expect(screen.queryByText('Test error message')).not.toBeInTheDocument();
7785
});
7886

7987
it('maintains error state when props update does not include children change', () => {
@@ -88,7 +96,7 @@ describe('ReactPanelErrorBoundary', () => {
8896
);
8997

9098
// Verify initial error state
91-
expect(screen.getByTestId('mock-error-view')).toBeInTheDocument();
99+
expect(screen.getByText('Test error message')).toBeInTheDocument();
92100

93101
// Rerender with same children
94102
rerender(
@@ -98,7 +106,6 @@ describe('ReactPanelErrorBoundary', () => {
98106
);
99107

100108
// Error view should still be present
101-
expect(screen.getByTestId('mock-error-view')).toBeInTheDocument();
102109
expect(screen.getByText('Test error message')).toBeInTheDocument();
103110
});
104111

plugins/ui/src/js/src/layout/ReactPanelErrorBoundary.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import Log from '@deephaven/log';
22
import React, { Component, ReactNode } from 'react';
33
import WidgetErrorView from '../widget/WidgetErrorView';
4+
import { type WidgetError } from '../widget/WidgetTypes';
45

56
const log = Log.module('ReactPanelErrorBoundary');
67

78
export interface ReactPanelErrorBoundaryProps {
9+
onReset?: () => void;
810
/** Children to catch errors from. Error will reset when the children have been updated. */
911
children: ReactNode;
1012
}
1113

1214
export interface ReactPanelErrorBoundaryState {
1315
/** Currently displayed error. Reset when children are updated. */
14-
error?: Error;
16+
error?: Error | WidgetError;
1517
}
1618

1719
/**
@@ -49,9 +51,43 @@ export class ReactPanelErrorBoundary extends Component<
4951
log.error('Error caught by ErrorBoundary', error, errorInfo);
5052
}
5153

54+
getError(): WidgetError | undefined {
55+
const { error } = this.state;
56+
const { onReset } = this.props;
57+
58+
if (error == null) {
59+
return error;
60+
}
61+
if (error instanceof Error) {
62+
return {
63+
name: error.name,
64+
message: error.message,
65+
stack: error.stack,
66+
action: onReset
67+
? {
68+
title: 'Reload',
69+
action: () => {
70+
onReset();
71+
},
72+
}
73+
: undefined,
74+
};
75+
}
76+
return {
77+
...error,
78+
action: {
79+
title: error.action?.title ?? 'Reload',
80+
action: () => {
81+
error.action?.action?.();
82+
onReset?.();
83+
},
84+
},
85+
};
86+
}
87+
5288
render(): ReactNode {
5389
const { children } = this.props;
54-
const { error } = this.state;
90+
const error = this.getError();
5591
// We need to check for undefined children because React will throw an error if we return undefined from a render method
5692
// Note this behaviour was changed in React 18: https://github.com/reactwg/react-18/discussions/75
5793
return error != null ? <WidgetErrorView error={error} /> : children ?? null;

0 commit comments

Comments
 (0)