Skip to content
Merged
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
131 changes: 131 additions & 0 deletions src/components/workspace/core/panel-error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright © 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { Component, type ErrorInfo, type ReactNode } from 'react';
import { Alert, Box, Button, Typography } from '@mui/material';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import RefreshIcon from '@mui/icons-material/Refresh';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { FormattedMessage } from 'react-intl';

const styles = {
container: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
centeredContent: (theme: any) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: 1,
gap: theme.spacing(2),
}),
alertMessage: {
width: '100%',
'& .MuiAlert-message': {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
'& .MuiAlert-action': {
flexShrink: 0,
},
},
};

interface PanelErrorBoundaryProps {
children: ReactNode;
}

interface PanelErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
copied: boolean;
}

export default class PanelErrorBoundary extends Component<PanelErrorBoundaryProps, PanelErrorBoundaryState> {
constructor(props: PanelErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null, copied: false };
}

static getDerivedStateFromError(error: Error): PanelErrorBoundaryState {
return { hasError: true, error, errorInfo: null, copied: false };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Panel error caught:', error, errorInfo);
this.setState({ errorInfo });
}

handleReload = () => {
this.setState({ hasError: false, error: null, errorInfo: null, copied: false });
};

getErrorText = (): string => {
if (!this.state.error) return '';

let errorText = `Error: ${this.state.error.toString()}`;
if (this.state.errorInfo?.componentStack) {
errorText += `\nComponent Stack: ${this.state.errorInfo.componentStack}`;
}
if (this.state.error.stack) {
errorText += `\nStack Trace: ${this.state.error.stack}`;
}
return errorText;
};

handleCopy = async () => {
const errorText = this.getErrorText();
if (errorText) {
await navigator.clipboard.writeText(errorText);
Comment thread
sBouzols marked this conversation as resolved.
this.setState({ copied: true });
setTimeout(() => this.setState({ copied: false }), 2000);
}
};

render() {
if (this.state.hasError) {
return (
<Box sx={styles.container}>
<Box sx={styles.centeredContent}>
<ErrorOutlineIcon fontSize="large" color="error" />
<Typography>
<FormattedMessage id="PanelError" />
</Typography>
<Button variant="outlined" size="small" startIcon={<RefreshIcon />} onClick={this.handleReload}>
<FormattedMessage id="Reload" />
</Button>
</Box>
{this.state.error && (
<Alert
severity="warning"
sx={styles.alertMessage}
action={
<Button
color="inherit"
size="small"
startIcon={<ContentCopyIcon />}
Comment thread
sBouzols marked this conversation as resolved.
onClick={this.handleCopy}
>
<FormattedMessage id={this.state.copied ? 'Copied' : 'CopyError'} />
</Button>
}
>
{this.state.error.toString()}
</Alert>
)}
</Box>
);
}

return this.props.children;
}
}
19 changes: 11 additions & 8 deletions src/components/workspace/core/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getPanelConfig } from '../constants/workspace.constants';
import type { AppState } from '../../../redux/reducer';
import { getSnapZone, type SnapRect } from './utils/snap-utils';
import { positionToRelative, sizeToRelative, calculatePanelDimensions } from './utils/coordinate-utils';
import PanelErrorBoundary from './panel-error-boundary';

const RESIZE_HANDLE_SIZE = 12;

Expand Down Expand Up @@ -176,14 +177,16 @@ export const Panel = memo(({ panelId, containerRect, snapPreview, onSnapPreview,
border: getBorder(theme, isFocused, panel.maximized),
})}
>
{studyUuid && currentRootNetworkUuid && currentNode
? PANEL_CONTENT_REGISTRY[panel.type]({
panelId,
studyUuid,
currentRootNetworkUuid,
currentNode,
})
: null}
{studyUuid && currentRootNetworkUuid && currentNode ? (
<PanelErrorBoundary key={`${panelId}-${panel.type}`}>
{PANEL_CONTENT_REGISTRY[panel.type]({
panelId,
studyUuid,
currentRootNetworkUuid,
currentNode,
})}
</PanelErrorBoundary>
) : null}
</Box>
</Box>
</Rnd>
Expand Down
6 changes: 5 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1768,5 +1768,9 @@
"unbuildAllNodesError": "An error occurred while unbuilding all nodes",
"uuidCopiedToClipboard": "Uuid copied to system clipboard",
"uuidCopiedToClipboardError": "Uuid could not be copied to system clipboard",
"uuid": "UUID"
"uuid": "UUID",
"PanelError": "Something went wrong in this panel",
"Reload": "Reload",
"CopyError": "Copy Error",
"Copied": "Copied"
}
6 changes: 5 additions & 1 deletion src/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1761,5 +1761,9 @@
"unbuildAllNodesError": "Une erreur est survenue lors de la déréalisation de l'ensemble des nœuds",
"uuidCopiedToClipboard": "L'UUID a été copié dans le presse-papier système",
"uuidCopiedToClipboardError": "L'UUID n'a pas pu être copié dans le presse-papier système",
"uuid": "UUID"
"uuid": "UUID",
"PanelError": "Une erreur est survenue dans ce panneau",
"Reload": "Recharger",
"CopyError": "Copier l'erreur",
"Copied": "Copiée"
}
Loading