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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import * as React from "react";
import { mount } from "enzyme";
import { DesignerPanel, DesignerPanelHeader } from "../../designer_panel";
import { DesignerPanel, DesignerPanelHeader } from "../designer_panel";
describe("<DesignerPanel />", () => {
it("renders default 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 />", () => {
it("renders default panel header", () => {
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 { Panel, TAB_COLOR, PanelColor } from "./panel_header";
import { t } from "../i18next_wrapper";
import { ErrorBoundary } from "../error_boundary";
interface DesignerPanelProps {
panelName: string;
@ -19,7 +20,9 @@ export const DesignerPanel = (props: DesignerPanelProps) => {
"panel-container",
`${color || PanelColor.gray}-panel`,
`${props.panelName}-panel`].join(" ")}>
{props.children}
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>;
};
@ -86,7 +89,9 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
<div className="text-input-wrapper">
{!props.noIcon &&
<i className="fa fa-search"></i>}
{props.children}
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>
</div>
{props.linkTo &&
@ -105,8 +110,11 @@ interface DesignerPanelContentProps {
}
export const DesignerPanelContent = (props: DesignerPanelContentProps) =>
<div className={
`panel-content ${props.panelName}-panel-content ${props.className || ""}`
}>
{props.children}
<div className={[
"panel-content",
`${props.panelName}-panel-content`,
props.className || ""].join(" ")}>
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,6 @@ import { BooleanSetting } from "../session_keys";
import { ReadOnlyIcon } from "../read_only_mode";
export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
state: NavBarState = {
mobileMenuOpen: false,
tickerListOpen: false,
@ -42,41 +41,87 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
close = (name: keyof NavBarState) => () =>
this.setState({ [name]: false });
syncButton = () => {
return <SyncButton
ReadOnlyStatus = () =>
<ReadOnlyIcon locked={!!this.props.getConfigValue(
BooleanSetting.user_interface_read_only_mode)} />
SyncButton = () =>
<SyncButton
bot={this.props.bot}
dispatch={this.props.dispatch}
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() {
return connectivityData({
ConnectionStatus = () => {
const data = connectivityData({
bot: this.props.bot,
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() {
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. */
updatePageInfo(pageName);
updatePageInfo(getPathArray()[2] || "");
const { toggle, close } = this;
const { mobileMenuOpen, tickerListOpen, accountMenuOpen } = this.state;
const { logs, timeSettings, getConfigValue, alertCount } = this.props;
const { toggle } = this;
const { tickerListOpen } = this.state;
const { logs, timeSettings, getConfigValue } = this.props;
const tickerListProps = {
logs, tickerListOpen, toggle, timeSettings, getConfigValue
};
@ -89,51 +134,17 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
<TickerList {...tickerListProps} />
<div className="nav-group">
<div className="nav-left">
<i
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>
<this.AppNavLinks />
</div>
<div className="nav-right">
<ReadOnlyIcon locked={!!isLocked} />
<div className="menu-popover">
<Popover
portalClassName={"nav-right"}
popoverClassName={"menu-popover"}
position={Position.BOTTOM_RIGHT}
isOpen={accountMenuOpen}
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} />
<ErrorBoundary>
<this.ReadOnlyStatus />
<this.AccountMenu />
<this.EstopButton />
<this.SyncButton />
<this.ConnectionStatus />
<RunTour currentTour={this.props.tour} />
</ErrorBoundary>
</div>
</div>
</div>

View File

@ -20,6 +20,7 @@ import { Actions } from "../../constants";
import { reduceVariables } from "../../sequences/locals_list/variable_support";
import { determineDropdown, withPrefix } from "../../resources/sequence_meta";
import { ResourceIndex } from "../../resources/interfaces";
import { ErrorBoundary } from "../../error_boundary";
/**
* 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">
<RegimenButtonGroup {...this.regimenProps} />
<RegimenNameInput {...this.regimenProps} />
<this.LocalsList />
<ErrorBoundary>
<this.LocalsList />
</ErrorBoundary>
<hr />
</div>
<OpenSchedulerButton dispatch={this.props.dispatch} />
<RegimenRows {...this.regimenProps}
calendar={this.props.calendar}
varsCollapsed={this.state.variablesCollapsed}
resources={this.props.resources} />
<ErrorBoundary>
<RegimenRows {...this.regimenProps}
calendar={this.props.calendar}
varsCollapsed={this.state.variablesCollapsed}
resources={this.props.resources} />
</ErrorBoundary>
</div>;
}
}

View File

@ -9,6 +9,8 @@ import { getStepTag } from "../resources/sequence_tagging";
import { HardwareFlags, FarmwareInfo } from "./interfaces";
import { ShouldDisplay } from "../devices/interfaces";
import { AddCommandButton } from "./sequence_editor_middle_active";
import { ErrorBoundary } from "../error_boundary";
import { TileUnknown } from "./step_tiles/tile_unknown";
export interface AllStepsProps {
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
* allows React to diff the list correctly. */
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"
key={readThatCommentAbove}>
<AddCommandButton dispatch={dispatch} index={index} />
@ -44,19 +59,9 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
intent="step_move"
draggerId={index}>
<div className="sequence-step">
{renderCeleryNode({
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,
})}
<ErrorBoundary fallback={<TileUnknown {...stepProps} />}>
{renderCeleryNode(stepProps)}
</ErrorBoundary>
</div>
</StepDragger>
</div>;

View File

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

View File

@ -13,12 +13,12 @@ import { conflictsString } from "../step_warning";
describe("<StepWrapper />", () => {
it("renders", () => {
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", () => {
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 />", () => {
it("renders", () => {
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-class")).toBeTruthy();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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