Merge pull request #1730 from FarmBot/panel-updates

Groups panel updates
pull/1731/head v9.2.1
Rick Carlino 2020-03-14 14:55:03 -05:00 committed by GitHub
commit 85be07efe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
148 changed files with 3412 additions and 2033 deletions

View File

@ -16,4 +16,5 @@ export const fakeDesignerState = (): DesignerState => ({
currentPoint: undefined, currentPoint: undefined,
openedSavedGarden: undefined, openedSavedGarden: undefined,
tryGroupSortType: undefined, tryGroupSortType: undefined,
editGroupAreaInMap: false,
}); });

View File

@ -26,11 +26,11 @@ import { getFirmwareConfig, getFbosConfig } from "./resources/getters";
import { intersection } from "lodash"; import { intersection } from "lodash";
import { t } from "./i18next_wrapper"; import { t } from "./i18next_wrapper";
import { ResourceIndex } from "./resources/interfaces"; import { ResourceIndex } from "./resources/interfaces";
import { isBotOnline } from "./devices/must_be_online"; import { isBotOnlineFromState } from "./devices/must_be_online";
import { getStatus } from "./connectivity/reducer_support";
import { getAllAlerts } from "./messages/state_to_props"; import { getAllAlerts } from "./messages/state_to_props";
import { PingDictionary } from "./devices/connectivity/qos"; import { PingDictionary } from "./devices/connectivity/qos";
import { getEnv, getShouldDisplayFn } from "./farmware/state_to_props"; import { getEnv, getShouldDisplayFn } from "./farmware/state_to_props";
import { filterAlerts } from "./messages/alerts";
/** For the logger module */ /** For the logger module */
init(); init();
@ -81,7 +81,7 @@ export function mapStateToProps(props: Everything): AppProps {
tour: props.resources.consumers.help.currentTour, tour: props.resources.consumers.help.currentTour,
resources: props.resources.index, resources: props.resources.index,
autoSync: !!(fbosConfig && fbosConfig.auto_sync), autoSync: !!(fbosConfig && fbosConfig.auto_sync),
alertCount: getAllAlerts(props.resources).length, alertCount: getAllAlerts(props.resources).filter(filterAlerts).length,
pings: props.bot.connectivity.pings, pings: props.bot.connectivity.pings,
env, env,
}; };
@ -124,8 +124,6 @@ export class RawApp extends React.Component<AppProps, {}> {
const syncLoaded = this.isLoaded; const syncLoaded = this.isLoaded;
const currentPage = getPathArray()[2]; const currentPage = getPathArray()[2];
const { location_data, mcu_params } = this.props.bot.hardware; const { location_data, mcu_params } = this.props.bot.hardware;
const { sync_status } = this.props.bot.hardware.informational_settings;
const bot2mqtt = this.props.bot.connectivity.uptime["bot.mqtt"];
return <div className="app"> return <div className="app">
{!syncLoaded && <LoadingPlant animate={this.props.animate} />} {!syncLoaded && <LoadingPlant animate={this.props.animate} />}
<HotKeys dispatch={this.props.dispatch} /> <HotKeys dispatch={this.props.dispatch} />
@ -151,7 +149,7 @@ export class RawApp extends React.Component<AppProps, {}> {
firmwareSettings={this.props.firmwareConfig || mcu_params} firmwareSettings={this.props.firmwareConfig || mcu_params}
xySwap={this.props.xySwap} xySwap={this.props.xySwap}
arduinoBusy={!!this.props.bot.hardware.informational_settings.busy} arduinoBusy={!!this.props.bot.hardware.informational_settings.busy}
botOnline={isBotOnline(sync_status, getStatus(bot2mqtt))} botOnline={isBotOnlineFromState(this.props.bot)}
env={this.props.env} env={this.props.env}
stepSize={this.props.bot.stepSize} />} stepSize={this.props.bot.stepSize} />}
</div>; </div>;

View File

