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,
openedSavedGarden: undefined,
tryGroupSortType: undefined,
editGroupAreaInMap: false,
});

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import axios from "axios";
import {
fetchReleases, fetchMinOsFeatureData,
fetchLatestGHBetaRelease,
fetchOsReleaseNotes,
} from "../devices/actions";
import { AuthState } from "./interfaces";
import { ReduxAction } from "../redux/interfaces";
@ -16,7 +17,6 @@ import { Actions } from "../constants";
import { connectDevice } from "../connectivity/connect_device";
import { getFirstPartyFarmwareList } from "../farmware/actions";
import { readOnlyInterceptor } from "../read_only_mode";
import { ExternalUrl } from "../external_urls";
export function didLogin(authState: AuthState, dispatch: Function) {
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" &&
dispatch(fetchLatestGHBetaRelease(beta_os_update_server));
dispatch(getFirstPartyFarmwareList());
dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions));
dispatch(fetchMinOsFeatureData());
dispatch(fetchOsReleaseNotes());
dispatch(setToken(authState));
Sync.fetchSyncData(dispatch);
dispatch(connectDevice(authState));

View File

@ -1,5 +1,5 @@
import { ConnectionStatus } from "./interfaces";
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 =
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
selection at that time may differ from the selection currently
displayed.`);
@ -1127,6 +1127,8 @@ export enum Actions {
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_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",
DISPLAY_ENCODER_DATA = "DISPLAY_ENCODER_DATA",
STASH_STATUS = "STASH_STATUS",
@ -1149,6 +1151,7 @@ export enum Actions {
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
TRY_SORT_TYPE = "TRY_SORT_TYPE",
EDIT_GROUP_AREA_IN_MAP = "EDIT_GROUP_AREA_IN_MAP",
// Regimens
PUSH_WEEK = "PUSH_WEEK",

View File

@ -18,10 +18,9 @@ describe("<Controls />", () => {
feeds: [fakeWebcamFeed()],
peripherals: [fakePeripheral()],
sensors: [fakeSensor()],
botToMqttStatus: "up",
firmwareSettings: bot.hardware.mcu_params,
shouldDisplay: () => true,
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
getWebAppConfigVal: jest.fn(key => mockConfig[key]),
sensorReadings: [],
timeSettings: fakeTimeSettings(),
env: {},
@ -65,6 +64,17 @@ describe("<Controls />", () => {
.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", () => {
const p = fakeProps();
mockConfig.hide_sensors = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -210,16 +210,6 @@
@extend %panel-item-base;
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 {
display: inline-block;
vertical-align: middle;

View File

@ -640,6 +640,9 @@
.add-tool-panel-content,
.edit-tool-panel-content {
max-height: calc(100vh - 14rem);
overflow-y: auto;
overflow-x: hidden;
button {
display: block;
margin-left: auto;
@ -687,6 +690,7 @@
}
}
button {
margin-bottom: 2rem;
.fa-plus {
margin-right: 0.5rem;
}
@ -805,7 +809,6 @@
.weeds-inventory-panel,
.zones-inventory-panel,
.group-detail-panel,
.groups-panel {
.panel-content {
max-height: calc(100vh - 19rem);
@ -817,6 +820,31 @@
.group-detail-panel {
.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 {
margin-top: 1rem;
.criteria-heading {
@ -825,7 +853,61 @@
.fb-button {
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"] {
width: auto;
margin-right: 1rem;
@ -841,29 +923,28 @@
.criteria-slug {
margin-top: 1rem;
}
.location-criteria {
.row {
margin-top: 1rem;
p {
font-size: 1.4rem;
font-weight: bold;
}
label {
margin-top: 0;
}
}
}
.day-criteria {
p {
display: inline;
vertical-align: bottom;
}
input {
line-height: 1.75rem;
}
}
.string-eq-criteria {
margin-top: 1rem;
.row {
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-gt-lt-criteria {
@ -873,11 +954,49 @@
}
p {
text-align: center;
margin-top: 0.5rem;
line-height: 2.75rem;
font-size: 1.2rem;
}
}
.expandable-header {
margin-top: 3rem;
.fb-toggle-button {
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 {
@ -906,19 +1025,34 @@
}
}
.zone-info-panel {
.panel-content {
.location-criteria {
.row {
margin-top: 1rem;
p {
font-size: 1.4rem;
font-weight: bold;
}
label {
margin-top: 0;
}
}
.lt-gt-criteria,
.location-criteria {
display: inline-block;
.row {
margin-left: 0;
div[class*=col-] {
padding: 0;
text-align: center;
}
margin-top: 1rem;
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,
.device-widget {
.row {
@ -369,15 +380,11 @@ a {
label {
padding: 0.5rem 0 0 0.5rem;
}
.centered-button-div {
.fb-button {
float: none !important;
}
label {
padding: 0;
}
text-align: center;
padding: 0.25rem;
h4 {
margin-bottom: 0;
}
p {
margin-bottom: 1rem !important;
}
.widget-body-tooltips {
.bp3-popover-wrapper {
@ -1302,15 +1309,6 @@ ul {
display: inline;
position: relative;
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-target {
@ -1649,11 +1647,9 @@ textarea:focus {
.section {
display: block !important;
}
.highlight,
.unhighlight {
display: flex;
.bp3-collapse {
padding-top: 1rem;
}
}
.highlight {

View File

@ -138,6 +138,15 @@ select {
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 {
input[type="checkbox"] {
width: 3rem;
@ -155,8 +164,10 @@ select {
}
}
&.disabled {
cursor: not-allowed;
input[type="checkbox"] {
cursor: not-allowed;
background: $light_gray;
pointer-events: none;
&:checked:after {
border-color: $gray;
}

View File

@ -407,14 +407,15 @@ describe("fetchLatestGHBetaRelease()", () => {
});
describe("fetchMinOsFeatureData()", () => {
const EXPECTED_URL = expect.stringContaining("FEATURE_MIN_VERSIONS.json");
afterEach(() =>
jest.restoreAllMocks());
it("fetches min OS feature data: empty", async () => {
mockGetRelease = Promise.resolve({ data: {} });
const dispatch = jest.fn();
await actions.fetchMinOsFeatureData("url")(dispatch);
expect(axios.get).toHaveBeenCalledWith("url");
await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).toHaveBeenCalledWith({
payload: {},
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" }
});
const dispatch = jest.fn();
await actions.fetchMinOsFeatureData("url")(dispatch);
expect(axios.get).toHaveBeenCalledWith("url");
await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).toHaveBeenCalledWith({
payload: { a_feature: "1.0.0", b_feature: "2.0.0" },
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK
@ -438,8 +439,8 @@ describe("fetchMinOsFeatureData()", () => {
mockGetRelease = Promise.resolve({ data: "bad" });
const dispatch = jest.fn();
const mockConsole = jest.spyOn(console, "log").mockImplementation(() => { });
await actions.fetchMinOsFeatureData("url")(dispatch);
expect(axios.get).toHaveBeenCalledWith("url");
await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).not.toHaveBeenCalled();
expect(mockConsole).toHaveBeenCalledWith(
expect.stringContaining("\"bad\""));
@ -449,8 +450,8 @@ describe("fetchMinOsFeatureData()", () => {
mockGetRelease = Promise.resolve({ data: { a: "0", b: 0 } });
const dispatch = jest.fn();
const mockConsole = jest.spyOn(console, "log").mockImplementation(() => { });
await actions.fetchMinOsFeatureData("url")(dispatch);
expect(axios.get).toHaveBeenCalledWith("url");
await actions.fetchMinOsFeatureData()(dispatch);
expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(dispatch).not.toHaveBeenCalled();
expect(mockConsole).toHaveBeenCalledWith(
expect.stringContaining("{\"a\":\"0\",\"b\":0}"));
@ -459,8 +460,8 @@ describe("fetchMinOsFeatureData()", () => {
it("fails to fetch min OS feature data", async () => {
mockGetRelease = Promise.reject("error");
const dispatch = jest.fn();
await actions.fetchMinOsFeatureData("url")(dispatch);
await expect(axios.get).toHaveBeenCalledWith("url");
await actions.fetchMinOsFeatureData()(dispatch);
await expect(axios.get).toHaveBeenCalledWith(EXPECTED_URL);
expect(error).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledWith({
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()", () => {
it("updates config: FbosConfig", () => {
const state = fakeState();

View File

@ -12,15 +12,9 @@ import {
import { FarmbotOsSettings } from "../components/farmbot_os_settings";
import { fakeTimeSettings } from "../../__test_support__/fake_time_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/>", () => {
const fakeProps = (): Props => ({
userToApi: undefined,
userToMqtt: undefined,
botToMqtt: undefined,
auth: auth,
bot: bot,
deviceAccount: fakeDevice(),
@ -31,12 +25,10 @@ describe("<Devices/>", () => {
sourceFwConfig: jest.fn(),
shouldDisplay: jest.fn(),
firmwareConfig: undefined,
isValidFbosConfig: false,
env: {},
saveFarmwareEnv: jest.fn(),
timeSettings: fakeTimeSettings(),
alerts: [],
webAppConfig: fakeWebAppConfig()
});
it("renders relevant panels", () => {
@ -50,14 +42,6 @@ describe("<Devices/>", () => {
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", () => {
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: "arduino", consistent: true });
@ -65,17 +49,4 @@ describe("<Devices/>", () => {
expect(wrapper.find(HardwareSettings).props().firmwareHardware)
.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 { incomingStatus } from "../../connectivity/connect_device";
import { Vector3, uuid } from "farmbot";
import { values, omit } from "lodash";
import { now } from "../connectivity/qos";
const statusOf = (state: BotState) => {
@ -46,15 +45,28 @@ describe("botReducer", () => {
.toBe(!initialState().controlPanelState.danger_zone);
});
it("bulk toggles control panel options", () => {
it("bulk toggles firmware control panel options", () => {
const state = botReducer(initialState(), {
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
payload: true
payload: { open: true, all: false }
});
const bulkToggable =
omit(state.controlPanelState, "power_and_reset");
values(bulkToggable).map(value => {
Object.entries(state.controlPanelState).filter(([k, _]) => ![
"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();
});
});
@ -83,6 +95,14 @@ describe("botReducer", () => {
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", () => {
const n = () => Math.round(Math.random() * 1000);
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 { isObject, isString, get, noop } from "lodash";
import { t } from "../i18next_wrapper";
import { ExternalUrl } from "../external_urls";
const ON = 1, OFF = 0;
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.
* @param url location of data
*/
export const fetchMinOsFeatureData = (url: string) =>
export const fetchMinOsFeatureData = () =>
(dispatch: Function) => {
axios
.get<MinOsFeatureLookup>(url)
.get<MinOsFeatureLookup>(ExternalUrl.featureMinVersions)
.then(resp => {
const data = resp.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
* found on the Devices page.
@ -271,8 +292,11 @@ export function toggleControlPanel(payload: keyof ControlPanelState) {
}
/** Toggle visibility of all hardware control panel sections. */
export function bulkToggleControlPanel(payload: boolean) {
return { type: Actions.BULK_TOGGLE_CONTROL_PANEL, payload };
export function bulkToggleControlPanel(open: boolean, all = false) {
return {
type: Actions.BULK_TOGGLE_CONTROL_PANEL,
payload: { open, all },
};
}
/** 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", () => ({
BootSequenceSelector: () => <div />
}));
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import * as React from "react";
import { FarmbotOsSettings } from "../farmbot_os_settings";
import { FarmbotOsSettings, FarmBotSettings } from "../farmbot_os_settings";
import { mount, shallow } from "enzyme";
import { bot } from "../../../__test_support__/fake_state/bot";
import { fakeResource } from "../../../__test_support__/fake_resource";
import { FarmbotOsProps } from "../../interfaces";
import axios from "axios";
import {
FarmbotOsProps, FarmbotSettingsProps, ControlPanelState,
} from "../../interfaces";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { edit } from "../../../api/crud";
import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources";
import { formEvent } from "../../../__test_support__/fake_html_events";
import { Content } from "../../../constants";
import { fakeDevice } from "../../../__test_support__/resource_index_builder";
describe("<FarmbotOsSettings />", () => {
beforeEach(() => {
@ -31,104 +25,59 @@ describe("<FarmbotOsSettings />", () => {
});
const fakeProps = (): FarmbotOsProps => ({
deviceAccount: fakeResource("Device", {
id: 0,
name: "",
ota_hour: 3,
tz_offset_hrs: 0
}),
deviceAccount: fakeDevice(),
dispatch: jest.fn(),
bot,
alerts: [],
botToMqttLastSeen: 0,
botToMqttStatus: "up",
sourceFbosConfig: x =>
({ value: bot.hardware.configuration[x], consistent: true }),
shouldDisplay: jest.fn(() => true),
isValidFbosConfig: false,
env: {},
saveFarmwareEnv: jest.fn(),
timeSettings: fakeTimeSettings(),
webAppConfig: fakeWebAppConfig()
});
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("button").length).toBe(7);
["name", "time zone", "farmbot os", "camera", "firmware"]
expect(osSettings.find("button").length).toBe(6);
["name", "time zone", "farmbot os", "camera"]
.map(string => expect(osSettings.text().toLowerCase()).toContain(string));
});
it("fetches OS release notes", async () => {
mockReleaseNoteResponse = Promise.resolve({
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", () => {
it("renders expanded", () => {
mockDev = true;
const p = fakeProps();
const newName = "new bot name";
const osSettings = shallow(<FarmbotOsSettings {...p} />);
osSettings.find("input")
.simulate("change", { currentTarget: { value: newName } });
expect(edit).toHaveBeenCalledWith(p.deviceAccount, { name: newName });
Object.keys(p.bot.controlPanelState).map((panel: keyof ControlPanelState) => {
p.bot.controlPanelState[panel] = true;
});
const wrapper = mount(<FarmbotOsSettings {...p} />);
["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", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
const osSettings = shallow(<FarmbotOsSettings {...p} />);
const osSettings = shallow(<FarmBotSettings {...p} />);
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 { mount, shallow } from "enzyme";
import { HardwareSettings } from "../hardware_settings";
import { HardwareSettingsProps } from "../../interfaces";
import { HardwareSettingsProps, ControlPanelState } from "../../interfaces";
import { Actions } from "../../../constants";
import { bot } from "../../../__test_support__/fake_state/bot";
import { panelState } from "../../../__test_support__/control_panel_state";
@ -19,7 +26,6 @@ describe("<HardwareSettings />", () => {
const fakeProps = (): HardwareSettingsProps => ({
bot,
controlPanelState: panelState(),
botToMqttStatus: "up",
dispatch: jest.fn(),
sourceFwConfig: x =>
({ value: fakeFirmwareConfig().body[x], consistent: true }),
@ -35,12 +41,23 @@ describe("<HardwareSettings />", () => {
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(
buttonElement: string,
buttonIndex: number,
buttonText: string,
type: string,
payload: boolean | string) {
payload: { open: boolean, all: boolean } | string) {
const p = fakeProps();
const wrapper = mount(<HardwareSettings {...p} />);
clickButton(wrapper, buttonIndex, buttonText, {
@ -51,12 +68,12 @@ describe("<HardwareSettings />", () => {
it("expands all", () => {
checkDispatch("button", 0, "expand all",
Actions.BULK_TOGGLE_CONTROL_PANEL, true);
Actions.BULK_TOGGLE_CONTROL_PANEL, { open: true, all: false });
});
it("collapses 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", () => {

View File

@ -1,18 +1,26 @@
import * as React from "react";
import { LockableButton } from "../lockable_button";
import { mount } from "enzyme";
import { LockableButton, LockableButtonProps } from "../lockable_button";
import { shallow } from "enzyme";
describe("<LockableButton/>", () => {
it("does not trigger callback when clicked and disabled", () => {
const fakeCB = jest.fn();
const btn = mount(<LockableButton disabled={true} onClick={fakeCB} />);
btn.simulate("click");
expect(fakeCB).not.toHaveBeenCalled();
describe("<LockableButton />", () => {
const fakeProps = (): LockableButtonProps => ({
disabled: false,
onClick: jest.fn(),
});
it("does trigger callback when clicked and enabled", () => {
const fakeCB = jest.fn();
const btn = mount(<LockableButton disabled={false} onClick={fakeCB} />);
it("does not trigger callback when clicked and disabled", () => {
const p = fakeProps();
p.disabled = true;
const btn = shallow(<LockableButton {...p} />);
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";
describe("McuInputBox", () => {
const fakeProps = (): McuInputBoxProps => {
return {
sourceFwConfig: (x) => {
return { value: bot.hardware.mcu_params[x], consistent: true };
},
setting: "encoder_enabled_x",
dispatch: jest.fn()
};
};
const fakeProps = (): McuInputBoxProps => ({
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
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", () => {
const mib = new McuInputBox(fakeProps());

View File

@ -6,38 +6,24 @@ import { BooleanMCUInputGroupProps } from "./interfaces";
import { Position } from "@blueprintjs/core";
import { t } from "../../i18next_wrapper";
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 {
tooltip,
label,
x,
y,
z,
disable,
grayscale,
caution,
displayAlert,
sourceFwConfig,
dispatch,
} = props;
get newFormat() { return DevSettings.futureFeaturesEnabled(); }
const xParam = sourceFwConfig(x);
const yParam = sourceFwConfig(y);
const zParam = sourceFwConfig(z);
return <Row>
<Highlight settingName={label}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t(label)}
{caution &&
<i className="fa fa-exclamation-triangle caution-icon" />}
</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2} className={"centered-button-div"}>
Toggles = () => {
const {
sourceFwConfig, dispatch, disable, grayscale, displayAlert,
x, y, z,
} = this.props;
const xParam = sourceFwConfig(x);
const yParam = sourceFwConfig(y);
const zParam = sourceFwConfig(z);
const width = this.newFormat ? 4 : 2;
return <div className={"mcu-inputs"}>
<Col xs={width} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.x}
disabled={disable?.x}
@ -46,7 +32,7 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
toggleAction={() =>
dispatch(settingToggle(x, sourceFwConfig, displayAlert))} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<Col xs={width} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.y}
disabled={disable?.y}
@ -55,7 +41,7 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
toggleAction={() =>
dispatch(settingToggle(y, sourceFwConfig, displayAlert))} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<Col xs={width} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.z}
disabled={disable?.z}
@ -64,6 +50,24 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
toggleAction={() =>
dispatch(settingToggle(z, sourceFwConfig, displayAlert))} />
</Col>
</Highlight>
</Row>;
</div>;
}
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 axios from "axios";
import { t } from "../../i18next_wrapper";
import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces";
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 { FarmbotOsProps, Feature, FarmbotSettingsProps } from "../interfaces";
import { Widget, WidgetHeader, WidgetBody } from "../../ui";
import { isBotOnlineFromState } from "../must_be_online";
import { CameraSelection } from "./fbos_settings/camera_selection";
import { BoardType } from "./fbos_settings/board_type";
import { FarmbotOsRow } from "./fbos_settings/farmbot_os_row";
import { AutoUpdateRow } from "./fbos_settings/auto_update_row";
import { AutoSyncRow } from "./fbos_settings/auto_sync_row";
import { PowerAndReset } from "./fbos_settings/power_and_reset";
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 { 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 {
label = 3,
@ -25,142 +24,82 @@ export enum ColWidth {
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
extends React.Component<FarmbotOsProps, FarmbotOsState> {
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) : "";
}
extends React.Component<FarmbotOsProps, {}> {
render() {
const { bot, sourceFbosConfig, botToMqttStatus } = this.props;
const { sync_status } = bot.hardware.informational_settings;
const botOnline = isBotOnline(sync_status, botToMqttStatus);
const { bot, sourceFbosConfig } = this.props;
const botOnline = isBotOnlineFromState(bot);
return <Widget className="device-widget">
<form onSubmit={(e) => e.preventDefault()}>
<WidgetHeader title="Device">
</WidgetHeader>
<WidgetBody>
<Row>
<Highlight settingName={DeviceSetting.name}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.name)}
</label>
</Col>
<Col xs={9}>
<input name="name"
onChange={this.changeBot}
onBlur={this.updateBot}
value={this.props.deviceAccount.body.name} />
</Col>
</Highlight>
</Row>
<Row>
<Highlight settingName={DeviceSetting.timezone}>
<Col xs={ColWidth.label}>
<label>
{t("TIME ZONE")}
</label>
</Col>
<Col xs={ColWidth.description}>
<div className="note">
{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>
<WidgetHeader title="Device">
</WidgetHeader>
<WidgetBody>
<FarmBotSettings
bot={bot}
env={this.props.env}
alerts={this.props.alerts}
saveFarmwareEnv={this.props.saveFarmwareEnv}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline}
timeSettings={this.props.timeSettings}
device={this.props.deviceAccount} />
<Firmware
bot={this.props.bot}
alerts={this.props.alerts}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline}
timeSettings={this.props.timeSettings} />
<PowerAndReset
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />
</WidgetBody>
</Widget>;
}
}

View File

@ -33,19 +33,32 @@ describe("<BoardType/>", () => {
shouldDisplay: () => false,
botOnline: true,
timeSettings: fakeTimeSettings(),
firmwareHardware: undefined,
});
it("Disconnected with valid FirmwareConfig", () => {
it("renders with valid firmwareHardware", () => {
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: "farmduino", consistent: false });
p.firmwareHardware = "farmduino";
const wrapper = mount(<BoardType {...p} />);
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", () => {
const p = fakeProps();
const wrapper = shallow(<BoardType {...p} />);
wrapper.find("FBSelect").simulate("change",
const wrapper = mount<BoardType>(<BoardType {...p} />);
const selection =
shallow(<div>{wrapper.instance().FirmwareSelection()}</div>);
selection.find("FBSelect").simulate("change",
{ label: "firmware_hardware", value: "farmduino" });
expect(edit).toHaveBeenCalledWith(fakeConfig, {
firmware_hardware: "farmduino"
@ -53,17 +66,19 @@ describe("<BoardType/>", () => {
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
it("deosn't call updateConfig", () => {
it("doesn't call updateConfig", () => {
const p = fakeProps();
const wrapper = shallow(<BoardType {...p} />);
wrapper.find("FBSelect").simulate("change",
const wrapper = mount<BoardType>(<BoardType {...p} />);
const selection =
shallow(<div>{wrapper.instance().FirmwareSelection()}</div>);
selection.find("FBSelect").simulate("change",
{ label: "firmware_hardware", value: "unknown" });
expect(edit).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
});
it("displays standard boards", () => {
const wrapper = shallow(<BoardType {...fakeProps()} />);
const wrapper = mount(<BoardType {...fakeProps()} />);
const { list } = wrapper.find("FBSelect").props();
expect(list).toEqual([
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
@ -78,7 +93,7 @@ describe("<BoardType/>", () => {
it("displays new boards", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
const wrapper = shallow(<BoardType {...p} />);
const wrapper = mount(<BoardType {...p} />);
const { list } = wrapper.find("FBSelect").props();
expect(list).toEqual([
{ 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 {
sequence2ddi, mapStateToProps, RawBootSequenceSelector,
} from "../boot_sequence_selector";
@ -11,9 +18,6 @@ import {
import React from "react";
import { mount } from "enzyme";
import { FBSelect } from "../../../../ui";
// import { mount } from "enzyme";
// import React from "react";
// import { FBSelect } from "../../../../ui";
describe("sequence2ddi", () => {
it("converts TaggedSequences", () => {
@ -98,4 +102,10 @@ describe("RawBootSequenceSelector", () => {
const el = mount(<RawBootSequenceSelector {...props} />);
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 { FarmbotOsRow } from "../farmbot_os_row";
import { FarmbotOsRow, getOsReleaseNotesForVersion } from "../farmbot_os_row";
import { mount } from "enzyme";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { FarmbotOsRowProps } from "../interfaces";
@ -10,15 +10,12 @@ import { fakeDevice } from "../../../../__test_support__/resource_index_builder"
describe("<FarmbotOsRow/>", () => {
const fakeProps = (): FarmbotOsRowProps => ({
bot,
osReleaseNotesHeading: "",
osReleaseNotes: "",
dispatch: jest.fn(x => x(jest.fn(), fakeState)),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
},
shouldDisplay: () => false,
botOnline: false,
botToMqttLastSeen: 0,
deviceAccount: fakeDevice(),
timeSettings: fakeTimeSettings(),
});
@ -37,3 +34,12 @@ describe("<FarmbotOsRow/>", () => {
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@");
});
it("handles missing firmware version", () => {
it("handles missing data", () => {
const p = fakeProps();
p.botInfoSettings.firmware_version = undefined;
p.botInfoSettings.node_name = "";
p.botInfoSettings.commit = "";
const wrapper = mount(<FbosDetails {...p} />);
expect(wrapper.text()).toContain("---");
});

View File

@ -8,11 +8,8 @@ import {
FirmwareHardwareStatusDetailsProps, FirmwareHardwareStatusDetails,
FirmwareHardwareStatusIconProps, FirmwareHardwareStatusIcon,
FirmwareHardwareStatusProps, FirmwareHardwareStatus,
FirmwareActions, FirmwareActionsProps,
} from "../firmware_hardware_status";
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";
describe("<FirmwareHardwareStatusDetails />", () => {
@ -96,16 +93,3 @@ describe("<FirmwareHardwareStatus />", () => {
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() }));
import * as React from "react";
import { fakeResource } from "../../../../__test_support__/fake_resource";
import { LastSeen, LastSeenProps } from "../last_seen_row";
import { LastSeen, LastSeenProps, getLastSeenNumber } from "../last_seen_row";
import { mount } from "enzyme";
import { SpecialStatus, TaggedDevice } from "farmbot";
import { SpecialStatus } from "farmbot";
import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings";
import { refresh } from "../../../../api/crud";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { fakeDevice } from "../../../../__test_support__/resource_index_builder";
describe("<LastSeen />", () => {
const resource = (): TaggedDevice => fakeResource("Device", {
id: 1,
name: "foo",
last_saw_api: "",
tz_offset_hrs: 0,
ota_hour: 3
});
const props = (): LastSeenProps => ({
device: resource(),
const fakeProps = (): LastSeenProps => ({
device: fakeDevice(),
botToMqttLastSeen: 0,
dispatch: jest.fn(),
timeSettings: fakeTimeSettings(),
});
it("blinks when loading", () => {
const p = props();
const p = fakeProps();
p.device.specialStatus = SpecialStatus.SAVING;
const wrapper = mount(<LastSeen {...p} />);
expect(wrapper.text()).toContain("Loading");
});
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");
});
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.botToMqttLastSeen = 0;
const wrapper = mount<LastSeen>(<LastSeen {...p} />);
@ -45,7 +38,7 @@ describe("<LastSeen />", () => {
});
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.botToMqttLastSeen = new Date("2016-08-07T19:40:01.487Z").getTime();
const wrapper = mount<LastSeen>(<LastSeen {...p} />);
@ -53,7 +46,7 @@ describe("<LastSeen />", () => {
});
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.botToMqttLastSeen = new Date("2017-08-07T20:40:01.487Z").getTime();
const wrapper = mount<LastSeen>(<LastSeen {...p} />);
@ -62,9 +55,22 @@ describe("<LastSeen />", () => {
});
it("handles a click", () => {
const p = props();
const p = fakeProps();
const wrapper = mount(<LastSeen {...p} />);
wrapper.find("i").simulate("click");
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 { OtaTimeSelector, changeOtaHour, assertIsHour } from "../ota_time_selector";
import { shallow } from "enzyme";
import {
OtaTimeSelector, changeOtaHour, assertIsHour, OtaTimeSelectorRow,
OtaTimeSelectorProps,
ASAP,
} from "../ota_time_selector";
import { shallow, mount } from "enzyme";
import { FBSelect } from "../../../../ui";
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", () => {
expect(assertIsHour(undefined)).toBe(undefined);
// tslint:disable-next-line:no-null-keyword
@ -14,29 +20,42 @@ describe("OTA time selector", () => {
expect(crashOn(-2)).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", () => {
const onUpdate = jest.fn();
const el = shallow(<OtaTimeSelector
timeFormat={"12h"}
disabled={false}
onChange={onUpdate}
value={3} />);
const p = fakeProps();
const el = shallow(<OtaTimeSelector {...p} />);
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", () => {
const onUpdate = jest.fn();
const el = shallow(<OtaTimeSelector
timeFormat={"12h"}
disabled={false}
onChange={onUpdate}
value={3} />);
const p = fakeProps();
const el = shallow(<OtaTimeSelector {...p} />);
el.find(FBSelect).simulate("change", { label: "no", value: -1 });
// 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", () => {
const device = fakeDevice();
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(),
}));
let mockDev = false;
jest.mock("../../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import * as React from "react";
import { PowerAndReset } from "../power_and_reset";
import { mount } from "enzyme";
@ -28,10 +21,6 @@ import {
import { edit, save } from "../../../../api/crud";
describe("<PowerAndReset/>", () => {
beforeEach(() => {
mockDev = false;
});
const fakeConfig = fakeFbosConfig();
const state = fakeState();
state.resources = buildResourceIndex([fakeConfig]);
@ -52,17 +41,6 @@ describe("<PowerAndReset/>", () => {
"Connection Attempt Period", "Change Ownership"]
.map(string => expect(wrapper.text().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", () => {
@ -90,21 +68,11 @@ describe("<PowerAndReset/>", () => {
p.sourceFbosConfig = () => ({ value: false, consistent: true });
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
clickButton(wrapper, 4, "yes");
clickButton(wrapper, 3, "yes");
expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true });
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", () => {
const p = fakeProps();
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 { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function AutoSyncRow(props: AutoSyncRowProps) {
const autoSync = props.sourceFbosConfig("auto_sync");
return <Row>
<Highlight settingName={DeviceSetting.autoSync}>
<Col xs={ColWidth.label}>
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.autoSync}>
<Row>
<Col xs={newFormat ? 9 : ColWidth.label}>
<label>
{t("AUTO SYNC")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.AUTO_SYNC)}
</p>
</Col>
<Col xs={ColWidth.button}>
{!newFormat &&
<Col xs={ColWidth.description}>
<p>
{t(Content.AUTO_SYNC)}
</p>
</Col>}
<Col xs={newFormat ? 3 : ColWidth.button}>
<ToggleButton
toggleValue={autoSync.value}
dim={!autoSync.consistent}
@ -30,6 +33,7 @@ export function AutoSyncRow(props: AutoSyncRowProps) {
props.dispatch(updateConfig({ auto_sync: !autoSync.value }));
}} />
</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 { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function AutoUpdateRow(props: AutoUpdateRowProps) {
const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
return <Row>
<Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<Col xs={ColWidth.label}>
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<Row>
<Col xs={newFormat ? 9 : ColWidth.label}>
<label>
{t(DeviceSetting.farmbotOSAutoUpdate)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.OS_AUTO_UPDATE)}
</p>
</Col>
<Col xs={ColWidth.button}>
{!newFormat &&
<Col xs={ColWidth.description}>
<p>
{t(Content.OS_AUTO_UPDATE)}
</p>
</Col>}
<Col xs={newFormat ? 3 : ColWidth.button}>
<ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent}
toggleAction={() => props.dispatch(updateConfig({
os_auto_update: !osAutoUpdate.value
}))} />
</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 { Row, Col, DropDownItem, FBSelect } from "../../../ui";
import { info } from "../../../toast/toast";
import { FirmwareHardware } from "farmbot";
import { ColWidth } from "../farmbot_os_settings";
import { updateConfig } from "../../actions";
import { BoardTypeProps } from "./interfaces";
@ -12,6 +11,7 @@ import {
} from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
interface BoardTypeState { sending: boolean }
@ -28,13 +28,10 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
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 {
return this.apiValue ? FIRMWARE_CHOICES_DDI[this.apiValue] : undefined;
return this.props.firmwareHardware
? FIRMWARE_CHOICES_DDI[this.props.firmwareHardware]
: undefined;
}
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() {
return <Row>
<Highlight settingName={DeviceSetting.firmware}>
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.firmware}>
<Row>
<Col xs={ColWidth.label}>
<label>
{t("FIRMWARE")}
</label>
</Col>
<Col xs={ColWidth.description}>
<FBSelect
key={this.apiValue}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</Col>
{!newFormat &&
<Col xs={ColWidth.description}>
<this.FirmwareSelection />
</Col>}
<Col xs={ColWidth.button}>
<FirmwareHardwareStatus
botOnline={this.props.botOnline}
apiFirmwareValue={this.apiValue}
apiFirmwareValue={this.props.firmwareHardware}
alerts={this.props.alerts}
bot={this.props.bot}
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings} />
</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 { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
interface Props {
list: DropDownItem[];
@ -56,23 +57,34 @@ export class RawBootSequenceSelector extends React.Component<Props, {}> {
this.props.dispatch(save(this.props.config.uuid));
}
SelectionInput = () =>
<FBSelect
allowEmpty={true}
list={this.props.list}
selectedItem={this.props.selectedItem}
onChange={this.onChange} />
render() {
return <Row>
<Highlight settingName={DeviceSetting.bootSequence}>
<Col xs={ColWidth.label}>
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.bootSequence}>
<Row>
<Col xs={newFormat ? 12 : ColWidth.label}>
<label>
{t("BOOT SEQUENCE")}
</label>
</Col>
<Col xs={7}>
<FBSelect
allowEmpty={true}
list={this.props.list}
selectedItem={this.props.selectedItem}
onChange={this.onChange} />
</Col>
</Highlight>
</Row>;
{!newFormat &&
<Col xs={ColWidth.description}>
<this.SelectionInput />
</Col>}
</Row>
{newFormat &&
<Row>
<Col xs={12} className="no-pad">
<this.SelectionInput />
</Col>
</Row>}
</Highlight>;
}
}

View File

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

View File

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

View File

@ -9,6 +9,23 @@ import { t } from "../../../i18next_wrapper";
import { ErrorBoundary } from "../../../error_boundary";
import { Highlight } from "../maybe_highlight";
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 =
(fbosVersion: string | undefined, onBeta: boolean | undefined): string => {
@ -17,56 +34,72 @@ const getVersionString =
return fbosVersion ? fbosVersion + extension : t(" unknown (offline)");
};
export function FarmbotOsRow(props: FarmbotOsRowProps) {
const { sourceFbosConfig, dispatch, bot, osReleaseNotes, botOnline } = props;
const { controller_version, currently_on_beta
} = bot.hardware.informational_settings;
const version = getVersionString(controller_version, currently_on_beta);
return <Row>
<Highlight settingName={DeviceSetting.farmbotOS}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.farmbotOS)}
</label>
</Col>
<Col xs={3}>
<Popover position={Position.BOTTOM_LEFT}>
<p>
{t("Version {{ version }}", { version })}
</p>
<ErrorBoundary>
<FbosDetails
botInfoSettings={bot.hardware.informational_settings}
dispatch={dispatch}
shouldDisplay={props.shouldDisplay}
sourceFbosConfig={sourceFbosConfig}
botToMqttLastSeen={props.botToMqttLastSeen}
timeSettings={props.timeSettings}
deviceAccount={props.deviceAccount} />
</ErrorBoundary>
</Popover>
</Col>
<Col xs={3}>
<Popover position={Position.BOTTOM}>
<p className="release-notes-button">
{t("Release Notes")}&nbsp;
export class FarmbotOsRow extends React.Component<FarmbotOsRowProps> {
Version = () => {
const { controller_version, currently_on_beta } =
this.props.bot.hardware.informational_settings;
const version = getVersionString(controller_version, currently_on_beta);
return <Popover position={Position.BOTTOM_LEFT}>
<p>
{t("Version {{ version }}", { version })}
</p>
<ErrorBoundary>
<FbosDetails
botInfoSettings={this.props.bot.hardware.informational_settings}
dispatch={this.props.dispatch}
shouldDisplay={this.props.shouldDisplay}
sourceFbosConfig={this.props.sourceFbosConfig}
botToMqttLastSeen={getLastSeenNumber(this.props.bot)}
timeSettings={this.props.timeSettings}
deviceAccount={this.props.deviceAccount} />
</ErrorBoundary>
</Popover>;
}
ReleaseNotes = () => {
const { osReleaseNotes, hardware } = this.props.bot;
const { controller_version } = hardware.informational_settings;
const releaseNotes =
getOsReleaseNotesForVersion(osReleaseNotes, controller_version);
return <Popover position={Position.BOTTOM} className="release-notes-wrapper">
<p className="release-notes-button">
{t("Release Notes")}&nbsp;
<i className="fa fa-caret-down" />
</p>
<div className="release-notes">
<h1>{props.osReleaseNotesHeading}</h1>
<Markdown>
{osReleaseNotes}
</Markdown>
</div>
</Popover>
</Col>
<Col xs={3}>
<OsUpdateButton
bot={bot}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={props.shouldDisplay}
botOnline={botOnline} />
</Col>
</Highlight>
</Row>;
</p>
<div className="release-notes">
<h1>{releaseNotes.heading}</h1>
<Markdown>
{releaseNotes.notes}
</Markdown>
</div>
</Popover>;
}
render() {
const { sourceFbosConfig, bot, botOnline } = this.props;
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={DeviceSetting.farmbotOS}>
<Row>
<Col xs={newFormat ? 5 : ColWidth.label}>
<label>
{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 { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { DevSettings } from "../../../account/dev/dev_support";
export interface FbosButtonRowProps {
botOnline: boolean;
@ -15,19 +16,21 @@ export interface FbosButtonRowProps {
}
export const FbosButtonRow = (props: FbosButtonRowProps) => {
return <Row>
<Highlight settingName={props.label}>
<Col xs={ColWidth.label}>
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight settingName={props.label}>
<Row>
<Col xs={newFormat ? 7 : ColWidth.label}>
<label>
{t(props.label)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(props.description)}
</p>
</Col>
<Col xs={ColWidth.button}>
{!newFormat &&
<Col xs={ColWidth.description}>
<p>
{t(props.description)}
</p>
</Col>}
<Col xs={newFormat ? 5 : ColWidth.button}>
<button
className={`fb-button ${props.color}`}
type="button"
@ -37,6 +40,7 @@ export const FbosButtonRow = (props: FbosButtonRowProps) => {
{t(props.buttonText)}
</button>
</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 { FirmwareAlerts } from "../../../messages/alerts";
import { TimeSettings } from "../../../interfaces";
import { trim } from "../../../util";
import { Alert } from "farmbot";
import { isFwHardwareValue, boardType } from "../firmware_hardware_support";
@ -56,22 +55,6 @@ export const FlashFirmwareBtn = (props: FlashFirmwareBtnProps) => {
</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 =
(props: FirmwareHardwareStatusDetailsProps) => {
return <div className="firmware-hardware-status-details">
@ -81,10 +64,6 @@ export const FirmwareHardwareStatusDetails =
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
<label>{t("Arduino/Farmduino")}</label>
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
<label>{t("Actions")}</label>
<FirmwareActions
apiFirmwareValue={props.apiFirmwareValue}
botOnline={props.botOnline} />
<FirmwareAlerts
alerts={props.alerts}
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,
InformationalSettings,
TaggedDevice,
FirmwareHardware,
} from "farmbot";
import { TimeSettings } from "../../../interfaces";
export interface NameRowProps {
dispatch: Function;
device: TaggedDevice;
widget?: boolean;
}
export interface TimezoneRowProps {
dispatch: Function;
device: TaggedDevice;
}
export interface AutoSyncRowProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
@ -50,6 +62,22 @@ export interface BoardTypeProps {
shouldDisplay: ShouldDisplay;
timeSettings: TimeSettings;
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 {
@ -67,13 +95,10 @@ export interface FactoryResetRowsProps {
export interface FarmbotOsRowProps {
bot: BotState;
osReleaseNotesHeading: string;
osReleaseNotes: string;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay;
botOnline: boolean;
botToMqttLastSeen: number;
timeSettings: TimeSettings;
deviceAccount: TaggedDevice;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,33 +6,44 @@ import { CalibrationRowProps } from "../interfaces";
import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core";
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>
<Highlight settingName={props.title}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t(props.title)}
</label>
<Help text={t(props.toolTip)}
requireClick={true} position={Position.RIGHT} />
</Col>
Axes = () => {
const { type, botOnline, axisTitle, hardware, action } = this.props;
return <div className="calibration-row-axes">
{axisTrackingStatus(hardware)
.map(row => {
const { axis } = row;
const hardwareDisabled = props.type == "zero" ? false : row.disabled;
return <Col xs={2} key={axis} className={"centered-button-div"}>
const hardwareDisabled = type == "zero" ? false : row.disabled;
return <Col xs={this.newFormat ? 4 : 2} key={axis}
className={"centered-button-div"}>
<LockableButton
disabled={hardwareDisabled || botDisconnected}
title={t(props.axisTitle)}
onClick={() => props.action(axis)}>
{`${t(props.axisTitle)} ${axis}`}
disabled={hardwareDisabled || !botOnline}
title={t(axisTitle)}
onClick={() => action(axis)}>
{`${t(axisTitle)} ${axis}`}
</LockableButton>
</Col>;
})}
</Highlight>
</Row>;
</div>;
}
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 { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DevSettings } from "../../../account/dev/dev_support";
export function DangerZone(props: DangerZoneProps) {
const { dispatch, onReset, botDisconnected } = props;
const { dispatch, onReset, botOnline } = props;
const { danger_zone } = props.controlPanelState;
const newFormat = DevSettings.futureFeaturesEnabled();
return <Highlight className={"section"}
settingName={DeviceSetting.dangerZone}>
<Header
@ -20,29 +21,32 @@ export function DangerZone(props: DangerZoneProps) {
panel={"danger_zone"}
dispatch={dispatch} />
<Collapse isOpen={!!danger_zone}>
<Row>
<Highlight settingName={DeviceSetting.resetHardwareParams}>
<Col xs={4}>
<Highlight settingName={DeviceSetting.resetHardwareParams}>
<Row>
<Col xs={newFormat ? 8 : 4}>
<label>
{t(DeviceSetting.resetHardwareParams)}
</label>
</Col>
<Col xs={6}>
<p>
{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}
</p>
</Col>
<Col xs={2} className={"centered-button-div"}>
{!newFormat &&
<Col xs={6}>
<p>
{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}
</p>
</Col>}
<Col xs={newFormat ? 4 : 2} className={"centered-button-div"}>
<button
className="fb-button red"
disabled={botDisconnected}
disabled={!botOnline}
title={t("RESET")}
onClick={onReset}>
{t("RESET")}
</button>
</Col>
</Highlight>
</Row>
</Row>
{newFormat &&
<Row><p>{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}</p></Row>}
</Highlight>
</Collapse>
</Highlight>;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,14 @@
import * as React from "react";
interface Props {
export interface LockableButtonProps {
onClick: Function;
disabled: boolean;
children?: React.ReactNode;
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";
return <button
className={"fb-button " + className}

View File

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

View File

@ -5,50 +5,65 @@ import { Row, Col, Help } from "../../ui/index";
import { Position } from "@blueprintjs/core";
import { Highlight } from "./maybe_highlight";
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 {
sourceFwConfig, dispatch, tooltip, label, x, y, z, intSize, gray, float,
} = props;
return <Row>
<Highlight settingName={label}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t(label)}
</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2}>
get newFormat() { return DevSettings.futureFeaturesEnabled(); }
Inputs = () => {
const {
sourceFwConfig, dispatch, intSize, gray, float,
x, y, z, xScale, yScale, zScale,
} = this.props;
return <div className={"mcu-inputs"}>
<Col xs={this.newFormat ? 4 : 2}>
<McuInputBox
setting={x}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.xScale}
scale={xScale}
gray={gray?.x} />
</Col>
<Col xs={2}>
<Col xs={this.newFormat ? 4 : 2}>
<McuInputBox
setting={y}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.yScale}
scale={yScale}
gray={gray?.y} />
</Col>
<Col xs={2}>
<Col xs={this.newFormat ? 4 : 2}>
<McuInputBox
setting={z}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.zScale}
scale={zScale}
gray={gray?.z} />
</Col>
</Highlight>
</Row>;
</div>;
}
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 { McuInputBox } from "./mcu_input_box";
import { PinGuardMCUInputGroupProps } from "./interfaces";
import { Row, Col } from "../../ui/index";
import { Row, Col, Help } from "../../ui/index";
import { settingToggle } from "../actions";
import { ToggleButton } from "../../controls/toggle_button";
import { isUndefined } from "lodash";
import { t } from "../../i18next_wrapper";
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
} = props;
const activeStateValue = sourceFwConfig(activeStateKey).value;
const inactiveState = isUndefined(activeStateValue)
? undefined
: !activeStateValue;
return <Row>
<Col xs={3}>
<label>
{label}
</label>
</Col>
<Col xs={3}>
<PinNumberDropdown
pinNumKey={pinNumKey}
dispatch={dispatch}
resources={props.resources}
sourceFwConfig={sourceFwConfig} />
</Col>
<Col xs={4}>
<McuInputBox
setting={timeoutKey}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
filter={32000} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton
customText={{ textFalse: t("low"), textTrue: t("high") }}
toggleValue={inactiveState}
dim={!sourceFwConfig(activeStateKey).consistent}
toggleAction={() =>
dispatch(settingToggle(activeStateKey, sourceFwConfig))} />
</Col>
</Row>;
get newFormat() { return DevSettings.futureFeaturesEnabled(); }
Number = () =>
<PinNumberDropdown
pinNumKey={this.props.pinNumKey}
dispatch={this.props.dispatch}
resources={this.props.resources}
sourceFwConfig={this.props.sourceFwConfig} />
Timeout = () =>
<McuInputBox
setting={this.props.timeoutKey}
sourceFwConfig={this.props.sourceFwConfig}
dispatch={this.props.dispatch}
filter={32000} />
State = () => {
const { sourceFwConfig, dispatch, activeStateKey } = this.props;
const activeStateValue = sourceFwConfig(activeStateKey).value;
const inactiveState = isUndefined(activeStateValue)
? undefined
: !activeStateValue;
return <ToggleButton
customText={{ textFalse: t("low"), textTrue: t("high") }}
toggleValue={inactiveState}
dim={!sourceFwConfig(activeStateKey).consistent}
toggleAction={() =>
dispatch(settingToggle(activeStateKey, sourceFwConfig))} />;
}
render() {
const { label } = this.props;
return !this.newFormat
? <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 { mapStateToProps } from "./state_to_props";
import { Props } from "./interfaces";
import { getStatus } from "../connectivity/reducer_support";
import { isFwHardwareValue } from "./components/firmware_hardware_support";
import { maybeOpenPanel } from "./components/maybe_highlight";
export class RawDevices extends React.Component<Props, {}> {
componentDidMount = () =>
this.props.dispatch(maybeOpenPanel(this.props.bot.controlPanelState));
render() {
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 firmwareHardware = isFwHardwareValue(value) ? value : undefined;
return <Page className="device-page">
@ -27,14 +26,10 @@ export class RawDevices extends React.Component<Props, {}> {
alerts={this.props.alerts}
bot={this.props.bot}
timeSettings={this.props.timeSettings}
botToMqttLastSeen={new Date(botToMqttLastSeen).getTime()}
botToMqttStatus={botToMqttStatus}
sourceFbosConfig={this.props.sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
isValidFbosConfig={this.props.isValidFbosConfig}
env={this.props.env}
saveFarmwareEnv={this.props.saveFarmwareEnv}
webAppConfig={this.props.webAppConfig} />
saveFarmwareEnv={this.props.saveFarmwareEnv} />
</Col>
<Col xs={12} sm={6}>
<HardwareSettings
@ -42,7 +37,6 @@ export class RawDevices extends React.Component<Props, {}> {
dispatch={this.props.dispatch}
resources={this.props.resources}
bot={this.props.bot}
botToMqttStatus={botToMqttStatus}
shouldDisplay={this.props.shouldDisplay}
firmwareHardware={firmwareHardware}
sourceFwConfig={this.props.sourceFwConfig}

View File

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

View File

@ -3,6 +3,8 @@ import { NetworkState } from "../connectivity/interfaces";
import { SyncStatus } from "farmbot";
import { Content } from "../constants";
import { t } from "../i18next_wrapper";
import { BotState } from "./interfaces";
import { getStatus } from "../connectivity/reducer_support";
/** Properties for the <MustBeOnline/> element. */
export interface MBOProps {
@ -23,6 +25,12 @@ export function isBotOnline(
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) {
const { children, hideBanner, lockOpen, networkState, syncStatus } = props;
const banner = hideBanner ? "" : "banner";

View File

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

View File

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

View File

@ -11,6 +11,10 @@ import { PinBindingColWidth } from "./pin_bindings";
import { PinBindingsListProps } from "./interfaces";
import { sysBtnBindings } from "./tagged_pin_binding_init";
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) => {
const { pinBindings, resources, dispatch } = props;
@ -26,22 +30,33 @@ export const PinBindingsList = (props: PinBindingsListProps) => {
const delBtnColor = (pin: number) =>
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"}>
{newFormat && <Row><label>{t("saved pin bindings")}</label></Row>}
{pinBindings
.sort((a, b) => sortByNameAndPin(a.pin_number, b.pin_number))
.map(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`}>
<Col xs={PinBindingColWidth.pin}>
{generatePinLabel(pin_number)}
<Col xs={newFormat ? 11 : PinBindingColWidth.pin}>
<p>{generatePinLabel(pin_number)}</p>
<p className="binding-action">{newFormat && binding}</p>
</Col>
<Col xs={PinBindingColWidth.type}>
{t(bindingTypeLabelLookup[binding_type || ""])}:&nbsp;
{sequence_id
? findSequenceById(resources, sequence_id).body.name
: t(getSpecialActionLabel(special_action))}
</Col>
<Col xs={PinBindingColWidth.button}>
{!newFormat &&
<Col xs={PinBindingColWidth.type}>
{binding}
</Col>}
<Col xs={newFormat ? 1 : PinBindingColWidth.button}>
<button
className={`fb-button ${delBtnColor(pin_number)} del-button`}
title={t("Delete")}

