Skip to content

Commit

Permalink
Added global error boundary and recovery functionality (#309)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenthoms authored Sep 14, 2023
1 parent 1951a2f commit f09f1bc
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 6 deletions.
105 changes: 105 additions & 0 deletions frontend/src/GlobalErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from "react";

import { BugAntIcon, Square2StackIcon } from "@heroicons/react/20/solid";
import { Button } from "@lib/components/Button";
import { IconButton } from "@lib/components/IconButton";
import { resolveClassNames } from "@lib/utils/resolveClassNames";

type Props = {
children?: React.ReactNode;
};

interface State {
error: Error | null;
copiedToClipboard: boolean;
}

export class GlobalErrorBoundary extends React.Component<Props, State> {
state: State = {
error: null,
copiedToClipboard: false,
};

static getDerivedStateFromError(err: Error): State {
return { error: err, copiedToClipboard: false };
}

render() {
const freshStartUrl = new URL(window.location.href);
freshStartUrl.searchParams.set("cleanStart", "true");

function reportIssue(errorMessage: string, errorStack: string) {
const title = encodeURIComponent(`[USER REPORTED ERROR] ${errorMessage}`);
const body = encodeURIComponent(
`<!-- ⚠️ DO NOT INCLUDE DATA/SCREENSHOTS THAT CAN'T BE PUBLICLY AVAILABLE.-->\n\n\
**How to reproduce**\nPlease describe what you were doing when the error occurred.\n\n\
**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n\
**Error stack**\n\`\`\`\n${errorStack}\n\`\`\``
);
const label = encodeURIComponent("user reported error");
window.open(
`https://github.com/equinor/webviz/issues/new?title=${title}&body=${body}&labels=${label}`,
"_blank"
);
}

const copyToClipboard = () => {
navigator.clipboard.writeText(freshStartUrl.toString());
this.setState({ copiedToClipboard: true });
setTimeout(() => this.setState({ copiedToClipboard: false }), 2000);
};

if (this.state.error) {
return (
<div className="h-screen w-screen bg-red-200 flex items-center justify-center">
<div className="flex flex-col w-1/2 min-w-[600px] bg-white shadow">
<div className="w-full bg-red-600 text-white p-4 flex items-center shadow">
Application terminated with error
</div>
<div className="w-full flex-grow p-4 flex flex-col gap-2">
The application was terminated due to the following error:
<div className="bg-slate-200 p-4 my-2 whitespace-nowrap font-mono text-sm">
<strong>{this.state.error.name}</strong>: {this.state.error.message}
</div>
You can use the following URL to start a clean session:
<div>
<div className="bg-slate-200 p-4 py-2 my-2 whitespace-nowrap font-mono text-sm flex items-center">
<a href={freshStartUrl.toString()} className="flex-grow">
{freshStartUrl.toString()}
</a>
<IconButton onClick={copyToClipboard} title="Copy URL to clipboard">
<Square2StackIcon className="w-4 h-4" />
</IconButton>
</div>
<div
className={resolveClassNames(
"h-2 m-2 whitespace-nowrap text-sm transition-opacity text-green-800",
{
"opacity-0": !this.state.copiedToClipboard,
}
)}
>
Copied to clipboard
</div>
</div>
</div>
<div className="p-4 bg-slate-100 flex gap-4 shadow">
<Button
onClick={() =>
reportIssue(
`${this.state.error?.name ?? ""}: ${this.state.error?.message ?? ""}`,
this.state.error?.stack ?? ""
)
}
startIcon={<BugAntIcon className="w-4 h-4" />}
>
Report issue
</Button>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const ViewContent = React.memo((props: ViewContentProps) => {
return <div className="h-full w-full flex justify-center items-center">Not imported</div>;
}

if (importState === ImportState.Importing || !props.moduleInstance.isInitialised()) {
if (importState === ImportState.Importing) {
return (
<div className="h-full w-full flex flex-col justify-center items-center">
<CircularProgress />
Expand All @@ -66,6 +66,15 @@ export const ViewContent = React.memo((props: ViewContentProps) => {
);
}

if (!props.moduleInstance.isInitialised()) {
return (
<div className="h-full w-full flex flex-col justify-center items-center">
<CircularProgress />
<div className="mt-4">Initialising...</div>
</div>
);
}

if (importState === ImportState.Failed) {
return (
<div className="h-full w-full flex justify-center items-center">
Expand Down
26 changes: 21 additions & 5 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ import { AuthProvider } from "@framework/internal/providers/AuthProvider";
import { CustomQueryClientProvider } from "@framework/internal/providers/QueryClientProvider";

import App from "./App";
import { GlobalErrorBoundary } from "./GlobalErrorBoundary";

/*
If the `cleanStart` query parameter is given,
the application will clear all local storage before rendering the application.
*/
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("cleanStart")) {
localStorage.clear();
urlParams.delete("cleanStart");
window.location.search = urlParams.toString();
}

// --------------------------------------------------------------------

const container = document.getElementById("root");

Expand All @@ -16,10 +30,12 @@ const root = createRoot(container);

root.render(
<React.StrictMode>
<AuthProvider>
<CustomQueryClientProvider>
<App />
</CustomQueryClientProvider>
</AuthProvider>
<GlobalErrorBoundary>
<AuthProvider>
<CustomQueryClientProvider>
<App />
</CustomQueryClientProvider>
</AuthProvider>
</GlobalErrorBoundary>
</React.StrictMode>
);

0 comments on commit f09f1bc

Please sign in to comment.