@ -3,8 +3,8 @@ jest.mock("axios", () => ({
response: { use: jest.fn() }, response: { use: jest.fn() },
request: { use: jest.fn() } request: { use: jest.fn() }
}, },
post: jest.fn(() => { return Promise.resolve({ data: { foo: "bar" } }); }), post: jest.fn(() => Promise.resolve({ data: { foo: "bar" } })),
get: jest.fn(() => { return Promise.resolve({ data: { foo: "bar" } }); }), get: jest.fn(() => Promise.resolve({ data: { foo: "bar" } })),
})); }));
jest.mock("../../api/api", () => ({ jest.mock("../../api/api", () => ({
@ -22,6 +22,7 @@ jest.mock("../../devices/actions", () => ({
fetchReleases: jest.fn(), fetchReleases: jest.fn(),
fetchLatestGHBetaRelease: jest.fn(), fetchLatestGHBetaRelease: jest.fn(),
fetchMinOsFeatureData: jest.fn(), fetchMinOsFeatureData: jest.fn(),
fetchOsReleaseNotes: jest.fn(),
})); }));
import { didLogin } from "../actions"; import { didLogin } from "../actions";

View File

@ -2,6 +2,7 @@ import axios from "axios";
import { import {
fetchReleases, fetchMinOsFeatureData, fetchReleases, fetchMinOsFeatureData,
fetchLatestGHBetaRelease, fetchLatestGHBetaRelease,
fetchOsReleaseNotes,
} from "../devices/actions"; } from "../devices/actions";
import { AuthState } from "./interfaces"; import { AuthState } from "./interfaces";
import { ReduxAction } from "../redux/interfaces"; import { ReduxAction } from "../redux/interfaces";
@ -16,7 +17,6 @@ import { Actions } from "../constants";
import { connectDevice } from "../connectivity/connect_device"; import { connectDevice } from "../connectivity/connect_device";
import { getFirstPartyFarmwareList } from "../farmware/actions"; import { getFirstPartyFarmwareList } from "../farmware/actions";
import { readOnlyInterceptor } from "../read_only_mode"; import { readOnlyInterceptor } from "../read_only_mode";
import { ExternalUrl } from "../external_urls";
export function didLogin(authState: AuthState, dispatch: Function) { export function didLogin(authState: AuthState, dispatch: Function) {
API.setBaseUrl(authState.token.unencoded.iss); API.setBaseUrl(authState.token.unencoded.iss);
@ -25,7 +25,8 @@ export function didLogin(authState: AuthState, dispatch: Function) {
beta_os_update_server && beta_os_update_server != "NOT_SET" && beta_os_update_server && beta_os_update_server != "NOT_SET" &&
dispatch(fetchLatestGHBetaRelease(beta_os_update_server)); dispatch(fetchLatestGHBetaRelease(beta_os_update_server));
dispatch(getFirstPartyFarmwareList()); dispatch(getFirstPartyFarmwareList());
dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions)); dispatch(fetchMinOsFeatureData());
dispatch(fetchOsReleaseNotes());
dispatch(setToken(authState)); dispatch(setToken(authState));
Sync.fetchSyncData(dispatch); Sync.fetchSyncData(dispatch);
dispatch(connectDevice(authState)); dispatch(connectDevice(authState));

View File

@ -1,5 +1,5 @@
import { ConnectionStatus } from "./interfaces"; import { ConnectionStatus } from "./interfaces";
export function getStatus(cs: ConnectionStatus | undefined): "up" | "down" { export function getStatus(cs: ConnectionStatus | undefined): "up" | "down" {
return (cs && cs.state) || "down"; return cs?.state || "down";
} }

View File

@ -858,7 +858,7 @@ export namespace Content {
export const CRITERIA_SELECTION_COUNT = export const CRITERIA_SELECTION_COUNT =
trim(`Criteria additions can only be removed by changing criteria. trim(`Criteria additions can only be removed by changing criteria.
Click and drag in the map to modify zone selection criteria. Click and drag in the map to modify selection criteria.
Criteria will be applied at the time of sequence execution. The final Criteria will be applied at the time of sequence execution. The final
selection at that time may differ from the selection currently selection at that time may differ from the selection currently
displayed.`); displayed.`);
@ -1127,6 +1127,8 @@ export enum Actions {
FETCH_BETA_OS_UPDATE_INFO_ERROR = "FETCH_BETA_OS_UPDATE_INFO_ERROR", FETCH_BETA_OS_UPDATE_INFO_ERROR = "FETCH_BETA_OS_UPDATE_INFO_ERROR",
FETCH_MIN_OS_FEATURE_INFO_OK = "FETCH_MIN_OS_FEATURE_INFO_OK", FETCH_MIN_OS_FEATURE_INFO_OK = "FETCH_MIN_OS_FEATURE_INFO_OK",
FETCH_MIN_OS_FEATURE_INFO_ERROR = "FETCH_MIN_OS_FEATURE_INFO_ERROR", FETCH_MIN_OS_FEATURE_INFO_ERROR = "FETCH_MIN_OS_FEATURE_INFO_ERROR",
FETCH_OS_RELEASE_NOTES_OK = "FETCH_OS_RELEASE_NOTES_OK",
FETCH_OS_RELEASE_NOTES_ERROR = "FETCH_OS_RELEASE_NOTES_ERROR",
INVERT_JOG_BUTTON = "INVERT_JOG_BUTTON", INVERT_JOG_BUTTON = "INVERT_JOG_BUTTON",
DISPLAY_ENCODER_DATA = "DISPLAY_ENCODER_DATA", DISPLAY_ENCODER_DATA = "DISPLAY_ENCODER_DATA",
STASH_STATUS = "STASH_STATUS", STASH_STATUS = "STASH_STATUS",
@ -1149,6 +1151,7 @@ export enum Actions {
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA", SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN", CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
TRY_SORT_TYPE = "TRY_SORT_TYPE", TRY_SORT_TYPE = "TRY_SORT_TYPE",
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
// Regimens // Regimens
PUSH_WEEK = "PUSH_WEEK", PUSH_WEEK = "PUSH_WEEK",

View File

@ -18,10 +18,9 @@ describe("<Controls />", () => {
feeds: [fakeWebcamFeed()], feeds: [fakeWebcamFeed()],
peripherals: [fakePeripheral()], peripherals: [fakePeripheral()],
sensors: [fakeSensor()], sensors: [fakeSensor()],
botToMqttStatus: "up",
firmwareSettings: bot.hardware.mcu_params, firmwareSettings: bot.hardware.mcu_params,
shouldDisplay: () => true, shouldDisplay: () => true,
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])), getWebAppConfigVal: jest.fn(key => mockConfig[key]),
sensorReadings: [], sensorReadings: [],
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
env: {}, env: {},
@ -65,6 +64,17 @@ describe("<Controls />", () => {
.map(string => expect(txt).not.toContain(string)); .map(string => expect(txt).not.toContain(string));
}); });
it("hides sensors widget based on model", () => {
mockConfig.hide_sensors = false;
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Controls {...p} />);
const txt = wrapper.text().toLowerCase();
["move", "peripherals"]
.map(string => expect(txt).toContain(string));
["sensors"].map(string => expect(txt).not.toContain(string));
});
it("doesn't show sensor readings widget", () => { it("doesn't show sensor readings widget", () => {
const p = fakeProps(); const p = fakeProps();
mockConfig.hide_sensors = true; mockConfig.hide_sensors = true;

View File

@ -9,7 +9,7 @@ import { Props } from "./interfaces";
import { Move } from "./move/move"; import { Move } from "./move/move";
import { BooleanSetting } from "../session_keys"; import { BooleanSetting } from "../session_keys";
import { SensorReadings } from "./sensor_readings/sensor_readings"; import { SensorReadings } from "./sensor_readings/sensor_readings";
import { isBotOnline } from "../devices/must_be_online"; import { isBotOnlineFromState } from "../devices/must_be_online";
import { hasSensors } from "../devices/components/firmware_hardware_support"; import { hasSensors } from "../devices/components/firmware_hardware_support";
/** Controls page. */ /** Controls page. */
@ -19,9 +19,7 @@ export class RawControls extends React.Component<Props, {}> {
} }
get botOnline() { get botOnline() {
return isBotOnline( return isBotOnlineFromState(this.props.bot);
this.props.bot.hardware.informational_settings.sync_status,
this.props.botToMqttStatus);
} }
get hideSensors() { get hideSensors() {
@ -34,7 +32,6 @@ export class RawControls extends React.Component<Props, {}> {
env={this.props.env} env={this.props.env}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
arduinoBusy={this.arduinoBusy} arduinoBusy={this.arduinoBusy}
botToMqttStatus={this.props.botToMqttStatus}
firmwareSettings={this.props.firmwareSettings} firmwareSettings={this.props.firmwareSettings}
firmwareHardware={this.props.firmwareHardware} firmwareHardware={this.props.firmwareHardware}
getWebAppConfigVal={this.props.getWebAppConfigVal} /> getWebAppConfigVal={this.props.getWebAppConfigVal} />

View File

@ -8,7 +8,6 @@ import {
TaggedSensor, TaggedSensor,
TaggedSensorReading, TaggedSensorReading,
} from "farmbot"; } from "farmbot";
import { NetworkState } from "../connectivity/interfaces";
import { GetWebAppConfigValue } from "../config_storage/actions"; import { GetWebAppConfigValue } from "../config_storage/actions";
import { TimeSettings } from "../interfaces"; import { TimeSettings } from "../interfaces";
@ -18,7 +17,6 @@ export interface Props {
feeds: TaggedWebcamFeed[]; feeds: TaggedWebcamFeed[];
peripherals: TaggedPeripheral[]; peripherals: TaggedPeripheral[];
sensors: TaggedSensor[]; sensors: TaggedSensor[];
botToMqttStatus: NetworkState;
firmwareSettings: McuParams; firmwareSettings: McuParams;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
getWebAppConfigVal: GetWebAppConfigValue; getWebAppConfigVal: GetWebAppConfigValue;
@ -60,4 +58,5 @@ export interface ToggleButtonProps {
dim?: boolean; dim?: boolean;
grayscale?: boolean; grayscale?: boolean;
title?: string; title?: string;
className?: string;
} }

View File

@ -29,7 +29,6 @@ describe("<Move />", () => {
dispatch: jest.fn(), dispatch: jest.fn(),
bot: bot, bot: bot,
arduinoBusy: false, arduinoBusy: false,
botToMqttStatus: "up",
firmwareSettings: bot.hardware.mcu_params, firmwareSettings: bot.hardware.mcu_params,
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])), getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
env: {}, env: {},

View File

@ -1,6 +1,5 @@
import { BotPosition, BotState, UserEnv } from "../../devices/interfaces"; import { BotPosition, BotState, UserEnv } from "../../devices/interfaces";
import { McuParams, Xyz, FirmwareHardware } from "farmbot"; import { McuParams, Xyz, FirmwareHardware } from "farmbot";
import { NetworkState } from "../../connectivity/interfaces";
import { GetWebAppConfigValue } from "../../config_storage/actions"; import { GetWebAppConfigValue } from "../../config_storage/actions";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
@ -11,7 +10,6 @@ export interface MoveProps {
dispatch: Function; dispatch: Function;
bot: BotState; bot: BotState;
arduinoBusy: boolean; arduinoBusy: boolean;
botToMqttStatus: NetworkState;
firmwareSettings: McuParams; firmwareSettings: McuParams;
getWebAppConfigVal: GetWebAppConfigValue; getWebAppConfigVal: GetWebAppConfigValue;
env: UserEnv; env: UserEnv;

View File

@ -13,6 +13,7 @@ import { MotorPositionPlot } from "./motor_position_plot";
import { Popover, Position } from "@blueprintjs/core"; import { Popover, Position } from "@blueprintjs/core";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { getStatus } from "../../connectivity/reducer_support";
export class Move extends React.Component<MoveProps, {}> { export class Move extends React.Component<MoveProps, {}> {
@ -23,7 +24,8 @@ export class Move extends React.Component<MoveProps, {}> {
!!this.props.getWebAppConfigVal(BooleanSetting[key]); !!this.props.getWebAppConfigVal(BooleanSetting[key]);
render() { render() {
const { location_data, informational_settings } = this.props.bot.hardware; const { bot } = this.props;
const { location_data, informational_settings } = bot.hardware;
const locationData = validBotLocationData(location_data); const locationData = validBotLocationData(location_data);
return <Widget className="move-widget"> return <Widget className="move-widget">
<WidgetHeader <WidgetHeader
@ -40,11 +42,11 @@ export class Move extends React.Component<MoveProps, {}> {
<WidgetBody> <WidgetBody>
<MustBeOnline <MustBeOnline
lockOpen={process.env.NODE_ENV !== "production"} lockOpen={process.env.NODE_ENV !== "production"}
networkState={this.props.botToMqttStatus} networkState={getStatus(bot.connectivity.uptime["bot.mqtt"])}
syncStatus={informational_settings.sync_status}> syncStatus={informational_settings.sync_status}>
<JogControlsGroup <JogControlsGroup
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
stepSize={this.props.bot.stepSize} stepSize={bot.stepSize}
botPosition={locationData.position} botPosition={locationData.position}
getValue={this.getValue} getValue={this.getValue}
arduinoBusy={this.props.arduinoBusy} arduinoBusy={this.props.arduinoBusy}

View File

@ -11,7 +11,6 @@ import { validFwConfig, validFbosConfig } from "../util";
import { getWebAppConfigValue } from "../config_storage/actions"; import { getWebAppConfigValue } from "../config_storage/actions";
import { getFirmwareConfig, getFbosConfig } from "../resources/getters"; import { getFirmwareConfig, getFbosConfig } from "../resources/getters";
import { uniq } from "lodash"; import { uniq } from "lodash";
import { getStatus } from "../connectivity/reducer_support";
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props"; import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
import { sourceFbosConfigValue } from "../devices/components/source_config_value"; import { sourceFbosConfigValue } from "../devices/components/source_config_value";
import { isFwHardwareValue } from "../devices/components/firmware_hardware_support"; import { isFwHardwareValue } from "../devices/components/firmware_hardware_support";
@ -35,7 +34,6 @@ export function mapStateToProps(props: Everything): Props {
bot: props.bot, bot: props.bot,
peripherals: uniq(selectAllPeripherals(props.resources.index)), peripherals: uniq(selectAllPeripherals(props.resources.index)),
sensors: uniq(selectAllSensors(props.resources.index)), sensors: uniq(selectAllSensors(props.resources.index)),
botToMqttStatus: getStatus(props.bot.connectivity.uptime["bot.mqtt"]),
firmwareSettings: fwConfig || mcu_params, firmwareSettings: fwConfig || mcu_params,
getWebAppConfigVal: getWebAppConfigValue(() => props), getWebAppConfigVal: getWebAppConfigValue(() => props),
shouldDisplay, shouldDisplay,

View File

@ -40,12 +40,16 @@ export class ToggleButton extends React.Component<ToggleButtonProps, {}> {
} }
render() { render() {
const addCss = (this.props.dim ? " dim" : "") const allCss = [
+ (this.props.grayscale ? " grayscale" : ""); this.css(),
this.props.className,
this.props.dim ? "dim" : "",
this.props.grayscale ? "grayscale" : "",
].join(" ");
const cb = () => !this.props.disabled && this.props.toggleAction(); const cb = () => !this.props.disabled && this.props.toggleAction();
return <button return <button
disabled={!!this.props.disabled} disabled={!!this.props.disabled}
className={this.css() + addCss} className={allCss}
title={this.props.title || ""} title={this.props.title || ""}
onClick={cb}> onClick={cb}>
{t(this.caption())} {t(this.caption())}

View File

@ -210,16 +210,6 @@
@extend %panel-item-base; @extend %panel-item-base;
padding-top: 0.6rem; padding-top: 0.6rem;
} }
.groups-panel-content {
padding: 0px;
}
.groups-list-wrapper {
padding: 0.5em 0em;
}
.group-delete-btn {
float: left;
margin-top: 1em;
}
.plant-search-item-name { .plant-search-item-name {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;

View File

@ -640,6 +640,9 @@
.add-tool-panel-content, .add-tool-panel-content,
.edit-tool-panel-content { .edit-tool-panel-content {
max-height: calc(100vh - 14rem);
overflow-y: auto;
overflow-x: hidden;
button { button {
display: block; display: block;
margin-left: auto; margin-left: auto;
@ -687,6 +690,7 @@
} }
} }
button { button {
margin-bottom: 2rem;
.fa-plus { .fa-plus {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
@ -805,7 +809,6 @@
.weeds-inventory-panel, .weeds-inventory-panel,
.zones-inventory-panel, .zones-inventory-panel,
.group-detail-panel,
.groups-panel { .groups-panel {
.panel-content { .panel-content {
max-height: calc(100vh - 19rem); max-height: calc(100vh - 19rem);
@ -817,6 +820,31 @@
.group-detail-panel { .group-detail-panel {
.panel-content { .panel-content {
max-height: calc(100vh - 14rem);
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 5rem;
.group-member-display {
i[class*=fa-caret-] {
float: right;
font-size: 2rem;
margin-top: 1.5rem;
}
.groups-list-wrapper {
padding: 0.5em 0em;
}
}
.group-member-display,
.group-sort-section {
.bp3-popover-wrapper {
display: inline;
margin-left: 1rem;
}
}
.group-delete-btn {
float: left;
margin-top: 1em;
}
.group-criteria { .group-criteria {
margin-top: 1rem; margin-top: 1rem;
.criteria-heading { .criteria-heading {
@ -825,7 +853,61 @@
.fb-button { .fb-button {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.group-criteria-presets { .point-type-checkboxes {
.point-type-section {
.fb-checkbox {
display: inline;
margin-right: 1rem;
vertical-align: top;
}
p {
display: inline;
text-transform: uppercase;
}
.point-type-checkbox {
position: relative;
height: 2rem;
margin-top: 0.75rem;
cursor: pointer;
.fb-checkbox {
display: inline-block;
height: 2rem;
}
i[class*=fa-caret-] {
position: absolute;
right: -0.5rem;
width: 3rem;
font-size: 2rem;
padding-left: 1rem;
}
}
.plant-criteria-options,
.point-criteria-options,
.tool-criteria-options {
margin-left: 3rem;
p {
&.category {
display: block;
padding-top: 1rem;
padding-bottom: 1rem;
text-transform: none;
font-size: 1.2rem;
font-weight: bold;
}
}
hr {
margin: 0.5rem;
}
.lt-gt-criteria {
margin-bottom: 1rem;
.row {
margin-left: 0 !important;
}
}
}
}
}
.criteria-radio-presets {
input[type="radio"] { input[type="radio"] {
width: auto; width: auto;
margin-right: 1rem; margin-right: 1rem;
@ -841,29 +923,28 @@
.criteria-slug { .criteria-slug {
margin-top: 1rem; margin-top: 1rem;
} }
.location-criteria {
.row {
margin-top: 1rem;
p {
font-size: 1.4rem;
font-weight: bold;
}
label {
margin-top: 0;
}
}
}
.day-criteria { .day-criteria {
p { p {
display: inline; display: inline;
vertical-align: bottom; vertical-align: bottom;
} }
input {
line-height: 1.75rem;
}
} }
.string-eq-criteria { .string-eq-criteria {
margin-top: 1rem; margin-top: 1rem;
.row { .row {
margin-top: 1rem; margin-top: 1rem;
} }
code {
display: inline-block;
margin-top: 2rem;
font-size: 1.2rem;
font-weight: bold;
color: $black;
background: none;
}
} }
.number-eq-criteria, .number-eq-criteria,
.number-gt-lt-criteria { .number-gt-lt-criteria {
@ -873,11 +954,49 @@
} }
p { p {
text-align: center; text-align: center;
margin-top: 0.5rem; line-height: 2.75rem;
font-size: 1.2rem;
} }
} }
.expandable-header { .fb-toggle-button {
margin-top: 3rem; width: 85px;
margin-top: 0;
&.red {
background: $dark_gray !important;
}
}
.clear-criteria {
margin-top: 2rem;
}
.basic,
.advanced {
margin-left: 1rem;
.day-criteria {
.row {
margin-left: 0;
}
div[class*=col-] {
padding: 0;
padding-right: 0.75rem;
}
}
}
.advanced {
.row {
margin-left: 0;
}
div[class*=col-] {
padding: 0;
}
.col-xs-9 {
margin-right: 0.5rem;
}
.col-xs-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
margin-top: 0.4rem;
text-align: center;
}
} }
} }
.criteria-point-count-breakdown { .criteria-point-count-breakdown {
@ -906,19 +1025,34 @@
} }
} }
.zone-info-panel { .lt-gt-criteria,
.panel-content { .location-criteria {
.location-criteria { display: inline-block;
.row { .row {
margin-top: 1rem; margin-left: 0;
p { div[class*=col-] {
font-size: 1.4rem; padding: 0;
font-weight: bold; text-align: center;
} }
label { margin-top: 1rem;
margin-top: 0; p {
} display: block !important;
} text-transform: uppercase;
font-size: 1.1rem;
margin-top: 0.75rem;
}
label {
margin-top: 0.5rem;
}
}
.edit-in-map {
float: right;
button {
margin: 1rem !important;
width: 5rem !important;
}
label {
margin-top: 1.1rem !important;
} }
} }
} }

View File

@ -361,6 +361,17 @@ a {
} }
} }
.centered-button-div {
.fb-button {
float: none !important;
}
label {
padding: 0;
}
text-align: center;
padding: 0.25rem;
}
.hardware-widget, .hardware-widget,
.device-widget { .device-widget {
.row { .row {
@ -369,15 +380,11 @@ a {
label { label {
padding: 0.5rem 0 0 0.5rem; padding: 0.5rem 0 0 0.5rem;
} }
.centered-button-div { h4 {
.fb-button { margin-bottom: 0;
float: none !important; }
} p {
label { margin-bottom: 1rem !important;
padding: 0;
}
text-align: center;
padding: 0.25rem;
} }
.widget-body-tooltips { .widget-body-tooltips {
.bp3-popover-wrapper { .bp3-popover-wrapper {
@ -1302,15 +1309,6 @@ ul {
display: inline; display: inline;
position: relative; position: relative;
margin-right: 1rem; margin-right: 1rem;
&.partial:after {
content: "";
position: absolute;
left: 0.75rem;
bottom: 1.2rem;
border: solid $dark_gray;
border-width: 0 0 3px 0;
padding: 0.6rem 0.3rem;
}
} }
.bp3-popover-wrapper, .bp3-popover-wrapper,
.bp3-popover-target { .bp3-popover-target {
@ -1649,11 +1647,9 @@ textarea:focus {
.section { .section {
display: block !important; display: block !important;
} .bp3-collapse {
padding-top: 1rem;
.highlight, }
.unhighlight {
display: flex;
} }
.highlight { .highlight {

View File

@ -138,6 +138,15 @@ select {
padding: 0.6rem 0.3rem; padding: 0.6rem 0.3rem;
} }
} }
&.partial:after {
content: "";
position: absolute;
left: 0.75rem;
bottom: 1.2rem;
border: solid $dark_gray;
border-width: 0 0 3px 0;
padding: 0.6rem 0.3rem;
}
&.large { &.large {
input[type="checkbox"] { input[type="checkbox"] {
width: 3rem; width: 3rem;
@ -155,8 +164,10 @@ select {
} }
} }
&.disabled { &.disabled {
cursor: not-allowed;
input[type="checkbox"] { input[type="checkbox"] {
cursor: not-allowed; background: $light_gray;
pointer-events: none;
&:checked:after { &:checked:after {
border-color: $gray; border-color: $gray;
} }

View File

@ -407,14 +407,15 @@ describe("fetchLatestGHBetaRelease()", () => {
}); });
describe("fetchMinOsFeatureData()", () => { describe("fetchMinOsFeatureData()", () => {
const EXPECTED_URL = expect.stringContaining("FEATURE_MIN_VERSIONS.json");
afterEach(() => afterEach(() =>
jest.restoreAllMocks()); jest.restoreAllMocks());
it("fetches min OS feature data: empty", async () => { it("fetches min OS feature data: empty", async () => {
mockGetRelease = Promise.resolve({ data: {} }); mockGetRelease = Promise.resolve({ data: {} });
const dispatch = jest.fn(); const dispatch = jest.fn();
await actions.fetchMinOsFeatureData("url")(dispatch); await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith("url"); expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).toHaveBeenCalledWith({ expect(dispatch).toHaveBeenCalledWith({
payload: {}, payload: {},
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK
@ -426,8 +427,8 @@ describe("fetchMinOsFeatureData()", () => {
data: { "a_feature": "1.0.0", "b_feature": "2.0.0" } data: { "a_feature": "1.0.0", "b_feature": "2.0.0" }
}); });
const dispatch = jest.fn(); const dispatch = jest.fn();
await actions.fetchMinOsFeatureData("url")(dispatch); await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith("url"); expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).toHaveBeenCalledWith({ expect(dispatch).toHaveBeenCalledWith({
payload: { a_feature: "1.0.0", b_feature: "2.0.0" }, payload: { a_feature: "1.0.0", b_feature: "2.0.0" },
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK
@ -438,8 +439,8 @@ describe("fetchMinOsFeatureData()", () => {
mockGetRelease = Promise.resolve({ data: "bad" }); mockGetRelease = Promise.resolve({ data: "bad" });
const dispatch = jest.fn(); const dispatch = jest.fn();
const mockConsole = jest.spyOn(console, "log").mockImplementation(() => { }); const mockConsole = jest.spyOn(console, "log").mockImplementation(() => { });
await actions.fetchMinOsFeatureData("url")(dispatch); await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith("url"); expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).not.toHaveBeenCalled(); expect(dispatch).not.toHaveBeenCalled();
expect(mockConsole).toHaveBeenCalledWith( expect(mockConsole).toHaveBeenCalledWith(
expect.stringContaining("\"bad\"")); expect.stringContaining("\"bad\""));
@ -449,8 +450,8 @@ describe("fetchMinOsFeatureData()", () => {
mockGetRelease = Promise.resolve({ data: { a: "0", b: 0 } }); mockGetRelease = Promise.resolve({ data: { a: "0", b: 0 } });
const dispatch = jest.fn(); const dispatch = jest.fn();
const mockConsole = jest.spyOn(console, "log").mockImplementation(() => { }); const mockConsole = jest.spyOn(console, "log").mockImplementation(() => { });
await actions.fetchMinOsFeatureData("url")(dispatch); await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith("url"); expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).not.toHaveBeenCalled(); expect(dispatch).not.toHaveBeenCalled();
expect(mockConsole).toHaveBeenCalledWith( expect(mockConsole).toHaveBeenCalledWith(
expect.stringContaining("{\"a\":\"0\",\"b\":0}")); expect.stringContaining("{\"a\":\"0\",\"b\":0}"));
@ -459,8 +460,8 @@ describe("fetchMinOsFeatureData()", () => {
it("fails to fetch min OS feature data", async () => { it("fails to fetch min OS feature data", async () => {
mockGetRelease = Promise.reject("error"); mockGetRelease = Promise.reject("error");
const dispatch = jest.fn(); const dispatch = jest.fn();
await actions.fetchMinOsFeatureData("url")(dispatch); await actions.fetchMinOsFeatureData()(dispatch);
await expect(axios.get).toHaveBeenCalledWith("url"); await expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(error).not.toHaveBeenCalled(); expect(error).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledWith({ expect(dispatch).toHaveBeenCalledWith({
payload: "error", payload: "error",
@ -469,6 +470,34 @@ describe("fetchMinOsFeatureData()", () => {
}); });
}); });
describe("fetchOsReleaseNotes()", () => {
const EXPECTED_URL = expect.stringContaining("RELEASE_NOTES.md");
it("fetches OS release notes", async () => {
mockGetRelease = Promise.resolve({
data: "intro\n\n# v6\n\n* note"
});
const dispatch = jest.fn();
await actions.fetchOsReleaseNotes()(dispatch);
await expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).toHaveBeenCalledWith({
payload: "intro\n\n# v6\n\n* note",
type: Actions.FETCH_OS_RELEASE_NOTES_OK
});
});
it("errors while fetching OS release notes", async () => {
mockGetRelease = Promise.reject({ error: "" });
const dispatch = jest.fn();
await actions.fetchOsReleaseNotes()(dispatch);
await expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).toHaveBeenCalledWith({
payload: { error: "" },
type: Actions.FETCH_OS_RELEASE_NOTES_ERROR
});
});
});
describe("updateConfig()", () => { describe("updateConfig()", () => {
it("updates config: FbosConfig", () => { it("updates config: FbosConfig", () => {
const state = fakeState(); const state = fakeState();

View File

@ -12,15 +12,9 @@ import {
import { FarmbotOsSettings } from "../components/farmbot_os_settings"; import { FarmbotOsSettings } from "../components/farmbot_os_settings";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
import { HardwareSettings } from "../components/hardware_settings"; import { HardwareSettings } from "../components/hardware_settings";
import { DeepPartial } from "redux";
import { save } from "../../api/crud";
import { fakeWebAppConfig } from "../../__test_support__/fake_state/resources";
describe("<Devices/>", () => { describe("<Devices/>", () => {
const fakeProps = (): Props => ({ const fakeProps = (): Props => ({
userToApi: undefined,
userToMqtt: undefined,
botToMqtt: undefined,
auth: auth, auth: auth,
bot: bot, bot: bot,
deviceAccount: fakeDevice(), deviceAccount: fakeDevice(),
@ -31,12 +25,10 @@ describe("<Devices/>", () => {
sourceFwConfig: jest.fn(), sourceFwConfig: jest.fn(),
shouldDisplay: jest.fn(), shouldDisplay: jest.fn(),
firmwareConfig: undefined, firmwareConfig: undefined,
isValidFbosConfig: false,
env: {}, env: {},
saveFarmwareEnv: jest.fn(), saveFarmwareEnv: jest.fn(),
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
alerts: [], alerts: [],
webAppConfig: fakeWebAppConfig()
}); });
it("renders relevant panels", () => { it("renders relevant panels", () => {
@ -50,14 +42,6 @@ describe("<Devices/>", () => {
expect(() => render(<Devices {...p} />)).toThrow(); 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);
});
it("provides correct firmwareHardware value", () => { it("provides correct firmwareHardware value", () => {
const p = fakeProps(); const p = fakeProps();
p.sourceFbosConfig = () => ({ value: "arduino", consistent: true }); p.sourceFbosConfig = () => ({ value: "arduino", consistent: true });
@ -65,17 +49,4 @@ describe("<Devices/>", () => {
expect(wrapper.find(HardwareSettings).props().firmwareHardware) expect(wrapper.find(HardwareSettings).props().firmwareHardware)
.toEqual("arduino"); .toEqual("arduino");
}); });
it("triggers a save", () => {
type P = FarmbotOsSettings["props"];
type DPP = DeepPartial<P>;
const props: DPP = {
deviceAccount: { uuid: "a.b.c" },
dispatch: jest.fn()
};
const el = new FarmbotOsSettings(props as P);
el.updateBot();
expect(save)
.toHaveBeenCalledWith(props.deviceAccount && props.deviceAccount.uuid);
});
}); });

View File

@ -5,7 +5,6 @@ import { defensiveClone } from "../../util";
import { stash } from "../../connectivity/data_consistency"; import { stash } from "../../connectivity/data_consistency";
import { incomingStatus } from "../../connectivity/connect_device"; import { incomingStatus } from "../../connectivity/connect_device";
import { Vector3, uuid } from "farmbot"; import { Vector3, uuid } from "farmbot";
import { values, omit } from "lodash";
import { now } from "../connectivity/qos"; import { now } from "../connectivity/qos";
const statusOf = (state: BotState) => { const statusOf = (state: BotState) => {
@ -46,15 +45,28 @@ describe("botReducer", () => {
.toBe(!initialState().controlPanelState.danger_zone); .toBe(!initialState().controlPanelState.danger_zone);
}); });
it("bulk toggles control panel options", () => { it("bulk toggles firmware control panel options", () => {
const state = botReducer(initialState(), { const state = botReducer(initialState(), {
type: Actions.BULK_TOGGLE_CONTROL_PANEL, type: Actions.BULK_TOGGLE_CONTROL_PANEL,
payload: true payload: { open: true, all: false }
}); });
const bulkToggable = const bulkToggable =
omit(state.controlPanelState, "power_and_reset"); Object.entries(state.controlPanelState).filter(([k, _]) => ![
values(bulkToggable).map(value => { "power_and_reset", "farmbot_os", "farm_designer", "firmware",
].includes(k));
Object.values(bulkToggable).map(value => {
expect(value).toBeTruthy();
});
});
it("bulk toggles all control panel options", () => {
const state = botReducer(initialState(), {
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
payload: { open: true, all: true }
});
Object.values(state.controlPanelState).map(value => {
expect(value).toBeTruthy(); expect(value).toBeTruthy();
}); });
}); });
@ -83,6 +95,14 @@ describe("botReducer", () => {
expect(r).toEqual({}); expect(r).toEqual({});
}); });
it("fetches OS release notes", () => {
const r = botReducer(initialState(), {
type: Actions.FETCH_OS_RELEASE_NOTES_OK,
payload: "notes"
}).osReleaseNotes;
expect(r).toEqual("notes");
});
it("Handles status_v8 info", () => { it("Handles status_v8 info", () => {
const n = () => Math.round(Math.random() * 1000); const n = () => Math.round(Math.random() * 1000);
const position: Vector3 = { x: n(), y: n(), z: n() }; const position: Vector3 = { x: n(), y: n(), z: n() };

View File

@ -23,6 +23,7 @@ import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { getFirmwareConfig, getFbosConfig } from "../resources/getters"; import { getFirmwareConfig, getFbosConfig } from "../resources/getters";
import { isObject, isString, get, noop } from "lodash"; import { isObject, isString, get, noop } from "lodash";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { ExternalUrl } from "../external_urls";
const ON = 1, OFF = 0; const ON = 1, OFF = 0;
export type ConfigKey = keyof McuParams; export type ConfigKey = keyof McuParams;
@ -236,12 +237,11 @@ function validMinOsFeatureLookup(x: MinOsFeatureLookup): boolean {
/** /**
* Fetch and save minimum FBOS version data for UI feature display. * Fetch and save minimum FBOS version data for UI feature display.
* @param url location of data
*/ */
export const fetchMinOsFeatureData = (url: string) => export const fetchMinOsFeatureData = () =>
(dispatch: Function) => { (dispatch: Function) => {
axios axios
.get<MinOsFeatureLookup>(url) .get<MinOsFeatureLookup>(ExternalUrl.featureMinVersions)
.then(resp => { .then(resp => {
const data = resp.data; const data = resp.data;
if (validMinOsFeatureLookup(data)) { if (validMinOsFeatureLookup(data)) {
@ -262,6 +262,27 @@ export const fetchMinOsFeatureData = (url: string) =>
}); });
}; };
/**
* Fetch and save FBOS release notes.
*/
export const fetchOsReleaseNotes = () =>
(dispatch: Function) => {
axios
.get<string>(ExternalUrl.osReleaseNotes)
.then(resp => {
dispatch({
type: Actions.FETCH_OS_RELEASE_NOTES_OK,
payload: resp.data
});
})
.catch((ferror) => {
dispatch({
type: Actions.FETCH_OS_RELEASE_NOTES_ERROR,
payload: ferror
});
});
};
/** /**
* Toggles visibility of individual sections in the giant controls panel * Toggles visibility of individual sections in the giant controls panel
* found on the Devices page. * found on the Devices page.
@ -271,8 +292,11 @@ export function toggleControlPanel(payload: keyof ControlPanelState) {
} }
/** Toggle visibility of all hardware control panel sections. */ /** Toggle visibility of all hardware control panel sections. */
export function bulkToggleControlPanel(payload: boolean) { export function bulkToggleControlPanel(open: boolean, all = false) {
return { type: Actions.BULK_TOGGLE_CONTROL_PANEL, payload }; return {
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
payload: { open, all },
};
} }
/** Factory reset all firmware settings. */ /** Factory reset all firmware settings. */

View File

@ -1,29 +1,23 @@
let mockReleaseNoteResponse = Promise.resolve({ data: "" });
jest.mock("axios", () => ({
get: jest.fn(() => mockReleaseNoteResponse)
}));
jest.mock("../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
jest.mock("../fbos_settings/boot_sequence_selector", () => ({ jest.mock("../fbos_settings/boot_sequence_selector", () => ({
BootSequenceSelector: () => <div /> BootSequenceSelector: () => <div />
})); }));
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import * as React from "react"; import * as React from "react";
import { FarmbotOsSettings } from "../farmbot_os_settings"; import { FarmbotOsSettings, FarmBotSettings } from "../farmbot_os_settings";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import { bot } from "../../../__test_support__/fake_state/bot"; import { bot } from "../../../__test_support__/fake_state/bot";
import { fakeResource } from "../../../__test_support__/fake_resource"; import {
import { FarmbotOsProps } from "../../interfaces"; FarmbotOsProps, FarmbotSettingsProps, ControlPanelState,
import axios from "axios"; } from "../../interfaces";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { edit } from "../../../api/crud"; import { fakeDevice } from "../../../__test_support__/resource_index_builder";
import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources";
import { formEvent } from "../../../__test_support__/fake_html_events";
import { Content } from "../../../constants";
describe("<FarmbotOsSettings />", () => { describe("<FarmbotOsSettings />", () => {
beforeEach(() => { beforeEach(() => {
@ -31,104 +25,59 @@ describe("<FarmbotOsSettings />", () => {
}); });
const fakeProps = (): FarmbotOsProps => ({ const fakeProps = (): FarmbotOsProps => ({
deviceAccount: fakeResource("Device", { deviceAccount: fakeDevice(),
id: 0,
name: "",
ota_hour: 3,
tz_offset_hrs: 0
}),
dispatch: jest.fn(), dispatch: jest.fn(),
bot, bot,
alerts: [], alerts: [],
botToMqttLastSeen: 0,
botToMqttStatus: "up",
sourceFbosConfig: x => sourceFbosConfig: x =>
({ value: bot.hardware.configuration[x], consistent: true }), ({ value: bot.hardware.configuration[x], consistent: true }),
shouldDisplay: jest.fn(() => true), shouldDisplay: jest.fn(() => true),
isValidFbosConfig: false,
env: {}, env: {},
saveFarmwareEnv: jest.fn(), saveFarmwareEnv: jest.fn(),
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
webAppConfig: fakeWebAppConfig()
}); });
it("renders settings", () => { it("renders settings", () => {
const osSettings = mount(<FarmbotOsSettings {...fakeProps()} />); const p = fakeProps();
p.bot.controlPanelState.farmbot_os = true;
const osSettings = mount(<FarmbotOsSettings {...p} />);
expect(osSettings.find("input").length).toBe(1); expect(osSettings.find("input").length).toBe(1);
expect(osSettings.find("button").length).toBe(7); expect(osSettings.find("button").length).toBe(6);
["name", "time zone", "farmbot os", "camera", "firmware"] ["name", "time zone", "farmbot os", "camera"]
.map(string => expect(osSettings.text().toLowerCase()).toContain(string)); .map(string => expect(osSettings.text().toLowerCase()).toContain(string));
}); });
it("fetches OS release notes", async () => { it("renders expanded", () => {
mockReleaseNoteResponse = Promise.resolve({ mockDev = true;
data: "intro\n\n# v6\n\n* note"
});
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
{...fakeProps()} />);
await expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("RELEASE_NOTES.md"));
expect(osSettings.instance().osReleaseNotes.heading)
.toEqual("FarmBot OS v6");
expect(osSettings.instance().osReleaseNotes.notes)
.toEqual("* note");
});
it("doesn't fetch OS release notes", async () => {
mockReleaseNoteResponse = Promise.resolve({ data: "" });
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
{...fakeProps()} />);
await expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("RELEASE_NOTES.md"));
expect(osSettings.instance().state.allOsReleaseNotes)
.toEqual("");
expect(osSettings.instance().osReleaseNotes.heading)
.toEqual("FarmBot OS v6");
expect(osSettings.instance().osReleaseNotes.notes)
.toEqual("Could not get release notes.");
});
it("errors while fetching OS release notes", async () => {
mockReleaseNoteResponse = Promise.reject({ error: "" });
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
{...fakeProps()} />);
await expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining("RELEASE_NOTES.md"));
expect(osSettings.instance().state.allOsReleaseNotes)
.toEqual("");
expect(osSettings.instance().osReleaseNotes.heading)
.toEqual("FarmBot OS v6");
expect(osSettings.instance().osReleaseNotes.notes)
.toEqual("Could not get release notes.");
});
it("changes bot name", () => {
const p = fakeProps(); const p = fakeProps();
const newName = "new bot name"; Object.keys(p.bot.controlPanelState).map((panel: keyof ControlPanelState) => {
const osSettings = shallow(<FarmbotOsSettings {...p} />); p.bot.controlPanelState[panel] = true;
osSettings.find("input") });
.simulate("change", { currentTarget: { value: newName } }); const wrapper = mount(<FarmbotOsSettings {...p} />);
expect(edit).toHaveBeenCalledWith(p.deviceAccount, { name: newName }); ["camera", "name"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
});
});
describe("<FarmBotSettings />", () => {
const fakeProps = (): FarmbotSettingsProps => ({
device: fakeDevice(),
dispatch: jest.fn(),
bot,
alerts: [],
botOnline: true,
sourceFbosConfig: x =>
({ value: bot.hardware.configuration[x], consistent: true }),
shouldDisplay: jest.fn(() => true),
env: {},
saveFarmwareEnv: jest.fn(),
timeSettings: fakeTimeSettings(),
}); });
it("displays boot sequence selector", () => { it("displays boot sequence selector", () => {
const p = fakeProps(); const p = fakeProps();
p.shouldDisplay = () => true; p.shouldDisplay = () => true;
const osSettings = shallow(<FarmbotOsSettings {...p} />); const osSettings = shallow(<FarmBotSettings {...p} />);
expect(osSettings.find("BootSequenceSelector").length).toEqual(1); expect(osSettings.find("BootSequenceSelector").length).toEqual(1);
}); });
it("prevents default form submit action", () => {
const osSettings = shallow(<FarmbotOsSettings {...fakeProps()} />);
const e = formEvent();
osSettings.find("form").simulate("submit", e);
expect(e.preventDefault).toHaveBeenCalled();
});
it("warns about timezone mismatch", () => {
const p = fakeProps();
p.deviceAccount.body.timezone = "different";
const osSettings = mount(<FarmbotOsSettings {...p} />);
expect(osSettings.text()).toContain(Content.DIFFERENT_TZ_WARNING);
});
}); });

View File

@ -1,7 +1,14 @@
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import * as React from "react"; import * as React from "react";
import { mount, shallow } from "enzyme"; import { mount, shallow } from "enzyme";
import { HardwareSettings } from "../hardware_settings"; import { HardwareSettings } from "../hardware_settings";
import { HardwareSettingsProps } from "../../interfaces"; import { HardwareSettingsProps, ControlPanelState } from "../../interfaces";
import { Actions } from "../../../constants"; import { Actions } from "../../../constants";
import { bot } from "../../../__test_support__/fake_state/bot"; import { bot } from "../../../__test_support__/fake_state/bot";
import { panelState } from "../../../__test_support__/control_panel_state"; import { panelState } from "../../../__test_support__/control_panel_state";
@ -19,7 +26,6 @@ describe("<HardwareSettings />", () => {
const fakeProps = (): HardwareSettingsProps => ({ const fakeProps = (): HardwareSettingsProps => ({
bot, bot,
controlPanelState: panelState(), controlPanelState: panelState(),
botToMqttStatus: "up",
dispatch: jest.fn(), dispatch: jest.fn(),
sourceFwConfig: x => sourceFwConfig: x =>
({ value: fakeFirmwareConfig().body[x], consistent: true }), ({ value: fakeFirmwareConfig().body[x], consistent: true }),
@ -35,12 +41,23 @@ describe("<HardwareSettings />", () => {
expect(wrapper.text().toLowerCase()).toContain(string)); expect(wrapper.text().toLowerCase()).toContain(string));
}); });
it("renders expanded", () => {
mockDev = true;
const p = fakeProps();
Object.keys(p.controlPanelState).map((panel: keyof ControlPanelState) => {
p.controlPanelState[panel] = true;
});
const wrapper = mount(<HardwareSettings {...p} />);
["steps", "mm"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
});
function checkDispatch( function checkDispatch(
buttonElement: string, buttonElement: string,
buttonIndex: number, buttonIndex: number,
buttonText: string, buttonText: string,
type: string, type: string,
payload: boolean | string) { payload: { open: boolean, all: boolean } | string) {
const p = fakeProps(); const p = fakeProps();
const wrapper = mount(<HardwareSettings {...p} />); const wrapper = mount(<HardwareSettings {...p} />);
clickButton(wrapper, buttonIndex, buttonText, { clickButton(wrapper, buttonIndex, buttonText, {
@ -51,12 +68,12 @@ describe("<HardwareSettings />", () => {
it("expands all", () => { it("expands all", () => {
checkDispatch("button", 0, "expand all", checkDispatch("button", 0, "expand all",
Actions.BULK_TOGGLE_CONTROL_PANEL, true); Actions.BULK_TOGGLE_CONTROL_PANEL, { open: true, all: false });
}); });
it("collapses all", () => { it("collapses all", () => {
checkDispatch("button", 1, "collapse all", checkDispatch("button", 1, "collapse all",
Actions.BULK_TOGGLE_CONTROL_PANEL, false); Actions.BULK_TOGGLE_CONTROL_PANEL, { open: false, all: false });
}); });
it("toggles motor category", () => { it("toggles motor category", () => {

View File

@ -1,18 +1,26 @@
import * as React from "react"; import * as React from "react";
import { LockableButton } from "../lockable_button"; import { LockableButton, LockableButtonProps } from "../lockable_button";
import { mount } from "enzyme"; import { shallow } from "enzyme";
describe("<LockableButton/>", () => { describe("<LockableButton />", () => {
it("does not trigger callback when clicked and disabled", () => { const fakeProps = (): LockableButtonProps => ({
const fakeCB = jest.fn(); disabled: false,
const btn = mount(<LockableButton disabled={true} onClick={fakeCB} />); onClick: jest.fn(),
btn.simulate("click");
expect(fakeCB).not.toHaveBeenCalled();
}); });
it("does trigger callback when clicked and enabled", () => {
const fakeCB = jest.fn(); it("does not trigger callback when clicked and disabled", () => {
const btn = mount(<LockableButton disabled={false} onClick={fakeCB} />); const p = fakeProps();
p.disabled = true;
const btn = shallow(<LockableButton {...p} />);
btn.simulate("click"); btn.simulate("click");
expect(fakeCB).toHaveBeenCalled(); expect(p.onClick).not.toHaveBeenCalled();
});
it("does trigger callback when clicked and enabled", () => {
const p = fakeProps();
p.disabled = false;
const btn = shallow(<LockableButton {...p} />);
btn.simulate("click");
expect(p.onClick).toHaveBeenCalled();
}); });
}); });

View File

@ -9,15 +9,20 @@ import { updateMCU } from "../../actions";
import { warning } from "../../../toast/toast"; import { warning } from "../../../toast/toast";
describe("McuInputBox", () => { describe("McuInputBox", () => {
const fakeProps = (): McuInputBoxProps => { const fakeProps = (): McuInputBoxProps => ({
return { sourceFwConfig: x =>
sourceFwConfig: (x) => { ({ value: bot.hardware.mcu_params[x], consistent: true }),
return { value: bot.hardware.mcu_params[x], consistent: true }; setting: "encoder_enabled_x",
}, dispatch: jest.fn()
setting: "encoder_enabled_x", });
dispatch: jest.fn()
}; it("renders inconsistency", () => {
}; const p = fakeProps();
p.sourceFwConfig = x =>
({ value: bot.hardware.mcu_params[x], consistent: false });
const wrapper = shallow(<McuInputBox {...p} />);
expect(wrapper.find("BlurableInput").hasClass("dim")).toBeTruthy();
});
it("clamps negative numbers", () => { it("clamps negative numbers", () => {
const mib = new McuInputBox(fakeProps()); const mib = new McuInputBox(fakeProps());

View File

@ -6,38 +6,24 @@ import { BooleanMCUInputGroupProps } from "./interfaces";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { Highlight } from "./maybe_highlight"; import { Highlight } from "./maybe_highlight";
import { DevSettings } from "../../account/dev/dev_support";
export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) { export class BooleanMCUInputGroup
extends React.Component<BooleanMCUInputGroupProps> {
const { get newFormat() { return DevSettings.futureFeaturesEnabled(); }
tooltip,
label,
x,
y,
z,
disable,
grayscale,
caution,
displayAlert,
sourceFwConfig,
dispatch,
} = props;
const xParam = sourceFwConfig(x); Toggles = () => {
const yParam = sourceFwConfig(y); const {
const zParam = sourceFwConfig(z); sourceFwConfig, dispatch, disable, grayscale, displayAlert,
x, y, z,
return <Row> } = this.props;
<Highlight settingName={label}> const xParam = sourceFwConfig(x);
<Col xs={6} className={"widget-body-tooltips"}> const yParam = sourceFwConfig(y);
<label> const zParam = sourceFwConfig(z);
{t(label)} const width = this.newFormat ? 4 : 2;
{caution && return <div className={"mcu-inputs"}>
<i className="fa fa-exclamation-triangle caution-icon" />} <Col xs={width} className={"centered-button-div"}>
</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton <ToggleButton
grayscale={grayscale?.x} grayscale={grayscale?.x}
disabled={disable?.x} disabled={disable?.x}
@ -46,7 +32,7 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
toggleAction={() => toggleAction={() =>
dispatch(settingToggle(x, sourceFwConfig, displayAlert))} /> dispatch(settingToggle(x, sourceFwConfig, displayAlert))} />
</Col> </Col>
<Col xs={2} className={"centered-button-div"}> <Col xs={width} className={"centered-button-div"}>
<ToggleButton <ToggleButton
grayscale={grayscale?.y} grayscale={grayscale?.y}
disabled={disable?.y} disabled={disable?.y}
@ -55,7 +41,7 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
toggleAction={() => toggleAction={() =>
dispatch(settingToggle(y, sourceFwConfig, displayAlert))} /> dispatch(settingToggle(y, sourceFwConfig, displayAlert))} />
</Col> </Col>
<Col xs={2} className={"centered-button-div"}> <Col xs={width} className={"centered-button-div"}>
<ToggleButton <ToggleButton
grayscale={grayscale?.z} grayscale={grayscale?.z}
disabled={disable?.z} disabled={disable?.z}
@ -64,6 +50,24 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
toggleAction={() => toggleAction={() =>
dispatch(settingToggle(z, sourceFwConfig, displayAlert))} /> dispatch(settingToggle(z, sourceFwConfig, displayAlert))} />
</Col> </Col>
</Highlight> </div>;
</Row>; }
render() {
const { tooltip, label, caution } = this.props;
return <Highlight settingName={label}>
<Row>
<Col xs={this.newFormat ? 12 : 6} className={"widget-body-tooltips"}>
<label>
{t(label)}
{caution &&
<i className="fa fa-exclamation-triangle caution-icon" />}
</label>
<Help text={tooltip} position={Position.TOP_RIGHT} />
</Col>
{!this.newFormat && <this.Toggles />}
</Row>
{this.newFormat && <Row><this.Toggles /></Row>}
</Highlight>;
}
} }

View File

@ -1,23 +1,22 @@
import * as React from "react"; import * as React from "react";
import axios from "axios"; import { FarmbotOsProps, Feature, FarmbotSettingsProps } from "../interfaces";
import { t } from "../../i18next_wrapper"; import { Widget, WidgetHeader, WidgetBody } from "../../ui";
import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces"; import { isBotOnlineFromState } from "../must_be_online";
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui";
import { save, edit } from "../../api/crud";
import { isBotOnline } from "../must_be_online";
import { Content, DeviceSetting } from "../../constants";
import { TimezoneSelector } from "../timezones/timezone_selector";
import { timezoneMismatch } from "../timezones/guess_timezone";
import { CameraSelection } from "./fbos_settings/camera_selection"; import { CameraSelection } from "./fbos_settings/camera_selection";
import { BoardType } from "./fbos_settings/board_type";
import { FarmbotOsRow } from "./fbos_settings/farmbot_os_row"; import { FarmbotOsRow } from "./fbos_settings/farmbot_os_row";
import { AutoUpdateRow } from "./fbos_settings/auto_update_row"; import { AutoUpdateRow } from "./fbos_settings/auto_update_row";
import { AutoSyncRow } from "./fbos_settings/auto_sync_row"; import { AutoSyncRow } from "./fbos_settings/auto_sync_row";
import { PowerAndReset } from "./fbos_settings/power_and_reset"; import { PowerAndReset } from "./fbos_settings/power_and_reset";
import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector"; import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector";
import { ExternalUrl } from "../../external_urls";
import { Highlight } from "./maybe_highlight";
import { OtaTimeSelectorRow } from "./fbos_settings/ota_time_selector"; import { OtaTimeSelectorRow } from "./fbos_settings/ota_time_selector";
import { NameRow } from "./fbos_settings/name_row";
import { TimezoneRow } from "./fbos_settings/timezone_row";
import { Firmware } from "./fbos_settings/firmware";
import { Highlight } from "./maybe_highlight";
import { Header } from "./hardware_settings/header";
import { DeviceSetting } from "../../constants";
import { Collapse } from "@blueprintjs/core";
import { DevSettings } from "../../account/dev/dev_support";
export enum ColWidth { export enum ColWidth {
label = 3, label = 3,
@ -25,142 +24,82 @@ export enum ColWidth {
button = 2 button = 2
} }
export const FarmBotSettings = (props: FarmbotSettingsProps) => {
const {
dispatch, device, shouldDisplay, timeSettings, sourceFbosConfig,
botOnline,
} = props;
const commonProps = { dispatch };
return <Highlight className={"section"}
settingName={DeviceSetting.farmbot}>
{DevSettings.futureFeaturesEnabled() &&
<Header {...commonProps}
title={DeviceSetting.farmbot}
panel={"farmbot_os"}
dispatch={dispatch}
expanded={props.bot.controlPanelState.farmbot_os} />}
<Collapse isOpen={!!props.bot.controlPanelState.farmbot_os}>
<NameRow {...commonProps} device={device} />
<TimezoneRow {...commonProps} device={device} />
<CameraSelection {...commonProps}
env={props.env}
botOnline={botOnline}
saveFarmwareEnv={props.saveFarmwareEnv}
shouldDisplay={shouldDisplay} />
<OtaTimeSelectorRow {...commonProps}
timeSettings={timeSettings}
device={device}
sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow {...commonProps}
sourceFbosConfig={sourceFbosConfig} />
<FarmbotOsRow {...commonProps}
bot={props.bot}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={shouldDisplay}
botOnline={botOnline}
timeSettings={timeSettings}
deviceAccount={device} />
<AutoSyncRow {...commonProps}
sourceFbosConfig={sourceFbosConfig} />
{shouldDisplay(Feature.boot_sequence) && <BootSequenceSelector />}
</Collapse>
</Highlight>;
};
export class FarmbotOsSettings export class FarmbotOsSettings
extends React.Component<FarmbotOsProps, FarmbotOsState> { extends React.Component<FarmbotOsProps, {}> {
state: FarmbotOsState = { allOsReleaseNotes: "" };
componentDidMount() {
this.fetchReleaseNotes(ExternalUrl.osReleaseNotes);
}
get osMajorVersion() {
return (this.props.bot.hardware.informational_settings
.controller_version || "6").split(".")[0];
}
fetchReleaseNotes = (url: string) => {
axios
.get<string>(url)
.then(resp => this.setState({ allOsReleaseNotes: resp.data }))
.catch(() => this.setState({ allOsReleaseNotes: "" }));
}
get osReleaseNotes() {
const notes = (this.state.allOsReleaseNotes
.split("# v")
.filter(x => x.startsWith(this.osMajorVersion))[0] || "")
.split("\n\n").slice(1).join("\n") || t("Could not get release notes.");
const heading = "FarmBot OS v" + this.osMajorVersion;
return { heading, notes };
}
changeBot = (e: React.FormEvent<HTMLInputElement>) => {
const { deviceAccount, dispatch } = this.props;
dispatch(edit(deviceAccount, { name: e.currentTarget.value }));
}
updateBot = () => {
const { deviceAccount, dispatch } = this.props;
dispatch(save(deviceAccount.uuid));
}
handleTimezone = (timezone: string) => {
const { deviceAccount, dispatch } = this.props;
dispatch(edit(deviceAccount, { timezone }));
dispatch(save(deviceAccount.uuid));
}
maybeWarnTz = () => {
const wrongTZ = timezoneMismatch(this.props.deviceAccount.body.timezone);
return wrongTZ ? t(Content.DIFFERENT_TZ_WARNING) : "";
}
render() { render() {
const { bot, sourceFbosConfig, botToMqttStatus } = this.props; const { bot, sourceFbosConfig } = this.props;
const { sync_status } = bot.hardware.informational_settings; const botOnline = isBotOnlineFromState(bot);
const botOnline = isBotOnline(sync_status, botToMqttStatus);
return <Widget className="device-widget"> return <Widget className="device-widget">
<form onSubmit={(e) => e.preventDefault()}> <WidgetHeader title="Device">
<WidgetHeader title="Device"> </WidgetHeader>
</WidgetHeader> <WidgetBody>
<WidgetBody> <FarmBotSettings
<Row> bot={bot}
<Highlight settingName={DeviceSetting.name}> env={this.props.env}
<Col xs={ColWidth.label}> alerts={this.props.alerts}
<label> saveFarmwareEnv={this.props.saveFarmwareEnv}
{t(DeviceSetting.name)} dispatch={this.props.dispatch}
</label> sourceFbosConfig={sourceFbosConfig}
</Col> shouldDisplay={this.props.shouldDisplay}
<Col xs={9}> botOnline={botOnline}
<input name="name" timeSettings={this.props.timeSettings}
onChange={this.changeBot} device={this.props.deviceAccount} />
onBlur={this.updateBot} <Firmware
value={this.props.deviceAccount.body.name} /> bot={this.props.bot}
</Col> alerts={this.props.alerts}
</Highlight> dispatch={this.props.dispatch}
</Row> sourceFbosConfig={sourceFbosConfig}
<Row> shouldDisplay={this.props.shouldDisplay}
<Highlight settingName={DeviceSetting.timezone}> botOnline={botOnline}
<Col xs={ColWidth.label}> timeSettings={this.props.timeSettings} />
<label> <PowerAndReset
{t("TIME ZONE")} controlPanelState={this.props.bot.controlPanelState}
</label> dispatch={this.props.dispatch}
</Col> sourceFbosConfig={sourceFbosConfig}
<Col xs={ColWidth.description}> botOnline={botOnline} />
<div className="note"> </WidgetBody>
{this.maybeWarnTz()}
</div>
<TimezoneSelector
currentTimezone={this.props.deviceAccount.body.timezone}
onUpdate={this.handleTimezone} />
</Col>
</Highlight>
</Row>
<CameraSelection
env={this.props.env}
botOnline={botOnline}
saveFarmwareEnv={this.props.saveFarmwareEnv}
shouldDisplay={this.props.shouldDisplay}
dispatch={this.props.dispatch} />
<BoardType
botOnline={botOnline}
bot={bot}
alerts={this.props.alerts}
dispatch={this.props.dispatch}
shouldDisplay={this.props.shouldDisplay}
timeSettings={this.props.timeSettings}
sourceFbosConfig={sourceFbosConfig} />
<OtaTimeSelectorRow
timeSettings={this.props.timeSettings}
device={this.props.deviceAccount}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<FarmbotOsRow
bot={this.props.bot}
osReleaseNotesHeading={this.osReleaseNotes.heading}
osReleaseNotes={this.osReleaseNotes.notes}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline}
botToMqttLastSeen={new Date(this.props.botToMqttLastSeen).getTime()}
timeSettings={this.props.timeSettings}
deviceAccount={this.props.deviceAccount} />
<AutoSyncRow
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
{this.props.shouldDisplay(Feature.boot_sequence) &&
<BootSequenceSelector />}
<PowerAndReset
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />
</WidgetBody>
</form>
</Widget>; </Widget>;
} }
} }

View File

@ -33,19 +33,32 @@ describe("<BoardType/>", () => {
shouldDisplay: () => false, shouldDisplay: () => false,
botOnline: true, botOnline: true,
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
firmwareHardware: undefined,
}); });
it("Disconnected with valid FirmwareConfig", () => { it("renders with valid firmwareHardware", () => {
const p = fakeProps(); const p = fakeProps();
p.sourceFbosConfig = () => ({ value: "farmduino", consistent: false }); p.firmwareHardware = "farmduino";
const wrapper = mount(<BoardType {...p} />); const wrapper = mount(<BoardType {...p} />);
expect(wrapper.text()).toContain("Farmduino"); expect(wrapper.text()).toContain("Farmduino");
}); });
it("sets sending status", () => {
const wrapper = mount<BoardType>(<BoardType {...fakeProps()} />);
expect(wrapper.state().sending).toBeFalsy();
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: true, consistent: false });
wrapper.setProps(p);
wrapper.mount();
expect(wrapper.state().sending).toBeTruthy();
});
it("calls updateConfig", () => { it("calls updateConfig", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = shallow(<BoardType {...p} />); const wrapper = mount<BoardType>(<BoardType {...p} />);
wrapper.find("FBSelect").simulate("change", const selection =
shallow(<div>{wrapper.instance().FirmwareSelection()}</div>);
selection.find("FBSelect").simulate("change",
{ label: "firmware_hardware", value: "farmduino" }); { label: "firmware_hardware", value: "farmduino" });
expect(edit).toHaveBeenCalledWith(fakeConfig, { expect(edit).toHaveBeenCalledWith(fakeConfig, {
firmware_hardware: "farmduino" firmware_hardware: "farmduino"
@ -53,17 +66,19 @@ describe("<BoardType/>", () => {
expect(save).toHaveBeenCalledWith(fakeConfig.uuid); expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
}); });
it("deosn't call updateConfig", () => { it("doesn't call updateConfig", () => {
const p = fakeProps(); const p = fakeProps();
const wrapper = shallow(<BoardType {...p} />); const wrapper = mount<BoardType>(<BoardType {...p} />);
wrapper.find("FBSelect").simulate("change", const selection =
shallow(<div>{wrapper.instance().FirmwareSelection()}</div>);
selection.find("FBSelect").simulate("change",
{ label: "firmware_hardware", value: "unknown" }); { label: "firmware_hardware", value: "unknown" });
expect(edit).not.toHaveBeenCalled(); expect(edit).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled(); expect(save).not.toHaveBeenCalled();
}); });
it("displays standard boards", () => { it("displays standard boards", () => {
const wrapper = shallow(<BoardType {...fakeProps()} />); const wrapper = mount(<BoardType {...fakeProps()} />);
const { list } = wrapper.find("FBSelect").props(); const { list } = wrapper.find("FBSelect").props();
expect(list).toEqual([ expect(list).toEqual([
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" }, { label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
@ -78,7 +93,7 @@ describe("<BoardType/>", () => {
it("displays new boards", () => { it("displays new boards", () => {
const p = fakeProps(); const p = fakeProps();
p.shouldDisplay = () => true; p.shouldDisplay = () => true;
const wrapper = shallow(<BoardType {...p} />); const wrapper = mount(<BoardType {...p} />);
const { list } = wrapper.find("FBSelect").props(); const { list } = wrapper.find("FBSelect").props();
expect(list).toEqual([ expect(list).toEqual([
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" }, { label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },

View File

@ -1,3 +1,10 @@
let mockDev = false;
jest.mock("../../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import { import {
sequence2ddi, mapStateToProps, RawBootSequenceSelector, sequence2ddi, mapStateToProps, RawBootSequenceSelector,
} from "../boot_sequence_selector"; } from "../boot_sequence_selector";
@ -11,9 +18,6 @@ import {
import React from "react"; import React from "react";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { FBSelect } from "../../../../ui"; import { FBSelect } from "../../../../ui";
// import { mount } from "enzyme";
// import React from "react";
// import { FBSelect } from "../../../../ui";
describe("sequence2ddi", () => { describe("sequence2ddi", () => {
it("converts TaggedSequences", () => { it("converts TaggedSequences", () => {
@ -98,4 +102,10 @@ describe("RawBootSequenceSelector", () => {
const el = mount(<RawBootSequenceSelector {...props} />); const el = mount(<RawBootSequenceSelector {...props} />);
expect(el.find(FBSelect).length).toEqual(1); expect(el.find(FBSelect).length).toEqual(1);
}); });
it("renders formatted", () => {
mockDev = true;
const el = mount(<RawBootSequenceSelector {...fakeProps()} />);
expect(el.find(FBSelect).length).toEqual(1);
});
}); });

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { FarmbotOsRow } from "../farmbot_os_row"; import { FarmbotOsRow, getOsReleaseNotesForVersion } from "../farmbot_os_row";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { bot } from "../../../../__test_support__/fake_state/bot"; import { bot } from "../../../../__test_support__/fake_state/bot";
import { FarmbotOsRowProps } from "../interfaces"; import { FarmbotOsRowProps } from "../interfaces";
@ -10,15 +10,12 @@ import { fakeDevice } from "../../../../__test_support__/resource_index_builder"
describe("<FarmbotOsRow/>", () => { describe("<FarmbotOsRow/>", () => {
const fakeProps = (): FarmbotOsRowProps => ({ const fakeProps = (): FarmbotOsRowProps => ({
bot, bot,
osReleaseNotesHeading: "",
osReleaseNotes: "",
dispatch: jest.fn(x => x(jest.fn(), fakeState)), dispatch: jest.fn(x => x(jest.fn(), fakeState)),
sourceFbosConfig: (x) => { sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true }; return { value: bot.hardware.configuration[x], consistent: true };
}, },
shouldDisplay: () => false, shouldDisplay: () => false,
botOnline: false, botOnline: false,
botToMqttLastSeen: 0,
deviceAccount: fakeDevice(), deviceAccount: fakeDevice(),
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
}); });
@ -37,3 +34,12 @@ describe("<FarmbotOsRow/>", () => {
expect(wrapper.text().toLowerCase()).toContain("1.0.0-beta"); expect(wrapper.text().toLowerCase()).toContain("1.0.0-beta");
}); });
}); });
describe("getOsReleaseNotesForVersion()", () => {
it("fetches OS release notes", () => {
const mockData = "intro\n\n# v6\n\n* note";
const result = getOsReleaseNotesForVersion(mockData, "6.0.0");
expect(result.heading).toEqual("FarmBot OS v6");
expect(result.notes).toEqual("* note");
});
});

View File

@ -71,9 +71,11 @@ describe("<FbosDetails/>", () => {
expect(wrapper.text()).not.toContain("name@"); expect(wrapper.text()).not.toContain("name@");
}); });
it("handles missing firmware version", () => { it("handles missing data", () => {
const p = fakeProps(); const p = fakeProps();
p.botInfoSettings.firmware_version = undefined; p.botInfoSettings.firmware_version = undefined;
p.botInfoSettings.node_name = "";
p.botInfoSettings.commit = "";
const wrapper = mount(<FbosDetails {...p} />); const wrapper = mount(<FbosDetails {...p} />);
expect(wrapper.text()).toContain("---"); expect(wrapper.text()).toContain("---");
}); });

View File

@ -8,11 +8,8 @@ import {
FirmwareHardwareStatusDetailsProps, FirmwareHardwareStatusDetails, FirmwareHardwareStatusDetailsProps, FirmwareHardwareStatusDetails,
FirmwareHardwareStatusIconProps, FirmwareHardwareStatusIcon, FirmwareHardwareStatusIconProps, FirmwareHardwareStatusIcon,
FirmwareHardwareStatusProps, FirmwareHardwareStatus, FirmwareHardwareStatusProps, FirmwareHardwareStatus,
FirmwareActions, FirmwareActionsProps,
} from "../firmware_hardware_status"; } from "../firmware_hardware_status";
import { bot } from "../../../../__test_support__/fake_state/bot"; import { bot } from "../../../../__test_support__/fake_state/bot";
import { clickButton } from "../../../../__test_support__/helpers";
import { flashFirmware } from "../../../actions";
import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings";
describe("<FirmwareHardwareStatusDetails />", () => { describe("<FirmwareHardwareStatusDetails />", () => {
@ -96,16 +93,3 @@ describe("<FirmwareHardwareStatus />", () => {
expect(wrapper.find(FirmwareHardwareStatusIcon).props().status).toBeTruthy(); expect(wrapper.find(FirmwareHardwareStatusIcon).props().status).toBeTruthy();
}); });
}); });
describe("<FirmwareActions />", () => {
const fakeProps = (): FirmwareActionsProps => ({
botOnline: true,
apiFirmwareValue: "arduino",
});
it("flashes firmware", () => {
const wrapper = mount(<FirmwareActions {...fakeProps()} />);
clickButton(wrapper, 0, "flash firmware");
expect(flashFirmware).toHaveBeenCalledWith("arduino");
});
});

View File

@ -0,0 +1,57 @@
const mockDevice = {
rebootFirmware: jest.fn(() => Promise.resolve()),
flashFirmware: jest.fn(() => Promise.resolve()),
};
jest.mock("../../../../device", () => ({ getDevice: () => mockDevice }));
import * as React from "react";
import { mount } from "enzyme";
import { Firmware } from "../firmware";
import { FirmwareProps } from "../interfaces";
import { fakeState } from "../../../../__test_support__/fake_state";
import { clickButton } from "../../../../__test_support__/helpers";
import { fakeFbosConfig } from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex,
} from "../../../../__test_support__/resource_index_builder";
import {
fakeTimeSettings,
} from "../../../../__test_support__/fake_time_settings";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<Firmware />", () => {
const fakeConfig = fakeFbosConfig();
const state = fakeState();
state.resources = buildResourceIndex([fakeConfig]);
const fakeProps = (): FirmwareProps => ({
dispatch: jest.fn(x => x(jest.fn(), () => state)),
sourceFbosConfig: () => ({ value: true, consistent: true }),
botOnline: true,
bot: bot,
alerts: [],
shouldDisplay: jest.fn(),
timeSettings: fakeTimeSettings(),
});
it("restarts firmware", () => {
const p = fakeProps();
p.bot.controlPanelState.firmware = true;
const wrapper = mount(<Firmware {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("Restart Firmware".toLowerCase());
clickButton(wrapper, 1, "restart");
expect(mockDevice.rebootFirmware).toHaveBeenCalled();
});
it("flashes firmware", () => {
const p = fakeProps();
p.bot.controlPanelState.firmware = true;
p.sourceFbosConfig = () => ({ value: "arduino", consistent: true });
const wrapper = mount(<Firmware {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("Flash Firmware".toLowerCase());
clickButton(wrapper, 2, "flash firmware");
expect(mockDevice.flashFirmware).toHaveBeenCalledWith("arduino");
});
});

View File

@ -1,43 +1,36 @@
jest.mock("../../../../api/crud", () => ({ refresh: jest.fn() })); jest.mock("../../../../api/crud", () => ({ refresh: jest.fn() }));
import * as React from "react"; import * as React from "react";
import { fakeResource } from "../../../../__test_support__/fake_resource"; import { LastSeen, LastSeenProps, getLastSeenNumber } from "../last_seen_row";
import { LastSeen, LastSeenProps } from "../last_seen_row";
import { mount } from "enzyme"; import { mount } from "enzyme";
import { SpecialStatus, TaggedDevice } from "farmbot"; import { SpecialStatus } from "farmbot";
import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings"; import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings";
import { refresh } from "../../../../api/crud"; import { refresh } from "../../../../api/crud";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { fakeDevice } from "../../../../__test_support__/resource_index_builder";
describe("<LastSeen />", () => { describe("<LastSeen />", () => {
const resource = (): TaggedDevice => fakeResource("Device", { const fakeProps = (): LastSeenProps => ({
id: 1, device: fakeDevice(),
name: "foo",
last_saw_api: "",
tz_offset_hrs: 0,
ota_hour: 3
});
const props = (): LastSeenProps => ({
device: resource(),
botToMqttLastSeen: 0, botToMqttLastSeen: 0,
dispatch: jest.fn(), dispatch: jest.fn(),
timeSettings: fakeTimeSettings(), timeSettings: fakeTimeSettings(),
}); });
it("blinks when loading", () => { it("blinks when loading", () => {
const p = props(); const p = fakeProps();
p.device.specialStatus = SpecialStatus.SAVING; p.device.specialStatus = SpecialStatus.SAVING;
const wrapper = mount(<LastSeen {...p} />); const wrapper = mount(<LastSeen {...p} />);
expect(wrapper.text()).toContain("Loading"); expect(wrapper.text()).toContain("Loading");
}); });
it("tells you the device has never been seen", () => { it("tells you the device has never been seen", () => {
const wrapper = mount(<LastSeen {...props()} />); const wrapper = mount(<LastSeen {...fakeProps()} />);
expect(wrapper.text()).toContain("network connectivity issue"); expect(wrapper.text()).toContain("network connectivity issue");
}); });
it("tells you when the device was last seen, no MQTT", () => { it("tells you when the device was last seen, no MQTT", () => {
const p = props(); const p = fakeProps();
p.device.body.last_saw_api = "2017-08-07T19:40:01.487Z"; p.device.body.last_saw_api = "2017-08-07T19:40:01.487Z";
p.botToMqttLastSeen = 0; p.botToMqttLastSeen = 0;
const wrapper = mount<LastSeen>(<LastSeen {...p} />); const wrapper = mount<LastSeen>(<LastSeen {...p} />);
@ -45,7 +38,7 @@ describe("<LastSeen />", () => {
}); });
it("tells you when the device was last seen, latest: API", () => { it("tells you when the device was last seen, latest: API", () => {
const p = props(); const p = fakeProps();
p.device.body.last_saw_api = "2017-08-07T19:40:01.487Z"; p.device.body.last_saw_api = "2017-08-07T19:40:01.487Z";
p.botToMqttLastSeen = new Date("2016-08-07T19:40:01.487Z").getTime(); p.botToMqttLastSeen = new Date("2016-08-07T19:40:01.487Z").getTime();
const wrapper = mount<LastSeen>(<LastSeen {...p} />); const wrapper = mount<LastSeen>(<LastSeen {...p} />);
@ -53,7 +46,7 @@ describe("<LastSeen />", () => {
}); });
it("tells you when the device was last seen, latest: message broker", () => { it("tells you when the device was last seen, latest: message broker", () => {
const p = props(); const p = fakeProps();
p.device.body.last_saw_api = "2017-08-07T19:40:01.487Z"; p.device.body.last_saw_api = "2017-08-07T19:40:01.487Z";
p.botToMqttLastSeen = new Date("2017-08-07T20:40:01.487Z").getTime(); p.botToMqttLastSeen = new Date("2017-08-07T20:40:01.487Z").getTime();
const wrapper = mount<LastSeen>(<LastSeen {...p} />); const wrapper = mount<LastSeen>(<LastSeen {...p} />);
@ -62,9 +55,22 @@ describe("<LastSeen />", () => {
}); });
it("handles a click", () => { it("handles a click", () => {
const p = props(); const p = fakeProps();
const wrapper = mount(<LastSeen {...p} />); const wrapper = mount(<LastSeen {...p} />);
wrapper.find("i").simulate("click"); wrapper.find("i").simulate("click");
expect(refresh).toHaveBeenCalled(); expect(refresh).toHaveBeenCalled();
}); });
}); });
describe("getLastSeenNumber()", () => {
it("returns number: unknown", () => {
const result = getLastSeenNumber(bot);
expect(result).toEqual(NaN);
});
it("returns number: known", () => {
bot.connectivity.uptime["bot.mqtt"] = { state: "up", at: 0 };
const result = getLastSeenNumber(bot);
expect(result).toEqual(0);
});
});

View File

@ -0,0 +1,34 @@
jest.mock("../../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { NameRow } from "../name_row";
import { NameRowProps } from "../interfaces";
import { edit, save } from "../../../../api/crud";
import { fakeDevice } from "../../../../__test_support__/resource_index_builder";
describe("<NameRow />", () => {
const fakeProps = (): NameRowProps => ({
device: fakeDevice(),
dispatch: jest.fn(),
});
it("changes bot name", () => {
const p = fakeProps();
const newName = "new bot name";
const osSettings = mount<NameRow>(<NameRow {...p} />);
shallow(osSettings.instance().NameInput())
.simulate("change", { currentTarget: { value: newName } });
expect(edit).toHaveBeenCalledWith(p.device, { name: newName });
});
it("saves bot name", () => {
const p = fakeProps();
const osSettings = mount<NameRow>(<NameRow {...p} />);
shallow(osSettings.instance().NameInput()).simulate("blur");
expect(save).toHaveBeenCalledWith(p.device.uuid);
});
});

View File

@ -1,10 +1,16 @@
import React from "react"; import React from "react";
import { OtaTimeSelector, changeOtaHour, assertIsHour } from "../ota_time_selector"; import {
import { shallow } from "enzyme"; OtaTimeSelector, changeOtaHour, assertIsHour, OtaTimeSelectorRow,
OtaTimeSelectorProps,
ASAP,
} from "../ota_time_selector";
import { shallow, mount } from "enzyme";
import { FBSelect } from "../../../../ui"; import { FBSelect } from "../../../../ui";
import { fakeDevice } from "../../../../__test_support__/resource_index_builder"; import { fakeDevice } from "../../../../__test_support__/resource_index_builder";
import { OtaTimeSelectorRowProps } from "../interfaces";
import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings";
describe("OTA time selector", () => { describe("assertIsHour()", () => {
it("asserts that a variable is an HOUR", () => { it("asserts that a variable is an HOUR", () => {
expect(assertIsHour(undefined)).toBe(undefined); expect(assertIsHour(undefined)).toBe(undefined);
// tslint:disable-next-line:no-null-keyword // tslint:disable-next-line:no-null-keyword
@ -14,29 +20,42 @@ describe("OTA time selector", () => {
expect(crashOn(-2)).toThrowError("Not an hour!"); expect(crashOn(-2)).toThrowError("Not an hour!");
expect(crashOn(24)).toThrowError("Not an hour!"); expect(crashOn(24)).toThrowError("Not an hour!");
}); });
});
describe("<OtaTimeSelector />", () => {
const fakeProps = (): OtaTimeSelectorProps => ({
timeFormat: "12h",
disabled: false,
onChange: jest.fn(),
value: 3,
});
it("selects an OTA update time", () => { it("selects an OTA update time", () => {
const onUpdate = jest.fn(); const p = fakeProps();
const el = shallow(<OtaTimeSelector const el = shallow(<OtaTimeSelector {...p} />);
timeFormat={"12h"}
disabled={false}
onChange={onUpdate}
value={3} />);
el.find(FBSelect).simulate("change", { label: "at 5 PM", value: 17 }); el.find(FBSelect).simulate("change", { label: "at 5 PM", value: 17 });
expect(onUpdate).toHaveBeenCalledWith(17); expect(p.onChange).toHaveBeenCalledWith(17);
}); });
it("unselects an OTA update time", () => { it("unselects an OTA update time", () => {
const onUpdate = jest.fn(); const p = fakeProps();
const el = shallow(<OtaTimeSelector const el = shallow(<OtaTimeSelector {...p} />);
timeFormat={"12h"}
disabled={false}
onChange={onUpdate}
value={3} />);
el.find(FBSelect).simulate("change", { label: "no", value: -1 }); el.find(FBSelect).simulate("change", { label: "no", value: -1 });
// tslint:disable-next-line:no-null-keyword // tslint:disable-next-line:no-null-keyword
expect(onUpdate).toHaveBeenCalledWith(null); expect(p.onChange).toHaveBeenCalledWith(null);
}); });
it("select a default value", () => {
const p = fakeProps();
p.value = undefined;
const el = shallow(<OtaTimeSelector {...p} />);
expect(el.find(FBSelect).props().selectedItem).toEqual({
label: ASAP(), value: -1
});
});
});
describe("changeOtaHour()", () => {
it("changes the OTA hour", () => { it("changes the OTA hour", () => {
const device = fakeDevice(); const device = fakeDevice();
const dispatch = jest.fn(); const dispatch = jest.fn();
@ -54,3 +73,28 @@ describe("OTA time selector", () => {
}); });
}); });
}); });
describe("<OtaTimeSelectorRow />", () => {
const fakeProps = (): OtaTimeSelectorRowProps => ({
dispatch: jest.fn(),
sourceFbosConfig: () => ({ value: "", consistent: true }),
device: fakeDevice(),
timeSettings: fakeTimeSettings(),
});
it("shows 12h formatted times", () => {
const p = fakeProps();
p.timeSettings.hour24 = false;
const wrapper = mount(<OtaTimeSelectorRow {...p} />);
expect(wrapper.find(FBSelect).props().list)
.toContainEqual({ label: "8:00 PM", value: 20 });
});
it("shows 24h formatted times", () => {
const p = fakeProps();
p.timeSettings.hour24 = true;
const wrapper = mount(<OtaTimeSelectorRow {...p} />);
expect(wrapper.find(FBSelect).props().list)
.toContainEqual({ label: "20:00", value: 20 });
});
});

View File

@ -6,13 +6,6 @@ jest.mock("../../../../api/crud", () => ({
save: jest.fn(), save: jest.fn(),
})); }));
let mockDev = false;
jest.mock("../../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import * as React from "react"; import * as React from "react";
import { PowerAndReset } from "../power_and_reset"; import { PowerAndReset } from "../power_and_reset";
import { mount } from "enzyme"; import { mount } from "enzyme";
@ -28,10 +21,6 @@ import {
import { edit, save } from "../../../../api/crud"; import { edit, save } from "../../../../api/crud";
describe("<PowerAndReset/>", () => { describe("<PowerAndReset/>", () => {
beforeEach(() => {
mockDev = false;
});
const fakeConfig = fakeFbosConfig(); const fakeConfig = fakeFbosConfig();
const state = fakeState(); const state = fakeState();
state.resources = buildResourceIndex([fakeConfig]); state.resources = buildResourceIndex([fakeConfig]);
@ -52,17 +41,6 @@ describe("<PowerAndReset/>", () => {
"Connection Attempt Period", "Change Ownership"] "Connection Attempt Period", "Change Ownership"]
.map(string => expect(wrapper.text().toLowerCase()) .map(string => expect(wrapper.text().toLowerCase())
.toContain(string.toLowerCase())); .toContain(string.toLowerCase()));
expect(wrapper.text().toLowerCase())
.toContain("Restart Firmware".toLowerCase());
});
it("doesn't render restart firmware", () => {
mockDev = true;
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.text().toLowerCase())
.not.toContain("Restart Firmware".toLowerCase());
}); });
it("renders as closed", () => { it("renders as closed", () => {
@ -90,21 +68,11 @@ describe("<PowerAndReset/>", () => {
p.sourceFbosConfig = () => ({ value: false, consistent: true }); p.sourceFbosConfig = () => ({ value: false, consistent: true });
p.controlPanelState.power_and_reset = true; p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />); const wrapper = mount(<PowerAndReset {...p} />);
clickButton(wrapper, 4, "yes"); clickButton(wrapper, 3, "yes");
expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true }); expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid); expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
}); });
it("restarts firmware", () => {
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("Restart Firmware".toLowerCase());
clickButton(wrapper, 2, "restart");
expect(mockDevice.rebootFirmware).toHaveBeenCalled();
});
it("shows change ownership button", () => { it("shows change ownership button", () => {
const p = fakeProps(); const p = fakeProps();
p.controlPanelState.power_and_reset = true; p.controlPanelState.power_and_reset = true;

View File

@ -0,0 +1,34 @@
jest.mock("../../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { TimezoneRow } from "../timezone_row";
import { TimezoneRowProps } from "../interfaces";
import { edit } from "../../../../api/crud";
import { Content } from "../../../../constants";
import { fakeDevice } from "../../../../__test_support__/resource_index_builder";
describe("<TimezoneRow />", () => {
const fakeProps = (): TimezoneRowProps => ({
device: fakeDevice(),
dispatch: jest.fn(),
});
it("warns about timezone mismatch", () => {
const p = fakeProps();
p.device.body.timezone = "different";
const osSettings = mount(<TimezoneRow {...p} />);
expect(osSettings.text()).toContain(Content.DIFFERENT_TZ_WARNING);
});
it("select timezone", () => {
const p = fakeProps();
const osSettings = mount<TimezoneRow>(<TimezoneRow {...p} />);
const selector = shallow(<div>{osSettings.instance().Selector()}</div>);
selector.find("TimezoneSelector").simulate("update", "fake timezone");
expect(edit).toHaveBeenCalledWith(p.device, { timezone: "fake timezone" });
});
});

View File

@ -7,22 +7,25 @@ import { ColWidth } from "../farmbot_os_settings";
import { AutoSyncRowProps } from "./interfaces"; import { AutoSyncRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function AutoSyncRow(props: AutoSyncRowProps) { export function AutoSyncRow(props: AutoSyncRowProps) {
const autoSync = props.sourceFbosConfig("auto_sync"); const autoSync = props.sourceFbosConfig("auto_sync");
return <Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Highlight settingName={DeviceSetting.autoSync}> return <Highlight settingName={DeviceSetting.autoSync}>
<Col xs={ColWidth.label}> <Row>
<Col xs={newFormat ? 9 : ColWidth.label}>
<label> <label>
{t("AUTO SYNC")} {t("AUTO SYNC")}
</label> </label>
</Col> </Col>
<Col xs={ColWidth.description}> {!newFormat &&
<p> <Col xs={ColWidth.description}>
{t(Content.AUTO_SYNC)} <p>
</p> {t(Content.AUTO_SYNC)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>}
<Col xs={newFormat ? 3 : ColWidth.button}>
<ToggleButton <ToggleButton
toggleValue={autoSync.value} toggleValue={autoSync.value}
dim={!autoSync.consistent} dim={!autoSync.consistent}
@ -30,6 +33,7 @@ export function AutoSyncRow(props: AutoSyncRowProps) {
props.dispatch(updateConfig({ auto_sync: !autoSync.value })); props.dispatch(updateConfig({ auto_sync: !autoSync.value }));
}} /> }} />
</Col> </Col>
</Highlight> </Row>
</Row>; {newFormat && <Row><p>{t(Content.AUTO_SYNC)}</p></Row>}
</Highlight>;
} }

View File

@ -7,29 +7,32 @@ import { Content, DeviceSetting } from "../../../constants";
import { AutoUpdateRowProps } from "./interfaces"; import { AutoUpdateRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function AutoUpdateRow(props: AutoUpdateRowProps) { export function AutoUpdateRow(props: AutoUpdateRowProps) {
const osAutoUpdate = props.sourceFbosConfig("os_auto_update"); const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
const newFormat = DevSettings.futureFeaturesEnabled();
return <Row> return <Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}> <Row>
<Col xs={ColWidth.label}> <Col xs={newFormat ? 9 : ColWidth.label}>
<label> <label>
{t(DeviceSetting.farmbotOSAutoUpdate)} {t(DeviceSetting.farmbotOSAutoUpdate)}
</label> </label>
</Col> </Col>
<Col xs={ColWidth.description}> {!newFormat &&
<p> <Col xs={ColWidth.description}>
{t(Content.OS_AUTO_UPDATE)} <p>
</p> {t(Content.OS_AUTO_UPDATE)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>}
<Col xs={newFormat ? 3 : ColWidth.button}>
<ToggleButton toggleValue={osAutoUpdate.value} <ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent} dim={!osAutoUpdate.consistent}
toggleAction={() => props.dispatch(updateConfig({ toggleAction={() => props.dispatch(updateConfig({
os_auto_update: !osAutoUpdate.value os_auto_update: !osAutoUpdate.value
}))} /> }))} />
</Col> </Col>
</Highlight> </Row>
</Row>; {newFormat && <Row><p>{t(Content.OS_AUTO_UPDATE)}</p></Row>}
</Highlight>;
} }

View File

@ -1,7 +1,6 @@
import * as React from "react"; import * as React from "react";
import { Row, Col, DropDownItem, FBSelect } from "../../../ui"; import { Row, Col, DropDownItem, FBSelect } from "../../../ui";
import { info } from "../../../toast/toast"; import { info } from "../../../toast/toast";
import { FirmwareHardware } from "farmbot";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { updateConfig } from "../../actions"; import { updateConfig } from "../../actions";
import { BoardTypeProps } from "./interfaces"; import { BoardTypeProps } from "./interfaces";
@ -12,6 +11,7 @@ import {
} from "../firmware_hardware_support"; } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants"; import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
interface BoardTypeState { sending: boolean } interface BoardTypeState { sending: boolean }
@ -28,13 +28,10 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
return !this.props.sourceFbosConfig("firmware_hardware").consistent; return !this.props.sourceFbosConfig("firmware_hardware").consistent;
} }
get apiValue(): FirmwareHardware | undefined {
const { value } = this.props.sourceFbosConfig("firmware_hardware");
return isFwHardwareValue(value) ? value : undefined;
}
get selectedBoard(): DropDownItem | undefined { get selectedBoard(): DropDownItem | undefined {
return this.apiValue ? FIRMWARE_CHOICES_DDI[this.apiValue] : undefined; return this.props.firmwareHardware
? FIRMWARE_CHOICES_DDI[this.props.firmwareHardware]
: undefined;
} }
sendOffConfig = (selectedItem: DropDownItem) => { sendOffConfig = (selectedItem: DropDownItem) => {
@ -47,32 +44,43 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
} }
} }
FirmwareSelection = () =>
<FBSelect
key={this.props.firmwareHardware}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
render() { render() {
return <Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Highlight settingName={DeviceSetting.firmware}> return <Highlight settingName={DeviceSetting.firmware}>
<Row>
<Col xs={ColWidth.label}> <Col xs={ColWidth.label}>
<label> <label>
{t("FIRMWARE")} {t("FIRMWARE")}
</label> </label>
</Col> </Col>
<Col xs={ColWidth.description}> {!newFormat &&
<FBSelect <Col xs={ColWidth.description}>
key={this.apiValue} <this.FirmwareSelection />
extraClass={this.state.sending ? "dim" : ""} </Col>}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</Col>
<Col xs={ColWidth.button}> <Col xs={ColWidth.button}>
<FirmwareHardwareStatus <FirmwareHardwareStatus
botOnline={this.props.botOnline} botOnline={this.props.botOnline}
apiFirmwareValue={this.apiValue} apiFirmwareValue={this.props.firmwareHardware}
alerts={this.props.alerts} alerts={this.props.alerts}
bot={this.props.bot} bot={this.props.bot}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings} /> timeSettings={this.props.timeSettings} />
</Col> </Col>
</Highlight> </Row>
</Row>; {newFormat &&
<Row>
<Col xs={12} className="no-pad">
<this.FirmwareSelection />
</Col>
</Row>}
</Highlight>;
} }
} }

View File

@ -11,6 +11,7 @@ import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants"; import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
interface Props { interface Props {
list: DropDownItem[]; list: DropDownItem[];
@ -56,23 +57,34 @@ export class RawBootSequenceSelector extends React.Component<Props, {}> {
this.props.dispatch(save(this.props.config.uuid)); this.props.dispatch(save(this.props.config.uuid));
} }
SelectionInput = () =>
<FBSelect
allowEmpty={true}
list={this.props.list}
selectedItem={this.props.selectedItem}
onChange={this.onChange} />
render() { render() {
return <Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Highlight settingName={DeviceSetting.bootSequence}> return <Highlight settingName={DeviceSetting.bootSequence}>
<Col xs={ColWidth.label}> <Row>
<Col xs={newFormat ? 12 : ColWidth.label}>
<label> <label>
{t("BOOT SEQUENCE")} {t("BOOT SEQUENCE")}
</label> </label>
</Col> </Col>
<Col xs={7}> {!newFormat &&
<FBSelect <Col xs={ColWidth.description}>
allowEmpty={true} <this.SelectionInput />
list={this.props.list} </Col>}
selectedItem={this.props.selectedItem} </Row>
onChange={this.onChange} /> {newFormat &&
</Col> <Row>
</Highlight> <Col xs={12} className="no-pad">
</Row>; <this.SelectionInput />
</Col>
</Row>}
</Highlight>;
} }
} }

View File

@ -10,6 +10,7 @@ import { Feature, UserEnv } from "../../interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Content, ToolTips, DeviceSetting } from "../../../constants"; import { Content, ToolTips, DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
/** Check if the camera has been disabled. */ /** Check if the camera has been disabled. */
export const cameraDisabled = (env: UserEnv): boolean => export const cameraDisabled = (env: UserEnv): boolean =>
@ -84,9 +85,10 @@ export class CameraSelection
} }
render() { render() {
return <Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Highlight settingName={DeviceSetting.camera}> return <Highlight settingName={DeviceSetting.camera}>
<Col xs={ColWidth.label}> <Row>
<Col xs={newFormat ? 5 : ColWidth.label}>
<label> <label>
{t("CAMERA")} {t("CAMERA")}
</label> </label>
@ -99,7 +101,7 @@ export class CameraSelection
onChange={this.sendOffConfig} onChange={this.sendOffConfig}
extraClass={this.props.botOnline ? "" : "disabled"} /> extraClass={this.props.botOnline ? "" : "disabled"} />
</Col> </Col>
</Highlight> </Row>
</Row>; </Highlight>;
} }
} }

View File

@ -8,25 +8,28 @@ import { FactoryResetRowsProps } from "./interfaces";
import { ColWidth } from "../farmbot_os_settings"; import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function FactoryResetRows(props: FactoryResetRowsProps) { export function FactoryResetRows(props: FactoryResetRowsProps) {
const { dispatch, sourceFbosConfig, botOnline } = props; const { dispatch, sourceFbosConfig, botOnline } = props;
const disableFactoryReset = sourceFbosConfig("disable_factory_reset"); const disableFactoryReset = sourceFbosConfig("disable_factory_reset");
const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {}; const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {};
const newFormat = DevSettings.futureFeaturesEnabled();
return <div className={"factory-reset-options"}> return <div className={"factory-reset-options"}>
<Row> <Highlight settingName={DeviceSetting.factoryReset}>
<Highlight settingName={DeviceSetting.factoryReset}> <Row>
<Col xs={ColWidth.label}> <Col xs={newFormat ? 6 : ColWidth.label}>
<label> <label>
{t(DeviceSetting.factoryReset)} {t(DeviceSetting.factoryReset)}
</label> </label>
</Col> </Col>
<Col xs={ColWidth.description}> {!newFormat &&
<p> <Col xs={ColWidth.description}>
{t(Content.FACTORY_RESET_WARNING)} <p>
</p> {t(Content.FACTORY_RESET_WARNING)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>}
<Col xs={newFormat ? 6 : ColWidth.button}>
<button <button
className="fb-button red" className="fb-button red"
type="button" type="button"
@ -36,21 +39,23 @@ export function FactoryResetRows(props: FactoryResetRowsProps) {
{t("FACTORY RESET")} {t("FACTORY RESET")}
</button> </button>
</Col> </Col>
</Highlight> </Row>
</Row> {newFormat && <Row><p>{t(Content.FACTORY_RESET_WARNING)}</p></Row>}
<Row> </Highlight>
<Highlight settingName={DeviceSetting.autoFactoryReset}> <Highlight settingName={DeviceSetting.autoFactoryReset}>
<Col xs={ColWidth.label}> <Row>
<Col xs={newFormat ? 9 : ColWidth.label}>
<label> <label>
{t(DeviceSetting.autoFactoryReset)} {t(DeviceSetting.autoFactoryReset)}
</label> </label>
</Col> </Col>
<Col xs={ColWidth.description}> {!newFormat &&
<p> <Col xs={ColWidth.description}>
{t(Content.AUTO_FACTORY_RESET)} <p>
</p> {t(Content.AUTO_FACTORY_RESET)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>}
<Col xs={newFormat ? 3 : ColWidth.button}>
<ToggleButton <ToggleButton
toggleValue={!disableFactoryReset.value} toggleValue={!disableFactoryReset.value}
dim={!disableFactoryReset.consistent} dim={!disableFactoryReset.consistent}
@ -60,28 +65,35 @@ export function FactoryResetRows(props: FactoryResetRowsProps) {
})); }));
}} /> }} />
</Col> </Col>
</Highlight> </Row>
</Row> {newFormat && <Row><p>{t(Content.AUTO_FACTORY_RESET)}</p></Row>}
<Row> </Highlight>
<Highlight settingName={DeviceSetting.connectionAttemptPeriod}> <Highlight settingName={DeviceSetting.connectionAttemptPeriod}>
<Col xs={ColWidth.label}> <Row>
<Col xs={newFormat ? 12 : ColWidth.label}>
<label style={maybeDisableTimer}> <label style={maybeDisableTimer}>
{t(DeviceSetting.connectionAttemptPeriod)} {t(DeviceSetting.connectionAttemptPeriod)}
</label> </label>
</Col> </Col>
<Col xs={ColWidth.description}> {!newFormat &&
<p style={maybeDisableTimer}> <Col xs={ColWidth.description}>
{t(Content.AUTO_FACTORY_RESET_PERIOD)} <p style={maybeDisableTimer}>
</p> {t(Content.AUTO_FACTORY_RESET_PERIOD)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>}
<Col xs={newFormat ? 12 : ColWidth.button}>
<BotConfigInputBox <BotConfigInputBox
setting="network_not_found_timer" setting="network_not_found_timer"
dispatch={dispatch} dispatch={dispatch}
disabled={!!disableFactoryReset.value} disabled={!!disableFactoryReset.value}
sourceFbosConfig={sourceFbosConfig} /> sourceFbosConfig={sourceFbosConfig} />
</Col> </Col>
</Highlight> </Row>
</Row> {newFormat && <Row>
<p className="network-not-found-timer">
{t(Content.AUTO_FACTORY_RESET_PERIOD)}
</p>
</Row>}
</Highlight>
</div>; </div>;
} }

View File

@ -9,6 +9,23 @@ import { t } from "../../../i18next_wrapper";
import { ErrorBoundary } from "../../../error_boundary"; import { ErrorBoundary } from "../../../error_boundary";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants"; import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
import { getLastSeenNumber } from "./last_seen_row";
export const getOsReleaseNotesForVersion = (
osReleaseNotes: string | undefined,
version: string | undefined,
) => {
const fallback = globalConfig.FBOS_END_OF_LIFE_VERSION || "9";
const majorVersion = (version || fallback).split(".")[0];
const allReleaseNotes = osReleaseNotes || "";
const thisReleaseNotes = allReleaseNotes.split("# v")
.filter(x => x.startsWith(majorVersion))[0] || "";
const notes = thisReleaseNotes.split("\n\n").slice(1).join("\n")
|| t("Could not get release notes.");
const heading = "FarmBot OS v" + majorVersion;
return { heading, notes };
};
const getVersionString = const getVersionString =
(fbosVersion: string | undefined, onBeta: boolean | undefined): string => { (fbosVersion: string | undefined, onBeta: boolean | undefined): string => {
@ -17,56 +34,72 @@ const getVersionString =
return fbosVersion ? fbosVersion + extension : t(" unknown (offline)"); return fbosVersion ? fbosVersion + extension : t(" unknown (offline)");
}; };
export function FarmbotOsRow(props: FarmbotOsRowProps) { export class FarmbotOsRow extends React.Component<FarmbotOsRowProps> {
const { sourceFbosConfig, dispatch, bot, osReleaseNotes, botOnline } = props;
const { controller_version, currently_on_beta Version = () => {
} = bot.hardware.informational_settings; const { controller_version, currently_on_beta } =
const version = getVersionString(controller_version, currently_on_beta); this.props.bot.hardware.informational_settings;
return <Row> const version = getVersionString(controller_version, currently_on_beta);
<Highlight settingName={DeviceSetting.farmbotOS}> return <Popover position={Position.BOTTOM_LEFT}>
<Col xs={ColWidth.label}> <p>
<label> {t("Version {{ version }}", { version })}
{t(DeviceSetting.farmbotOS)} </p>
</label> <ErrorBoundary>
</Col> <FbosDetails
<Col xs={3}> botInfoSettings={this.props.bot.hardware.informational_settings}
<Popover position={Position.BOTTOM_LEFT}> dispatch={this.props.dispatch}
<p> shouldDisplay={this.props.shouldDisplay}
{t("Version {{ version }}", { version })} sourceFbosConfig={this.props.sourceFbosConfig}
</p> botToMqttLastSeen={getLastSeenNumber(this.props.bot)}
<ErrorBoundary> timeSettings={this.props.timeSettings}
<FbosDetails deviceAccount={this.props.deviceAccount} />
botInfoSettings={bot.hardware.informational_settings} </ErrorBoundary>
dispatch={dispatch} </Popover>;
shouldDisplay={props.shouldDisplay} }
sourceFbosConfig={sourceFbosConfig}
botToMqttLastSeen={props.botToMqttLastSeen} ReleaseNotes = () => {
timeSettings={props.timeSettings} const { osReleaseNotes, hardware } = this.props.bot;
deviceAccount={props.deviceAccount} /> const { controller_version } = hardware.informational_settings;
</ErrorBoundary> const releaseNotes =
</Popover> getOsReleaseNotesForVersion(osReleaseNotes, controller_version);
</Col> return <Popover position={Position.BOTTOM} className="release-notes-wrapper">
<Col xs={3}> <p className="release-notes-button">
<Popover position={Position.BOTTOM}> {t("Release Notes")}&nbsp;
<p className="release-notes-button">
{t("Release Notes")}&nbsp;
<i className="fa fa-caret-down" /> <i className="fa fa-caret-down" />
</p> </p>
<div className="release-notes"> <div className="release-notes">
<h1>{props.osReleaseNotesHeading}</h1> <h1>{releaseNotes.heading}</h1>
<Markdown> <Markdown>
{osReleaseNotes} {releaseNotes.notes}
</Markdown> </Markdown>
</div> </div>
</Popover> </Popover>;
</Col> }
<Col xs={3}>
<OsUpdateButton render() {
bot={bot} const { sourceFbosConfig, bot, botOnline } = this.props;
sourceFbosConfig={sourceFbosConfig} const newFormat = DevSettings.futureFeaturesEnabled();
shouldDisplay={props.shouldDisplay} return <Highlight settingName={DeviceSetting.farmbotOS}>
botOnline={botOnline} /> <Row>
</Col> <Col xs={newFormat ? 5 : ColWidth.label}>
</Highlight> <label>
</Row>; {t(DeviceSetting.farmbotOS)}
</label>
</Col>
{!newFormat && <Col xs={3}><this.Version /></Col>}
{!newFormat && <Col xs={3}><this.ReleaseNotes /></Col>}
<Col xs={newFormat ? 7 : 3}>
<OsUpdateButton
bot={bot}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline} />
</Col>
</Row>
<Row>
{newFormat && <Col xs={7} className="no-pad"><this.Version /></Col>}
{newFormat && <Col xs={5} className="no-pad"><this.ReleaseNotes /></Col>}
</Row>
</Highlight>;
}
} }

View File

@ -4,6 +4,7 @@ import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants"; import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
export interface FbosButtonRowProps { export interface FbosButtonRowProps {
botOnline: boolean; botOnline: boolean;
@ -15,19 +16,21 @@ export interface FbosButtonRowProps {
} }
export const FbosButtonRow = (props: FbosButtonRowProps) => { export const FbosButtonRow = (props: FbosButtonRowProps) => {
return <Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Highlight settingName={props.label}> return <Highlight settingName={props.label}>
<Col xs={ColWidth.label}> <Row>
<Col xs={newFormat ? 7 : ColWidth.label}>
<label> <label>
{t(props.label)} {t(props.label)}
</label> </label>
</Col> </Col>
<Col xs={ColWidth.description}> {!newFormat &&
<p> <Col xs={ColWidth.description}>
{t(props.description)} <p>
</p> {t(props.description)}
</Col> </p>
<Col xs={ColWidth.button}> </Col>}
<Col xs={newFormat ? 5 : ColWidth.button}>
<button <button
className={`fb-button ${props.color}`} className={`fb-button ${props.color}`}
type="button" type="button"
@ -37,6 +40,7 @@ export const FbosButtonRow = (props: FbosButtonRowProps) => {
{t(props.buttonText)} {t(props.buttonText)}
</button> </button>
</Col> </Col>
</Highlight> </Row>
</Row>; {newFormat && <Row><p>{t(props.description)}</p></Row>}
</Highlight>;
}; };

View File

@ -0,0 +1,49 @@
import * as React from "react";
import { Header } from "../hardware_settings/header";
import { Collapse } from "@blueprintjs/core";
import { FirmwareProps } from "./interfaces";
import { FbosButtonRow } from "./fbos_button_row";
import { Content, DeviceSetting } from "../../../constants";
import { restartFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { BoardType } from "./board_type";
import { isFwHardwareValue } from "../firmware_hardware_support";
import { FlashFirmwareRow } from "./flash_firmware_row";
export function Firmware(props: FirmwareProps) {
const { dispatch, sourceFbosConfig, botOnline } = props;
const { firmware } = props.bot.controlPanelState;
const { value } = props.sourceFbosConfig("firmware_hardware");
const firmwareHardware = isFwHardwareValue(value) ? value : undefined;
return <Highlight className={"section"}
settingName={DeviceSetting.firmwareSection}>
<Header
title={DeviceSetting.firmwareSection}
panel={"firmware"}
dispatch={dispatch}
expanded={firmware} />
<Collapse isOpen={!!firmware}>
<BoardType
botOnline={botOnline}
bot={props.bot}
alerts={props.alerts}
dispatch={props.dispatch}
shouldDisplay={props.shouldDisplay}
timeSettings={props.timeSettings}
firmwareHardware={firmwareHardware}
sourceFbosConfig={sourceFbosConfig} />
<FbosButtonRow
botOnline={botOnline}
label={DeviceSetting.restartFirmware}
description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />
<FlashFirmwareRow
botOnline={botOnline}
firmwareHardware={firmwareHardware} />
</Collapse>
</Highlight>;
}

View File

@ -6,7 +6,6 @@ import { t } from "../../../i18next_wrapper";
import { BotState } from "../../interfaces"; import { BotState } from "../../interfaces";
import { FirmwareAlerts } from "../../../messages/alerts"; import { FirmwareAlerts } from "../../../messages/alerts";
import { TimeSettings } from "../../../interfaces"; import { TimeSettings } from "../../../interfaces";
import { trim } from "../../../util";
import { Alert } from "farmbot"; import { Alert } from "farmbot";
import { isFwHardwareValue, boardType } from "../firmware_hardware_support"; import { isFwHardwareValue, boardType } from "../firmware_hardware_support";
@ -56,22 +55,6 @@ export const FlashFirmwareBtn = (props: FlashFirmwareBtnProps) => {
</button>; </button>;
}; };
export interface FirmwareActionsProps {
apiFirmwareValue: string | undefined;
botOnline: boolean;
}
export const FirmwareActions = (props: FirmwareActionsProps) => {
const { apiFirmwareValue } = props;
return <div className="firmware-actions">
<p>
{trim(`${t("Flash the")} ${lookup(apiFirmwareValue) || ""}
${t("firmware to your device")}:`)}
</p>
<FlashFirmwareBtn {...props} />
</div>;
};
export const FirmwareHardwareStatusDetails = export const FirmwareHardwareStatusDetails =
(props: FirmwareHardwareStatusDetailsProps) => { (props: FirmwareHardwareStatusDetailsProps) => {
return <div className="firmware-hardware-status-details"> return <div className="firmware-hardware-status-details">
@ -81,10 +64,6 @@ export const FirmwareHardwareStatusDetails =
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p> <p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
<label>{t("Arduino/Farmduino")}</label> <label>{t("Arduino/Farmduino")}</label>
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p> <p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
<label>{t("Actions")}</label>
<FirmwareActions
apiFirmwareValue={props.apiFirmwareValue}
botOnline={props.botOnline} />
<FirmwareAlerts <FirmwareAlerts
alerts={props.alerts} alerts={props.alerts}
dispatch={props.dispatch} dispatch={props.dispatch}

View File

@ -0,0 +1,42 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { DeviceSetting } from "../../../constants";
import { ColWidth } from "../farmbot_os_settings";
import { FlashFirmwareRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
import { trim } from "lodash";
import { FlashFirmwareBtn, lookup } from "./firmware_hardware_status";
export class FlashFirmwareRow extends React.Component<FlashFirmwareRowProps> {
Description = () =>
<p>
{trim(`${t("Flash the")} ${lookup(this.props.firmwareHardware) || ""}
${t("firmware to your device")}:`)}
</p>;
render() {
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.flashFirmware}>
<Row>
<Col xs={newFormat ? 6 : ColWidth.label}>
<label>
{t(DeviceSetting.flashFirmware)}
</label>
</Col>
{!newFormat && <Col xs={ColWidth.description}>
<this.Description />
</Col>}
<Col xs={newFormat ? 6 : ColWidth.button}>
<FlashFirmwareBtn
apiFirmwareValue={this.props.firmwareHardware}
botOnline={this.props.botOnline} />
</Col>
</Row>
{newFormat && <Row> <this.Description /> </Row>}
</Highlight>;
}
}

View File

@ -10,9 +10,21 @@ import {
Alert, Alert,
InformationalSettings, InformationalSettings,
TaggedDevice, TaggedDevice,
FirmwareHardware,
} from "farmbot"; } from "farmbot";
import { TimeSettings } from "../../../interfaces"; import { TimeSettings } from "../../../interfaces";
export interface NameRowProps {
dispatch: Function;
device: TaggedDevice;
widget?: boolean;
}
export interface TimezoneRowProps {
dispatch: Function;
device: TaggedDevice;
}
export interface AutoSyncRowProps { export interface AutoSyncRowProps {
dispatch: Function; dispatch: Function;
sourceFbosConfig: SourceFbosConfig; sourceFbosConfig: SourceFbosConfig;
@ -50,6 +62,22 @@ export interface BoardTypeProps {
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
timeSettings: TimeSettings; timeSettings: TimeSettings;
sourceFbosConfig: SourceFbosConfig; sourceFbosConfig: SourceFbosConfig;
firmwareHardware: FirmwareHardware | undefined;
}
export interface FirmwareProps {
botOnline: boolean;
bot: BotState;
alerts: Alert[];
dispatch: Function;
shouldDisplay: ShouldDisplay;
timeSettings: TimeSettings;
sourceFbosConfig: SourceFbosConfig;
}
export interface FlashFirmwareRowProps {
botOnline: boolean;
firmwareHardware: FirmwareHardware | undefined;
} }
export interface PowerAndResetProps { export interface PowerAndResetProps {
@ -67,13 +95,10 @@ export interface FactoryResetRowsProps {
export interface FarmbotOsRowProps { export interface FarmbotOsRowProps {
bot: BotState; bot: BotState;
osReleaseNotesHeading: string;
osReleaseNotes: string;
dispatch: Function; dispatch: Function;
sourceFbosConfig: SourceFbosConfig; sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
botOnline: boolean; botOnline: boolean;
botToMqttLastSeen: number;
timeSettings: TimeSettings; timeSettings: TimeSettings;
deviceAccount: TaggedDevice; deviceAccount: TaggedDevice;
} }

View File

@ -6,6 +6,14 @@ import { t } from "../../../i18next_wrapper";
import { TimeSettings } from "../../../interfaces"; import { TimeSettings } from "../../../interfaces";
import { timeFormatString } from "../../../util"; import { timeFormatString } from "../../../util";
import { refresh } from "../../../api/crud"; import { refresh } from "../../../api/crud";
import { BotState } from "../../interfaces";
export const getLastSeenNumber = (bot: BotState): number => {
const { uptime } = bot.connectivity;
const bot2Mqtt = uptime["bot.mqtt"];
const botToMqttLastSeen = bot2Mqtt?.state === "up" ? bot2Mqtt.at : "";
return new Date(botToMqttLastSeen).getTime();
};
export interface LastSeenProps { export interface LastSeenProps {
dispatch: Function; dispatch: Function;

View File

@ -0,0 +1,34 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { DeviceSetting } from "../../../constants";
import { ColWidth } from "../farmbot_os_settings";
import { NameRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { edit, save } from "../../../api/crud";
import { DevSettings } from "../../../account/dev/dev_support";
export class NameRow extends React.Component<NameRowProps> {
NameInput = () =>
<input name="name"
onChange={e => this.props.dispatch(edit(this.props.device, {
name: e.currentTarget.value
}))}
onBlur={() => this.props.dispatch(save(this.props.device.uuid))}
value={this.props.device.body.name} />;
render() {
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.name}>
<Row>
<Col xs={newFormat ? 12 : ColWidth.label}>
<label>
{t(DeviceSetting.name)}
</label>
</Col>
{!newFormat && <Col xs={7}><this.NameInput /></Col>}
</Row>
{newFormat && <Row><this.NameInput /></Row>}
</Highlight>;
}
}

View File

@ -7,6 +7,7 @@ import { ColWidth } from "../farmbot_os_settings";
import { DeviceSetting } from "../../../constants"; import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { OtaTimeSelectorRowProps } from "./interfaces"; import { OtaTimeSelectorRowProps } from "./interfaces";
import { DevSettings } from "../../../account/dev/dev_support";
// tslint:disable-next-line:no-null-keyword // tslint:disable-next-line:no-null-keyword
const UNDEFINED = null as unknown as undefined; const UNDEFINED = null as unknown as undefined;
@ -40,7 +41,7 @@ type HOUR =
| 23; | 23;
type TimeTable = Record<HOUR, DropDownItem>; type TimeTable = Record<HOUR, DropDownItem>;
type EveryTimeTable = Record<PreferredHourFormat, TimeTable>; type EveryTimeTable = Record<PreferredHourFormat, TimeTable>;
const ASAP = () => t("As soon as possible"); export const ASAP = () => t("As soon as possible");
const TIME_TABLE_12H = (): TimeTable => ({ const TIME_TABLE_12H = (): TimeTable => ({
0: { label: t("Midnight"), value: 0 }, 0: { label: t("Midnight"), value: 0 },
1: { label: "1:00 AM", value: 1 }, 1: { label: "1:00 AM", value: 1 },
@ -102,7 +103,7 @@ const TIME_FORMATS = (): EveryTimeTable => ({
"24h": TIME_TABLE_24H() "24h": TIME_TABLE_24H()
}); });
interface OtaTimeSelectorProps { export interface OtaTimeSelectorProps {
disabled: boolean; disabled: boolean;
timeFormat: PreferredHourFormat; timeFormat: PreferredHourFormat;
onChange(hour24: number | undefined): void; onChange(hour24: number | undefined): void;
@ -116,7 +117,8 @@ export const changeOtaHour =
dispatch(save(device.uuid)); dispatch(save(device.uuid));
}; };
export function assertIsHour(val: number | undefined): asserts val is (HOUR | undefined) { export function assertIsHour(
val: number | undefined): asserts val is (HOUR | undefined) {
if ((val === null) || (val === undefined)) { if ((val === null) || (val === undefined)) {
return; return;
} }
@ -147,9 +149,10 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => {
.sort((_x, _y) => (_x.value > _y.value) ? 1 : -1); .sort((_x, _y) => (_x.value > _y.value) ? 1 : -1);
const selectedItem = (typeof value == "number") ? const selectedItem = (typeof value == "number") ?
theTimeTable[value as HOUR] : theTimeTable[DEFAULT_HOUR]; theTimeTable[value as HOUR] : theTimeTable[DEFAULT_HOUR];
return <Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Highlight settingName={DeviceSetting.applySoftwareUpdates}> return <Highlight settingName={DeviceSetting.applySoftwareUpdates}>
<Col xs={ColWidth.label}> <Row>
<Col xs={newFormat ? 5 : ColWidth.label}>
<label> <label>
{t(DeviceSetting.applySoftwareUpdates)} {t(DeviceSetting.applySoftwareUpdates)}
</label> </label>
@ -161,8 +164,8 @@ export const OtaTimeSelector = (props: OtaTimeSelectorProps): JSX.Element => {
list={list} list={list}
extraClass={disabled ? "disabled" : ""} /> extraClass={disabled ? "disabled" : ""} />
</Col> </Col>
</Highlight> </Row>
</Row>; </Highlight>;
}; };
export function OtaTimeSelectorRow(props: OtaTimeSelectorRowProps) { export function OtaTimeSelectorRow(props: OtaTimeSelectorRowProps) {

View File

@ -6,10 +6,9 @@ import { PowerAndResetProps } from "./interfaces";
import { ChangeOwnershipForm } from "./change_ownership_form"; import { ChangeOwnershipForm } from "./change_ownership_form";
import { FbosButtonRow } from "./fbos_button_row"; import { FbosButtonRow } from "./fbos_button_row";
import { Content, DeviceSetting } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { reboot, powerOff, restartFirmware } from "../../actions"; import { reboot, powerOff } from "../../actions";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function PowerAndReset(props: PowerAndResetProps) { export function PowerAndReset(props: PowerAndResetProps) {
const { dispatch, sourceFbosConfig, botOnline } = props; const { dispatch, sourceFbosConfig, botOnline } = props;
@ -36,14 +35,6 @@ export function PowerAndReset(props: PowerAndResetProps) {
buttonText={t("SHUTDOWN")} buttonText={t("SHUTDOWN")}
color={"red"} color={"red"}
action={powerOff} /> action={powerOff} />
{!DevSettings.futureFeaturesEnabled() &&
<FbosButtonRow
botOnline={botOnline}
label={DeviceSetting.restartFirmware}
description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />}
<FactoryResetRows <FactoryResetRows
dispatch={dispatch} dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} sourceFbosConfig={sourceFbosConfig}

View File

@ -0,0 +1,51 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { DeviceSetting, Content } from "../../../constants";
import { ColWidth } from "../farmbot_os_settings";
import { TimezoneRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { edit, save } from "../../../api/crud";
import { timezoneMismatch } from "../../timezones/guess_timezone";
import { TimezoneSelector } from "../../timezones/timezone_selector";
import { DevSettings } from "../../../account/dev/dev_support";
export class TimezoneRow extends React.Component<TimezoneRowProps> {
Note = () =>
<div className="note">
{timezoneMismatch(this.props.device.body.timezone)
? t(Content.DIFFERENT_TZ_WARNING) : ""}
</div>;
Selector = () =>
<TimezoneSelector
currentTimezone={this.props.device.body.timezone}
onUpdate={timezone => {
this.props.dispatch(edit(this.props.device, { timezone }));
this.props.dispatch(save(this.props.device.uuid));
}} />;
render() {
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.timezone}>
<Row>
<Col xs={newFormat ? 12 : ColWidth.label}>
<label>
{t("TIME ZONE")}
</label>
</Col>
{!newFormat &&
<Col xs={ColWidth.description}>
<this.Note />
<this.Selector />
</Col>}
</Row>
{newFormat && <Row>
<Col xs={12}><this.Note /></Col>
<Col xs={12} className="no-pad"><this.Selector /></Col>
</Row>}
</Highlight>;
}
}

View File

@ -26,9 +26,6 @@ const NO_TMC = ["arduino", "farmduino", "farmduino_k14"];
export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) => export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_TMC.includes(firmwareHardware); !firmwareHardware || !NO_TMC.includes(firmwareHardware);
export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) =>
!!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware));
export const hasButtons = (firmwareHardware: FirmwareHardware | undefined) => export const hasButtons = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_BUTTONS.includes(firmwareHardware); !firmwareHardware || !NO_BUTTONS.includes(firmwareHardware);

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions"; import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index"; import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index";
import { HardwareSettingsProps, SourceFwConfig } from "../interfaces"; import { HardwareSettingsProps, SourceFwConfig } from "../interfaces";
import { isBotOnline } from "../must_be_online"; import { isBotOnlineFromState } from "../must_be_online";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { DangerZone } from "./hardware_settings/danger_zone"; import { DangerZone } from "./hardware_settings/danger_zone";
import { PinGuard } from "./hardware_settings/pin_guard"; import { PinGuard } from "./hardware_settings/pin_guard";
@ -17,24 +17,18 @@ import { FwParamExportMenu } from "./hardware_settings/export_menu";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { PinBindings } from "./hardware_settings/pin_bindings"; import { PinBindings } from "./hardware_settings/pin_bindings";
import { ErrorHandling } from "./hardware_settings/error_handling"; import { ErrorHandling } from "./hardware_settings/error_handling";
import { maybeOpenPanel } from "./maybe_highlight";
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import type { McuParamName } from "farmbot"; import type { McuParamName } from "farmbot";
export class HardwareSettings extends export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> { React.Component<HardwareSettingsProps, {}> {
componentDidMount = () =>
this.props.dispatch(maybeOpenPanel(this.props.controlPanelState));
render() { render() {
const { const {
bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig, bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig,
botToMqttStatus, firmwareHardware, resources firmwareHardware, resources
} = this.props; } = this.props;
const { informational_settings } = this.props.bot.hardware; const botOnline = !isBotOnlineFromState(bot);
const { sync_status } = informational_settings;
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
const commonProps = { dispatch, controlPanelState }; const commonProps = { dispatch, controlPanelState };
return <Widget className="hardware-widget"> return <Widget className="hardware-widget">
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}> <WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>
@ -63,7 +57,7 @@ export class HardwareSettings extends
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
firmwareConfig={firmwareConfig} firmwareConfig={firmwareConfig}
firmwareHardware={firmwareHardware} firmwareHardware={firmwareHardware}
botDisconnected={botDisconnected} /> botOnline={botOnline} />
<Motors {...commonProps} <Motors {...commonProps}
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} /> firmwareHardware={firmwareHardware} />
@ -82,7 +76,7 @@ export class HardwareSettings extends
sourceFwConfig={sourceFwConfig} /> sourceFwConfig={sourceFwConfig} />
<DangerZone {...commonProps} <DangerZone {...commonProps}
onReset={MCUFactoryReset} onReset={MCUFactoryReset}
botDisconnected={botDisconnected} /> botOnline={botOnline} />
</WidgetBody> </WidgetBody>
</Widget>; </Widget>;
} }

View File

@ -9,7 +9,7 @@ describe("<CalibrationRow />", () => {
const fakeProps = (): CalibrationRowProps => ({ const fakeProps = (): CalibrationRowProps => ({
type: "calibrate", type: "calibrate",
hardware: bot.hardware.mcu_params, hardware: bot.hardware.mcu_params,
botDisconnected: false, botOnline: true,
action: jest.fn(), action: jest.fn(),
toolTip: "calibrate", toolTip: "calibrate",
title: DeviceSetting.calibration, title: DeviceSetting.calibration,

View File

@ -33,7 +33,7 @@ describe("<HomingAndCalibration />", () => {
value: bot.hardware.mcu_params[x], consistent: true value: bot.hardware.mcu_params[x], consistent: true
}), }),
firmwareConfig: fakeFirmwareConfig().body, firmwareConfig: fakeFirmwareConfig().body,
botDisconnected: false, botOnline: true,
firmwareHardware: undefined, firmwareHardware: undefined,
}); });

View File

@ -6,33 +6,44 @@ import { CalibrationRowProps } from "../interfaces";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function CalibrationRow(props: CalibrationRowProps) { export class CalibrationRow extends React.Component<CalibrationRowProps> {
const { hardware, botDisconnected } = props; get newFormat() { return DevSettings.futureFeaturesEnabled(); }
return <Row> Axes = () => {
<Highlight settingName={props.title}> const { type, botOnline, axisTitle, hardware, action } = this.props;
<Col xs={6} className={"widget-body-tooltips"}> return <div className="calibration-row-axes">
<label>
{t(props.title)}
</label>
<Help text={t(props.toolTip)}
requireClick={true} position={Position.RIGHT} />
</Col>
{axisTrackingStatus(hardware) {axisTrackingStatus(hardware)
.map(row => { .map(row => {
const { axis } = row; const { axis } = row;
const hardwareDisabled = props.type == "zero" ? false : row.disabled; const hardwareDisabled = type == "zero" ? false : row.disabled;
return <Col xs={2} key={axis} className={"centered-button-div"}> return <Col xs={this.newFormat ? 4 : 2} key={axis}
className={"centered-button-div"}>
<LockableButton <LockableButton
disabled={hardwareDisabled || botDisconnected} disabled={hardwareDisabled || !botOnline}
title={t(props.axisTitle)} title={t(axisTitle)}
onClick={() => props.action(axis)}> onClick={() => action(axis)}>
{`${t(props.axisTitle)} ${axis}`} {`${t(axisTitle)} ${axis}`}
</LockableButton> </LockableButton>
</Col>; </Col>;
})} })}
</Highlight> </div>;
</Row>; }
render() {
return <Highlight settingName={this.props.title}>
<Row>
<Col xs={this.newFormat ? 12 : 6} className={"widget-body-tooltips"}>
<label>
{t(this.props.title)}
</label>
<Help text={t(this.props.toolTip)} position={Position.TOP_RIGHT} />
</Col>
{!this.newFormat && <this.Axes />}
</Row>
{this.newFormat && <Row><this.Axes /></Row>}
</Highlight>;
}
} }

View File

@ -6,12 +6,13 @@ import { Collapse } from "@blueprintjs/core";
import { Content, DeviceSetting } from "../../../constants"; import { Content, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function DangerZone(props: DangerZoneProps) { export function DangerZone(props: DangerZoneProps) {
const { dispatch, onReset, botDisconnected } = props; const { dispatch, onReset, botOnline } = props;
const { danger_zone } = props.controlPanelState; const { danger_zone } = props.controlPanelState;
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight className={"section"} return <Highlight className={"section"}
settingName={DeviceSetting.dangerZone}> settingName={DeviceSetting.dangerZone}>
<Header <Header
@ -20,29 +21,32 @@ export function DangerZone(props: DangerZoneProps) {
panel={"danger_zone"} panel={"danger_zone"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!danger_zone}> <Collapse isOpen={!!danger_zone}>
<Row> <Highlight settingName={DeviceSetting.resetHardwareParams}>
<Highlight settingName={DeviceSetting.resetHardwareParams}> <Row>
<Col xs={4}> <Col xs={newFormat ? 8 : 4}>
<label> <label>
{t(DeviceSetting.resetHardwareParams)} {t(DeviceSetting.resetHardwareParams)}
</label> </label>
</Col> </Col>
<Col xs={6}> {!newFormat &&
<p> <Col xs={6}>
{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)} <p>
</p> {t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}
</Col> </p>
<Col xs={2} className={"centered-button-div"}> </Col>}
<Col xs={newFormat ? 4 : 2} className={"centered-button-div"}>
<button <button
className="fb-button red" className="fb-button red"
disabled={botDisconnected} disabled={!botOnline}
title={t("RESET")} title={t("RESET")}
onClick={onReset}> onClick={onReset}>
{t("RESET")} {t("RESET")}
</button> </button>
</Col> </Col>
</Highlight> </Row>
</Row> {newFormat &&
<Row><p>{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}</p></Row>}
</Highlight>
</Collapse> </Collapse>
</Highlight>; </Highlight>;
} }

View File

@ -31,9 +31,7 @@ export function Encoders(props: EncodersProps) {
panel={"encoders"} panel={"encoders"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!encoders}> <Collapse isOpen={!!encoders}>
<div className="label-headings"> <SpacePanelHeader />
<SpacePanelHeader />
</div>
<BooleanMCUInputGroup <BooleanMCUInputGroup
label={!showEncoders label={!showEncoders
? DeviceSetting.enableStallDetection ? DeviceSetting.enableStallDetection

View File

@ -20,9 +20,7 @@ export function EndStops(props: EndStopsProps) {
panel={"endstops"} panel={"endstops"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!endstops}> <Collapse isOpen={!!endstops}>
<div className="label-headings"> <SpacePanelHeader />
<SpacePanelHeader />
</div>
<BooleanMCUInputGroup <BooleanMCUInputGroup
label={DeviceSetting.enableEndstops} label={DeviceSetting.enableEndstops}
tooltip={ToolTips.ENABLE_ENDSTOPS} tooltip={ToolTips.ENABLE_ENDSTOPS}

View File

@ -25,9 +25,7 @@ export function ErrorHandling(props: ErrorHandlingProps) {
panel={"error_handling"} panel={"error_handling"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!error_handling}> <Collapse isOpen={!!error_handling}>
<div className="label-headings"> <SpacePanelHeader />
<SpacePanelHeader />
</div>
<NumericMCUInputGroup <NumericMCUInputGroup
label={DeviceSetting.timeoutAfter} label={DeviceSetting.timeoutAfter}
tooltip={ToolTips.TIMEOUT_AFTER} tooltip={ToolTips.TIMEOUT_AFTER}

View File

@ -19,7 +19,7 @@ import { SpacePanelHeader } from "./space_panel_header";
export function HomingAndCalibration(props: HomingAndCalibrationProps) { export function HomingAndCalibration(props: HomingAndCalibrationProps) {
const { const {
dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected, dispatch, bot, sourceFwConfig, firmwareConfig, botOnline,
firmwareHardware firmwareHardware
} = props; } = props;
const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params; const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params;
@ -41,9 +41,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch} dispatch={dispatch}
expanded={homing_and_calibration} /> expanded={homing_and_calibration} />
<Collapse isOpen={!!homing_and_calibration}> <Collapse isOpen={!!homing_and_calibration}>
<div className="label-headings"> <SpacePanelHeader />
<SpacePanelHeader />
</div>
<CalibrationRow <CalibrationRow
type={"find_home"} type={"find_home"}
title={DeviceSetting.homing} title={DeviceSetting.homing}
@ -55,7 +53,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
.findHome({ speed: CONFIG_DEFAULTS.speed, axis }) .findHome({ speed: CONFIG_DEFAULTS.speed, axis })
.catch(commandErr("'Find Home' request"))} .catch(commandErr("'Find Home' request"))}
hardware={hardware} hardware={hardware}
botDisconnected={botDisconnected} /> botOnline={botOnline} />
<CalibrationRow <CalibrationRow
type={"calibrate"} type={"calibrate"}
title={DeviceSetting.calibration} title={DeviceSetting.calibration}
@ -66,7 +64,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
action={axis => getDevice().calibrate({ axis }) action={axis => getDevice().calibrate({ axis })
.catch(commandErr("Calibration"))} .catch(commandErr("Calibration"))}
hardware={hardware} hardware={hardware}
botDisconnected={botDisconnected} /> botOnline={botOnline} />
<CalibrationRow <CalibrationRow
type={"zero"} type={"zero"}
title={DeviceSetting.setZeroPosition} title={DeviceSetting.setZeroPosition}
@ -75,7 +73,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
action={axis => getDevice().setZero(axis) action={axis => getDevice().setZero(axis)
.catch(commandErr("Zeroing"))} .catch(commandErr("Zeroing"))}
hardware={hardware} hardware={hardware}
botDisconnected={botDisconnected} /> botOnline={botOnline} />
<BooleanMCUInputGroup <BooleanMCUInputGroup
label={DeviceSetting.findHomeOnBoot} label={DeviceSetting.findHomeOnBoot}
tooltip={!hasEncoders(firmwareHardware) tooltip={!hasEncoders(firmwareHardware)

View File

@ -44,9 +44,7 @@ export function Motors(props: MotorsProps) {
panel={"motors"} panel={"motors"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!controlPanelState.motors}> <Collapse isOpen={!!controlPanelState.motors}>
<div className="label-headings"> <SpacePanelHeader />
<SpacePanelHeader />
</div>
<NumericMCUInputGroup <NumericMCUInputGroup
label={DeviceSetting.maxSpeed} label={DeviceSetting.maxSpeed}
tooltip={ToolTips.MAX_SPEED} tooltip={ToolTips.MAX_SPEED}
@ -140,6 +138,7 @@ export function Motors(props: MotorsProps) {
label={DeviceSetting.enable2ndXMotor} label={DeviceSetting.enable2ndXMotor}
tooltip={ToolTips.ENABLE_X2_MOTOR}> tooltip={ToolTips.ENABLE_X2_MOTOR}>
<ToggleButton <ToggleButton
className={"no-float"}
toggleValue={enable2ndXMotor.value} toggleValue={enable2ndXMotor.value}
dim={!enable2ndXMotor.consistent} dim={!enable2ndXMotor.consistent}
toggleAction={() => dispatch( toggleAction={() => dispatch(
@ -149,6 +148,7 @@ export function Motors(props: MotorsProps) {
label={DeviceSetting.invert2ndXMotor} label={DeviceSetting.invert2ndXMotor}
tooltip={ToolTips.INVERT_MOTORS}> tooltip={ToolTips.INVERT_MOTORS}>
<ToggleButton <ToggleButton
className={"no-float"}
grayscale={!enable2ndXMotor.value} grayscale={!enable2ndXMotor.value}
toggleValue={invert2ndXMotor.value} toggleValue={invert2ndXMotor.value}
dim={!invert2ndXMotor.consistent} dim={!invert2ndXMotor.consistent}

View File

@ -7,12 +7,13 @@ import { Row, Col, Help } from "../../../ui/index";
import { ToolTips, DeviceSetting } from "../../../constants"; import { ToolTips, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function PinGuard(props: PinGuardProps) { export function PinGuard(props: PinGuardProps) {
const { pin_guard } = props.controlPanelState; const { pin_guard } = props.controlPanelState;
const { dispatch, sourceFwConfig, resources } = props; const { dispatch, sourceFwConfig, resources } = props;
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight className={"section"} return <Highlight className={"section"}
settingName={DeviceSetting.pinGuard}> settingName={DeviceSetting.pinGuard}>
<Header <Header
@ -21,25 +22,27 @@ export function PinGuard(props: PinGuardProps) {
panel={"pin_guard"} panel={"pin_guard"}
dispatch={dispatch} /> dispatch={dispatch} />
<Collapse isOpen={!!pin_guard}> <Collapse isOpen={!!pin_guard}>
<Row> {!newFormat &&
<Col xs={3} xsOffset={3} className={"widget-body-tooltips"}> <Row>
<label> <Col xs={3} xsOffset={3}
{t("Pin Number")} className={"widget-body-tooltips"}>
</label> <label>
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER} requireClick={true} {t("Pin Number")}
position={Position.RIGHT} /> </label>
</Col> <Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
<Col xs={4}> position={Position.TOP_RIGHT} />
<label> </Col>
{t("Timeout (sec)")} <Col xs={4}>
</label> <label>
</Col> {t("Timeout (sec)")}
<Col xs={2} className={"centered-button-div"}> </label>
<label> </Col>
{t("To State")} <Col xs={2} className={"centered-button-div"}>
</label> <label>
</Col> {t("To State")}
</Row> </label>
</Col>
</Row>}
<PinGuardMCUInputGroup <PinGuardMCUInputGroup
label={t("Pin Guard {{ num }}", { num: 1 })} label={t("Pin Guard {{ num }}", { num: 1 })}
pinNumKey={"pin_guard_1_pin_nr"} pinNumKey={"pin_guard_1_pin_nr"}

View File

@ -4,6 +4,7 @@ import { Position } from "@blueprintjs/core";
import { DeviceSetting } from "../../../constants"; import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight"; import { Highlight } from "../maybe_highlight";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { DevSettings } from "../../../account/dev/dev_support";
export interface SingleSettingRowProps { export interface SingleSettingRowProps {
label: DeviceSetting; label: DeviceSetting;
@ -13,15 +14,19 @@ export interface SingleSettingRowProps {
} }
export const SingleSettingRow = export const SingleSettingRow =
({ label, tooltip, settingType, children }: SingleSettingRowProps) => ({ label, tooltip, settingType, children }: SingleSettingRowProps) => {
<Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Highlight settingName={label}> return <Highlight settingName={label}>
<Col xs={6} className={"widget-body-tooltips"}> <Row>
<Col xs={newFormat ? 12 : 6} className={"widget-body-tooltips"}>
<label>{t(label)}</label> <label>{t(label)}</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} /> <Help text={tooltip} position={Position.RIGHT} />
</Col> </Col>
{settingType === "button" {settingType === "button"
? <Col xs={2} className={"centered-button-div"}>{children}</Col> ? <Col xs={newFormat ? 5 : 2} className={"centered-button-div"}>
: <Col xs={6}>{children}</Col>} {children}
</Highlight> </Col>
</Row>; : <Col xs={newFormat ? 8 : 6}>{children}</Col>}
</Row>
</Highlight>;
};

View File

@ -1,23 +1,29 @@
import * as React from "react"; import * as React from "react";
import { Row, Col } from "../../../ui/index"; import { Row, Col } from "../../../ui/index";
import { t } from "../../../i18next_wrapper"; import { t } from "../../../i18next_wrapper";
import { DevSettings } from "../../../account/dev/dev_support";
export function SpacePanelHeader(_: {}) { export function SpacePanelHeader() {
return <Row> const newFormat = DevSettings.futureFeaturesEnabled();
<Col xs={2} xsOffset={6} className={"centered-button-div"}> const width = newFormat ? 4 : 2;
<label> const offset = newFormat ? 0 : 6;
{t("X AXIS")} return <div className="label-headings">
</label> <Row>
</Col> <Col xs={width} xsOffset={offset} className={"centered-button-div"}>
<Col xs={2} className={"centered-button-div"}> <label>
<label> {t("X AXIS")}
{t("Y AXIS")} </label>
</label> </Col>
</Col> <Col xs={width} className={"centered-button-div"}>
<Col xs={2} className={"centered-button-div"}> <label>
<label> {t("Y AXIS")}
{t("Z AXIS")} </label>
</label> </Col>
</Col> <Col xs={width} className={"centered-button-div"}>
</Row>; <label>
{t("Z AXIS")}
</label>
</Col>
</Row>
</div>;
} }

View File

@ -18,7 +18,7 @@ export interface HomingAndCalibrationProps {
controlPanelState: ControlPanelState; controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
firmwareConfig: FirmwareConfig | undefined; firmwareConfig: FirmwareConfig | undefined;
botDisconnected: boolean; botOnline: boolean;
firmwareHardware: FirmwareHardware | undefined; firmwareHardware: FirmwareHardware | undefined;
} }
@ -39,7 +39,7 @@ export interface BooleanMCUInputGroupProps {
export interface CalibrationRowProps { export interface CalibrationRowProps {
type: "find_home" | "calibrate" | "zero"; type: "find_home" | "calibrate" | "zero";
hardware: McuParams; hardware: McuParams;
botDisconnected: boolean; botOnline: boolean;
action(axis: Axis): void; action(axis: Axis): void;
toolTip: string; toolTip: string;
title: DeviceSetting; title: DeviceSetting;
@ -116,5 +116,5 @@ export interface DangerZoneProps {
dispatch: Function; dispatch: Function;
controlPanelState: ControlPanelState; controlPanelState: ControlPanelState;
onReset(): void; onReset(): void;
botDisconnected: boolean; botOnline: boolean;
} }

View File

@ -1,13 +1,14 @@
import * as React from "react"; import * as React from "react";
interface Props { export interface LockableButtonProps {
onClick: Function; onClick: Function;
disabled: boolean; disabled: boolean;
children?: React.ReactNode; children?: React.ReactNode;
title?: string; title?: string;
} }
export function LockableButton({ onClick, disabled, children, title }: Props) { export function LockableButton(props: LockableButtonProps) {
const { onClick, disabled, children, title } = props;
const className = disabled ? "gray" : "yellow"; const className = disabled ? "gray" : "yellow";
return <button return <button
className={"fb-button " + className} className={"fb-button " + className}

View File

@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import { ControlPanelState } from "../interfaces"; import { ControlPanelState } from "../interfaces";
import { toggleControlPanel } from "../actions"; import { toggleControlPanel, bulkToggleControlPanel } from "../actions";
import { urlFriendly } from "../../util"; import { urlFriendly } from "../../util";
import { DeviceSetting } from "../../constants"; import { DeviceSetting } from "../../constants";
import { trim } from "lodash"; import { trim } from "lodash";
@ -150,12 +150,16 @@ const getHighlightName = () => location.search.split("?highlight=").pop();
export const highlight = { opened: false, highlighted: false }; export const highlight = { opened: false, highlighted: false };
/** Open a panel if a setting in that panel is highlighted. */ /** Open a panel if a setting in that panel is highlighted. */
export const maybeOpenPanel = (panelState: ControlPanelState) => export const maybeOpenPanel = (
panelState: ControlPanelState,
closeOthers = false,
) =>
(dispatch: Function) => { (dispatch: Function) => {
if (highlight.opened) { return; } if (highlight.opened) { return; }
const urlFriendlySettingName = urlFriendly(getHighlightName() || ""); const urlFriendlySettingName = urlFriendly(getHighlightName() || "");
if (!urlFriendlySettingName) { return; } if (!urlFriendlySettingName) { return; }
const panel = URL_FRIENDLY_LOOKUP[urlFriendlySettingName]; const panel = URL_FRIENDLY_LOOKUP[urlFriendlySettingName];
closeOthers && dispatch(bulkToggleControlPanel(false, closeOthers));
const panelIsOpen = panelState[panel]; const panelIsOpen = panelState[panel];
if (panelIsOpen) { return; } if (panelIsOpen) { return; }
dispatch(toggleControlPanel(panel)); dispatch(toggleControlPanel(panel));
@ -176,6 +180,7 @@ export interface HighlightProps {
settingName: DeviceSetting; settingName: DeviceSetting;
children: React.ReactChild children: React.ReactChild
| React.ReactChild[] | React.ReactChild[]
| (React.ReactChild | false)[]
| (React.ReactChild | React.ReactChild[])[]; | (React.ReactChild | React.ReactChild[])[];
className?: string; className?: string;
} }
@ -196,7 +201,10 @@ export class Highlight extends React.Component<HighlightProps, HighlightState> {
} }
render() { render() {
return <div className={`${this.props.className} ${this.state.className}`}> return <div className={[
this.props.className,
this.state.className,
].join(" ")}>
{this.props.children} {this.props.children}
</div>; </div>;
} }

View File

@ -5,50 +5,65 @@ import { Row, Col, Help } from "../../ui/index";
import { Position } from "@blueprintjs/core"; import { Position } from "@blueprintjs/core";
import { Highlight } from "./maybe_highlight"; import { Highlight } from "./maybe_highlight";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support";
export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) { export class NumericMCUInputGroup
extends React.Component<NumericMCUInputGroupProps> {
const { get newFormat() { return DevSettings.futureFeaturesEnabled(); }
sourceFwConfig, dispatch, tooltip, label, x, y, z, intSize, gray, float,
} = props; Inputs = () => {
return <Row> const {
<Highlight settingName={label}> sourceFwConfig, dispatch, intSize, gray, float,
<Col xs={6} className={"widget-body-tooltips"}> x, y, z, xScale, yScale, zScale,
<label> } = this.props;
{t(label)} return <div className={"mcu-inputs"}>
</label> <Col xs={this.newFormat ? 4 : 2}>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2}>
<McuInputBox <McuInputBox
setting={x} setting={x}
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} dispatch={dispatch}
intSize={intSize} intSize={intSize}
float={float} float={float}
scale={props.xScale} scale={xScale}
gray={gray?.x} /> gray={gray?.x} />
</Col> </Col>
<Col xs={2}> <Col xs={this.newFormat ? 4 : 2}>
<McuInputBox <McuInputBox
setting={y} setting={y}
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} dispatch={dispatch}
intSize={intSize} intSize={intSize}
float={float} float={float}
scale={props.yScale} scale={yScale}
gray={gray?.y} /> gray={gray?.y} />
</Col> </Col>
<Col xs={2}> <Col xs={this.newFormat ? 4 : 2}>
<McuInputBox <McuInputBox
setting={z} setting={z}
sourceFwConfig={sourceFwConfig} sourceFwConfig={sourceFwConfig}
dispatch={dispatch} dispatch={dispatch}
intSize={intSize} intSize={intSize}
float={float} float={float}
scale={props.zScale} scale={zScale}
gray={gray?.z} /> gray={gray?.z} />
</Col> </Col>
</Highlight> </div>;
</Row>; }
render() {
const { tooltip, label } = this.props;
return <Highlight settingName={label}>
<Row>
<Col xs={this.newFormat ? 12 : 6} className={"widget-body-tooltips"}>
<label>
{t(label)}
</label>
<Help text={tooltip} position={Position.TOP_RIGHT} />
</Col>
{!this.newFormat && <this.Inputs />}
</Row>
{this.newFormat && <Row><this.Inputs /></Row>}
</Highlight>;
}
} }

View File

@ -1,48 +1,108 @@
import * as React from "react"; import * as React from "react";
import { McuInputBox } from "./mcu_input_box"; import { McuInputBox } from "./mcu_input_box";
import { PinGuardMCUInputGroupProps } from "./interfaces"; import { PinGuardMCUInputGroupProps } from "./interfaces";
import { Row, Col } from "../../ui/index"; import { Row, Col, Help } from "../../ui/index";
import { settingToggle } from "../actions"; import { settingToggle } from "../actions";
import { ToggleButton } from "../../controls/toggle_button"; import { ToggleButton } from "../../controls/toggle_button";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { PinNumberDropdown } from "./pin_number_dropdown"; import { PinNumberDropdown } from "./pin_number_dropdown";
import { DevSettings } from "../../account/dev/dev_support";
import { ToolTips } from "../../constants";
import { Position } from "@blueprintjs/core";
export function PinGuardMCUInputGroup(props: PinGuardMCUInputGroupProps) { export class PinGuardMCUInputGroup
extends React.Component<PinGuardMCUInputGroupProps> {
const { sourceFwConfig, dispatch, label, pinNumKey, timeoutKey, activeStateKey get newFormat() { return DevSettings.futureFeaturesEnabled(); }
} = props;
const activeStateValue = sourceFwConfig(activeStateKey).value; Number = () =>
const inactiveState = isUndefined(activeStateValue) <PinNumberDropdown
? undefined pinNumKey={this.props.pinNumKey}
: !activeStateValue; dispatch={this.props.dispatch}
return <Row> resources={this.props.resources}
<Col xs={3}> sourceFwConfig={this.props.sourceFwConfig} />
<label>
{label} Timeout = () =>
</label> <McuInputBox
</Col> setting={this.props.timeoutKey}
<Col xs={3}> sourceFwConfig={this.props.sourceFwConfig}
<PinNumberDropdown dispatch={this.props.dispatch}
pinNumKey={pinNumKey} filter={32000} />
dispatch={dispatch}
resources={props.resources} State = () => {
sourceFwConfig={sourceFwConfig} /> const { sourceFwConfig, dispatch, activeStateKey } = this.props;
</Col> const activeStateValue = sourceFwConfig(activeStateKey).value;
<Col xs={4}> const inactiveState = isUndefined(activeStateValue)
<McuInputBox ? undefined
setting={timeoutKey} : !activeStateValue;
sourceFwConfig={sourceFwConfig} return <ToggleButton
dispatch={dispatch} customText={{ textFalse: t("low"), textTrue: t("high") }}
filter={32000} /> toggleValue={inactiveState}
</Col> dim={!sourceFwConfig(activeStateKey).consistent}
<Col xs={2} className={"centered-button-div"}> toggleAction={() =>
<ToggleButton dispatch(settingToggle(activeStateKey, sourceFwConfig))} />;
customText={{ textFalse: t("low"), textTrue: t("high") }} }
toggleValue={inactiveState}
dim={!sourceFwConfig(activeStateKey).consistent} render() {
toggleAction={() => const { label } = this.props;
dispatch(settingToggle(activeStateKey, sourceFwConfig))} /> return !this.newFormat
</Col> ? <Row>
</Row>; <Col xs={3}>
<label>
{label}
</label>
</Col>
<Col xs={3}>
<this.Number />
</Col>
<Col xs={4}>
<this.Timeout />
</Col>
<Col xs={2} className={"centered-button-div"}>
<this.State />
</Col>
</Row>
: <div className={"pin-guard-input-row"}>
<Row>
<Col xs={12}>
<label>
{label}
</label>
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Pin Number")}
</label>
<Help text={ToolTips.PIN_GUARD_PIN_NUMBER}
position={Position.TOP_RIGHT} />
</Col>
<Col xs={5} className="no-pad">
<this.Number />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("Timeout (sec)")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.Timeout />
</Col>
</Row>
<Row>
<Col xs={5} xsOffset={1} className="no-pad">
<label>
{t("To State")}
</label>
</Col>
<Col xs={5} className="no-pad">
<this.State />
</Col>
</Row>
</div>;
}
} }

View File

@ -5,17 +5,16 @@ import { FarmbotOsSettings } from "./components/farmbot_os_settings";
import { Page, Col, Row } from "../ui/index"; import { Page, Col, Row } from "../ui/index";
import { mapStateToProps } from "./state_to_props"; import { mapStateToProps } from "./state_to_props";
import { Props } from "./interfaces"; import { Props } from "./interfaces";
import { getStatus } from "../connectivity/reducer_support";
import { isFwHardwareValue } from "./components/firmware_hardware_support"; import { isFwHardwareValue } from "./components/firmware_hardware_support";
import { maybeOpenPanel } from "./components/maybe_highlight";
export class RawDevices extends React.Component<Props, {}> { export class RawDevices extends React.Component<Props, {}> {
componentDidMount = () =>
this.props.dispatch(maybeOpenPanel(this.props.bot.controlPanelState));
render() { render() {
if (this.props.auth) { if (this.props.auth) {
const { botToMqtt } = this.props;
const botToMqttStatus = getStatus(botToMqtt);
const botToMqttLastSeen = (botToMqtt && botToMqttStatus === "up")
? botToMqtt.at
: "";
const { value } = this.props.sourceFbosConfig("firmware_hardware"); const { value } = this.props.sourceFbosConfig("firmware_hardware");
const firmwareHardware = isFwHardwareValue(value) ? value : undefined; const firmwareHardware = isFwHardwareValue(value) ? value : undefined;
return <Page className="device-page"> return <Page className="device-page">
@ -27,14 +26,10 @@ export class RawDevices extends React.Component<Props, {}> {
alerts={this.props.alerts} alerts={this.props.alerts}
bot={this.props.bot} bot={this.props.bot}
timeSettings={this.props.timeSettings} timeSettings={this.props.timeSettings}
botToMqttLastSeen={new Date(botToMqttLastSeen).getTime()}
botToMqttStatus={botToMqttStatus}
sourceFbosConfig={this.props.sourceFbosConfig} sourceFbosConfig={this.props.sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
isValidFbosConfig={this.props.isValidFbosConfig}
env={this.props.env} env={this.props.env}
saveFarmwareEnv={this.props.saveFarmwareEnv} saveFarmwareEnv={this.props.saveFarmwareEnv} />
webAppConfig={this.props.webAppConfig} />
</Col> </Col>
<Col xs={12} sm={6}> <Col xs={12} sm={6}>
<HardwareSettings <HardwareSettings
@ -42,7 +37,6 @@ export class RawDevices extends React.Component<Props, {}> {
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
resources={this.props.resources} resources={this.props.resources}
bot={this.props.bot} bot={this.props.bot}
botToMqttStatus={botToMqttStatus}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
firmwareHardware={firmwareHardware} firmwareHardware={firmwareHardware}
sourceFwConfig={this.props.sourceFwConfig} sourceFwConfig={this.props.sourceFwConfig}

View File

@ -10,13 +10,10 @@ import {
JobProgress, JobProgress,
FirmwareHardware, FirmwareHardware,
Alert, Alert,
TaggedWebAppConfig,
} from "farmbot"; } from "farmbot";
import { ResourceIndex } from "../resources/interfaces"; import { ResourceIndex } from "../resources/interfaces";
import { WD_ENV } from "../farmware/weed_detector/remote_env/interfaces"; import { WD_ENV } from "../farmware/weed_detector/remote_env/interfaces";
import { import { ConnectionState, NetworkState } from "../connectivity/interfaces";
ConnectionStatus, ConnectionState, NetworkState,
} from "../connectivity/interfaces";
import { IntegerSize } from "../util"; import { IntegerSize } from "../util";
import { Farmwares } from "../farmware/interfaces"; import { Farmwares } from "../farmware/interfaces";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
@ -24,9 +21,6 @@ import { GetWebAppConfigValue } from "../config_storage/actions";
import { TimeSettings } from "../interfaces"; import { TimeSettings } from "../interfaces";
export interface Props { export interface Props {
userToApi: ConnectionStatus | undefined;
userToMqtt: ConnectionStatus | undefined;
botToMqtt: ConnectionStatus | undefined;
auth: AuthState | undefined; auth: AuthState | undefined;
bot: BotState; bot: BotState;
deviceAccount: TaggedDevice; deviceAccount: TaggedDevice;
@ -37,12 +31,10 @@ export interface Props {
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
firmwareConfig: FirmwareConfig | undefined; firmwareConfig: FirmwareConfig | undefined;
isValidFbosConfig: boolean;
env: UserEnv; env: UserEnv;
saveFarmwareEnv: SaveFarmwareEnv; saveFarmwareEnv: SaveFarmwareEnv;
timeSettings: TimeSettings; timeSettings: TimeSettings;
alerts: Alert[]; alerts: Alert[];
webAppConfig: TaggedWebAppConfig;
} }
/** Function to save a Farmware env variable to the API. */ /** Function to save a Farmware env variable to the API. */
@ -112,6 +104,8 @@ export interface BotState {
currentBetaOSCommit?: string; currentBetaOSCommit?: string;
/** JSON string of minimum required FBOS versions for various features. */ /** JSON string of minimum required FBOS versions for various features. */
minOsFeatureData?: MinOsFeatureLookup; minOsFeatureData?: MinOsFeatureLookup;
/** Notes notifying users of changes that may require intervention. */
osReleaseNotes?: string;
/** Is the bot in sync with the api */ /** Is the bot in sync with the api */
dirty: boolean; dirty: boolean;
/** The state of the bot, as reported by the bot over MQTT. */ /** The state of the bot, as reported by the bot over MQTT. */
@ -164,20 +158,25 @@ export interface FarmbotOsProps {
bot: BotState; bot: BotState;
alerts: Alert[]; alerts: Alert[];
deviceAccount: TaggedDevice; deviceAccount: TaggedDevice;
botToMqttStatus: NetworkState;
botToMqttLastSeen: number;
dispatch: Function; dispatch: Function;
sourceFbosConfig: SourceFbosConfig; sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
isValidFbosConfig: boolean;
env: UserEnv; env: UserEnv;
saveFarmwareEnv: SaveFarmwareEnv; saveFarmwareEnv: SaveFarmwareEnv;
timeSettings: TimeSettings; timeSettings: TimeSettings;
webAppConfig: TaggedWebAppConfig;
} }
export interface FarmbotOsState { export interface FarmbotSettingsProps {
allOsReleaseNotes: string; bot: BotState;
alerts: Alert[];
device: TaggedDevice;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay;
env: UserEnv;
saveFarmwareEnv: SaveFarmwareEnv;
timeSettings: TimeSettings;
botOnline: boolean;
} }
export interface McuInputBoxProps { export interface McuInputBoxProps {
@ -235,7 +234,6 @@ export interface FarmwareProps {
export interface HardwareSettingsProps { export interface HardwareSettingsProps {
controlPanelState: ControlPanelState; controlPanelState: ControlPanelState;
dispatch: Function; dispatch: Function;
botToMqttStatus: NetworkState;
bot: BotState; bot: BotState;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
sourceFwConfig: SourceFwConfig; sourceFwConfig: SourceFwConfig;

View File

@ -3,6 +3,8 @@ import { NetworkState } from "../connectivity/interfaces";
import { SyncStatus } from "farmbot"; import { SyncStatus } from "farmbot";
import { Content } from "../constants"; import { Content } from "../constants";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { BotState } from "./interfaces";
import { getStatus } from "../connectivity/reducer_support";
/** Properties for the <MustBeOnline/> element. */ /** Properties for the <MustBeOnline/> element. */
export interface MBOProps { export interface MBOProps {
@ -23,6 +25,12 @@ export function isBotOnline(
return !!(isBotUp(syncStatus) && botToMqttStatus === "up"); return !!(isBotUp(syncStatus) && botToMqttStatus === "up");
} }
export function isBotOnlineFromState(bot: BotState) {
const { sync_status } = bot.hardware.informational_settings;
const { uptime } = bot.connectivity;
return isBotOnline(sync_status, getStatus(uptime["bot.mqtt"]));
}
export function MustBeOnline(props: MBOProps) { export function MustBeOnline(props: MBOProps) {
const { children, hideBanner, lockOpen, networkState, syncStatus } = props; const { children, hideBanner, lockOpen, networkState, syncStatus } = props;
const banner = hideBanner ? "" : "banner"; const banner = hideBanner ? "" : "banner";

View File

@ -24,6 +24,7 @@ import {
PinBindingType, PinBindingSpecialAction, PinBindingType, PinBindingSpecialAction,
} from "farmbot/dist/resources/api_resources"; } from "farmbot/dist/resources/api_resources";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support";
export class PinBindingInputGroup export class PinBindingInputGroup
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> { extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {
@ -104,41 +105,62 @@ export class PinBindingInputGroup
(ddi: { label: string, value: PinBindingSpecialAction }) => (ddi: { label: string, value: PinBindingSpecialAction }) =>
this.setState({ specialActionInput: ddi.value }); this.setState({ specialActionInput: ddi.value });
render() { Number = () =>
const { <PinNumberInputGroup
pinNumberInput, bindingType, specialActionInput, sequenceIdInput pinNumberInput={this.state.pinNumberInput}
} = this.state; boundPins={this.boundPins}
setSelectedPin={this.setSelectedPin} />
return <Row> Type = () =>
<Col xs={PinBindingColWidth.pin}> <BindingTypeDropDown
<PinNumberInputGroup bindingType={this.state.bindingType}
pinNumberInput={pinNumberInput} setBindingType={this.setBindingType} />
boundPins={this.boundPins}
setSelectedPin={this.setSelectedPin} /> Action = () =>
</Col> this.state.bindingType == PinBindingType.special
<Col xs={PinBindingColWidth.type}> ? <ActionTargetDropDown
<BindingTypeDropDown specialActionInput={this.state.specialActionInput}
bindingType={bindingType} setSpecialAction={this.setSpecialAction} />
setBindingType={this.setBindingType} /> : <SequenceTargetDropDown
{bindingType == PinBindingType.special sequenceIdInput={this.state.sequenceIdInput}
? <ActionTargetDropDown resources={this.props.resources}
specialActionInput={specialActionInput} setSequenceIdInput={this.setSequenceIdInput} />
setSpecialAction={this.setSpecialAction} />
: <SequenceTargetDropDown render() {
sequenceIdInput={sequenceIdInput} const newFormat = DevSettings.futureFeaturesEnabled();
resources={this.props.resources} return <div className="pin-binding-input-rows">
setSequenceIdInput={this.setSequenceIdInput} />} {newFormat && <Row><label>{t("add new pin binding")}</label></Row>}
</Col> {newFormat && <this.Number />}
<Col xs={PinBindingColWidth.button}> {newFormat && <Row>
<button <Col xs={5}>
className="fb-button green" <this.Type />
type="button" </Col>
title={t("BIND")} <Col xs={7}>
onClick={this.bindPin}> <this.Action />
<i className={"fa fa-plus"} /> </Col>
</button> </Row>}
</Col> <Row>
</Row>; {!newFormat &&
<Col xs={PinBindingColWidth.pin}>
<this.Number />
</Col>}
{!newFormat && <Col xs={PinBindingColWidth.type}>
<this.Type />
<this.Action />
</Col>}
<Col xs={newFormat ? 12 : PinBindingColWidth.button}>
<button
className="fb-button green"
type="button"
title={t("BIND")}
onClick={this.bindPin}>
{newFormat
? t("Save")
: <i className={"fa fa-plus"} />}
</button>
</Col>
</Row>
</div>;
} }
} }
@ -153,9 +175,9 @@ export const PinNumberInputGroup = (props: {
label: generatePinLabel(pinNumberInput), label: generatePinLabel(pinNumberInput),
value: "" + pinNumberInput value: "" + pinNumberInput
} : undefined; } : undefined;
const newFormat = DevSettings.futureFeaturesEnabled();
return <Row> return <Row>
<Col xs={3}> <Col xs={newFormat ? 2 : 3}>
<Popover position={Position.TOP}> <Popover position={Position.TOP}>
<i className="fa fa-th-large" /> <i className="fa fa-th-large" />
<RpiGpioDiagram <RpiGpioDiagram
@ -164,7 +186,7 @@ export const PinNumberInputGroup = (props: {
selectedPin={pinNumberInput} /> selectedPin={pinNumberInput} />
</Popover> </Popover>
</Col> </Col>
<Col xs={9}> <Col xs={newFormat ? 10 : 9}>
<FBSelect <FBSelect
key={"pin_number_input_" + pinNumberInput} key={"pin_number_input_" + pinNumberInput}
onChange={ddi => onChange={ddi =>

View File

@ -16,6 +16,7 @@ import {
PinBinding, PinBinding,
} from "farmbot/dist/resources/api_resources"; } from "farmbot/dist/resources/api_resources";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support";
/** Width of UI columns in Pin Bindings widget. */ /** Width of UI columns in Pin Bindings widget. */
export enum PinBindingColWidth { export enum PinBindingColWidth {
@ -70,13 +71,15 @@ const PinBindingsListHeader = () =>
export const PinBindingsContent = (props: PinBindingsContentProps) => { export const PinBindingsContent = (props: PinBindingsContentProps) => {
const { dispatch, resources, firmwareHardware } = props; const { dispatch, resources, firmwareHardware } = props;
const pinBindings = apiPinBindings(resources); const pinBindings = apiPinBindings(resources);
const newFormat = DevSettings.futureFeaturesEnabled();
return <div className="pin-bindings"> return <div className="pin-bindings">
<Row> <Row>
{newFormat && <Help text={ToolTips.PIN_BINDINGS}
position={Position.TOP_RIGHT} />}
<StockPinBindingsButton <StockPinBindingsButton
dispatch={dispatch} firmwareHardware={firmwareHardware} /> dispatch={dispatch} firmwareHardware={firmwareHardware} />
<Popover <Popover
position={Position.RIGHT_TOP} position={Position.TOP_RIGHT}
interactionKind={PopoverInteractionKind.HOVER} interactionKind={PopoverInteractionKind.HOVER}
portalClassName={"bindings-warning-icon"} portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}> popoverClassName={"help"}>
@ -87,7 +90,7 @@ export const PinBindingsContent = (props: PinBindingsContentProps) => {
</Popover> </Popover>
</Row> </Row>
<div className={"pin-bindings-list-and-input"}> <div className={"pin-bindings-list-and-input"}>
<PinBindingsListHeader /> {!newFormat && <PinBindingsListHeader />}
<PinBindingsList <PinBindingsList
pinBindings={pinBindings} pinBindings={pinBindings}
dispatch={dispatch} dispatch={dispatch}

View File

@ -11,6 +11,10 @@ import { PinBindingColWidth } from "./pin_bindings";
import { PinBindingsListProps } from "./interfaces"; import { PinBindingsListProps } from "./interfaces";
import { sysBtnBindings } from "./tagged_pin_binding_init"; import { sysBtnBindings } from "./tagged_pin_binding_init";
import { t } from "../../i18next_wrapper"; import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support";
import {
PinBindingType, PinBindingSpecialAction,
} from "farmbot/dist/resources/api_resources";
export const PinBindingsList = (props: PinBindingsListProps) => { export const PinBindingsList = (props: PinBindingsListProps) => {
const { pinBindings, resources, dispatch } = props; const { pinBindings, resources, dispatch } = props;
@ -26,22 +30,33 @@ export const PinBindingsList = (props: PinBindingsListProps) => {
const delBtnColor = (pin: number) => const delBtnColor = (pin: number) =>
sysBtnBindings.includes(pin) ? "pseudo-disabled" : "red"; sysBtnBindings.includes(pin) ? "pseudo-disabled" : "red";
const bindingText = (
sequence_id: number | undefined,
binding_type: PinBindingType | undefined,
special_action: PinBindingSpecialAction | undefined,
) =>
`${t(bindingTypeLabelLookup[binding_type || ""])}: ${(sequence_id
? findSequenceById(resources, sequence_id).body.name
: t(getSpecialActionLabel(special_action)))}`;
const newFormat = DevSettings.futureFeaturesEnabled();
return <div className={"bindings-list"}> return <div className={"bindings-list"}>
{newFormat && <Row><label>{t("saved pin bindings")}</label></Row>}
{pinBindings {pinBindings
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number)) .sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
.map(x => { .map(x => {
const { pin_number, sequence_id, binding_type, special_action } = x; const { pin_number, sequence_id, binding_type, special_action } = x;
const binding = bindingText(sequence_id, binding_type, special_action);
return <Row key={`pin_${pin_number}_binding`}> return <Row key={`pin_${pin_number}_binding`}>
<Col xs={PinBindingColWidth.pin}> <Col xs={newFormat ? 11 : PinBindingColWidth.pin}>
{generatePinLabel(pin_number)} <p>{generatePinLabel(pin_number)}</p>
<p className="binding-action">{newFormat && binding}</p>
</Col> </Col>
<Col xs={PinBindingColWidth.type}> {!newFormat &&
{t(bindingTypeLabelLookup[binding_type || ""])}:&nbsp; <Col xs={PinBindingColWidth.type}>
{sequence_id {binding}
? findSequenceById(resources, sequence_id).body.name </Col>}
: t(getSpecialActionLabel(special_action))} <Col xs={newFormat ? 1 : PinBindingColWidth.button}>
</Col>
<Col xs={PinBindingColWidth.button}>
<button <button
className={`fb-button ${delBtnColor(pin_number)} del-button`} className={`fb-button ${delBtnColor(pin_number)} del-button`}
title={t("Delete")} title={t("Delete")}

View File

@ -35,7 +35,7 @@ export const initialState = (): BotState => ({
pin_guard: false, pin_guard: false,
farm_designer: false, farm_designer: false,
firmware: false, firmware: false,
farmbot_os: false, farmbot_os: true,
}, },
hardware: { hardware: {
gpio_registry: {}, gpio_registry: {},
@ -79,6 +79,7 @@ export const initialState = (): BotState => ({
currentOSVersion: undefined, currentOSVersion: undefined,
currentBetaOSVersion: undefined, currentBetaOSVersion: undefined,
minOsFeatureData: undefined, minOsFeatureData: undefined,
osReleaseNotes: undefined,
connectivity: { connectivity: {
uptime: { uptime: {
"bot.mqtt": undefined, "bot.mqtt": undefined,
@ -118,21 +119,24 @@ export const botReducer = generateReducer<BotState>(initialState())
s.controlPanelState[a.payload] = !s.controlPanelState[a.payload]; s.controlPanelState[a.payload] = !s.controlPanelState[a.payload];
return s; return s;
}) })
.add<boolean>(Actions.BULK_TOGGLE_CONTROL_PANEL, (s, a) => { .add<{ open: boolean, all: boolean }>(
s.controlPanelState.homing_and_calibration = a.payload; Actions.BULK_TOGGLE_CONTROL_PANEL, (s, a) => {
s.controlPanelState.motors = a.payload; s.controlPanelState.homing_and_calibration = a.payload.open;
s.controlPanelState.encoders = a.payload; s.controlPanelState.motors = a.payload.open;
s.controlPanelState.endstops = a.payload; s.controlPanelState.encoders = a.payload.open;
s.controlPanelState.error_handling = a.payload; s.controlPanelState.endstops = a.payload.open;
s.controlPanelState.pin_bindings = a.payload; s.controlPanelState.error_handling = a.payload.open;
s.controlPanelState.pin_guard = a.payload; s.controlPanelState.pin_bindings = a.payload.open;
s.controlPanelState.danger_zone = a.payload; s.controlPanelState.pin_guard = a.payload.open;
s.controlPanelState.power_and_reset = a.payload; s.controlPanelState.danger_zone = a.payload.open;
s.controlPanelState.farm_designer = a.payload; if (a.payload.all) {
s.controlPanelState.firmware = a.payload; s.controlPanelState.power_and_reset = a.payload.open;
s.controlPanelState.farmbot_os = a.payload; s.controlPanelState.farm_designer = a.payload.open;
return s; s.controlPanelState.firmware = a.payload.open;
}) s.controlPanelState.farmbot_os = a.payload.open;
}
return s;
})
.add<OsUpdateInfo>(Actions.FETCH_OS_UPDATE_INFO_OK, (s, { payload }) => { .add<OsUpdateInfo>(Actions.FETCH_OS_UPDATE_INFO_OK, (s, { payload }) => {
s.currentOSVersion = payload.version; s.currentOSVersion = payload.version;
return s; return s;
@ -147,6 +151,11 @@ export const botReducer = generateReducer<BotState>(initialState())
s.minOsFeatureData = payload; s.minOsFeatureData = payload;
return s; return s;
}) })
.add<string>(Actions.FETCH_OS_RELEASE_NOTES_OK,
(s, { payload }) => {
s.osReleaseNotes = payload;
return s;
})
.add<DeepPartial<HardwareState>>(Actions.STATUS_UPDATE, (s, { payload }) => { .add<DeepPartial<HardwareState>>(Actions.STATUS_UPDATE, (s, { payload }) => {
s.hardware = merge(s.hardware, payload); s.hardware = merge(s.hardware, payload);
legacyStatusHandler(s, incomingLegacyStatus(s.hardware)); legacyStatusHandler(s, incomingLegacyStatus(s.hardware));

View File

@ -12,9 +12,7 @@ import { validFwConfig, validFbosConfig } from "../util";
import { import {
saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn, saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn,
} from "../farmware/state_to_props"; } from "../farmware/state_to_props";
import { import { getFbosConfig, getFirmwareConfig } from "../resources/getters";
getFbosConfig, getFirmwareConfig, getWebAppConfig,
} from "../resources/getters";
import { getAllAlerts } from "../messages/state_to_props"; import { getAllAlerts } from "../messages/state_to_props";
export function mapStateToProps(props: Everything): Props { export function mapStateToProps(props: Everything): Props {
@ -23,14 +21,7 @@ export function mapStateToProps(props: Everything): Props {
const firmwareConfig = validFwConfig(getFirmwareConfig(props.resources.index)); const firmwareConfig = validFwConfig(getFirmwareConfig(props.resources.index));
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot); const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot); const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const webAppConfig = getWebAppConfig(props.resources.index);
if (!webAppConfig) {
throw new Error("Missing web app config");
}
return { return {
userToApi: props.bot.connectivity.uptime["user.api"],
userToMqtt: props.bot.connectivity.uptime["user.mqtt"],
botToMqtt: props.bot.connectivity.uptime["bot.mqtt"],
deviceAccount: getDeviceAccountSettings(props.resources.index), deviceAccount: getDeviceAccountSettings(props.resources.index),
auth: props.auth, auth: props.auth,
bot: props.bot, bot: props.bot,
@ -41,11 +32,9 @@ export function mapStateToProps(props: Everything): Props {
sourceFwConfig: sourceFwConfigValue(firmwareConfig, hardware.mcu_params), sourceFwConfig: sourceFwConfigValue(firmwareConfig, hardware.mcu_params),
shouldDisplay, shouldDisplay,
firmwareConfig, firmwareConfig,
isValidFbosConfig: !!fbosConfig,
env, env,
saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index), saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index),
timeSettings: maybeGetTimeSettings(props.resources.index), timeSettings: maybeGetTimeSettings(props.resources.index),
alerts: getAllAlerts(props.resources), alerts: getAllAlerts(props.resources),
webAppConfig
}; };
} }

View File

@ -16,7 +16,7 @@ describe("findBySlug()", () => {
it("returns crop default result: no slug provided", () => { it("returns crop default result: no slug provided", () => {
const result = findBySlug([fakeCropLiveSearchResult()]); const result = findBySlug([fakeCropLiveSearchResult()]);
expect(result).toEqual({ expect(result).toEqual({
crop: expect.objectContaining({ name: "Name" }), crop: expect.objectContaining({ name: "" }),
image: DEFAULT_ICON image: DEFAULT_ICON
}); });
}); });

View File

@ -118,6 +118,7 @@ export interface DesignerState {
currentPoint: CurrentPointPayl | undefined; currentPoint: CurrentPointPayl | undefined;
openedSavedGarden: string | undefined; openedSavedGarden: string | undefined;
tryGroupSortType: PointGroupSortType | "nn" | undefined; tryGroupSortType: PointGroupSortType | "nn" | undefined;
editGroupAreaInMap: boolean;
} }
export type TaggedExecutable = TaggedSequence | TaggedRegimen; export type TaggedExecutable = TaggedSequence | TaggedRegimen;

View File

@ -32,7 +32,7 @@ jest.mock("../drawn_point/drawn_point_actions", () => ({
jest.mock("../background/selection_box_actions", () => ({ jest.mock("../background/selection_box_actions", () => ({
startNewSelectionBox: jest.fn(), startNewSelectionBox: jest.fn(),
resizeBox: jest.fn(), resizeBox: jest.fn(),
maybeUpdateGroupCriteria: jest.fn(), maybeUpdateGroup: jest.fn(),
})); }));
jest.mock("../../move_to", () => ({ chooseLocation: jest.fn() })); jest.mock("../../move_to", () => ({ chooseLocation: jest.fn() }));
@ -61,7 +61,7 @@ import {
dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant, dropPlant, beginPlantDrag, maybeSavePlantLocation, dragPlant,
} from "../layers/plants/plant_actions"; } from "../layers/plants/plant_actions";
import { import {
startNewSelectionBox, resizeBox, maybeUpdateGroupCriteria, startNewSelectionBox, resizeBox, maybeUpdateGroup,
} from "../background/selection_box_actions"; } from "../background/selection_box_actions";
import { getGardenCoordinates } from "../util"; import { getGardenCoordinates } from "../util";
import { chooseLocation } from "../../move_to"; import { chooseLocation } from "../../move_to";
@ -158,7 +158,7 @@ describe("<GardenMap/>", () => {
wrapper.setState({ isDragging: true }); wrapper.setState({ isDragging: true });
wrapper.find(".drop-area-svg").simulate("mouseUp", DEFAULT_EVENT); wrapper.find(".drop-area-svg").simulate("mouseUp", DEFAULT_EVENT);
expect(maybeSavePlantLocation).toHaveBeenCalled(); expect(maybeSavePlantLocation).toHaveBeenCalled();
expect(maybeUpdateGroupCriteria).toHaveBeenCalled(); expect(maybeUpdateGroup).toHaveBeenCalled();
expect(wrapper.instance().state.isDragging).toBeFalsy(); expect(wrapper.instance().state.isDragging).toBeFalsy();
}); });
@ -224,7 +224,9 @@ describe("<GardenMap/>", () => {
}); });
it("starts drag on background: selecting zone", () => { it("starts drag on background: selecting zone", () => {
const wrapper = mount(<GardenMap {...fakeProps()} />); const p = fakeProps();
p.designer.editGroupAreaInMap = true;
const wrapper = mount(<GardenMap {...p} />);
mockMode = Mode.editGroup; mockMode = Mode.editGroup;
const e = { pageX: 1000, pageY: 2000 }; const e = { pageX: 1000, pageY: 2000 };
wrapper.find(".drop-area-background").simulate("mouseDown", e); wrapper.find(".drop-area-background").simulate("mouseDown", e);
@ -255,7 +257,9 @@ describe("<GardenMap/>", () => {
}); });
it("drags: selecting zone", () => { it("drags: selecting zone", () => {
const wrapper = shallow(<GardenMap {...fakeProps()} />); const p = fakeProps();
p.designer.editGroupAreaInMap = true;
const wrapper = shallow(<GardenMap {...p} />);
mockMode = Mode.editGroup; mockMode = Mode.editGroup;
const e = { pageX: 2000, pageY: 2000 }; const e = { pageX: 2000, pageY: 2000 };
wrapper.find(".drop-area-svg").simulate("mouseMove", e); wrapper.find(".drop-area-svg").simulate("mouseMove", e);

View File

@ -8,18 +8,25 @@ jest.mock("../../../point_groups/criteria", () => ({
editGtLtCriteria: jest.fn(), editGtLtCriteria: jest.fn(),
})); }));
jest.mock("../../../../api/crud", () => ({
overwrite: jest.fn(),
save: jest.fn(),
}));
import { import {
fakePlant, fakePointGroup, fakePlant, fakePointGroup,
} from "../../../../__test_support__/fake_state/resources"; } from "../../../../__test_support__/fake_state/resources";
import { import {
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps, getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps,
StartNewSelectionBoxProps, StartNewSelectionBoxProps,
maybeUpdateGroupCriteria, maybeUpdateGroup,
MaybeUpdateGroupCriteriaProps, MaybeUpdateGroupProps,
} from "../selection_box_actions"; } from "../selection_box_actions";
import { Actions } from "../../../../constants"; import { Actions } from "../../../../constants";
import { history } from "../../../../history"; import { history } from "../../../../history";
import { editGtLtCriteria } from "../../../point_groups/criteria"; import { editGtLtCriteria } from "../../../point_groups/criteria";
import { overwrite, save } from "../../../../api/crud";
import { cloneDeep } from "lodash";
describe("getSelected", () => { describe("getSelected", () => {
it("returns some", () => { it("returns some", () => {
@ -156,24 +163,55 @@ describe("startNewSelectionBox", () => {
}); });
}); });
describe("maybeUpdateGroupCriteria()", () => { describe("maybeUpdateGroup()", () => {
const fakeProps = (): MaybeUpdateGroupCriteriaProps => ({ const fakeProps = (): MaybeUpdateGroupProps => ({
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined }, selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
dispatch: jest.fn(), dispatch: jest.fn(),
group: fakePointGroup(), group: fakePointGroup(),
shouldDisplay: () => true, shouldDisplay: () => true,
editGroupAreaInMap: false,
boxSelected: undefined,
});
it("updates group", () => {
const p = fakeProps();
p.editGroupAreaInMap = false;
const plant1 = fakePlant();
const plant2 = fakePlant();
p.boxSelected = [plant1.uuid, plant2.uuid];
p.group && (p.group.body.point_ids = [plant1.body.id || 0]);
maybeUpdateGroup(p);
expect(editGtLtCriteria).not.toHaveBeenCalled();
const expectedBody = cloneDeep(p.group?.body);
expectedBody && (expectedBody.point_ids = [
plant1.body.id || 0, plant2.body.id || 0,
]);
expect(overwrite).toHaveBeenCalledWith(p.group, expectedBody);
expect(save).not.toHaveBeenCalled();
}); });
it("updates criteria", () => { it("updates criteria", () => {
const p = fakeProps(); const p = fakeProps();
maybeUpdateGroupCriteria(p); p.editGroupAreaInMap = true;
maybeUpdateGroup(p);
expect(editGtLtCriteria).toHaveBeenCalledWith(p.group, p.selectionBox); expect(editGtLtCriteria).toHaveBeenCalledWith(p.group, p.selectionBox);
}); });
it("doesn't update criteria", () => { it("doesn't update criteria", () => {
const p = fakeProps(); const p = fakeProps();
p.shouldDisplay = () => false; p.shouldDisplay = () => false;
maybeUpdateGroupCriteria(p); maybeUpdateGroup(p);
expect(editGtLtCriteria).not.toHaveBeenCalled(); expect(editGtLtCriteria).not.toHaveBeenCalled();
}); });
it("handles missing group or box", () => {
const p = fakeProps();
p.group = undefined;
p.selectionBox = undefined;
maybeUpdateGroup(p);
expect(p.dispatch).not.toHaveBeenCalled();
expect(editGtLtCriteria).not.toHaveBeenCalled();
expect(overwrite).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
});
}); });

View File

@ -1,4 +1,4 @@
import { isNumber } from "lodash"; import { isNumber, uniq, cloneDeep, isEqual } from "lodash";
import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces"; import { TaggedPlant, AxisNumberProperty, Mode } from "../interfaces";
import { SelectionBoxData } from "./selection_box"; import { SelectionBoxData } from "./selection_box";
import { GardenMapState } from "../../interfaces"; import { GardenMapState } from "../../interfaces";
@ -8,6 +8,9 @@ import { getMode } from "../util";
import { editGtLtCriteria } from "../../point_groups/criteria"; import { editGtLtCriteria } from "../../point_groups/criteria";
import { TaggedPointGroup } from "farmbot"; import { TaggedPointGroup } from "farmbot";
import { ShouldDisplay, Feature } from "../../../devices/interfaces"; import { ShouldDisplay, Feature } from "../../../devices/interfaces";
import { overwrite } from "../../../api/crud";
import { unpackUUID } from "../../../util";
import { UUID } from "../../../resources/interfaces";
/** Return all plants within the selection box. */ /** Return all plants within the selection box. */
export const getSelected = ( export const getSelected = (
@ -85,17 +88,32 @@ export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => {
} }
}; };
export interface MaybeUpdateGroupCriteriaProps { export interface MaybeUpdateGroupProps {
selectionBox: SelectionBoxData | undefined; selectionBox: SelectionBoxData | undefined;
dispatch: Function; dispatch: Function;
group: TaggedPointGroup | undefined; group: TaggedPointGroup | undefined;
shouldDisplay: ShouldDisplay; shouldDisplay: ShouldDisplay;
editGroupAreaInMap: boolean;
boxSelected: UUID[] | undefined;
} }
export const maybeUpdateGroupCriteria = export const maybeUpdateGroup =
(props: MaybeUpdateGroupCriteriaProps) => { (props: MaybeUpdateGroupProps) => {
if (props.selectionBox && props.group && if (props.selectionBox && props.group) {
props.shouldDisplay(Feature.criteria_groups)) { if (props.editGroupAreaInMap
props.dispatch(editGtLtCriteria(props.group, props.selectionBox)); && props.shouldDisplay(Feature.criteria_groups)) {
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
} else {
const nextGroupBody = cloneDeep(props.group.body);
props.boxSelected?.map(uuid => {
const { kind, remoteId } = unpackUUID(uuid);
remoteId && kind == "Point" && nextGroupBody.point_ids.push(remoteId);
});
nextGroupBody.point_ids = uniq(nextGroupBody.point_ids);
if (!isEqual(props.group.body.point_ids, nextGroupBody.point_ids)) {
props.dispatch(overwrite(props.group, nextGroupBody));
props.dispatch(selectPlant(undefined));
}
}
} }
}; };

View File

@ -11,7 +11,7 @@ import {
import { import {
Grid, MapBackground, Grid, MapBackground,
TargetCoordinate, TargetCoordinate,
SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroupCriteria, SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroup,
} from "./background"; } from "./background";
import { import {
PlantLayer, PlantLayer,
@ -88,11 +88,13 @@ export class GardenMap extends
isDragging: this.state.isDragging, isDragging: this.state.isDragging,
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
}); });
maybeUpdateGroupCriteria({ maybeUpdateGroup({
selectionBox: this.state.selectionBox, selectionBox: this.state.selectionBox,
group: this.group, group: this.group,
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
shouldDisplay: this.props.shouldDisplay, shouldDisplay: this.props.shouldDisplay,
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
boxSelected: this.props.designer.selectedPlants,
}); });
this.setState({ this.setState({
isDragging: false, qPageX: 0, qPageY: 0, isDragging: false, qPageX: 0, qPageY: 0,
@ -142,7 +144,7 @@ export class GardenMap extends
gardenCoords: this.getGardenCoordinates(e), gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState, setMapState: this.setMapState,
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
plantActions: false, plantActions: !this.props.designer.editGroupAreaInMap,
}); });
break; break;
case Mode.createPoint: case Mode.createPoint:
@ -179,7 +181,7 @@ export class GardenMap extends
gardenCoords: this.getGardenCoordinates(e), gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState, setMapState: this.setMapState,
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
plantActions: false, plantActions: !this.props.designer.editGroupAreaInMap,
}); });
break; break;
default: default:
@ -283,7 +285,7 @@ export class GardenMap extends
gardenCoords: this.getGardenCoordinates(e), gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState, setMapState: this.setMapState,
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
plantActions: false, plantActions: !this.props.designer.editGroupAreaInMap,
}); });
break; break;
case Mode.boxSelect: case Mode.boxSelect:

View File

@ -6,6 +6,7 @@ import { Color } from "../../../../../ui/index";
import { import {
fakeMapTransformProps, fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props"; } from "../../../../../__test_support__/map_transform_props";
import { svgMount } from "../../../../../__test_support__/svg_mount";
describe("<BotFigure/>", () => { describe("<BotFigure/>", () => {
const fakeProps = (): BotFigureProps => ({ const fakeProps = (): BotFigureProps => ({
@ -35,7 +36,7 @@ describe("<BotFigure/>", () => {
p.mapTransformProps.quadrant = quadrant; p.mapTransformProps.quadrant = quadrant;
p.mapTransformProps.xySwap = xySwap; p.mapTransformProps.xySwap = xySwap;
p.figureName = figureName; p.figureName = figureName;
const result = shallow<BotFigure>(<BotFigure {...p} />); const result = svgMount(<BotFigure {...p} />);
const expectedGantryProps = expect.objectContaining({ const expectedGantryProps = expect.objectContaining({
id: "gantry", id: "gantry",
@ -65,7 +66,7 @@ describe("<BotFigure/>", () => {
const p = fakeProps(); const p = fakeProps();
p.mapTransformProps.quadrant = 2; p.mapTransformProps.quadrant = 2;
p.position = { x: 100, y: 200, z: 0 }; p.position = { x: 100, y: 200, z: 0 };
const result = shallow<BotFigure>(<BotFigure {...p} />); const result = svgMount(<BotFigure {...p} />);
const gantry = result.find("#gantry"); const gantry = result.find("#gantry");
expect(gantry.length).toEqual(1); expect(gantry.length).toEqual(1);
expect(gantry.props().x).toEqual(90); expect(gantry.props().x).toEqual(90);
@ -77,7 +78,7 @@ describe("<BotFigure/>", () => {
it("changes color on e-stop", () => { it("changes color on e-stop", () => {
const p = fakeProps(); const p = fakeProps();
p.eStopStatus = true; p.eStopStatus = true;
const wrapper = shallow<BotFigure>(<BotFigure {...p} />); const wrapper = svgMount(<BotFigure {...p} />);
expect(wrapper.find("#gantry").props().fill).toEqual(Color.virtualRed); expect(wrapper.find("#gantry").props().fill).toEqual(Color.virtualRed);
}); });
@ -118,7 +119,7 @@ describe("<BotFigure/>", () => {
it("shows mounted tool", () => { it("shows mounted tool", () => {
const p = fakeProps(); const p = fakeProps();
p.mountedToolName = "Seeder"; p.mountedToolName = "Seeder";
const wrapper = shallow<BotFigure>(<BotFigure {...p} />); const wrapper = svgMount(<BotFigure {...p} />);
expect(wrapper.find("#UTM-wrapper").find("#mounted-tool").length) expect(wrapper.find("#UTM-wrapper").find("#mounted-tool").length)
.toEqual(1); .toEqual(1);
}); });

View File

@ -303,7 +303,6 @@ describe("<ToolSlotSVG />", () => {
const fakeProps = (): ToolSlotSVGProps => ({ const fakeProps = (): ToolSlotSVGProps => ({
toolSlot: fakeToolSlot(), toolSlot: fakeToolSlot(),
toolName: "seeder", toolName: "seeder",
renderRotation: false,
xySwap: false, xySwap: false,
quadrant: 2, quadrant: 2,
}); });

View File

@ -341,13 +341,12 @@ const SeedTrough = (props: ToolGraphicProps) => {
export interface ToolSlotSVGProps { export interface ToolSlotSVGProps {
toolSlot: TaggedToolSlotPointer; toolSlot: TaggedToolSlotPointer;
toolName: string | undefined; toolName: string | undefined;
renderRotation: boolean;
xySwap?: boolean; xySwap?: boolean;
quadrant?: BotOriginQuadrant; quadrant?: BotOriginQuadrant;
} }
export const ToolSlotSVG = (props: ToolSlotSVGProps) => { export const ToolSlotSVG = (props: ToolSlotSVGProps) => {
const xySwap = props.renderRotation ? !!props.xySwap : false; const xySwap = !!props.xySwap;
const toolProps = { const toolProps = {
x: 0, y: 0, x: 0, y: 0,
hovered: false, hovered: false,
@ -355,13 +354,11 @@ export const ToolSlotSVG = (props: ToolSlotSVGProps) => {
uuid: props.toolSlot.uuid, uuid: props.toolSlot.uuid,
xySwap, xySwap,
}; };
const pulloutDirection = props.renderRotation const pulloutDirection = props.toolSlot.body.pullout_direction
? props.toolSlot.body.pullout_direction || ToolPulloutDirection.POSITIVE_X;
: ToolPulloutDirection.POSITIVE_X; const quadrant = props.quadrant || 2;
const quadrant = props.renderRotation && props.quadrant ? props.quadrant : 2;
const viewBox = props.renderRotation ? "-25 0 50 1" : "-25 0 50 1";
return props.toolSlot.body.gantry_mounted return props.toolSlot.body.gantry_mounted
? <svg width="3rem" height="3rem" viewBox={viewBox}> ? <svg width="3rem" height="3rem" viewBox={"-25 0 50 1"}>
<GantryToolSlot x={0} y={0} xySwap={xySwap} /> <GantryToolSlot x={0} y={0} xySwap={xySwap} />
{props.toolSlot.body.tool_id && {props.toolSlot.body.tool_id &&
<Tool tool={reduceToolName(props.toolName)} toolProps={toolProps} />} <Tool tool={reduceToolName(props.toolName)} toolProps={toolProps} />}
@ -369,7 +366,7 @@ export const ToolSlotSVG = (props: ToolSlotSVGProps) => {
: <svg width="3rem" height="3rem" viewBox={`-50 0 100 1`}> : <svg width="3rem" height="3rem" viewBox={`-50 0 100 1`}>
{props.toolSlot.body.pullout_direction && {props.toolSlot.body.pullout_direction &&
<ToolbaySlot <ToolbaySlot
id={props.toolSlot.body.id} id={-(props.toolSlot.body.id || 1)}
x={0} x={0}
y={0} y={0}
pulloutDirection={pulloutDirection} pulloutDirection={pulloutDirection}

View File

@ -51,7 +51,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
onClick={() => history.push(`/app/designer/tool-slots/${id}`)}> onClick={() => history.push(`/app/designer/tool-slots/${id}`)}>
{pullout_direction && !gantry_mounted && {pullout_direction && !gantry_mounted &&
<ToolbaySlot <ToolbaySlot
id={-(id || 1)} id={id}
x={qx} x={qx}
y={qy} y={qy}
pulloutDirection={pullout_direction} pulloutDirection={pullout_direction}

View File

@ -7,7 +7,6 @@ import {
import { import {
fakeMapTransformProps, fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props"; } from "../../../../../__test_support__/map_transform_props";
import { PointGroup } from "farmbot/dist/resources/api_resources";
describe("<ZonesLayer />", () => { describe("<ZonesLayer />", () => {
const fakeProps = (): ZonesLayerProps => ({ const fakeProps = (): ZonesLayerProps => ({
@ -69,7 +68,6 @@ describe("<ZonesLayer />", () => {
const p = fakeProps(); const p = fakeProps();
p.visible = false; p.visible = false;
p.groups[0].body.id = 1; p.groups[0].body.id = 1;
p.groups[0].body.criteria = undefined as unknown as PointGroup["criteria"];
p.currentGroup = p.groups[0].uuid; p.currentGroup = p.groups[0].uuid;
const wrapper = svgMount(<ZonesLayer {...p} />); const wrapper = svgMount(<ZonesLayer {...p} />);
expect(wrapper.html()) expect(wrapper.html())

View File

@ -9,7 +9,7 @@ import {
import { import {
fakeMapTransformProps, fakeMapTransformProps,
} from "../../../../../__test_support__/map_transform_props"; } from "../../../../../__test_support__/map_transform_props";
import { PointGroup } from "farmbot/dist/resources/api_resources"; import { DEFAULT_CRITERIA } from "../../../../point_groups/criteria/interfaces";
const fakeProps = (): ZonesProps => ({ const fakeProps = (): ZonesProps => ({
group: fakePointGroup(), group: fakePointGroup(),
@ -25,7 +25,7 @@ describe("<Zones0D />", () => {
it("renders none: no data", () => { it("renders none: no data", () => {
const p = fakeProps(); const p = fakeProps();
p.group.body.id = 1; p.group.body.id = 1;
p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; p.group.body.criteria = DEFAULT_CRITERIA;
const wrapper = svgMount(<Zones0D {...p} />); const wrapper = svgMount(<Zones0D {...p} />);
expect(wrapper.find("#zones-0D-1").length).toEqual(1); expect(wrapper.find("#zones-0D-1").length).toEqual(1);
expect(wrapper.find("circle").length).toEqual(0); expect(wrapper.find("circle").length).toEqual(0);
@ -63,7 +63,7 @@ describe("<Zones1D />", () => {
it("renders none: no data", () => { it("renders none: no data", () => {
const p = fakeProps(); const p = fakeProps();
p.group.body.id = 1; p.group.body.id = 1;
p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; p.group.body.criteria = DEFAULT_CRITERIA;
const wrapper = svgMount(<Zones1D {...p} />); const wrapper = svgMount(<Zones1D {...p} />);
expect(wrapper.find("#zones-1D-1").length).toEqual(1); expect(wrapper.find("#zones-1D-1").length).toEqual(1);
expect(wrapper.find("line").length).toEqual(0); expect(wrapper.find("line").length).toEqual(0);
@ -110,7 +110,7 @@ describe("<Zones2D />", () => {
it("renders none", () => { it("renders none", () => {
const p = fakeProps(); const p = fakeProps();
p.group.body.id = 1; p.group.body.id = 1;
p.group.body.criteria = undefined as unknown as PointGroup["criteria"]; p.group.body.criteria = DEFAULT_CRITERIA;
const wrapper = svgMount(<Zones2D {...p} />); const wrapper = svgMount(<Zones2D {...p} />);
expect(wrapper.find("#zones-2D-1").length).toEqual(1); expect(wrapper.find("#zones-2D-1").length).toEqual(1);
expect(wrapper.find("rect").length).toEqual(0); expect(wrapper.find("rect").length).toEqual(0);

View File

@ -26,9 +26,9 @@ type Point = { x: number, y: number };
export enum ZoneType { points, lines, area, none } export enum ZoneType { points, lines, area, none }
export const getZoneType = (group: TaggedPointGroup): ZoneType => { export const getZoneType = (group: TaggedPointGroup): ZoneType => {
const numEq = group.body.criteria?.number_eq || {}; const numEq = group.body.criteria.number_eq;
const numGt = group.body.criteria?.number_gt || {}; const numGt = group.body.criteria.number_gt;
const numLt = group.body.criteria?.number_lt || {}; const numLt = group.body.criteria.number_lt;
const hasXEq = !!numEq.x?.length; const hasXEq = !!numEq.x?.length;
const hasYEq = !!numEq.y?.length; const hasYEq = !!numEq.y?.length;
if (hasXEq && hasYEq) { if (hasXEq && hasYEq) {
@ -46,8 +46,8 @@ export const getZoneType = (group: TaggedPointGroup): ZoneType => {
/** Bounds for area selected by criteria or bot extents. */ /** Bounds for area selected by criteria or bot extents. */
const getBoundary = (props: GetBoundaryProps): Boundary => { const getBoundary = (props: GetBoundaryProps): Boundary => {
const { criteria } = props.group.body; const { criteria } = props.group.body;
const gt = criteria?.number_gt || {}; const gt = criteria.number_gt;
const lt = criteria?.number_lt || {}; const lt = criteria.number_lt;
const x1 = gt.x || 0; const x1 = gt.x || 0;
const x2 = lt.x || props.botSize.x.value; const x2 = lt.x || props.botSize.x.value;
const y1 = gt.y || 0; const y1 = gt.y || 0;
@ -67,8 +67,8 @@ const filter: <T extends Point | Line>(
/** Coordinates selected by both x and y number equal values. */ /** Coordinates selected by both x and y number equal values. */
const getPoints = const getPoints =
(boundary: Boundary, group: TaggedPointGroup): Point[] => { (boundary: Boundary, group: TaggedPointGroup): Point[] => {
const xs = group.body.criteria?.number_eq.x; const xs = group.body.criteria.number_eq.x;
const ys = group.body.criteria?.number_eq.y; const ys = group.body.criteria.number_eq.y;
const points: Point[] = []; const points: Point[] = [];
xs?.map(x => ys?.map(y => points.push({ x, y }))); xs?.map(x => ys?.map(y => points.push({ x, y })));
return filter<Point>(boundary, points); return filter<Point>(boundary, points);
@ -95,12 +95,12 @@ export const Zones0D = (props: ZonesProps) => {
/** Lines selected by an x or y number equal value. */ /** Lines selected by an x or y number equal value. */
const getLines = const getLines =
(boundary: Boundary, group: TaggedPointGroup): Line[] => { (boundary: Boundary, group: TaggedPointGroup): Line[] => {
const xs = group.body.criteria?.number_eq.x; const xs = group.body.criteria.number_eq.x;
const ys = group.body.criteria?.number_eq.y; const ys = group.body.criteria.number_eq.y;
const onlyXs = !!xs?.length && !ys?.length; const onlyXs = !!xs?.length && !ys?.length;
const onlyYs = !!ys?.length && !xs?.length; const onlyYs = !!ys?.length && !xs?.length;
const xLineData = onlyXs ? xs?.map(x => ({ x })) : undefined; const xLineData = (onlyXs && xs) ? xs.map(x => ({ x })) : undefined;
const yLineData = onlyYs ? ys?.map(y => ({ y })) : undefined; const yLineData = (onlyYs && ys) ? ys.map(y => ({ y })) : undefined;
return filter<Line>(boundary, xLineData || yLineData); return filter<Line>(boundary, xLineData || yLineData);
}; };

View File

@ -80,7 +80,7 @@ const LayerToggles = (props: GardenMapLegendProps) => {
{DevSettings.futureFeaturesEnabled() && {DevSettings.futureFeaturesEnabled() &&
<LayerToggle <LayerToggle
value={props.showZones} value={props.showZones}
label={t("Zones?")} label={t("areas?")}
onClick={toggle(BooleanSetting.show_zones)} />} onClick={toggle(BooleanSetting.show_zones)} />}
{DevSettings.futureFeaturesEnabled() && props.hasSensorReadings && {DevSettings.futureFeaturesEnabled() && props.hasSensorReadings &&
<LayerToggle <LayerToggle

View File

@ -15,19 +15,16 @@ import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader, DesignerPanel, DesignerPanelContent, DesignerPanelHeader,
} from "./designer_panel"; } from "./designer_panel";
import { t } from "../i18next_wrapper"; import { t } from "../i18next_wrapper";
import { isBotOnline } from "../devices/must_be_online"; import { isBotOnlineFromState } from "../devices/must_be_online";
import { getStatus } from "../connectivity/reducer_support";
import { PanelColor } from "./panel_header"; import { PanelColor } from "./panel_header";
export function mapStateToProps(props: Everything): MoveToProps { export function mapStateToProps(props: Everything): MoveToProps {
const botToMqttStatus = getStatus(props.bot.connectivity.uptime["bot.mqtt"]);
const { sync_status } = props.bot.hardware.informational_settings;
return { return {
chosenLocation: props.resources.consumers.farm_designer.chosenLocation, chosenLocation: props.resources.consumers.farm_designer.chosenLocation,
currentBotLocation: currentBotLocation:
validBotLocationData(props.bot.hardware.location_data).position, validBotLocationData(props.bot.hardware.location_data).position,
dispatch: props.dispatch, dispatch: props.dispatch,
botOnline: isBotOnline(sync_status, botToMqttStatus), botOnline: isBotOnlineFromState(props.bot),
}; };
} }

View File

@ -99,9 +99,9 @@ describe("<CropInfo />", () => {
p.cropSearchResults[0].crop.row_spacing = undefined; p.cropSearchResults[0].crop.row_spacing = undefined;
p.cropSearchResults[0].crop.common_names = []; p.cropSearchResults[0].crop.common_names = [];
const wrapper = mount(<CropInfo {...p} />); const wrapper = mount(<CropInfo {...p} />);
expect(wrapper.text().toLowerCase()).toContain("iconnot set"); expect(wrapper.text().toLowerCase()).toContain("iconnot available");
expect(wrapper.text().toLowerCase()).toContain("spacingnot set"); expect(wrapper.text().toLowerCase()).toContain("spacingnot available");
expect(wrapper.text().toLowerCase()).toContain("common namesnot set"); expect(wrapper.text().toLowerCase()).toContain("common namesnot available");
}); });
}); });

View File

@ -58,7 +58,7 @@ const OMITTED_PROPERTIES = [
"guides_count", "guides_count",
]; ];
const NO_VALUE = t("Not Set"); const NO_VALUE = t("Not available");
/** /**
* If there's a value, give it an img element to render the * If there's a value, give it an img element to render the

Some files were not shown because too many files have changed in this diff Show More