connection status indicator
parent
b42a0aee10
commit
e873b9a6f4
|
@ -1,6 +1,4 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
let mockPath = "";
|
||||
jest.mock("../history", () => ({
|
||||
|
@ -15,12 +13,14 @@ import {
|
|||
fakeUser, fakeWebAppConfig
|
||||
} from "../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../__test_support__/fake_state";
|
||||
import { buildResourceIndex } from "../__test_support__/resource_index_builder";
|
||||
import {
|
||||
buildResourceIndex
|
||||
} from "../__test_support__/resource_index_builder";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { ResourceName } from "farmbot";
|
||||
|
||||
const FULLY_LOADED: ResourceName[] = [
|
||||
"Sequence", "Regimen", "FarmEvent", "Point", "Tool"];
|
||||
"Sequence", "Regimen", "FarmEvent", "Point", "Tool", "Device"];
|
||||
|
||||
const fakeProps = (): AppProps => {
|
||||
return {
|
||||
|
@ -37,6 +37,7 @@ const fakeProps = (): AppProps => {
|
|||
animate: false,
|
||||
getConfigValue: jest.fn(),
|
||||
tour: undefined,
|
||||
resources: buildResourceIndex().index,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -111,14 +112,18 @@ describe("<App />: Loading", () => {
|
|||
|
||||
describe("<App />: NavBar", () => {
|
||||
it("displays links", () => {
|
||||
const wrapper = mount(<App {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.loaded = FULLY_LOADED;
|
||||
const wrapper = mount(<App {...p} />);
|
||||
expect(wrapper.text())
|
||||
.toContain("Farm DesignerControlsDeviceSequencesRegimensToolsFarmware");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("displays ticker", () => {
|
||||
const wrapper = mount(<App {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.loaded = FULLY_LOADED;
|
||||
const wrapper = mount(<App {...p} />);
|
||||
expect(wrapper.text()).toContain("No logs yet.");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { init, error } from "farmbot-toastr";
|
||||
import { NavBar } from "./nav";
|
||||
import { Everything } from "./interfaces";
|
||||
import { LoadingPlant } from "./loading_plant";
|
||||
import { BotState, Xyz } from "./devices/interfaces";
|
||||
import { ResourceName, TaggedUser, TaggedLog } from "farmbot";
|
||||
import {
|
||||
ResourceName, TaggedUser, TaggedLog
|
||||
} from "farmbot";
|
||||
import {
|
||||
maybeFetchUser,
|
||||
maybeGetTimeOffset,
|
||||
maybeFetchUser, maybeGetTimeOffset, getDeviceAccountSettings
|
||||
} from "./resources/selectors";
|
||||
import { HotKeys } from "./hotkeys";
|
||||
import { ControlsPopup } from "./controls_popup";
|
||||
|
@ -19,12 +15,15 @@ import { Content } from "./constants";
|
|||
import { validBotLocationData, validFwConfig } from "./util";
|
||||
import { BooleanSetting } from "./session_keys";
|
||||
import { getPathArray } from "./history";
|
||||
import { getWebAppConfigValue, GetWebAppConfigValue } from "./config_storage/actions";
|
||||
import {
|
||||
getWebAppConfigValue, GetWebAppConfigValue
|
||||
} from "./config_storage/actions";
|
||||
import { takeSortedLogs } from "./logs/state_to_props";
|
||||
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
|
||||
import { getFirmwareConfig } from "./resources/getters";
|
||||
import { intersection } from "lodash";
|
||||
import { t } from "./i18next_wrapper";
|
||||
import { ResourceIndex } from "./resources/interfaces";
|
||||
|
||||
/** For the logger module */
|
||||
init();
|
||||
|
@ -43,6 +42,7 @@ export interface AppProps {
|
|||
animate: boolean;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
tour: string | undefined;
|
||||
resources: ResourceIndex;
|
||||
}
|
||||
|
||||
export function mapStateToProps(props: Everything): AppProps {
|
||||
|
@ -65,6 +65,7 @@ export function mapStateToProps(props: Everything): AppProps {
|
|||
animate: !webAppConfigValue(BooleanSetting.disable_animations),
|
||||
getConfigValue: webAppConfigValue,
|
||||
tour: props.resources.consumers.help.currentTour,
|
||||
resources: props.resources.index,
|
||||
};
|
||||
}
|
||||
/** Time at which the app gives up and asks the user to refresh */
|
||||
|
@ -79,6 +80,7 @@ const MUST_LOAD: ResourceName[] = [
|
|||
"Regimen",
|
||||
"FarmEvent",
|
||||
"Point",
|
||||
"Device",
|
||||
"Tool" // Sequence editor needs this for rendering.
|
||||
];
|
||||
|
||||
|
@ -108,7 +110,7 @@ export class App extends React.Component<AppProps, {}> {
|
|||
return <div className="app">
|
||||
{!syncLoaded && <LoadingPlant animate={this.props.animate} />}
|
||||
<HotKeys dispatch={this.props.dispatch} />
|
||||
<NavBar
|
||||
{syncLoaded && <NavBar
|
||||
timeOffset={this.props.timeOffset}
|
||||
consistent={this.props.consistent}
|
||||
user={this.props.user}
|
||||
|
@ -116,7 +118,8 @@ export class App extends React.Component<AppProps, {}> {
|
|||
dispatch={this.props.dispatch}
|
||||
logs={this.props.logs}
|
||||
getConfigValue={this.props.getConfigValue}
|
||||
tour={this.props.tour} />
|
||||
tour={this.props.tour}
|
||||
device={getDeviceAccountSettings(this.props.resources)} />}
|
||||
{syncLoaded && this.props.children}
|
||||
{!(["controls", "account", "regimens"].includes(currentPage)) &&
|
||||
<ControlsPopup
|
||||
|
|
|
@ -684,6 +684,47 @@ export namespace TourContent {
|
|||
trim(`Toggle various settings to customize your web app experience.`);
|
||||
}
|
||||
|
||||
export namespace DiagnosticMessages {
|
||||
export const OK = trim(`All systems nominal.`);
|
||||
|
||||
export const MISC = trim(`Some other issue is preventing FarmBot from
|
||||
working. Please see the table above for more information.`);
|
||||
|
||||
export const TOTAL_BREAKAGE = trim(`There is no access to FarmBot or the
|
||||
message broker. This is usually caused by outdated browsers
|
||||
(Internet Explorer) or firewalls that block WebSockets on port 3002.`);
|
||||
|
||||
export const REMOTE_FIREWALL = trim(`FarmBot and the browser are both
|
||||
connected to the internet (or have been recently). Try rebooting FarmBot
|
||||
and refreshing the browser. If the issue persists, something may be
|
||||
preventing FarmBot from accessing the message broker (used to communicate
|
||||
with your web browser in real-time). If you are on a company or school
|
||||
network, a firewall may be blocking port 5672.`);
|
||||
|
||||
export const WIFI_OR_CONFIG = trim(`Your browser is connected correctly,
|
||||
but we have no recent record of FarmBot connecting to the internet.
|
||||
This usually happens because of a bad WiFi signal in the garden, a bad
|
||||
password during configuration, or a very long power outage.`);
|
||||
|
||||
export const NO_WS_AVAILABLE = trim(`You are either offline, using a web
|
||||
browser that does not support WebSockets, or are behind a firewall that
|
||||
blocks port 3002. Do not attempt to debug FarmBot hardware until you solve
|
||||
this issue first. You will not be able to troubleshoot hardware issues
|
||||
without a reliable browser and internet connection.`);
|
||||
|
||||
export const INACTIVE = trim(`FarmBot and the browser both have internet
|
||||
connectivity, but we haven't seen any activity from FarmBot on the Web
|
||||
App in a while. This could mean that FarmBot has not synced in a while,
|
||||
which might not be a problem. If you are experiencing usability issues,
|
||||
however, it could be a sign of HTTP blockage on FarmBot's local internet
|
||||
connection.`);
|
||||
|
||||
export const ARDUINO_DISCONNECTED = trim(`Arduino is possibly unplugged.
|
||||
Check the USB cable between the Raspberry Pi and the Arduino. Reboot
|
||||
FarmBot after a reconnection. If the issue persists, reconfiguration
|
||||
of FarmBot OS may be necessary.`);
|
||||
}
|
||||
|
||||
export enum Actions {
|
||||
|
||||
// Resources
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../../devices/timezones/guess_timezone", () => ({
|
||||
maybeSetTimezone: jest.fn()
|
||||
}));
|
||||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
|
@ -15,9 +9,6 @@ import {
|
|||
} from "../../__test_support__/fake_state/resources";
|
||||
import { Dictionary } from "farmbot";
|
||||
import { Props } from "../interfaces";
|
||||
import { fakeDevice } from "../../__test_support__/resource_index_builder";
|
||||
|
||||
import { maybeSetTimezone } from "../../devices/timezones/guess_timezone";
|
||||
|
||||
describe("<Controls />", () => {
|
||||
const mockConfig: Dictionary<boolean> = {};
|
||||
|
@ -35,7 +26,6 @@ describe("<Controls />", () => {
|
|||
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
|
||||
sensorReadings: [],
|
||||
timeOffset: 0,
|
||||
device: undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -79,12 +69,4 @@ describe("<Controls />", () => {
|
|||
const txt = wrapper.text().toLowerCase();
|
||||
expect(txt).toContain("sensor history");
|
||||
});
|
||||
|
||||
it("silently sets user timezone as needed", () => {
|
||||
const p = fakeProps();
|
||||
p.device = fakeDevice({ timezone: undefined });
|
||||
mount(<Controls {...p} />);
|
||||
const { dispatch, device } = p;
|
||||
expect(maybeSetTimezone).toHaveBeenCalledWith(dispatch, device);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,17 +10,10 @@ import { Move } from "./move/move";
|
|||
import { BooleanSetting } from "../session_keys";
|
||||
import { Feature } from "../devices/interfaces";
|
||||
import { SensorReadings } from "./sensor_readings/sensor_readings";
|
||||
import { maybeSetTimezone } from "../devices/timezones/guess_timezone";
|
||||
|
||||
/** Controls page. */
|
||||
@connect(mapStateToProps)
|
||||
export class Controls extends React.Component<Props, {}> {
|
||||
|
||||
componentDidMount = () => {
|
||||
this.props.device &&
|
||||
maybeSetTimezone(this.props.dispatch, this.props.device);
|
||||
}
|
||||
|
||||
get arduinoBusy() {
|
||||
return !!this.props.bot.hardware.informational_settings.busy;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BotState, Xyz, BotPosition, ShouldDisplay } from "../devices/interfaces";
|
||||
import { Vector3, McuParams, TaggedDevice } from "farmbot/dist";
|
||||
import { Vector3, McuParams } from "farmbot/dist";
|
||||
import {
|
||||
TaggedWebcamFeed,
|
||||
TaggedPeripheral,
|
||||
|
@ -21,7 +21,6 @@ export interface Props {
|
|||
getWebAppConfigVal: GetWebAppConfigValue;
|
||||
sensorReadings: TaggedSensorReading[];
|
||||
timeOffset: number;
|
||||
device: TaggedDevice | undefined;
|
||||
}
|
||||
|
||||
export interface AxisDisplayGroupProps {
|
||||
|
|
|
@ -16,29 +16,26 @@ import { getFirmwareConfig } from "../resources/getters";
|
|||
import { uniq } from "lodash";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const { mcu_params } = props.bot.hardware;
|
||||
const bot2mqtt = props.bot.connectivity["bot.mqtt"];
|
||||
const botToMqttStatus = bot2mqtt ? bot2mqtt.state : "down";
|
||||
const device = maybeGetDevice(props.resources.index);
|
||||
|
||||
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
|
||||
const getWebAppConfigVal = getWebAppConfigValue(() => props);
|
||||
const { mcu_params } = props.bot.hardware;
|
||||
|
||||
const device = maybeGetDevice(props.resources.index);
|
||||
const installedOsVersion = determineInstalledOsVersion(props.bot, device);
|
||||
const peripherals = uniq(selectAllPeripherals(props.resources.index));
|
||||
const resources = props.resources;
|
||||
const sensors = uniq(selectAllSensors(props.resources.index));
|
||||
|
||||
return {
|
||||
feeds: selectAllWebcamFeeds(resources.index),
|
||||
feeds: selectAllWebcamFeeds(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
bot: props.bot,
|
||||
peripherals,
|
||||
sensors,
|
||||
peripherals: uniq(selectAllPeripherals(props.resources.index)),
|
||||
sensors: uniq(selectAllSensors(props.resources.index)),
|
||||
botToMqttStatus,
|
||||
firmwareSettings: fwConfig || mcu_params,
|
||||
shouldDisplay: shouldDisplay(installedOsVersion, props.bot.minOsFeatureData),
|
||||
getWebAppConfigVal,
|
||||
getWebAppConfigVal: getWebAppConfigValue(() => props),
|
||||
sensorReadings: selectAllSensorReadings(props.resources.index),
|
||||
timeOffset: maybeGetTimeOffset(props.resources.index),
|
||||
device
|
||||
};
|
||||
}
|
||||
|
|
|
@ -132,6 +132,29 @@ fieldset {
|
|||
pointer-events: all;
|
||||
}
|
||||
|
||||
.connectivity-popover-portal {
|
||||
.bp3-transition-container {
|
||||
z-index: 999;
|
||||
}
|
||||
.connectivity-popover {
|
||||
.connectivity {
|
||||
padding: 1rem;
|
||||
max-width: 600px;
|
||||
.row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.connectivity-diagram svg {
|
||||
max-height: 200px !important;
|
||||
}
|
||||
.fbos-info {
|
||||
@media (max-width:767px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connectivity-diagnosis {
|
||||
h4 {
|
||||
margin-left: 3rem;
|
||||
|
|
|
@ -92,49 +92,55 @@ nav {
|
|||
margin-right: 0.8rem;
|
||||
}
|
||||
}
|
||||
.app-version {
|
||||
color: $white;
|
||||
a {
|
||||
color: $white;
|
||||
.connection-status-popover {
|
||||
display: inline;
|
||||
.bp3-popover-wrapper {
|
||||
margin: 1.85rem;
|
||||
}
|
||||
}
|
||||
.bp3-popover-content {
|
||||
position: relative;
|
||||
width: 22rem;
|
||||
a:not(.app-version) {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.6rem;
|
||||
.menu-popover {
|
||||
display: inline;
|
||||
.bp3-popover-content {
|
||||
position: relative;
|
||||
width: 22rem;
|
||||
a:not(.app-version) {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.app-version {
|
||||
margin: 1rem -1rem -1rem;
|
||||
background: $dark_gray;
|
||||
color: $white;
|
||||
padding: 0.5rem 0 0 1rem;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
label {
|
||||
color: $white;
|
||||
}
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
color: $gray;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.app-version {
|
||||
margin: 1rem -1rem -1rem;
|
||||
background: $dark_gray;
|
||||
.bp3-overlay-content {
|
||||
margin-top: 1.6rem;
|
||||
color: $white;
|
||||
padding: 0.5rem 0 0 1rem;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
label {
|
||||
}
|
||||
.bp3-popover-wrapper {
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
p {
|
||||
display: inline;
|
||||
color: $gray;
|
||||
font-size: 1.2rem;
|
||||
.bp3-popover-arrow-fill {
|
||||
fill: $dark_gray;
|
||||
}
|
||||
.bp3-popover-content {
|
||||
background: $dark_gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bp3-overlay-content {
|
||||
margin-top: 1.6rem;
|
||||
color: $white;
|
||||
}
|
||||
.bp3-popover-wrapper {
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
.bp3-popover-arrow-fill {
|
||||
fill: $dark_gray;
|
||||
}
|
||||
.bp3-popover-content {
|
||||
background: $dark_gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,6 @@ jest.mock("react-redux", () => ({
|
|||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../actions", () => ({
|
||||
resetConnectionInfo: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { shallow, render } from "enzyme";
|
||||
import { Devices } from "../devices";
|
||||
|
@ -16,10 +12,9 @@ import {
|
|||
fakeDevice, buildResourceIndex, FAKE_RESOURCES
|
||||
} from "../../__test_support__/resource_index_builder";
|
||||
import { FarmbotOsSettings } from "../components/farmbot_os_settings";
|
||||
import { resetConnectionInfo } from "../actions";
|
||||
|
||||
describe("<Devices/>", () => {
|
||||
const p = (): Props => ({
|
||||
const fakeProps = (): Props => ({
|
||||
userToApi: undefined,
|
||||
userToMqtt: undefined,
|
||||
botToMqtt: undefined,
|
||||
|
@ -38,23 +33,22 @@ describe("<Devices/>", () => {
|
|||
saveFarmwareEnv: jest.fn(),
|
||||
});
|
||||
|
||||
it("resets connection info", () => {
|
||||
const el = shallow<Devices>(<Devices {...p()} />);
|
||||
const devices: Devices = el.instance();
|
||||
jest.resetAllMocks();
|
||||
expect(devices.props.dispatch).not.toHaveBeenCalled();
|
||||
devices.refresh();
|
||||
expect(devices.props.dispatch).toHaveBeenCalled();
|
||||
expect(resetConnectionInfo).toHaveBeenCalled();
|
||||
});
|
||||
it("renders relevant panels", () => {
|
||||
const el = shallow(<Devices {...p()} />);
|
||||
const el = shallow(<Devices {...fakeProps()} />);
|
||||
expect(el.find(FarmbotOsSettings).length).toBe(1);
|
||||
});
|
||||
|
||||
it("Crashes when logged out", () => {
|
||||
const props = p();
|
||||
props.auth = undefined;
|
||||
expect(() => render(<Devices {...props} />)).toThrow();
|
||||
it("crashes when logged out", () => {
|
||||
const p = fakeProps();
|
||||
p.auth = undefined;
|
||||
expect(() => render(<Devices {...p} />)).toThrow();
|
||||
});
|
||||
|
||||
it("has correct connection status", () => {
|
||||
const p = fakeProps();
|
||||
p.botToMqtt = { at: "123", state: "up" };
|
||||
const wrapper = shallow(<Devices {...p} />);
|
||||
expect(wrapper.find(FarmbotOsSettings).props().botToMqttLastSeen)
|
||||
.toEqual("123");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Connectivity, ConnectivityProps } from "../connectivity";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { StatusRowProps } from "../connectivity_row";
|
||||
import { fill } from "lodash";
|
||||
|
||||
describe("<Connectivity />", () => {
|
||||
const statusRow = {
|
||||
connectionName: "AB",
|
||||
from: "A",
|
||||
to: "B",
|
||||
connectionStatus: false,
|
||||
children: "Can't do things with stuff."
|
||||
};
|
||||
const rowData: StatusRowProps[] = fill(Array(5), statusRow);
|
||||
const flags = {
|
||||
userMQTT: false,
|
||||
userAPI: false,
|
||||
botMQTT: false,
|
||||
botAPI: false,
|
||||
botFirmware: false,
|
||||
};
|
||||
|
||||
const fakeProps = (): ConnectivityProps => ({
|
||||
bot,
|
||||
rowData,
|
||||
flags,
|
||||
});
|
||||
|
||||
it("sets hovered connection", () => {
|
||||
const wrapper = mount<Connectivity>(<Connectivity {...fakeProps()} />);
|
||||
wrapper.find(".saucer").at(6).simulate("mouseEnter");
|
||||
expect(wrapper.instance().state.hoveredConnection).toEqual("AB");
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { diagnose, Diagnosis } from "../diagnosis";
|
||||
import { DiagnosticMessages } from "../diagnostic_messages";
|
||||
import { diagnose, Diagnosis, DiagnosisSaucer } from "../diagnosis";
|
||||
import { DiagnosticMessages } from "../../../constants";
|
||||
import { mount } from "enzyme";
|
||||
|
||||
describe("<Diagnosis/>", () => {
|
||||
|
@ -26,6 +26,20 @@ describe("<Diagnosis/>", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("<DiagnosisSaucer />", () => {
|
||||
it("renders green", () => {
|
||||
const flags = {
|
||||
userMQTT: true,
|
||||
userAPI: true,
|
||||
botMQTT: true,
|
||||
botAPI: true,
|
||||
botFirmware: true,
|
||||
};
|
||||
const wrapper = mount(<DiagnosisSaucer {...flags} />);
|
||||
expect(wrapper.find(".saucer").hasClass("green")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("diagnose()", () => {
|
||||
function testDiagnosis(msg: string,
|
||||
userAPI: boolean,
|
||||
|
|
|
@ -1,46 +1,33 @@
|
|||
jest.mock("../../actions", () => ({ resetConnectionInfo: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { render, mount } from "enzyme";
|
||||
import { render, shallow } from "enzyme";
|
||||
import { ConnectivityPanel } from "../index";
|
||||
import { StatusRowProps } from "../connectivity_row";
|
||||
import { SpecialStatus } from "farmbot";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { fill } from "lodash";
|
||||
import { fakeDevice } from "../../../__test_support__/resource_index_builder";
|
||||
import { resetConnectionInfo } from "../../actions";
|
||||
|
||||
describe("<ConnectivityPanel/>", () => {
|
||||
function test() {
|
||||
const onRefresh = jest.fn();
|
||||
const statusRow = {
|
||||
connectionName: "AB",
|
||||
from: "A",
|
||||
to: "B",
|
||||
connectionStatus: false,
|
||||
children: "Can't do things with stuff."
|
||||
};
|
||||
const rowData: StatusRowProps[] = fill(Array(5), statusRow);
|
||||
|
||||
return {
|
||||
component: <ConnectivityPanel
|
||||
onRefresh={onRefresh}
|
||||
rowData={rowData}
|
||||
status={SpecialStatus.SAVED}
|
||||
fbosInfo={bot.hardware.informational_settings}>
|
||||
<p>I am a child component.</p>
|
||||
</ConnectivityPanel>,
|
||||
rowData: rowData
|
||||
};
|
||||
}
|
||||
const fakeProps = (): ConnectivityPanel["props"] => ({
|
||||
bot: bot,
|
||||
dispatch: jest.fn(),
|
||||
deviceAccount: fakeDevice(),
|
||||
status: SpecialStatus.SAVED,
|
||||
});
|
||||
|
||||
it("renders the default use case", () => {
|
||||
const testcase = test();
|
||||
const el = render(testcase.component);
|
||||
expect(el.text()).toContain("I am a child");
|
||||
expect(el.text()).toContain(testcase.rowData[0].children);
|
||||
const p = fakeProps();
|
||||
const wrapper = render(<ConnectivityPanel {...p} />);
|
||||
["Check Again", "Connectivity"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("sets hovered connection", () => {
|
||||
const testcase = test();
|
||||
const el = mount<ConnectivityPanel>(testcase.component);
|
||||
el.find(".saucer").last().simulate("mouseEnter");
|
||||
expect(el.instance().state.hoveredConnection).toEqual("AB");
|
||||
it("resets connection info", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow<ConnectivityPanel>(<ConnectivityPanel {...p} />);
|
||||
wrapper.instance().refresh();
|
||||
expect(p.dispatch).toHaveBeenCalled();
|
||||
expect(resetConnectionInfo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react";
|
||||
import { BotState } from "../interfaces";
|
||||
import { Diagnosis, DiagnosisProps } from "./diagnosis";
|
||||
import { ConnectivityRow, StatusRowProps } from "./connectivity_row";
|
||||
import { Row, Col } from "../../ui";
|
||||
import { ConnectivityDiagram } from "./diagram";
|
||||
import {
|
||||
ChipTemperatureDisplay, WiFiStrengthDisplay
|
||||
} from "../components/fbos_settings/fbos_details";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
export interface ConnectivityProps {
|
||||
bot: BotState;
|
||||
rowData: StatusRowProps[];
|
||||
flags: DiagnosisProps;
|
||||
}
|
||||
|
||||
interface ConnectivityState {
|
||||
hoveredConnection: string | undefined;
|
||||
}
|
||||
|
||||
export class Connectivity
|
||||
extends React.Component<ConnectivityProps, ConnectivityState> {
|
||||
state: ConnectivityState = { hoveredConnection: undefined };
|
||||
|
||||
hover = (name: string) =>
|
||||
() => this.setState({ hoveredConnection: name });
|
||||
|
||||
render() {
|
||||
const { soc_temp, wifi_level } = this.props.bot.hardware.informational_settings;
|
||||
return <div className="connectivity">
|
||||
<Row>
|
||||
<Col md={12} lg={4}>
|
||||
<ConnectivityDiagram
|
||||
rowData={this.props.rowData}
|
||||
hover={this.hover}
|
||||
hoveredConnection={this.state.hoveredConnection} />
|
||||
<div className="fbos-info">
|
||||
<label>{t("Raspberry Pi Info")}</label>
|
||||
<ChipTemperatureDisplay temperature={soc_temp} />
|
||||
<WiFiStrengthDisplay wifiStrength={wifi_level} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col md={12} lg={8}>
|
||||
<ConnectivityRow from={t("from")} to={t("to")} header={true} />
|
||||
{this.props.rowData
|
||||
.map((x, y) => <ConnectivityRow {...x} key={y}
|
||||
hover={this.hover}
|
||||
hoveredConnection={this.state.hoveredConnection} />)}
|
||||
<hr style={{ marginLeft: "3rem" }} />
|
||||
<Diagnosis {...this.props.flags} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { DiagnosticMessages } from "./diagnostic_messages";
|
||||
import { DiagnosticMessages } from "../../constants";
|
||||
import { Col, Row } from "../../ui/index";
|
||||
import { bitArray } from "../../util";
|
||||
import { TRUTH_TABLE } from "./truth_table";
|
||||
|
@ -14,11 +14,20 @@ export type DiagnosisName =
|
|||
|
||||
export type DiagnosisProps = Record<DiagnosisName, boolean>;
|
||||
|
||||
export const diagnosisStatus = (props: DiagnosisProps): boolean =>
|
||||
props.userMQTT && props.botAPI && props.botMQTT && props.botFirmware;
|
||||
|
||||
export const DiagnosisSaucer = (props: DiagnosisProps) => {
|
||||
const diagnosisBoolean = diagnosisStatus(props);
|
||||
const diagnosisColor = diagnosisBoolean ? "green" : "red";
|
||||
const title = diagnosisBoolean ? t("Ok") : t("Error");
|
||||
return <div className={"saucer active " + diagnosisColor} title={title} />;
|
||||
};
|
||||
|
||||
export function Diagnosis(props: DiagnosisProps) {
|
||||
const diagnosisStatus =
|
||||
props.userMQTT && props.botAPI && props.botMQTT && props.botFirmware;
|
||||
const diagnosisColor = diagnosisStatus ? "green" : "red";
|
||||
const title = diagnosisStatus ? t("Ok") : t("Error");
|
||||
const diagnosisBoolean = diagnosisStatus(props);
|
||||
const diagnosisColor = diagnosisBoolean ? "green" : "red";
|
||||
const title = diagnosisBoolean ? t("Ok") : t("Error");
|
||||
return <div>
|
||||
<div className={"connectivity-diagnosis"}>
|
||||
<h4>{t("Diagnosis")}</h4>
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import { trim } from "../../util";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
|
||||
export namespace DiagnosticMessages {
|
||||
// "SCV good to go, sir." is also appropriate.
|
||||
export const OK = t("All systems nominal.");
|
||||
|
||||
export const MISC = trim(t(`Some other issue is preventing FarmBot from
|
||||
working. Please see the table above for more information.`));
|
||||
|
||||
export const TOTAL_BREAKAGE = trim(t(`There is no access to FarmBot or the
|
||||
message broker. This is usually caused by outdated browsers
|
||||
(Internet Explorer) or firewalls that block WebSockets on port 3002.`));
|
||||
|
||||
export const REMOTE_FIREWALL = trim(t(`FarmBot and the browser are both
|
||||
connected to the internet (or have been recently). Try rebooting FarmBot
|
||||
and refreshing the browser. If the issue persists, something may be
|
||||
preventing FarmBot from accessing the message broker (used to communicate
|
||||
with your web browser in real-time). If you are on a company or school
|
||||
network, a firewall may be blocking port 5672.`));
|
||||
|
||||
export const WIFI_OR_CONFIG = trim(t(`Your browser is connected correctly,
|
||||
but we have no recent record of FarmBot connecting to the internet.
|
||||
This usually happens because of a bad WiFi signal in the garden, a bad
|
||||
password during configuration, or a very long power outage.`));
|
||||
|
||||
export const NO_WS_AVAILABLE = trim(t(`You are either offline, using a web
|
||||
browser that does not support WebSockets, or are behind a firewall that
|
||||
blocks port 3002. Do not attempt to debug FarmBot hardware until you solve
|
||||
this issue first. You will not be able to troubleshoot hardware issues
|
||||
without a reliable browser and internet connection.`));
|
||||
|
||||
export const INACTIVE = trim(t(`FarmBot and the browser both have internet
|
||||
connectivity, but we haven't seen any activity from FarmBot on the Web
|
||||
App in a while. This could mean that FarmBot has not synced in a while,
|
||||
which might not be a problem. If you are experiencing usability issues,
|
||||
however, it could be a sign of HTTP blockage on FarmBot's local internet
|
||||
connection.`));
|
||||
|
||||
export const ARDUINO_DISCONNECTED = trim(t(`Arduino is possibly unplugged.
|
||||
Check the USB cable between the Raspberry Pi and the Arduino. Reboot
|
||||
FarmBot after a reconnection. If the issue persists, reconfiguration
|
||||
of FarmBot OS may be necessary.`));
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { TaggedDevice } from "farmbot";
|
||||
import { BotState } from "../interfaces";
|
||||
import { DiagnosisName, DiagnosisProps } from "./diagnosis";
|
||||
import { StatusRowProps } from "./connectivity_row";
|
||||
import {
|
||||
browserToMQTT, browserToAPI, botToMQTT, botToAPI, botToFirmware
|
||||
} from "./status_checks";
|
||||
|
||||
interface ConnectivityDataProps {
|
||||
bot: BotState;
|
||||
device: TaggedDevice;
|
||||
}
|
||||
|
||||
export const connectivityData = (props: ConnectivityDataProps) => {
|
||||
const fwVersion = props.bot.hardware
|
||||
.informational_settings.firmware_version;
|
||||
|
||||
/** A record of all the things we know about connectivity right now. */
|
||||
const data: Record<DiagnosisName, StatusRowProps> = {
|
||||
userMQTT: browserToMQTT(props.bot.connectivity["user.mqtt"]),
|
||||
userAPI: browserToAPI(props.bot.connectivity["user.api"]),
|
||||
botMQTT: botToMQTT(props.bot.connectivity["bot.mqtt"]),
|
||||
botAPI: botToAPI(props.device.body.last_saw_api),
|
||||
botFirmware: botToFirmware(fwVersion),
|
||||
};
|
||||
|
||||
const flags: DiagnosisProps = {
|
||||
userMQTT: !!data.userMQTT.connectionStatus,
|
||||
userAPI: !!data.userAPI,
|
||||
botMQTT: !!data.botMQTT.connectionStatus,
|
||||
botAPI: !!data.botAPI.connectionStatus,
|
||||
botFirmware: !!data.botFirmware.connectionStatus,
|
||||
};
|
||||
|
||||
/** Shuffle these around to change the ordering of the status table. */
|
||||
const rowData: StatusRowProps[] = [
|
||||
data.userAPI,
|
||||
data.userMQTT,
|
||||
data.botMQTT,
|
||||
data.botAPI,
|
||||
data.botFirmware,
|
||||
];
|
||||
return { data, flags, rowData };
|
||||
};
|
|
@ -1,68 +1,45 @@
|
|||
import * as React from "react";
|
||||
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui/index";
|
||||
import { ConnectivityRow, StatusRowProps } from "./connectivity_row";
|
||||
import { Widget, WidgetHeader, WidgetBody } from "../../ui";
|
||||
import { RetryBtn } from "./retry_btn";
|
||||
import { SpecialStatus, InformationalSettings } from "farmbot";
|
||||
import { ConnectivityDiagram } from "./diagram";
|
||||
import { SpecialStatus, TaggedDevice } from "farmbot";
|
||||
import { ToolTips } from "../../constants";
|
||||
import {
|
||||
ChipTemperatureDisplay, WiFiStrengthDisplay
|
||||
} from "../components/fbos_settings/fbos_details";
|
||||
import { t } from "../../i18next_wrapper";
|
||||
import { Connectivity } from "./connectivity";
|
||||
import { BotState } from "../interfaces";
|
||||
import { connectivityData } from "./generate_data";
|
||||
import { resetConnectionInfo } from "../actions";
|
||||
|
||||
interface Props {
|
||||
onRefresh(): void;
|
||||
rowData: StatusRowProps[];
|
||||
children?: React.ReactChild;
|
||||
status: SpecialStatus;
|
||||
fbosInfo: InformationalSettings;
|
||||
dispatch: Function;
|
||||
bot: BotState;
|
||||
deviceAccount: TaggedDevice;
|
||||
}
|
||||
|
||||
interface ConnectivityState {
|
||||
hoveredConnection: string | undefined;
|
||||
}
|
||||
export class ConnectivityPanel extends React.Component<Props, {}> {
|
||||
get data() {
|
||||
return connectivityData({
|
||||
bot: this.props.bot, device: this.props.deviceAccount
|
||||
});
|
||||
}
|
||||
|
||||
export class ConnectivityPanel extends React.Component<Props, ConnectivityState> {
|
||||
state: ConnectivityState = { hoveredConnection: undefined };
|
||||
|
||||
hover = (name: string) =>
|
||||
() => this.setState({ hoveredConnection: name });
|
||||
refresh = () => this.props.dispatch(resetConnectionInfo());
|
||||
|
||||
render() {
|
||||
const { rowData } = this.props;
|
||||
const { soc_temp, wifi_level } = this.props.fbosInfo;
|
||||
return <Widget className="connectivity-widget">
|
||||
<WidgetHeader
|
||||
title={t("Connectivity")}
|
||||
helpText={ToolTips.CONNECTIVITY}>
|
||||
<RetryBtn
|
||||
status={this.props.status}
|
||||
onClick={this.props.onRefresh}
|
||||
flags={rowData.map(x => !!x.connectionStatus)} />
|
||||
onClick={this.refresh}
|
||||
flags={this.data.rowData.map(x => !!x.connectionStatus)} />
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<Row>
|
||||
<Col md={12} lg={4}>
|
||||
<ConnectivityDiagram
|
||||
rowData={rowData}
|
||||
hover={this.hover}
|
||||
hoveredConnection={this.state.hoveredConnection} />
|
||||
<div className="fbos-info">
|
||||
<label>{t("Raspberry Pi Info")}</label>
|
||||
<ChipTemperatureDisplay temperature={soc_temp} />
|
||||
<WiFiStrengthDisplay wifiStrength={wifi_level} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col md={12} lg={8}>
|
||||
<ConnectivityRow from={t("from")} to={t("to")} header={true} />
|
||||
{rowData
|
||||
.map((x, y) => <ConnectivityRow {...x} key={y}
|
||||
hover={this.hover}
|
||||
hoveredConnection={this.state.hoveredConnection} />)}
|
||||
<hr style={{ marginLeft: "3rem" }} />
|
||||
{this.props.children}
|
||||
</Col>
|
||||
</Row>
|
||||
<Connectivity
|
||||
bot={this.props.bot}
|
||||
rowData={this.data.rowData}
|
||||
flags={this.data.flags} />
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Dictionary } from "farmbot";
|
||||
import { DiagnosticMessages } from "./diagnostic_messages";
|
||||
import { DiagnosticMessages } from "../../constants";
|
||||
|
||||
// I don't like this at all.
|
||||
// If anyone has a cleaner solution, I'd love to hear it.
|
||||
|
|
|
@ -5,49 +5,12 @@ import { FarmbotOsSettings } from "./components/farmbot_os_settings";
|
|||
import { Page, Col, Row } from "../ui/index";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { Props } from "./interfaces";
|
||||
import { ConnectivityPanel } from "./connectivity/index";
|
||||
import {
|
||||
botToMQTT, botToAPI, browserToMQTT, botToFirmware, browserToAPI
|
||||
} from "./connectivity/status_checks";
|
||||
import { Diagnosis, DiagnosisName } from "./connectivity/diagnosis";
|
||||
import { StatusRowProps } from "./connectivity/connectivity_row";
|
||||
import { resetConnectionInfo } from "./actions";
|
||||
import { PinBindings } from "./pin_bindings/pin_bindings";
|
||||
import { selectAllDiagnosticDumps } from "../resources/selectors";
|
||||
import { ConnectivityPanel } from "./connectivity";
|
||||
|
||||
@connect(mapStateToProps)
|
||||
export class Devices extends React.Component<Props, {}> {
|
||||
state = { online: navigator.onLine };
|
||||
|
||||
/** A record of all the things we know about connectivity right now. */
|
||||
get flags(): Record<DiagnosisName, StatusRowProps> {
|
||||
const fwVersion = this.props.bot.hardware
|
||||
.informational_settings.firmware_version;
|
||||
|
||||
return {
|
||||
userMQTT: browserToMQTT(this.props.userToMqtt),
|
||||
userAPI: browserToAPI(this.props.userToApi),
|
||||
botMQTT: botToMQTT(this.props.botToMqtt),
|
||||
botAPI: botToAPI(this.props.deviceAccount.body.last_saw_api),
|
||||
botFirmware: botToFirmware(fwVersion),
|
||||
};
|
||||
}
|
||||
|
||||
/** Shuffle these around to change the ordering of the status table. */
|
||||
get rowData(): StatusRowProps[] {
|
||||
return [
|
||||
this.flags.userAPI,
|
||||
this.flags.userMQTT,
|
||||
this.flags.botMQTT,
|
||||
this.flags.botAPI,
|
||||
this.flags.botFirmware,
|
||||
];
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
this.props.dispatch(resetConnectionInfo());
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.auth) {
|
||||
const { botToMqtt } = this.props;
|
||||
|
@ -72,16 +35,9 @@ export class Devices extends React.Component<Props, {}> {
|
|||
saveFarmwareEnv={this.props.saveFarmwareEnv} />
|
||||
<ConnectivityPanel
|
||||
status={this.props.deviceAccount.specialStatus}
|
||||
onRefresh={this.refresh}
|
||||
rowData={this.rowData}
|
||||
fbosInfo={this.props.bot.hardware.informational_settings}>
|
||||
<Diagnosis
|
||||
userAPI={!!this.flags.userAPI}
|
||||
userMQTT={!!this.flags.userMQTT.connectionStatus}
|
||||
botMQTT={!!this.flags.botMQTT.connectionStatus}
|
||||
botAPI={!!this.flags.botAPI.connectionStatus}
|
||||
botFirmware={!!this.flags.botFirmware.connectionStatus} />
|
||||
</ConnectivityPanel>
|
||||
bot={this.props.bot}
|
||||
dispatch={this.props.dispatch}
|
||||
deviceAccount={this.props.deviceAccount} />
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<HardwareSettings
|
||||
|
|
|
@ -4,21 +4,23 @@ import { TimezoneSelector } from "../timezone_selector";
|
|||
import { inferTimezone } from "../guess_timezone";
|
||||
|
||||
describe("<TimezoneSelector/>", () => {
|
||||
const fakeProps = (): TimezoneSelector["props"] => ({
|
||||
currentTimezone: undefined,
|
||||
onUpdate: jest.fn(),
|
||||
});
|
||||
|
||||
it("handles a dropdown selection", () => {
|
||||
const props: TimezoneSelector["props"] =
|
||||
({ currentTimezone: undefined, onUpdate: jest.fn() });
|
||||
const instance = new TimezoneSelector(props);
|
||||
const p = fakeProps();
|
||||
const instance = new TimezoneSelector(p);
|
||||
const ddi = { value: "UTC", label: "_" };
|
||||
instance.itemSelected(ddi);
|
||||
expect(props.onUpdate).toHaveBeenCalledWith(ddi.value);
|
||||
expect(p.onUpdate).toHaveBeenCalledWith(ddi.value);
|
||||
});
|
||||
|
||||
it("triggers life cycle callbacks", () => {
|
||||
const props: TimezoneSelector["props"] =
|
||||
({ currentTimezone: undefined, onUpdate: jest.fn() });
|
||||
const el = mount<TimezoneSelector>(<TimezoneSelector {...props} />);
|
||||
el.simulate("change");
|
||||
// componentWillMount() triggers timezone inference:
|
||||
expect(props.onUpdate).toHaveBeenCalledWith(inferTimezone(undefined));
|
||||
const p = fakeProps();
|
||||
const el = mount(<TimezoneSelector {...p} />);
|
||||
el.mount();
|
||||
expect(p.onUpdate).toHaveBeenCalledWith(inferTimezone(undefined));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
jest.mock("../../devices/timezones/guess_timezone", () => ({
|
||||
maybeSetTimezone: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { NavBar } from "../index";
|
||||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import { taggedUser } from "../../__test_support__/user";
|
||||
import { NavBarProps } from "../interfaces";
|
||||
import { fakeDevice } from "../../__test_support__/resource_index_builder";
|
||||
import { maybeSetTimezone } from "../../devices/timezones/guess_timezone";
|
||||
|
||||
describe("NavBar", () => {
|
||||
const fakeProps = (): NavBarProps => ({
|
||||
|
@ -16,6 +21,7 @@ describe("NavBar", () => {
|
|||
dispatch: jest.fn(),
|
||||
getConfigValue: jest.fn(),
|
||||
tour: undefined,
|
||||
device: fakeDevice(),
|
||||
});
|
||||
|
||||
it("has correct parent classname", () => {
|
||||
|
@ -31,4 +37,12 @@ describe("NavBar", () => {
|
|||
link.simulate("click");
|
||||
expect(wrapper.instance().state.mobileMenuOpen).toBeFalsy();
|
||||
});
|
||||
|
||||
it("silently sets user timezone as needed", () => {
|
||||
const p = fakeProps();
|
||||
p.device = fakeDevice({ timezone: undefined });
|
||||
const wrapper = mount(<NavBar {...p} />);
|
||||
wrapper.mount();
|
||||
expect(maybeSetTimezone).toHaveBeenCalledWith(p.dispatch, p.device);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,21 +21,21 @@ describe("<SyncButton/>", function () {
|
|||
expect(result.hasClass("gray")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is not gray when disconnected", () => {
|
||||
it("is gray when disconnected", () => {
|
||||
const p = fakeProps();
|
||||
p.consistent = false;
|
||||
p.bot.hardware.informational_settings.sync_status = "unknown";
|
||||
const result = shallow(<SyncButton {...p} />);
|
||||
expect(result.hasClass("gray")).toBeFalsy();
|
||||
expect(result.hasClass("gray")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("defaults to `disconnected` and `red` when uncertain", () => {
|
||||
it("defaults to `unknown` and `gray` when uncertain", () => {
|
||||
const p = fakeProps();
|
||||
// tslint:disable-next-line:no-any
|
||||
p.bot.hardware.informational_settings.sync_status = "new" as any;
|
||||
const result = shallow(<SyncButton {...p} />);
|
||||
expect(result.text()).toContain("new");
|
||||
expect(result.hasClass("red")).toBeTruthy();
|
||||
expect(result.hasClass("gray")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("syncs when clicked", () => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { NavBarProps, NavBarState } from "./interfaces";
|
||||
import { EStopButton } from "../devices/components/e_stop_btn";
|
||||
import { Session } from "../session";
|
||||
|
@ -15,6 +14,10 @@ import { Popover, Position } from "@blueprintjs/core";
|
|||
import { ErrorBoundary } from "../error_boundary";
|
||||
import { RunTour } from "../help/tour";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { Connectivity } from "../devices/connectivity/connectivity";
|
||||
import { connectivityData } from "../devices/connectivity/generate_data";
|
||||
import { DiagnosisSaucer } from "../devices/connectivity/diagnosis";
|
||||
import { maybeSetTimezone } from "../devices/timezones/guess_timezone";
|
||||
|
||||
export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
||||
|
||||
|
@ -24,6 +27,11 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
accountMenuOpen: false
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
const { device } = this.props;
|
||||
device && maybeSetTimezone(this.props.dispatch, device);
|
||||
}
|
||||
|
||||
logout = () => Session.clear();
|
||||
|
||||
toggle = (name: keyof NavBarState) => () =>
|
||||
|
@ -38,6 +46,14 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
dispatch={this.props.dispatch}
|
||||
consistent={this.props.consistent} />;
|
||||
}
|
||||
|
||||
get connectivityData() {
|
||||
return connectivityData({
|
||||
bot: this.props.bot,
|
||||
device: this.props.device
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasName = this.props.user && this.props.user.body.name;
|
||||
|
||||
|
@ -80,19 +96,32 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
</span>
|
||||
</div>
|
||||
<div className="nav-right">
|
||||
<Popover
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
isOpen={accountMenuOpen}
|
||||
onClose={this.close("accountMenuOpen")}
|
||||
usePortal={false}>
|
||||
<div className="nav-name"
|
||||
onClick={this.toggle("accountMenuOpen")}>
|
||||
{firstName}
|
||||
</div>
|
||||
{AdditionalMenu({ logout: this.logout, close })}
|
||||
</Popover>
|
||||
<div className="menu-popover">
|
||||
<Popover
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
isOpen={accountMenuOpen}
|
||||
onClose={this.close("accountMenuOpen")}
|
||||
usePortal={false}>
|
||||
<div className="nav-name"
|
||||
onClick={this.toggle("accountMenuOpen")}>
|
||||
{firstName}
|
||||
</div>
|
||||
{AdditionalMenu({ logout: this.logout, close })}
|
||||
</Popover>
|
||||
</div>
|
||||
<EStopButton bot={this.props.bot} />
|
||||
{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} />
|
||||
</Popover>
|
||||
</div>
|
||||
<RunTour currentTour={this.props.tour} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BotState } from "../devices/interfaces";
|
||||
import { TaggedUser, TaggedLog } from "farmbot";
|
||||
import { TaggedUser, TaggedLog, TaggedDevice } from "farmbot";
|
||||
import { GetWebAppConfigValue } from "../config_storage/actions";
|
||||
|
||||
export interface SyncButtonProps {
|
||||
|
@ -18,6 +18,7 @@ export interface NavBarProps {
|
|||
timeOffset: number;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
tour: string | undefined;
|
||||
device: TaggedDevice;
|
||||
}
|
||||
|
||||
export interface NavBarState {
|
||||
|
|
|
@ -10,9 +10,9 @@ const COLOR_MAPPING: Record<SyncStatus, string> = {
|
|||
"sync_now": "yellow",
|
||||
"syncing": "yellow",
|
||||
"sync_error": "red",
|
||||
"booting": "yellow",
|
||||
"maintenance": "yellow",
|
||||
"unknown": "red"
|
||||
"booting": "gray",
|
||||
"maintenance": "gray",
|
||||
"unknown": "gray"
|
||||
};
|
||||
|
||||
const TEXT_MAPPING: () => Record<SyncStatus, string> = () => ({
|
||||
|
@ -20,9 +20,9 @@ const TEXT_MAPPING: () => Record<SyncStatus, string> = () => ({
|
|||
"sync_now": t("SYNC NOW"),
|
||||
"syncing": t("SYNCING"),
|
||||
"sync_error": t("SYNC ERROR"),
|
||||
"booting": t("BOOTING"),
|
||||
"unknown": t("DISCONNECTED"),
|
||||
"maintenance": t("MAINTENANCE DOWNTIME")
|
||||
"booting": t("UNKNOWN"),
|
||||
"unknown": t("UNKNOWN"),
|
||||
"maintenance": t("UNKNOWN")
|
||||
});
|
||||
|
||||
/** Animation during syncing action */
|
||||
|
@ -31,7 +31,7 @@ const spinner = <span className="btn-spinner sync" />;
|
|||
export function SyncButton({ bot, dispatch, consistent }: SyncButtonProps) {
|
||||
const { sync_status } = bot.hardware.informational_settings;
|
||||
const syncStatus = sync_status || "unknown";
|
||||
const normalColor = COLOR_MAPPING[syncStatus] || "red";
|
||||
const normalColor = COLOR_MAPPING[syncStatus] || "gray";
|
||||
const color = (!consistent && (syncStatus === "sync_now"))
|
||||
? "gray"
|
||||
: normalColor;
|
||||
|
|
20
package.json
20
package.json
|
@ -23,17 +23,17 @@
|
|||
"author": "farmbot.io",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "3.15.0",
|
||||
"@blueprintjs/datetime": "3.8.0",
|
||||
"@blueprintjs/core": "3.15.1",
|
||||
"@blueprintjs/datetime": "3.9.0",
|
||||
"@blueprintjs/select": "3.8.0",
|
||||
"@types/enzyme": "3.9.1",
|
||||
"@types/jest": "24.0.11",
|
||||
"@types/lodash": "4.14.123",
|
||||
"@types/markdown-it": "0.0.7",
|
||||
"@types/moxios": "0.4.8",
|
||||
"@types/node": "11.13.0",
|
||||
"@types/node": "11.13.2",
|
||||
"@types/promise-timeout": "1.3.0",
|
||||
"@types/react": "16.8.12",
|
||||
"@types/react": "16.8.13",
|
||||
"@types/react-color": "3.0.0",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/react-redux": "7.0.6",
|
||||
|
@ -42,7 +42,7 @@
|
|||
"browser-speech": "1.1.1",
|
||||
"coveralls": "3.0.3",
|
||||
"enzyme": "3.9.0",
|
||||
"enzyme-adapter-react-16": "1.11.2",
|
||||
"enzyme-adapter-react-16": "1.12.1",
|
||||
"farmbot": "7.0.0-rc17",
|
||||
"farmbot-toastr": "1.0.3",
|
||||
"i18next": "15.0.9",
|
||||
|
@ -61,19 +61,19 @@
|
|||
"react-color": "2.17.0",
|
||||
"react-dom": "16.8.6",
|
||||
"react-joyride": "2.0.5",
|
||||
"react-redux": "6.0.1",
|
||||
"react-redux": "7.0.1",
|
||||
"react-test-renderer": "16.8.6",
|
||||
"react-transition-group": "2.8.0",
|
||||
"react-transition-group": "2.9.0",
|
||||
"redux": "4.0.1",
|
||||
"redux-immutable-state-invariant": "2.1.0",
|
||||
"redux-thunk": "2.3.0",
|
||||
"sass": "1.17.4",
|
||||
"sass": "1.18.0",
|
||||
"sass-lint": "1.12.1",
|
||||
"takeme": "0.11.1",
|
||||
"ts-jest": "24.0.1",
|
||||
"ts-jest": "24.0.2",
|
||||
"ts-lint": "4.5.1",
|
||||
"tslint": "5.15.0",
|
||||
"typescript": "3.4.2",
|
||||
"typescript": "3.4.3",
|
||||
"which": "1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
Loading…
Reference in New Issue