View File

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

View File

@ -12,9 +12,7 @@ import { validFwConfig, validFbosConfig } from "../util";
import {
saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn,
} from "../farmware/state_to_props";
import {
getFbosConfig, getFirmwareConfig, getWebAppConfig,
} from "../resources/getters";
import { getFbosConfig, getFirmwareConfig } from "../resources/getters";
import { getAllAlerts } from "../messages/state_to_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 shouldDisplay = getShouldDisplayFn(props.resources.index, 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 {
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),
auth: props.auth,
bot: props.bot,
@ -41,11 +32,9 @@ export function mapStateToProps(props: Everything): Props {
sourceFwConfig: sourceFwConfigValue(firmwareConfig, hardware.mcu_params),
shouldDisplay,
firmwareConfig,
isValidFbosConfig: !!fbosConfig,
env,
saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index),
timeSettings: maybeGetTimeSettings(props.resources.index),
alerts: getAllAlerts(props.resources),
webAppConfig
};
}

View File

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

View File

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

View File

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

View File

@ -8,18 +8,25 @@ jest.mock("../../../point_groups/criteria", () => ({
editGtLtCriteria: jest.fn(),
}));
jest.mock("../../../../api/crud", () => ({
overwrite: jest.fn(),
save: jest.fn(),
}));
import {
fakePlant, fakePointGroup,
} from "../../../../__test_support__/fake_state/resources";
import {
getSelected, resizeBox, startNewSelectionBox, ResizeSelectionBoxProps,
StartNewSelectionBoxProps,
maybeUpdateGroupCriteria,
MaybeUpdateGroupCriteriaProps,
maybeUpdateGroup,
MaybeUpdateGroupProps,
} from "../selection_box_actions";
import { Actions } from "../../../../constants";
import { history } from "../../../../history";
import { editGtLtCriteria } from "../../../point_groups/criteria";
import { overwrite, save } from "../../../../api/crud";
import { cloneDeep } from "lodash";
describe("getSelected", () => {
it("returns some", () => {
@ -156,24 +163,55 @@ describe("startNewSelectionBox", () => {
});
});
describe("maybeUpdateGroupCriteria()", () => {
const fakeProps = (): MaybeUpdateGroupCriteriaProps => ({
describe("maybeUpdateGroup()", () => {
const fakeProps = (): MaybeUpdateGroupProps => ({
selectionBox: { x0: 0, y0: 0, x1: undefined, y1: undefined },
dispatch: jest.fn(),
group: fakePointGroup(),
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", () => {
const p = fakeProps();
maybeUpdateGroupCriteria(p);
p.editGroupAreaInMap = true;
maybeUpdateGroup(p);
expect(editGtLtCriteria).toHaveBeenCalledWith(p.group, p.selectionBox);
});
it("doesn't update criteria", () => {
const p = fakeProps();
p.shouldDisplay = () => false;
maybeUpdateGroupCriteria(p);
maybeUpdateGroup(p);
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 { SelectionBoxData } from "./selection_box";
import { GardenMapState } from "../../interfaces";
@ -8,6 +8,9 @@ import { getMode } from "../util";
import { editGtLtCriteria } from "../../point_groups/criteria";
import { TaggedPointGroup } from "farmbot";
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. */
export const getSelected = (
@ -85,17 +88,32 @@ export const startNewSelectionBox = (props: StartNewSelectionBoxProps) => {
}
};
export interface MaybeUpdateGroupCriteriaProps {
export interface MaybeUpdateGroupProps {
selectionBox: SelectionBoxData | undefined;
dispatch: Function;
group: TaggedPointGroup | undefined;
shouldDisplay: ShouldDisplay;
editGroupAreaInMap: boolean;
boxSelected: UUID[] | undefined;
}
export const maybeUpdateGroupCriteria =
(props: MaybeUpdateGroupCriteriaProps) => {
if (props.selectionBox && props.group &&
props.shouldDisplay(Feature.criteria_groups)) {
props.dispatch(editGtLtCriteria(props.group, props.selectionBox));
export const maybeUpdateGroup =
(props: MaybeUpdateGroupProps) => {
if (props.selectionBox && props.group) {
if (props.editGroupAreaInMap
&& 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 {
Grid, MapBackground,
TargetCoordinate,
SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroupCriteria,
SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroup,
} from "./background";
import {
PlantLayer,
@ -88,11 +88,13 @@ export class GardenMap extends
isDragging: this.state.isDragging,
dispatch: this.props.dispatch,
});
maybeUpdateGroupCriteria({
maybeUpdateGroup({
selectionBox: this.state.selectionBox,
group: this.group,
dispatch: this.props.dispatch,
shouldDisplay: this.props.shouldDisplay,
editGroupAreaInMap: this.props.designer.editGroupAreaInMap,
boxSelected: this.props.designer.selectedPlants,
});
this.setState({
isDragging: false, qPageX: 0, qPageY: 0,
@ -142,7 +144,7 @@ export class GardenMap extends
gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState,
dispatch: this.props.dispatch,
plantActions: false,
plantActions: !this.props.designer.editGroupAreaInMap,
});
break;
case Mode.createPoint:
@ -179,7 +181,7 @@ export class GardenMap extends
gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState,
dispatch: this.props.dispatch,
plantActions: false,
plantActions: !this.props.designer.editGroupAreaInMap,
});
break;
default:
@ -283,7 +285,7 @@ export class GardenMap extends
gardenCoords: this.getGardenCoordinates(e),
setMapState: this.setMapState,
dispatch: this.props.dispatch,
plantActions: false,
plantActions: !this.props.designer.editGroupAreaInMap,
});
break;
case Mode.boxSelect:

View File

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

View File

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

View File

@ -341,13 +341,12 @@ const SeedTrough = (props: ToolGraphicProps) => {
export interface ToolSlotSVGProps {
toolSlot: TaggedToolSlotPointer;
toolName: string | undefined;
renderRotation: boolean;
xySwap?: boolean;
quadrant?: BotOriginQuadrant;
}
export const ToolSlotSVG = (props: ToolSlotSVGProps) => {
const xySwap = props.renderRotation ? !!props.xySwap : false;
const xySwap = !!props.xySwap;
const toolProps = {
x: 0, y: 0,
hovered: false,
@ -355,13 +354,11 @@ export const ToolSlotSVG = (props: ToolSlotSVGProps) => {
uuid: props.toolSlot.uuid,
xySwap,
};
const pulloutDirection = props.renderRotation
? props.toolSlot.body.pullout_direction
: ToolPulloutDirection.POSITIVE_X;
const quadrant = props.renderRotation && props.quadrant ? props.quadrant : 2;
const viewBox = props.renderRotation ? "-25 0 50 1" : "-25 0 50 1";
const pulloutDirection = props.toolSlot.body.pullout_direction
|| ToolPulloutDirection.POSITIVE_X;
const quadrant = props.quadrant || 2;
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} />
{props.toolSlot.body.tool_id &&
<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`}>
{props.toolSlot.body.pullout_direction &&
<ToolbaySlot
id={props.toolSlot.body.id}
id={-(props.toolSlot.body.id || 1)}
x={0}
y={0}
pulloutDirection={pulloutDirection}

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import {
import {
fakeMapTransformProps,
} 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 => ({
group: fakePointGroup(),
@ -25,7 +25,7 @@ describe("<Zones0D />", () => {
it("renders none: no data", () => {
const p = fakeProps();
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} />);
expect(wrapper.find("#zones-0D-1").length).toEqual(1);
expect(wrapper.find("circle").length).toEqual(0);
@ -63,7 +63,7 @@ describe("<Zones1D />", () => {
it("renders none: no data", () => {
const p = fakeProps();
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} />);
expect(wrapper.find("#zones-1D-1").length).toEqual(1);
expect(wrapper.find("line").length).toEqual(0);
@ -110,7 +110,7 @@ describe("<Zones2D />", () => {
it("renders none", () => {
const p = fakeProps();
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} />);
expect(wrapper.find("#zones-2D-1").length).toEqual(1);
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 const getZoneType = (group: TaggedPointGroup): ZoneType => {
const numEq = group.body.criteria?.number_eq || {};
const numGt = group.body.criteria?.number_gt || {};
const numLt = group.body.criteria?.number_lt || {};
const numEq = group.body.criteria.number_eq;
const numGt = group.body.criteria.number_gt;
const numLt = group.body.criteria.number_lt;
const hasXEq = !!numEq.x?.length;
const hasYEq = !!numEq.y?.length;
if (hasXEq && hasYEq) {
@ -46,8 +46,8 @@ export const getZoneType = (group: TaggedPointGroup): ZoneType => {
/** Bounds for area selected by criteria or bot extents. */
const getBoundary = (props: GetBoundaryProps): Boundary => {
const { criteria } = props.group.body;
const gt = criteria?.number_gt || {};
const lt = criteria?.number_lt || {};
const gt = criteria.number_gt;
const lt = criteria.number_lt;
const x1 = gt.x || 0;
const x2 = lt.x || props.botSize.x.value;
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. */
const getPoints =
(boundary: Boundary, group: TaggedPointGroup): Point[] => {
const xs = group.body.criteria?.number_eq.x;
const ys = group.body.criteria?.number_eq.y;
const xs = group.body.criteria.number_eq.x;
const ys = group.body.criteria.number_eq.y;
const points: Point[] = [];
xs?.map(x => ys?.map(y => points.push({ x, y })));
return filter<Point>(boundary, points);
@ -95,12 +95,12 @@ export const Zones0D = (props: ZonesProps) => {
/** Lines selected by an x or y number equal value. */
const getLines =
(boundary: Boundary, group: TaggedPointGroup): Line[] => {
const xs = group.body.criteria?.number_eq.x;
const ys = group.body.criteria?.number_eq.y;
const xs = group.body.criteria.number_eq.x;
const ys = group.body.criteria.number_eq.y;
const onlyXs = !!xs?.length && !ys?.length;
const onlyYs = !!ys?.length && !xs?.length;
const xLineData = onlyXs ? xs?.map(x => ({ x })) : undefined;
const yLineData = onlyYs ? ys?.map(y => ({ y })) : undefined;
const xLineData = (onlyXs && xs) ? xs.map(x => ({ x })) : undefined;
const yLineData = (onlyYs && ys) ? ys.map(y => ({ y })) : undefined;
return filter<Line>(boundary, xLineData || yLineData);
};

View File

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

View File

@ -15,19 +15,16 @@ import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader,
} from "./designer_panel";
import { t } from "../i18next_wrapper";
import { isBotOnline } from "../devices/must_be_online";
import { getStatus } from "../connectivity/reducer_support";
import { isBotOnlineFromState } from "../devices/must_be_online";
import { PanelColor } from "./panel_header";
export function mapStateToProps(props: Everything): MoveToProps {
const botToMqttStatus = getStatus(props.bot.connectivity.uptime["bot.mqtt"]);
const { sync_status } = props.bot.hardware.informational_settings;
return {
chosenLocation: props.resources.consumers.farm_designer.chosenLocation,
currentBotLocation:
validBotLocationData(props.bot.hardware.location_data).position,
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.common_names = [];
const wrapper = mount(<CropInfo {...p} />);
expect(wrapper.text().toLowerCase()).toContain("iconnot set");
expect(wrapper.text().toLowerCase()).toContain("spacingnot set");
expect(wrapper.text().toLowerCase()).toContain("common namesnot set");
expect(wrapper.text().toLowerCase()).toContain("iconnot available");
expect(wrapper.text().toLowerCase()).toContain("spacingnot available");
expect(wrapper.text().toLowerCase()).toContain("common namesnot available");
});
});

View File

@ -58,7 +58,7 @@ const OMITTED_PROPERTIES = [
"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

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