increase error containment
parent
5e52d3c6dd
commit
c2d80bd55c
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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:
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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 />;
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -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>;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)} />
|
||||||
|
|
|
@ -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)} />
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} />}
|
||||||
|
|
Loading…
Reference in New Issue