increase error containment

pull/1641/head
gabrielburnworth 2019-12-26 12:20:10 -08:00
parent 5e52d3c6dd
commit c2d80bd55c
26 changed files with 234 additions and 150 deletions

View File

@ -0,0 +1,14 @@
jest.mock("../session", () => ({ Session: { clear: jest.fn() } }));
import * as React from "react";
import { mount } from "enzyme";
import { Apology } from "../apology";
import { Session } from "../session";
describe("<Apology />", () => {
it("clears session", () => {
const wrapper = mount(<Apology />);
wrapper.find("a").first().simulate("click");
expect(Session.clear).toHaveBeenCalled();
});
});

View File

@ -1,20 +1,29 @@
import * as React from "react"; import * as React from "react";
import { Session } from "./session"; import { Session } from "./session";
const STYLE: React.CSSProperties = { const OUTER_STYLE: React.CSSProperties = {
border: "2px solid #434343", borderRadius: "10px",
background: "#a4c2f4", background: "repeating-linear-gradient(-45deg," +
fontSize: "18px", "#ffff55, #ffff55 20px," +
"#ff5555 20px, #ff5555 40px)",
fontSize: "100%",
color: "black", color: "black",
display: "block", display: "block",
overflow: "auto", overflow: "auto",
padding: "1rem", padding: "1rem",
margin: "1rem",
};
const INNER_STYLE: React.CSSProperties = {
borderRadius: "10px",
background: "#ffffffdd",
padding: "1rem",
}; };
export function Apology(_: {}) { export function Apology(_: {}) {
return <div style={STYLE}> return <div style={OUTER_STYLE}>
<div> <div style={INNER_STYLE}>
<h1>Page Error</h1> <h1 style={{ fontSize: "175%" }}>Page Error</h1>
<span> <span>
We can't render this part of the page due to an unrecoverable error. We can't render this part of the page due to an unrecoverable error.
Here are some things you can try: Here are some things you can try:

View File

@ -6,6 +6,7 @@ import { ColWidth } from "../farmbot_os_settings";
import { FarmbotOsRowProps } from "./interfaces"; import { FarmbotOsRowProps } from "./interfaces";
import { FbosDetails } from "./fbos_details"; import { FbosDetails } from "./fbos_details";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { ErrorBoundary } from "../../../error_boundary";
const getVersionString = const getVersionString =
(fbosVersion: string | undefined, onBeta: boolean | undefined): string => { (fbosVersion: string | undefined, onBeta: boolean | undefined): string => {
@ -30,14 +31,16 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) {
<p> <p>
{t("Version {{ version }}", { version })} {t("Version {{ version }}", { version })}
</p> </p>
<FbosDetails <ErrorBoundary>
botInfoSettings={bot.hardware.informational_settings} <FbosDetails
dispatch={dispatch} botInfoSettings={bot.hardware.informational_settings}
shouldDisplay={props.shouldDisplay} dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} shouldDisplay={props.shouldDisplay}
botToMqttLastSeen={props.botToMqttLastSeen} sourceFbosConfig={sourceFbosConfig}
timeSettings={props.timeSettings} botToMqttLastSeen={props.botToMqttLastSeen}
deviceAccount={props.deviceAccount} /> timeSettings={props.timeSettings}
deviceAccount={props.deviceAccount} />
</ErrorBoundary>
</Popover> </Popover>
</Col> </Col>
<Col xs={3}> <Col xs={3}>

View File

@ -3,7 +3,7 @@ import { catchErrors } from "./util";
import { Apology } from "./apology"; import { Apology } from "./apology";
interface State { hasError?: boolean; } interface State { hasError?: boolean; }
interface Props { } interface Props { fallback?: React.ReactElement }
export class ErrorBoundary extends React.Component<Props, State> { export class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
@ -16,7 +16,7 @@ export class ErrorBoundary extends React.Component<Props, State> {
this.setState({ hasError: true }); this.setState({ hasError: true });
} }
no = () => <Apology />; no = () => this.props.fallback || <Apology />;
ok = () => this.props.children || <div />; ok = () => this.props.children || <div />;

View File

@ -1,17 +1,17 @@
import * as React from "react"; import * as React from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { DesignerPanel, DesignerPanelHeader } from "../../designer_panel"; import { DesignerPanel, DesignerPanelHeader } from "../designer_panel";
describe("<DesignerPanel />", () => { describe("<DesignerPanel />", () => {
it("renders default panel", () => { it("renders default panel", () => {
const wrapper = mount(<DesignerPanel panelName={"test-panel"} />); const wrapper = mount(<DesignerPanel panelName={"test-panel"} />);
expect(wrapper.find("div").hasClass("gray-panel")).toBeTruthy(); expect(wrapper.find("div").first().hasClass("gray-panel")).toBeTruthy();
}); });
}); });
describe("<DesignerPanelHeader />", () => { describe("<DesignerPanelHeader />", () => {
it("renders default panel header", () => { it("renders default panel header", () => {
const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />); const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />);
expect(wrapper.find("div").hasClass("gray-panel")).toBeTruthy(); expect(wrapper.find("div").first().hasClass("gray-panel")).toBeTruthy();
}); });
}); });

View File

@ -4,6 +4,7 @@ import { last, trim } from "lodash";
import { Link } from "../link"; import { Link } from "../link";
import { Panel, TAB_COLOR, PanelColor } from "./panel_header"; import { Panel, TAB_COLOR, PanelColor } from "./panel_header";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { ErrorBoundary } from "../error_boundary";
interface DesignerPanelProps { interface DesignerPanelProps {
panelName: string; panelName: string;
@ -19,7 +20,9 @@ export const DesignerPanel = (props: DesignerPanelProps) => {
"panel-container", "panel-container",
`${color || PanelColor.gray}-panel`, `${color || PanelColor.gray}-panel`,
`${props.panelName}-panel`].join(" ")}> `${props.panelName}-panel`].join(" ")}>
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>; </div>;
}; };
@ -86,7 +89,9 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
<div className="text-input-wrapper"> <div className="text-input-wrapper">
{!props.noIcon && {!props.noIcon &&
<i className="fa fa-search"></i>} <i className="fa fa-search"></i>}
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div> </div>
</div> </div>
{props.linkTo && {props.linkTo &&
@ -105,8 +110,11 @@ interface DesignerPanelContentProps {
} }
export const DesignerPanelContent = (props: DesignerPanelContentProps) => export const DesignerPanelContent = (props: DesignerPanelContentProps) =>
<div className={ <div className={[
`panel-content ${props.panelName}-panel-content ${props.className || ""}` "panel-content",
}> `${props.panelName}-panel-content`,
{props.children} props.className || ""].join(" ")}>
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>; </div>;

View File

@ -47,8 +47,7 @@ export class RawPlantInfo extends React.Component<EditPlantInfoProps, {}> {
panel={Panel.Plants} panel={Panel.Plants}
title={`${t("Edit")} ${info.name}`} title={`${t("Edit")} ${info.name}`}
backTo={"/app/designer/plants"} backTo={"/app/designer/plants"}
onBack={unselectPlant(this.props.dispatch)}> onBack={unselectPlant(this.props.dispatch)} />
</DesignerPanelHeader>
<PlantPanel <PlantPanel
info={info} info={info}
onDestroy={this.destroy} onDestroy={this.destroy}

View File

@ -76,8 +76,7 @@ export class GroupDetailActive
panelName={Panel.Groups} panelName={Panel.Groups}
panel={Panel.Groups} panel={Panel.Groups}
title={t("Edit Group")} title={t("Edit Group")}
backTo={"/app/designer/groups"}> backTo={"/app/designer/groups"} />
</DesignerPanelHeader>
<DesignerPanelContent <DesignerPanelContent
panelName={"groups"}> panelName={"groups"}>
<label>{t("GROUP NAME")}{this.saved ? "" : "*"}</label> <label>{t("GROUP NAME")}{this.saved ? "" : "*"}</label>

View File

@ -49,8 +49,7 @@ export class RawEditPoint extends React.Component<EditPointProps, {}> {
backTo={this.backTo} backTo={this.backTo}
onBack={() => this.props.dispatch({ onBack={() => this.props.dispatch({
type: Actions.TOGGLE_HOVERED_POINT, payload: undefined type: Actions.TOGGLE_HOVERED_POINT, payload: undefined
})}> })} />
</DesignerPanelHeader>
<DesignerPanelContent panelName={this.panelName}> <DesignerPanelContent panelName={this.panelName}>
<EditPointProperties point={point} <EditPointProperties point={point}
updatePoint={updatePoint(point, this.props.dispatch)} /> updatePoint={updatePoint(point, this.props.dispatch)} />

View File

@ -49,8 +49,7 @@ export class RawEditWeed extends React.Component<EditWeedProps, {}> {
backTo={this.backTo} backTo={this.backTo}
onBack={() => this.props.dispatch({ onBack={() => this.props.dispatch({
type: Actions.TOGGLE_HOVERED_POINT, payload: undefined type: Actions.TOGGLE_HOVERED_POINT, payload: undefined
})}> })} />
</DesignerPanelHeader>
<DesignerPanelContent panelName={this.panelName}> <DesignerPanelContent panelName={this.panelName}>
<EditPointProperties point={point} <EditPointProperties point={point}
updatePoint={updatePoint(point, this.props.dispatch)} /> updatePoint={updatePoint(point, this.props.dispatch)} />

View File

@ -37,8 +37,7 @@ export class RawEditZone extends React.Component<EditZoneProps, {}> {
panelName={"zone-info"} panelName={"zone-info"}
panel={Panel.Zones} panel={Panel.Zones}
title={`${t("Edit")} zone`} title={`${t("Edit")} zone`}
backTo={"/app/designer/zones"}> backTo={"/app/designer/zones"} />
</DesignerPanelHeader>
<DesignerPanelContent panelName={"zone-info"}> <DesignerPanelContent panelName={"zone-info"}>
</DesignerPanelContent> </DesignerPanelContent>
</DesignerPanel>; </DesignerPanel>;

View File

@ -12,6 +12,7 @@ import { fakeDevice } from "../../__test_support__/resource_index_builder";
import { maybeSetTimezone } from "../../devices/timezones/guess_timezone"; import { maybeSetTimezone } from "../../devices/timezones/guess_timezone";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
import { fakePings } from "../../__test_support__/fake_state/pings"; import { fakePings } from "../../__test_support__/fake_state/pings";
import { Link } from "../../link";
describe("NavBar", () => { describe("NavBar", () => {
const fakeProps = (): NavBarProps => ({ const fakeProps = (): NavBarProps => ({
@ -35,8 +36,8 @@ describe("NavBar", () => {
}); });
it("closes nav menu", () => { it("closes nav menu", () => {
const wrapper = shallow<NavBar>(<NavBar {...fakeProps()} />); const wrapper = mount<NavBar>(<NavBar {...fakeProps()} />);
const link = wrapper.find("Link").first(); const link = wrapper.find(Link).first();
link.simulate("click"); link.simulate("click");
expect(wrapper.instance().state.mobileMenuOpen).toBeFalsy(); expect(wrapper.instance().state.mobileMenuOpen).toBeFalsy();
link.simulate("click"); link.simulate("click");

View File

@ -22,7 +22,6 @@ import { BooleanSetting } from "../session_keys";
import { ReadOnlyIcon } from "../read_only_mode"; import { ReadOnlyIcon } from "../read_only_mode";
export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> { export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
state: NavBarState = { state: NavBarState = {
mobileMenuOpen: false, mobileMenuOpen: false,
tickerListOpen: false, tickerListOpen: false,
@ -42,41 +41,87 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
close = (name: keyof NavBarState) => () => close = (name: keyof NavBarState) => () =>
this.setState({ [name]: false }); this.setState({ [name]: false });
syncButton = () => { ReadOnlyStatus = () =>
return <SyncButton <ReadOnlyIcon locked={!!this.props.getConfigValue(
BooleanSetting.user_interface_read_only_mode)} />
SyncButton = () =>
<SyncButton
bot={this.props.bot} bot={this.props.bot}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
autoSync={this.props.autoSync} autoSync={this.props.autoSync}
consistent={this.props.consistent} />; consistent={this.props.consistent} />
EstopButton = () =>
<EStopButton
bot={this.props.bot}
forceUnlock={!!this.props.getConfigValue(
BooleanSetting.disable_emergency_unlock_confirmation)} />
AccountMenu = () => {
const hasName = this.props.user && this.props.user.body.name;
const firstName = hasName ?
`${hasName.split(" ")[0].slice(0, 9)}` : `${t("Menu")}`;
return <div className="menu-popover">
<Popover
portalClassName={"nav-right"}
popoverClassName={"menu-popover"}
position={Position.BOTTOM_RIGHT}
isOpen={this.state.accountMenuOpen}
onClose={this.close("accountMenuOpen")}>
<div className="nav-name" data-title={firstName}
onClick={this.toggle("accountMenuOpen")}>
{firstName}
</div>
{AdditionalMenu({ logout: this.logout, close: this.close })}
</Popover>
</div>;
} }
get connectivityData() { ConnectionStatus = () => {
return connectivityData({ const data = connectivityData({
bot: this.props.bot, bot: this.props.bot,
device: this.props.device device: this.props.device
}); });
return <div className="connection-status-popover">
<Popover position={Position.BOTTOM_RIGHT}
portalClassName={"connectivity-popover-portal"}
popoverClassName="connectivity-popover">
<DiagnosisSaucer {...data.flags} />
<ErrorBoundary>
<Connectivity
bot={this.props.bot}
rowData={data.rowData}
flags={data.flags}
pings={this.props.pings} />
</ErrorBoundary>
</Popover>
</div>;
}
AppNavLinks = () => {
const { close } = this;
const { mobileMenuOpen } = this.state;
const { alertCount } = this.props;
return <div>
<i className={"fa fa-bars mobile-menu-icon"}
onClick={this.toggle("mobileMenuOpen")} />
<span className="mobile-menu-container">
{MobileMenu({ close, mobileMenuOpen, alertCount })}
</span>
<span className="top-menu-container">
{NavLinks({ close, alertCount })}
</span>
</div>;
} }
render() { render() {
const isLocked = this.props.getConfigValue("user_interface_read_only_mode");
const hasName = this.props.user && this.props.user.body.name;
const firstName = hasName ?
`${hasName.split(" ")[0].slice(0, 9)}` : `${t("Menu")}`;
const menuIconClassNames: string[] = [
"fa", "fa-bars", "mobile-menu-icon"
];
/** The way our app is laid out, we'll pretty much always want this bit. */
const pageName = getPathArray()[2] || "";
/** Change document meta title on every route change. */ /** Change document meta title on every route change. */
updatePageInfo(pageName); updatePageInfo(getPathArray()[2] || "");
const { toggle, close } = this; const { toggle } = this;
const { mobileMenuOpen, tickerListOpen, accountMenuOpen } = this.state; const { tickerListOpen } = this.state;
const { logs, timeSettings, getConfigValue, alertCount } = this.props; const { logs, timeSettings, getConfigValue } = this.props;
const tickerListProps = { const tickerListProps = {
logs, tickerListOpen, toggle, timeSettings, getConfigValue logs, tickerListOpen, toggle, timeSettings, getConfigValue
}; };
@ -89,51 +134,17 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
<TickerList {...tickerListProps} /> <TickerList {...tickerListProps} />
<div className="nav-group"> <div className="nav-group">
<div className="nav-left"> <div className="nav-left">
<i <this.AppNavLinks />
className={menuIconClassNames.join(" ")}
onClick={this.toggle("mobileMenuOpen")} />
<span className="mobile-menu-container">
{MobileMenu({ close, mobileMenuOpen, alertCount })}
</span>
<span className="top-menu-container">
{NavLinks({ close, alertCount })}
</span>
</div> </div>
<div className="nav-right"> <div className="nav-right">
<ReadOnlyIcon locked={!!isLocked} /> <ErrorBoundary>
<this.ReadOnlyStatus />
<div className="menu-popover"> <this.AccountMenu />
<Popover <this.EstopButton />
portalClassName={"nav-right"} <this.SyncButton />
popoverClassName={"menu-popover"} <this.ConnectionStatus />
position={Position.BOTTOM_RIGHT} <RunTour currentTour={this.props.tour} />
isOpen={accountMenuOpen} </ErrorBoundary>
onClose={this.close("accountMenuOpen")}>
<div className="nav-name" data-title={firstName}
onClick={this.toggle("accountMenuOpen")}>
{firstName}
</div>
{AdditionalMenu({ logout: this.logout, close })}
</Popover>
</div>
<EStopButton
bot={this.props.bot}
forceUnlock={!!this.props.getConfigValue(
BooleanSetting.disable_emergency_unlock_confirmation)} />
{this.syncButton()}
<div className="connection-status-popover">
<Popover position={Position.BOTTOM_RIGHT}
portalClassName={"connectivity-popover-portal"}
popoverClassName="connectivity-popover">
<DiagnosisSaucer {...this.connectivityData.flags} />
<Connectivity
bot={this.props.bot}
rowData={this.connectivityData.rowData}
flags={this.connectivityData.flags}
pings={this.props.pings} />
</Popover>
</div>
<RunTour currentTour={this.props.tour} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -20,6 +20,7 @@ import { Actions } from "../../constants";
import { reduceVariables } from "../../sequences/locals_list/variable_support"; import { reduceVariables } from "../../sequences/locals_list/variable_support";
import { determineDropdown, withPrefix } from "../../resources/sequence_meta"; import { determineDropdown, withPrefix } from "../../resources/sequence_meta";
import { ResourceIndex } from "../../resources/interfaces"; import { ResourceIndex } from "../../resources/interfaces";
import { ErrorBoundary } from "../../error_boundary";
/** /**
* The bottom half of the regimen editor panel (when there's something to * The bottom half of the regimen editor panel (when there's something to
@ -57,14 +58,18 @@ export class ActiveEditor
<div id="regimen-editor-tools" className="regimen-editor-tools"> <div id="regimen-editor-tools" className="regimen-editor-tools">
<RegimenButtonGroup {...this.regimenProps} /> <RegimenButtonGroup {...this.regimenProps} />
<RegimenNameInput {...this.regimenProps} /> <RegimenNameInput {...this.regimenProps} />
<this.LocalsList /> <ErrorBoundary>
<this.LocalsList />
</ErrorBoundary>
<hr /> <hr />
</div> </div>
<OpenSchedulerButton dispatch={this.props.dispatch} /> <OpenSchedulerButton dispatch={this.props.dispatch} />
<RegimenRows {...this.regimenProps} <ErrorBoundary>
calendar={this.props.calendar} <RegimenRows {...this.regimenProps}
varsCollapsed={this.state.variablesCollapsed} calendar={this.props.calendar}
resources={this.props.resources} /> varsCollapsed={this.state.variablesCollapsed}
resources={this.props.resources} />
</ErrorBoundary>
</div>; </div>;
} }
} }

View File

@ -9,6 +9,8 @@ import { getStepTag } from "../resources/sequence_tagging";
import { HardwareFlags, FarmwareInfo } from "./interfaces"; import { HardwareFlags, FarmwareInfo } from "./interfaces";
import { ShouldDisplay } from "../devices/interfaces"; import { ShouldDisplay } from "../devices/interfaces";
import { AddCommandButton } from "./sequence_editor_middle_active"; import { AddCommandButton } from "./sequence_editor_middle_active";
import { ErrorBoundary } from "../error_boundary";
import { TileUnknown } from "./step_tiles/tile_unknown";
export interface AllStepsProps { export interface AllStepsProps {
sequence: TaggedSequence; sequence: TaggedSequence;
@ -34,6 +36,19 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
* is guaranteed to be unique no matter where the step gets moved and * is guaranteed to be unique no matter where the step gets moved and
* allows React to diff the list correctly. */ * allows React to diff the list correctly. */
const readThatCommentAbove = getStepTag(currentStep); const readThatCommentAbove = getStepTag(currentStep);
const stepProps = {
currentStep,
index,
dispatch,
currentSequence: sequence,
resources: this.props.resources,
hardwareFlags: this.props.hardwareFlags,
farmwareInfo: this.props.farmwareInfo,
shouldDisplay: this.props.shouldDisplay,
confirmStepDeletion: this.props.confirmStepDeletion,
showPins: this.props.showPins,
expandStepOptions: this.props.expandStepOptions,
};
return <div className="sequence-steps" return <div className="sequence-steps"
key={readThatCommentAbove}> key={readThatCommentAbove}>
<AddCommandButton dispatch={dispatch} index={index} /> <AddCommandButton dispatch={dispatch} index={index} />
@ -44,19 +59,9 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
intent="step_move" intent="step_move"
draggerId={index}> draggerId={index}>
<div className="sequence-step"> <div className="sequence-step">
{renderCeleryNode({ <ErrorBoundary fallback={<TileUnknown {...stepProps} />}>
currentStep, {renderCeleryNode(stepProps)}
index, </ErrorBoundary>
dispatch,
currentSequence: sequence,
resources: this.props.resources,
hardwareFlags: this.props.hardwareFlags,
farmwareInfo: this.props.farmwareInfo,
shouldDisplay: this.props.shouldDisplay,
confirmStepDeletion: this.props.confirmStepDeletion,
showPins: this.props.showPins,
expandStepOptions: this.props.expandStepOptions,
})}
</div> </div>
</StepDragger> </StepDragger>
</div>; </div>;

View File

@ -30,6 +30,7 @@ import { BooleanSetting } from "../session_keys";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { NO_GROUPS } from "./locals_list/default_value_form"; import { NO_GROUPS } from "./locals_list/default_value_form";
import { ErrorBoundary } from "../error_boundary";
export const onDrop = export const onDrop =
(dispatch1: Function, sequence: TaggedSequence) => (dispatch1: Function, sequence: TaggedSequence) =>
@ -201,19 +202,21 @@ const SequenceHeader = (props: SequenceHeaderProps) => {
getWebAppConfigValue={props.getWebAppConfigValue} getWebAppConfigValue={props.getWebAppConfigValue}
menuOpen={props.menuOpen} /> menuOpen={props.menuOpen} />
<SequenceNameAndColor {...sequenceAndDispatch} /> <SequenceNameAndColor {...sequenceAndDispatch} />
<LocalsList <ErrorBoundary>
variableData={variableData} <LocalsList
sequenceUuid={sequence.uuid} variableData={variableData}
resources={props.resources} sequenceUuid={sequence.uuid}
onChange={localListCallback(props)(declarations)} resources={props.resources}
locationDropdownKey={JSON.stringify(sequence)} onChange={localListCallback(props)(declarations)}
allowedVariableNodes={AllowedVariableNodes.parameter} locationDropdownKey={JSON.stringify(sequence)}
collapsible={true} allowedVariableNodes={AllowedVariableNodes.parameter}
collapsed={props.variablesCollapsed} collapsible={true}
toggleVarShow={props.toggleVarShow} collapsed={props.variablesCollapsed}
shouldDisplay={props.shouldDisplay} toggleVarShow={props.toggleVarShow}
hideGroups={true} shouldDisplay={props.shouldDisplay}
customFilterRule={NO_GROUPS} /> hideGroups={true}
customFilterRule={NO_GROUPS} />
</ErrorBoundary>
</div>; </div>;
}; };
@ -271,7 +274,9 @@ export class SequenceEditorMiddleActive extends
<hr /> <hr />
<div className="sequence" id="sequenceDiv" <div className="sequence" id="sequenceDiv"
style={{ height: this.stepSectionHeight }}> style={{ height: this.stepSectionHeight }}>
<AllSteps {...this.stepProps} /> <ErrorBoundary>
<AllSteps {...this.stepProps} />
</ErrorBoundary>
<Row> <Row>
<Col xs={12}> <Col xs={12}>
<DropArea isLocked={true} <DropArea isLocked={true}

View File

@ -13,12 +13,12 @@ import { conflictsString } from "../step_warning";
describe("<StepWrapper />", () => { describe("<StepWrapper />", () => {
it("renders", () => { it("renders", () => {
const wrapper = mount(<StepWrapper />); const wrapper = mount(<StepWrapper />);
expect(wrapper.find("div").hasClass("step-wrapper")).toBeTruthy(); expect(wrapper.find("div").first().hasClass("step-wrapper")).toBeTruthy();
}); });
it("renders with extra className", () => { it("renders with extra className", () => {
const wrapper = mount(<StepWrapper className={"step-class"} />); const wrapper = mount(<StepWrapper className={"step-class"} />);
expect(wrapper.find("div").hasClass("step-class")).toBeTruthy(); expect(wrapper.find("div").first().hasClass("step-class")).toBeTruthy();
}); });
}); });
@ -50,7 +50,7 @@ describe("<StepHeader />", () => {
describe("<StepContent />", () => { describe("<StepContent />", () => {
it("renders", () => { it("renders", () => {
const wrapper = mount(<StepContent className={"step-class"} />); const wrapper = mount(<StepContent className={"step-class"} />);
const div = wrapper.find("div").last(); const div = wrapper.find("div").at(2);
expect(div.hasClass("step-content")).toBeTruthy(); expect(div.hasClass("step-content")).toBeTruthy();
expect(div.hasClass("step-class")).toBeTruthy(); expect(div.hasClass("step-class")).toBeTruthy();
}); });

View File

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { Row, Col } from "../../ui/index"; import { Row, Col } from "../../ui/index";
import { ErrorBoundary } from "../../error_boundary";
interface StepContentProps { interface StepContentProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -11,7 +12,9 @@ export function StepContent(props: StepContentProps) {
return <Row> return <Row>
<Col sm={12}> <Col sm={12}>
<div className={`step-content ${className}`}> <div className={`step-content ${className}`}>
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div> </div>
</Col> </Col>
</Row>; </Row>;

View File

@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import { ErrorBoundary } from "../../error_boundary";
interface StepWrapperProps { interface StepWrapperProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -8,6 +9,8 @@ interface StepWrapperProps {
export function StepWrapper(props: StepWrapperProps) { export function StepWrapper(props: StepWrapperProps) {
const { className } = props; const { className } = props;
return <div className={`step-wrapper ${className ? className : ""}`}> return <div className={`step-wrapper ${className ? className : ""}`}>
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>; </div>;
} }

View File

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import { Col, ToolTip, DocSlug } from "."; import { Col, ToolTip, DocSlug } from ".";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { ErrorBoundary } from "../error_boundary";
interface CenterProps { interface CenterProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -20,9 +21,10 @@ export function CenterPanel(props: CenterProps) {
<i>{t(props.title)}</i> <i>{t(props.title)}</i>
</h3> </h3>
{props.helpText && {props.helpText &&
<ToolTip helpText={t(props.helpText)} docPage={props.docPage} /> <ToolTip helpText={t(props.helpText)} docPage={props.docPage} />}
} <ErrorBoundary>
{props.children} {props.children}
</ErrorBoundary>
</div> </div>
</Col>; </Col>;
} }

View File

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { ErrorBoundary } from "../error_boundary";
export enum EmptyStateGraphic { export enum EmptyStateGraphic {
plants = "plants", plants = "plants",
@ -28,7 +29,11 @@ interface EmptyStateWrapperProps {
export const EmptyStateWrapper = (props: EmptyStateWrapperProps) => export const EmptyStateWrapper = (props: EmptyStateWrapperProps) =>
!!props.notEmpty !!props.notEmpty
? <div className="non-empty-state">{props.children}</div> ? <div className="non-empty-state">
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>
: <div className={`empty-state ${props.colorScheme || ""}`}> : <div className={`empty-state ${props.colorScheme || ""}`}>
<img <img
className="empty-state-graphic" className="empty-state-graphic"

View File

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import { Col, ToolTip } from "."; import { Col, ToolTip } from ".";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { ErrorBoundary } from "../error_boundary";
interface LeftPanelProps { interface LeftPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -17,7 +18,9 @@ export function LeftPanel(props: LeftPanelProps) {
<i>{t(props.title)}</i> <i>{t(props.title)}</i>
</h3> </h3>
{props.helpText && <ToolTip helpText={props.helpText} />} {props.helpText && <ToolTip helpText={props.helpText} />}
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div> </div>
</Col>; </Col>;
} }

View File

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import { Col, ToolTip, DocSlug } from "."; import { Col, ToolTip, DocSlug } from ".";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { ErrorBoundary } from "../error_boundary";
interface RightPanelProps { interface RightPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -22,7 +23,9 @@ export function RightPanel(props: RightPanelProps) {
<i>{t(props.title)}</i> <i>{t(props.title)}</i>
</h3> </h3>
<ToolTip helpText={props.helpText} docPage={props.docPage} /> <ToolTip helpText={props.helpText} docPage={props.docPage} />
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>} </div>}
</Col>; </Col>;
} }

View File

@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import { ErrorBoundary } from "../error_boundary";
interface WidgetProps { interface WidgetProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -9,6 +10,8 @@ export function Widget(props: WidgetProps) {
let className = `widget-wrapper `; let className = `widget-wrapper `;
if (props.className) { className += props.className; } if (props.className) { className += props.className; }
return <div className={className}> return <div className={className}>
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>; </div>;
} }

View File

@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import { ErrorBoundary } from "../error_boundary";
interface WidgetBodyProps { interface WidgetBodyProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -6,6 +7,8 @@ interface WidgetBodyProps {
export function WidgetBody(props: WidgetBodyProps) { export function WidgetBody(props: WidgetBodyProps) {
return <div className="widget-body"> return <div className="widget-body">
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>; </div>;
} }

View File

@ -2,6 +2,7 @@ import * as React from "react";
import { DocSlug } from "./doc_link"; import { DocSlug } from "./doc_link";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { ToolTip } from "./tooltip"; import { ToolTip } from "./tooltip";
import { ErrorBoundary } from "../error_boundary";
interface WidgetHeaderProps { interface WidgetHeaderProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -12,7 +13,9 @@ interface WidgetHeaderProps {
export function WidgetHeader(props: WidgetHeaderProps) { export function WidgetHeader(props: WidgetHeaderProps) {
return <div className="widget-header"> return <div className="widget-header">
{props.children} <ErrorBoundary>
{props.children}
</ErrorBoundary>
<h5>{t(props.title)}</h5> <h5>{t(props.title)}</h5>
{props.helpText && {props.helpText &&
<ToolTip helpText={props.helpText} docPage={props.docPage} />} <ToolTip helpText={props.helpText} docPage={props.docPage} />}