error boundary for screens (#45)

* error boundary for screens

* error boundary for plugins

* remove changes.diff

* move load-error-text class to app.less

* implement mikes suggestions

* apply error boundary to workspace view

* fix minor issues
This commit is contained in:
Red J Adaya 2023-10-26 09:24:14 +08:00 committed by GitHub
parent 49f18a3e94
commit 7e9e76f089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 40 deletions

View File

@ -373,6 +373,15 @@ a.a-block {
}
}
.load-error-text {
color: @term-red;
padding-top: 5px;
}
.view-error {
padding: 10px 20px;
}
@keyframes loader-ring {
0% {
transform: rotate(0deg);

View File

@ -24,6 +24,7 @@ import { TosModal } from "./common/modals/modals";
import { WorkspaceView } from "../app/workspace/workspaceview";
import { MainSideBar } from "./sidebar/MainSideBar";
import { DisconnectedModal, ClientStopModal, AlertModal, WelcomeModal } from "./common/modals/modals";
import { ErrorBoundary } from "./common/error/errorboundary";
import "./app.less";
dayjs.extend(localizedFormat);
@ -110,9 +111,11 @@ class App extends React.Component<{}, {}> {
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content">
<MainSideBar />
<WorkspaceView />
<HistoryView />
<BookmarksView />
<ErrorBoundary>
<WorkspaceView />
<HistoryView />
<BookmarksView />
</ErrorBoundary>
</div>
<AlertModal />
<If condition={GlobalModel.needsTos()}>

View File

@ -0,0 +1,65 @@
import React, { Component, ReactNode } from "react";
import { RendererContext } from "../../../plugins/types/types";
import cn from "classnames";
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
plugin?: string;
lineContext?: RendererContext;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = {
hasError: false,
error: null,
};
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
const { plugin, lineContext } = this.props;
if (plugin && lineContext) {
console.log(`Plugin Name: ${plugin}\n`);
console.log(`Line Context: \n`);
console.log(`${JSON.stringify(lineContext, null, 4)}\n`);
}
console.log(error);
}
resetErrorBoundary = (): void => {
this.setState({ hasError: false, error: null });
};
renderFallback() {
const { error } = this.state;
const { plugin } = this.props;
return (
<div className={cn("load-error-text", { "view-error": !plugin })}>
<div>{`${error?.message}`}</div>
{plugin && <div>An error occurred in the {plugin} plugin</div>}
</div>
);
}
render() {
const { hasError } = this.state;
if (hasError) {
return this.renderFallback();
}
return this.props.children;
}
}
export { ErrorBoundary };

View File

@ -38,6 +38,7 @@ import { isBlank } from "../../util/util";
import { PluginModel } from "../../plugins/plugins";
import { Prompt } from "../common/prompt/prompt";
import * as lineutil from "./lineutil";
import { ErrorBoundary } from "../../app/common/error/errorboundary";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CommentIcon } from "../assets/icons/line/comment.svg";
@ -594,7 +595,6 @@ class LineCmd extends React.Component<
let isRunning = cmd.isRunning();
let isExpanded = this.isCmdExpanded.get();
let rsdiff = this.rtnStateDiff.get();
// console.log("render", "#" + line.linenum, termHeight, usedRows, cmd.getStatus(), (this.rtnStateDiff.get() != null), (!cmd.isRunning() ? "cmd-done" : "running"));
let mainDivCn = cn(
"line",
"line-cmd",
@ -664,39 +664,41 @@ class LineCmd extends React.Component<
</div>
</div>
<If condition={!this.isMinimised.get()}>
<If condition={rendererPlugin == null && !isNoneRenderer}>
<TerminalRenderer
screen={screen}
line={line}
width={width}
staticRender={staticRender}
visible={visible}
onHeightChange={this.handleHeightChange}
collapsed={false}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "simple"}>
<SimpleBlobRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
scrollToBringIntoViewport={this.scrollToBringIntoViewport}
isSelected={isSelected}
shouldFocus={shouldCmdFocus}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}>
<IncrementalRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
isSelected={isSelected}
/>
</If>
<ErrorBoundary plugin={rendererPlugin?.name} lineContext={lineutil.getRendererContext(line)}>
<If condition={rendererPlugin == null && !isNoneRenderer}>
<TerminalRenderer
screen={screen}
line={line}
width={width}
staticRender={staticRender}
visible={visible}
onHeightChange={this.handleHeightChange}
collapsed={false}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "simple"}>
<SimpleBlobRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
scrollToBringIntoViewport={this.scrollToBringIntoViewport}
isSelected={isSelected}
shouldFocus={shouldCmdFocus}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}>
<IncrementalRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
isSelected={isSelected}
/>
</If>
</ErrorBoundary>
<If condition={cmd.getRtnState()}>
<div
key="rtnstate"

View File

@ -11,6 +11,7 @@ import { GlobalModel } from "../../model/model";
import { CmdInput } from "./cmdinput/cmdinput";
import { ScreenView } from "./screen/screenview";
import { ScreenTabs } from "./screen/tabs";
import { ErrorBoundary } from "../../app/common/error/errorboundary";
import "./workspace.less";
dayjs.extend(localizedFormat);
@ -31,12 +32,15 @@ class WorkspaceView extends React.Component<{}, {}> {
cmdInputHeight = 110;
}
let isHidden = GlobalModel.activeMainView.get() != "session";
return (
<div className={cn("session-view", { "is-hidden": isHidden })} data-sessionid={session.sessionId}>
<ScreenTabs session={session} />
<ScreenView screen={activeScreen} />
<div style={{ height: cmdInputHeight }}></div>
<CmdInput />
<ErrorBoundary>
<ScreenView screen={activeScreen} />
<div style={{ height: cmdInputHeight }}></div>
<CmdInput />
</ErrorBoundary>
</div>
);
}