Skip to content

Commit 8c444b0

Browse files
committed
Add workspace panel error boundary
Signed-off-by: Ayoub LABIDI <ayoub.labidi@protonmail.com>
1 parent 658fb9f commit 8c444b0

6 files changed

Lines changed: 159 additions & 21 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright © 2026, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { Component, type ErrorInfo, type ReactNode } from 'react';
9+
import { Alert, Box, Button, Typography } from '@mui/material';
10+
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
11+
import RefreshIcon from '@mui/icons-material/Refresh';
12+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
13+
import { FormattedMessage } from 'react-intl';
14+
15+
const styles = {
16+
container: {
17+
display: 'flex',
18+
flexDirection: 'column',
19+
height: '100%',
20+
},
21+
centeredContent: (theme: any) => ({
22+
display: 'flex',
23+
flexDirection: 'column',
24+
alignItems: 'center',
25+
justifyContent: 'center',
26+
flex: 1,
27+
gap: theme.spacing(2),
28+
}),
29+
icon: (theme: any) => ({
30+
fontSize: 48,
31+
color: theme.palette.error.main,
32+
}),
33+
alertMessage: {
34+
width: '100%',
35+
'& .MuiAlert-message': {
36+
overflow: 'hidden',
37+
textOverflow: 'ellipsis',
38+
whiteSpace: 'nowrap',
39+
},
40+
'& .MuiAlert-action': {
41+
flexShrink: 0,
42+
},
43+
},
44+
};
45+
46+
interface PanelErrorBoundaryProps {
47+
children: ReactNode;
48+
onReset?: () => void;
49+
}
50+
51+
interface PanelErrorBoundaryState {
52+
hasError: boolean;
53+
error: Error | null;
54+
errorInfo: ErrorInfo | null;
55+
copied: boolean;
56+
}
57+
58+
export default class PanelErrorBoundary extends Component<PanelErrorBoundaryProps, PanelErrorBoundaryState> {
59+
constructor(props: PanelErrorBoundaryProps) {
60+
super(props);
61+
this.state = { hasError: false, error: null, errorInfo: null, copied: false };
62+
}
63+
64+
static getDerivedStateFromError(error: Error): PanelErrorBoundaryState {
65+
return { hasError: true, error, errorInfo: null, copied: false };
66+
}
67+
68+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
69+
console.error('Panel error caught:', error, errorInfo);
70+
this.setState({ errorInfo });
71+
}
72+
73+
handleReset = () => {
74+
this.setState({ hasError: false, error: null, errorInfo: null, copied: false });
75+
this.props.onReset?.();
76+
};
77+
78+
getErrorText = (): string => {
79+
if (!this.state.error) return '';
80+
81+
let errorText = `Error: ${this.state.error.toString()}`;
82+
if (this.state.errorInfo?.componentStack) {
83+
errorText += `\nComponent Stack: ${this.state.errorInfo.componentStack}`;
84+
}
85+
if (this.state.error.stack) {
86+
errorText += `\nStack Trace: ${this.state.error.stack}`;
87+
}
88+
return errorText;
89+
};
90+
91+
handleCopy = async () => {
92+
const errorText = this.getErrorText();
93+
if (errorText) {
94+
await navigator.clipboard.writeText(errorText);
95+
this.setState({ copied: true });
96+
setTimeout(() => this.setState({ copied: false }), 2000);
97+
}
98+
};
99+
100+
render() {
101+
if (this.state.hasError) {
102+
return (
103+
<Box sx={styles.container}>
104+
<Box sx={styles.centeredContent}>
105+
<ErrorOutlineIcon sx={styles.icon} />
106+
<Typography>
107+
<FormattedMessage id="PanelError" />
108+
</Typography>
109+
<Button variant="outlined" size="small" startIcon={<RefreshIcon />} onClick={this.handleReset}>
110+
<FormattedMessage id="Reload" />
111+
</Button>
112+
</Box>
113+
{this.state.error && (
114+
<Alert
115+
severity="warning"
116+
sx={styles.alertMessage}
117+
action={
118+
<Button
119+
color="inherit"
120+
size="small"
121+
startIcon={<ContentCopyIcon />}
122+
onClick={this.handleCopy}
123+
>
124+
<FormattedMessage id={this.state.copied ? 'Copied' : 'CopyError'} />
125+
</Button>
126+
}
127+
>
128+
{this.state.error.toString()}
129+
</Alert>
130+
)}
131+
</Box>
132+
);
133+
}
134+
135+
return this.props.children;
136+
}
137+
}

