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 { @keyframes loader-ring {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);

View File

@ -24,6 +24,7 @@ import { TosModal } from "./common/modals/modals";
import { WorkspaceView } from "../app/workspace/workspaceview"; import { WorkspaceView } from "../app/workspace/workspaceview";
import { MainSideBar } from "./sidebar/MainSideBar"; import { MainSideBar } from "./sidebar/MainSideBar";
import { DisconnectedModal, ClientStopModal, AlertModal, WelcomeModal } from "./common/modals/modals"; import { DisconnectedModal, ClientStopModal, AlertModal, WelcomeModal } from "./common/modals/modals";
import { ErrorBoundary } from "./common/error/errorboundary";
import "./app.less"; import "./app.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -110,9 +111,11 @@ class App extends React.Component<{}, {}> {
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}> <div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content"> <div className="main-content">
<MainSideBar /> <MainSideBar />
<WorkspaceView /> <ErrorBoundary>
<HistoryView /> <WorkspaceView />
<BookmarksView /> <HistoryView />
<BookmarksView />
</ErrorBoundary>
</div> </div>
<AlertModal /> <AlertModal />
<If condition={GlobalModel.needsTos()}> <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 { PluginModel } from "../../plugins/plugins";
import { Prompt } from "../common/prompt/prompt"; import { Prompt } from "../common/prompt/prompt";
import * as lineutil from "./lineutil"; 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 CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CommentIcon } from "../assets/icons/line/comment.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 isRunning = cmd.isRunning();
let isExpanded = this.isCmdExpanded.get(); let isExpanded = this.isCmdExpanded.get();
let rsdiff = this.rtnStateDiff.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( let mainDivCn = cn(
"line", "line",
"line-cmd", "line-cmd",
@ -664,39 +664,41 @@ class LineCmd extends React.Component<
</div> </div>
</div> </div>
<If condition={!this.isMinimised.get()}> <If condition={!this.isMinimised.get()}>
<If condition={rendererPlugin == null && !isNoneRenderer}> <ErrorBoundary plugin={rendererPlugin?.name} lineContext={lineutil.getRendererContext(line)}>
<TerminalRenderer <If condition={rendererPlugin == null && !isNoneRenderer}>
screen={screen} <TerminalRenderer
line={line} screen={screen}
width={width} line={line}
staticRender={staticRender} width={width}
visible={visible} staticRender={staticRender}
onHeightChange={this.handleHeightChange} visible={visible}
collapsed={false} onHeightChange={this.handleHeightChange}
/> collapsed={false}
</If> />
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "simple"}> </If>
<SimpleBlobRenderer <If condition={rendererPlugin != null && rendererPlugin.rendererType == "simple"}>
rendererContainer={screen} <SimpleBlobRenderer
lineId={line.lineid} rendererContainer={screen}
plugin={rendererPlugin} lineId={line.lineid}
onHeightChange={this.handleHeightChange} plugin={rendererPlugin}
initParams={this.makeRendererModelInitializeParams()} onHeightChange={this.handleHeightChange}
scrollToBringIntoViewport={this.scrollToBringIntoViewport} initParams={this.makeRendererModelInitializeParams()}
isSelected={isSelected} scrollToBringIntoViewport={this.scrollToBringIntoViewport}
shouldFocus={shouldCmdFocus} isSelected={isSelected}
/> shouldFocus={shouldCmdFocus}
</If> />
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}> </If>
<IncrementalRenderer <If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}>
rendererContainer={screen} <IncrementalRenderer
lineId={line.lineid} rendererContainer={screen}
plugin={rendererPlugin} lineId={line.lineid}
onHeightChange={this.handleHeightChange} plugin={rendererPlugin}
initParams={this.makeRendererModelInitializeParams()} onHeightChange={this.handleHeightChange}
isSelected={isSelected} initParams={this.makeRendererModelInitializeParams()}
/> isSelected={isSelected}
</If> />
</If>
</ErrorBoundary>
<If condition={cmd.getRtnState()}> <If condition={cmd.getRtnState()}>
<div <div
key="rtnstate" key="rtnstate"

View File

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