src/components/grid-layout/cards/card-styles.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,6 @@ export const cardStyles = {
2020
},
2121
},
2222
},
23-
alertMessage: (theme) => ({
24-
borderRadius: '0 0 0 0',
25-
border:
26-
theme.palette.mode === 'light'
27-
? `1px solid ${theme.palette.grey[500]}`
28-
: `1px solid ${theme.palette.grey[800]}`,
29-
borderTop: 'none', // remove the top border to avoid double border with CustomCardHeader
30-
borderBottom: 'none',
31-
}),
3223
diagramContainer: (theme) => ({
3324
flexGrow: 1,
3425
overflow: 'hidden',

src/components/workspace/core/panel.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getPanelConfig } from '../constants/workspace.constants';
1919
import type { AppState } from '../../../redux/reducer';
2020
import { getSnapZone, type SnapRect } from './utils/snap-utils';
2121
import { positionToRelative, sizeToRelative, calculatePanelDimensions } from './utils/coordinate-utils';
22+
import PanelErrorBoundary from '../../common/panel-error-boundary';
2223

2324
const RESIZE_HANDLE_SIZE = 12;
2425

@@ -176,14 +177,16 @@ export const Panel = memo(({ panelId, containerRect, snapPreview, onSnapPreview,
176177
border: getBorder(theme, isFocused, panel.maximized),
177178
})}
178179
>
179-
{studyUuid && currentRootNetworkUuid && currentNode
180-
? PANEL_CONTENT_REGISTRY[panel.type]({
181-
panelId,
182-
studyUuid,
183-
currentRootNetworkUuid,
184-
currentNode,
185-
})
186-
: null}
180+
{studyUuid && currentRootNetworkUuid && currentNode ? (
181+
<PanelErrorBoundary key={`${panelId}-${panel.type}`}>
182+
{PANEL_CONTENT_REGISTRY[panel.type]({
183+
panelId,
184+
studyUuid,
185+
currentRootNetworkUuid,
186+
currentNode,
187+
})}
188+
</PanelErrorBoundary>
189+
) : null}
187190
</Box>
188191
</Box>
189192
</Rnd>

src/components/workspace/diagrams/diagram-wrapper.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import { Box, CircularProgress } from '@mui/material';
99
import { ReactNode } from 'react';
1010
import AlertCustomMessageNode from '../../utils/alert-custom-message-node';
11-
import { cardStyles } from '../../grid-layout/cards/card-styles';
1211
import { ErrorMessageDescriptor } from '@gridsuite/commons-ui';
1312

1413
interface DiagramWrapperProps {
@@ -28,7 +27,7 @@ export const DiagramWrapper = ({ loading, hasSvg, globalError, children }: Diagr
2827
}
2928

3029
if (globalError) {
31-
return <AlertCustomMessageNode message={globalError} noMargin style={cardStyles.alertMessage} />;
30+
return <AlertCustomMessageNode message={globalError} noMargin />;
3231
}
3332

3433
return <Box sx={{ height: '100%', width: '100%' }}>{children}</Box>;

src/translations/en.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1762,5 +1762,9 @@
17621762
"unbuildAllNodesError": "An error occurred while unbuilding all nodes",
17631763
"uuidCopiedToClipboard": "Uuid copied to system clipboard",
17641764
"uuidCopiedToClipboardError": "Uuid could not be copied to system clipboard",
1765-
"uuid": "UUID"
1765+
"uuid": "UUID",
1766+
"PanelError": "Something went wrong in this panel",
1767+
"Reload": "Reload",
1768+
"CopyError": "Copy Error",
1769+
"Copied": "Copied"
17661770
}

src/translations/fr.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1755,5 +1755,9 @@
17551755
"unbuildAllNodesError": "Une erreur est survenue lors de la déréalisation de l'ensemble des nœuds",
17561756
"uuidCopiedToClipboard": "L'UUID a été copié dans le presse-papier système",
17571757
"uuidCopiedToClipboardError": "L'UUID n'a pas pu être copié dans le presse-papier système",
1758-
"uuid": "UUID"
1758+
"uuid": "UUID",
1759+
"PanelError": "Une erreur est survenue dans ce panneau",
1760+
"Reload": "Recharger",
1761+
"CopyError": "Copier l'erreur",
1762+
"Copied": "Copiée"
17591763
}

0 commit comments

Comments
 (0)