Merge branch 'staging' of https://github.com/FarmBot/Farmbot-Web-App into sync_object_sequel

pull/1022/head
Rick Carlino 2018-10-24 07:43:03 -05:00
commit 793c6cf895
8 changed files with 110 additions and 67 deletions

View File

@ -43,7 +43,7 @@
"@types/react": "^16.4.18", "@types/react": "^16.4.18",
"@types/react-color": "2.13.6", "@types/react-color": "2.13.6",
"@types/react-dom": "^16.0.9", "@types/react-dom": "^16.0.9",
"@types/react-joyride": "^2.0.0", "@types/react-joyride": "^2.0.1",
"@types/react-redux": "6.0.9", "@types/react-redux": "6.0.9",
"axios": "^0.18.0", "axios": "^0.18.0",
"boxed_value": "^1.0.0", "boxed_value": "^1.0.0",

View File

@ -3,7 +3,7 @@ const mockDevice = {
powerOff: jest.fn(() => { return Promise.resolve(); }), powerOff: jest.fn(() => { return Promise.resolve(); }),
resetOS: jest.fn(), resetOS: jest.fn(),
reboot: jest.fn(() => { return Promise.resolve(); }), reboot: jest.fn(() => { return Promise.resolve(); }),
send: jest.fn(() => { return Promise.resolve(); }), rebootFirmware: jest.fn(() => { return Promise.resolve(); }),
checkArduinoUpdates: jest.fn(() => { return Promise.resolve(); }), checkArduinoUpdates: jest.fn(() => { return Promise.resolve(); }),
emergencyLock: jest.fn(() => { return Promise.resolve(); }), emergencyLock: jest.fn(() => { return Promise.resolve(); }),
emergencyUnlock: jest.fn(() => { return Promise.resolve(); }), emergencyUnlock: jest.fn(() => { return Promise.resolve(); }),
@ -15,6 +15,8 @@ const mockDevice = {
sync: jest.fn(() => { return Promise.resolve(); }), sync: jest.fn(() => { return Promise.resolve(); }),
readStatus: jest.fn(() => Promise.resolve()), readStatus: jest.fn(() => Promise.resolve()),
updateConfig: jest.fn(() => Promise.resolve()), updateConfig: jest.fn(() => Promise.resolve()),
registerGpio: jest.fn(() => Promise.reject()),
unregisterGpio: jest.fn(() => Promise.reject()),
dumpInfo: jest.fn(() => Promise.resolve()), dumpInfo: jest.fn(() => Promise.resolve()),
}; };
@ -88,7 +90,7 @@ describe("reboot()", function () {
describe("restartFirmware()", function () { describe("restartFirmware()", function () {
it("calls restartFirmware", async () => { it("calls restartFirmware", async () => {
await actions.restartFirmware(); await actions.restartFirmware();
expect(mockDevice.send).toHaveBeenCalled(); expect(mockDevice.rebootFirmware).toHaveBeenCalled();
expect(success).toHaveBeenCalled(); expect(success).toHaveBeenCalled();
}); });
}); });
@ -107,9 +109,28 @@ describe("emergencyLock() / emergencyUnlock", function () {
}); });
describe("sync()", function () { describe("sync()", function () {
it("calls sync", () => {
const state = fakeState();
state.bot.hardware.informational_settings.controller_version = "999.0.0";
actions.sync()(jest.fn(), () => state);
expect(mockDevice.sync).toHaveBeenCalled();
});
it("calls badVersion", () => {
const state = fakeState();
state.bot.hardware.informational_settings.controller_version = "1.0.0";
actions.sync()(jest.fn(), () => state);
expect(mockDevice.sync).not.toHaveBeenCalled();
expect(info).toBeCalledWith(
expect.stringContaining("old version"),
expect.stringContaining("Please Update"),
"red");
});
it("doesn't call sync: disconnected", () => { it("doesn't call sync: disconnected", () => {
const getState = () => fakeState(); const state = fakeState();
actions.sync()(jest.fn(), getState); state.bot.hardware.informational_settings.controller_version = undefined;
actions.sync()(jest.fn(), () => state);
expect(mockDevice.sync).not.toHaveBeenCalled(); expect(mockDevice.sync).not.toHaveBeenCalled();
const expectedMessage = ["FarmBot is not connected.", "Disconnected", "red"]; const expectedMessage = ["FarmBot is not connected.", "Disconnected", "red"];
expect(info).toBeCalledWith(...expectedMessage); expect(info).toBeCalledWith(...expectedMessage);
@ -189,6 +210,17 @@ describe("settingToggle()", () => {
type: Actions.EDIT_RESOURCE type: Actions.EDIT_RESOURCE
}); });
}); });
it("displays an alert message", () => {
const fakeConfig = fakeFirmwareConfig();
fakeConfig.body.api_migrated = false;
window.alert = jest.fn();
const msg = "this is an alert.";
actions.settingToggle(
"param_mov_nr_retry", jest.fn(() => ({ value: "" })),
msg)(jest.fn(), fakeState);
expect(window.alert).toHaveBeenCalledWith(msg);
});
}); });
describe("updateMCU()", () => { describe("updateMCU()", () => {
@ -236,6 +268,22 @@ describe("updateMCU()", () => {
expect(warning).toHaveBeenCalledWith( expect(warning).toHaveBeenCalledWith(
"Minimum speed should always be lower than maximum"); "Minimum speed should always be lower than maximum");
}); });
it("catches error", async () => {
mockDevice.updateMcu = jest.fn(() => { return Promise.reject(); });
const dispatch = jest.fn();
const state = fakeState();
const fakeConfig = fakeFirmwareConfig();
fakeConfig.body.api_migrated = false;
state.resources = buildResourceIndex([fakeConfig]);
await actions.updateMCU(
"param_mov_nr_retry", "1")(dispatch, () => state);
await expect(mockDevice.updateMcu).toHaveBeenCalled();
expect(dispatch).toHaveBeenLastCalledWith({
payload: undefined, type: Actions.SETTING_UPDATE_END
});
expect(error).toHaveBeenCalledWith("Firmware config update failed");
});
}); });
describe("pinToggle()", function () { describe("pinToggle()", function () {
@ -454,6 +502,22 @@ describe("updateConfig()", () => {
}); });
}); });
describe("registerGpioPin()", () => {
it("catches error", async () => {
await actions.registerGpioPin({ pin_number: 1, sequence_id: 1 })(jest.fn());
await expect(mockDevice.registerGpio).toHaveBeenCalled();
expect(error).toHaveBeenCalledWith("Register GPIO Pin failed");
});
});
describe("unregisterGpioPin()", () => {
it("catches error", async () => {
await actions.unregisterGpioPin(1)(jest.fn());
await expect(mockDevice.unregisterGpio).toHaveBeenCalled();
expect(error).toHaveBeenCalledWith("Unregister GPIO Pin failed");
});
});
describe("badVersion()", () => { describe("badVersion()", () => {
it("warns of old FBOS version", () => { it("warns of old FBOS version", () => {
actions.badVersion(); actions.badVersion();

View File

@ -9,16 +9,11 @@ import {
} from "./interfaces"; } from "./interfaces";
import { Thunk, ReduxAction } from "../redux/interfaces"; import { Thunk, ReduxAction } from "../redux/interfaces";
import { import {
McuParams, Configuration, rpcRequest, TaggedDevice, McuParams, Configuration, TaggedFirmwareConfig
TaggedFirmwareConfig
} from "farmbot"; } from "farmbot";
import { Sequence } from "../sequences/interfaces"; import { Sequence } from "../sequences/interfaces";
import { ControlPanelState } from "../devices/interfaces"; import { ControlPanelState } from "../devices/interfaces";
import { API } from "../api/index"; import { getFirmwareConfig } from "../resources/selectors";
import { User } from "../auth/interfaces";
import {
getDeviceAccountSettings, getFirmwareConfig
} from "../resources/selectors";
import { oneOf, versionOK, trim } from "../util"; import { oneOf, versionOK, trim } from "../util";
import { Actions, Content } from "../constants"; import { Actions, Content } from "../constants";
import { mcuParamValidator } from "./update_interceptor"; import { mcuParamValidator } from "./update_interceptor";
@ -54,14 +49,18 @@ export function isLog(x: any): x is Log {
return false; return false;
} }
} }
/** Toast message upon request error. */
export const commandErr = export const commandErr =
(noun = "Command") => () => error(t(`${noun} failed`)); (noun = "Command") => () => error(t(`${noun} failed`));
/** Toast message upon request success. */
export const commandOK = (noun = "Command") => () => { export const commandOK = (noun = "Command") => () => {
const msg = t(noun) + t(" request sent to device."); const msg = t(noun) + t(" request sent to device.");
success(msg, t("Request sent")); success(msg, t("Request sent"));
}; };
/** Update FBOS. */
export function checkControllerUpdates() { export function checkControllerUpdates() {
const noun = "Check for Updates"; const noun = "Check for Updates";
commandOK(noun)(); commandOK(noun)();
@ -70,6 +69,7 @@ export function checkControllerUpdates() {
.catch(commandErr(noun)); .catch(commandErr(noun));
} }
/** Shutdown FBOS. */
export function powerOff() { export function powerOff() {
const noun = "Power Off Bot"; const noun = "Power Off Bot";
getDevice() getDevice()
@ -77,6 +77,7 @@ export function powerOff() {
.then(commandOK(noun), commandErr(noun)); .then(commandOK(noun), commandErr(noun));
} }
/** Factory reset FBOS. */
export function factoryReset() { export function factoryReset() {
if (!confirm(t(Content.FACTORY_RESET_ALERT))) { if (!confirm(t(Content.FACTORY_RESET_ALERT))) {
return; return;
@ -84,6 +85,7 @@ export function factoryReset() {
getDevice().resetOS(); getDevice().resetOS();
} }
/** Reboot FBOS. */
export function reboot() { export function reboot() {
const noun = "Reboot Bot"; const noun = "Reboot Bot";
getDevice() getDevice()
@ -91,13 +93,11 @@ export function reboot() {
.then(commandOK(noun), commandErr(noun)); .then(commandOK(noun), commandErr(noun));
} }
/** Restart Farmduino firmware serial connection. */
export function restartFirmware() { export function restartFirmware() {
const noun = "Restart Firmware"; const noun = "Restart Firmware";
getDevice() // TODO: add `restartFirmware()` to FBJS getDevice()
.send(rpcRequest([{ .rebootFirmware()
kind: "reboot",
args: { package: "arduino_firmware" }
}]))
.then(commandOK(noun), commandErr(noun)); .then(commandOK(noun), commandErr(noun));
} }
@ -120,23 +120,15 @@ export function emergencyUnlock() {
export function sync(): Thunk { export function sync(): Thunk {
const noun = "Sync"; const noun = "Sync";
return function (_dispatch, getState) { return function (_dispatch, getState) {
const IS_OK = versionOK(getState() const currentFBOSversion =
.bot getState().bot.hardware.informational_settings.controller_version;
.hardware const IS_OK = versionOK(currentFBOSversion, EXPECTED_MAJOR, EXPECTED_MINOR);
.informational_settings
.controller_version, EXPECTED_MAJOR, EXPECTED_MINOR);
if (IS_OK) { if (IS_OK) {
getDevice() getDevice()
.sync() .sync()
// TODO: Probably wrong. Fix when there is time to QA - RC 5/2/18
.then(() => { commandOK(noun); })
.catch(commandErr(noun)); .catch(commandErr(noun));
} else { } else {
if (getState() if (currentFBOSversion) {
.bot
.hardware
.informational_settings
.controller_version) {
badVersion(); badVersion();
} else { } else {
info(t("FarmBot is not connected."), t("Disconnected"), "red"); info(t("FarmBot is not connected."), t("Disconnected"), "red");
@ -160,11 +152,8 @@ export function requestDiagnostic() {
return getDevice().dumpInfo().then(commandOK(noun), commandErr(noun)); return getDevice().dumpInfo().then(commandOK(noun), commandErr(noun));
} }
export let saveAccountChanges: Thunk = function (_dispatch, getState) { /** Fetch FarmBot OS release data. */
return save(getDeviceAccountSettings(getState().resources.index)); export const fetchReleases =
};
export let fetchReleases =
(url: string, options = { beta: false }) => (url: string, options = { beta: false }) =>
(dispatch: Function) => { (dispatch: Function) => {
axios axios
@ -231,15 +220,6 @@ export let fetchMinOsFeatureData = (url: string) =>
}); });
}; };
export function save(input: TaggedDevice) {
return function (dispatch: Function) {
return axios
.put<User>(API.current.devicePath, input.body)
.then(resp => dispatch({ type: Actions.SAVE_DEVICE_OK, payload: resp.data }))
.catch(() => error(t("Error saving device settings.")));
};
}
/** /**
* Toggles visibility of individual sections in the giant controls panel * Toggles visibility of individual sections in the giant controls panel
* found on the Devices page. * found on the Devices page.
@ -248,10 +228,12 @@ export function toggleControlPanel(payload: keyof ControlPanelState) {
return { type: Actions.TOGGLE_CONTROL_PANEL_OPTION, payload }; return { type: Actions.TOGGLE_CONTROL_PANEL_OPTION, payload };
} }
/** Toggle visibility of all hardware control panel sections. */
export function bulkToggleControlPanel(payload: boolean) { export function bulkToggleControlPanel(payload: boolean) {
return { type: Actions.BULK_TOGGLE_CONTROL_PANEL, payload }; return { type: Actions.BULK_TOGGLE_CONTROL_PANEL, payload };
} }
/** Factory reset all firmware settings. */
export function MCUFactoryReset() { export function MCUFactoryReset() {
if (!confirm(t(Content.MCU_RESET_ALERT))) { if (!confirm(t(Content.MCU_RESET_ALERT))) {
return; return;
@ -259,6 +241,7 @@ export function MCUFactoryReset() {
return getDevice().resetMCU().catch(commandErr("MCU Reset")); return getDevice().resetMCU().catch(commandErr("MCU Reset"));
} }
/** Toggle a firmware setting. */
export function settingToggle( export function settingToggle(
name: ConfigKey, name: ConfigKey,
sourceFwConfig: SourceFwConfig, sourceFwConfig: SourceFwConfig,
@ -325,6 +308,7 @@ export function findHome(axis: Axis, speed = CONFIG_DEFAULTS.speed) {
.catch(commandErr(noun)); .catch(commandErr(noun));
} }
/** Start hardware settings update spinner. */
const startUpdate = () => { const startUpdate = () => {
return { return {
type: Actions.SETTING_UPDATE_START, type: Actions.SETTING_UPDATE_START,
@ -332,18 +316,20 @@ const startUpdate = () => {
}; };
}; };
const updateOK = (dispatch: Function, noun: string) => { /** Stop hardware settings update spinner. */
const updateOK = (dispatch: Function) => {
dispatch({ type: Actions.SETTING_UPDATE_END, payload: undefined }); dispatch({ type: Actions.SETTING_UPDATE_END, payload: undefined });
commandOK(noun);
}; };
/** Stop hardware settings update spinner and display an error toast. */
const updateNO = (dispatch: Function, noun: string) => { const updateNO = (dispatch: Function, noun: string) => {
dispatch({ type: Actions.SETTING_UPDATE_END, payload: undefined }); dispatch({ type: Actions.SETTING_UPDATE_END, payload: undefined });
commandErr(noun); commandErr(noun)();
}; };
/** Update firmware setting. */
export function updateMCU(key: ConfigKey, val: string) { export function updateMCU(key: ConfigKey, val: string) {
const noun = "configuration update"; const noun = "Firmware config update";
return function (dispatch: Function, getState: () => Everything) { return function (dispatch: Function, getState: () => Everything) {
const firmwareConfig = getFirmwareConfig(getState().resources.index); const firmwareConfig = getFirmwareConfig(getState().resources.index);
const getParams = () => { const getParams = () => {
@ -362,7 +348,7 @@ export function updateMCU(key: ConfigKey, val: string) {
dispatch(startUpdate()); dispatch(startUpdate());
getDevice() getDevice()
.updateMcu({ [key]: val }) .updateMcu({ [key]: val })
.then(() => updateOK(dispatch, noun)) .then(() => updateOK(dispatch))
.catch(() => updateNO(dispatch, noun)); .catch(() => updateNO(dispatch, noun));
} }
} }
@ -374,8 +360,9 @@ export function updateMCU(key: ConfigKey, val: string) {
}; };
} }
/** Update FBOS setting. */
export function updateConfig(config: Configuration) { export function updateConfig(config: Configuration) {
const noun = "Update Config"; const noun = "FarmBot OS config update";
return function (dispatch: Function, getState: () => Everything) { return function (dispatch: Function, getState: () => Everything) {
const fbosConfig = getFbosConfig(getState().resources.index); const fbosConfig = getFbosConfig(getState().resources.index);
if (fbosConfig && fbosConfig.body.api_migrated) { if (fbosConfig && fbosConfig.body.api_migrated) {
@ -389,27 +376,30 @@ export function updateConfig(config: Configuration) {
}; };
} }
/** Register a sequence to an RPi GPIO pin (FBOS < 6.4.4). */
export function registerGpioPin( export function registerGpioPin(
pinBinding: { pin_number: number, sequence_id: number }) { pinBinding: { pin_number: number, sequence_id: number }) {
const noun = "Register GPIO Pin"; const noun = "Register GPIO Pin";
return function (dispatch: Function) { return function (dispatch: Function) {
getDevice() getDevice()
.registerGpio(pinBinding) .registerGpio(pinBinding)
.then(() => updateOK(dispatch, noun)) .then(() => updateOK(dispatch))
.catch(() => updateNO(dispatch, noun)); .catch(() => updateNO(dispatch, noun));
}; };
} }
/** Remove binding from an RPi GPIO pin (FBOS < 6.4.4). */
export function unregisterGpioPin(pin_number: number) { export function unregisterGpioPin(pin_number: number) {
const noun = "Unregister GPIO Pin"; const noun = "Unregister GPIO Pin";
return function (dispatch: Function) { return function (dispatch: Function) {
getDevice() getDevice()
.unregisterGpio({ pin_number }) .unregisterGpio({ pin_number })
.then(() => updateOK(dispatch, noun)) .then(() => updateOK(dispatch))
.catch(() => updateNO(dispatch, noun)); .catch(() => updateNO(dispatch, noun));
}; };
} }
/** Change jog button movement amount. */
export function changeStepSize(integer: number) { export function changeStepSize(integer: number) {
return { return {
type: Actions.CHANGE_STEP_SIZE, type: Actions.CHANGE_STEP_SIZE,
@ -426,6 +416,7 @@ export function resetNetwork(): ReduxAction<{}> {
return { type: Actions.RESET_NETWORK, payload: {} }; return { type: Actions.RESET_NETWORK, payload: {} };
} }
/** for connectivity panel */
export function resetConnectionInfo() { export function resetConnectionInfo() {
return function (dispatch: Function) { return function (dispatch: Function) {
dispatch(resetNetwork()); dispatch(resetNetwork());

View File

@ -1,6 +1,6 @@
const mockDevice = { const mockDevice = {
updateConfig: jest.fn(() => { return Promise.resolve(); }), updateConfig: jest.fn(() => { return Promise.resolve(); }),
send: jest.fn(() => { return Promise.resolve(); }), rebootFirmware: jest.fn(() => { return Promise.resolve(); }),
}; };
jest.mock("../../../../device", () => ({ jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice) getDevice: () => (mockDevice)
@ -79,14 +79,7 @@ describe("<PowerAndReset/>", () => {
expect(wrapper.text().toLowerCase()) expect(wrapper.text().toLowerCase())
.toContain("Restart Firmware".toLowerCase()); .toContain("Restart Firmware".toLowerCase());
clickButton(wrapper, 2, "restart"); clickButton(wrapper, 2, "restart");
expect(mockDevice.send).toHaveBeenCalledWith( expect(mockDevice.rebootFirmware).toHaveBeenCalled();
expect.objectContaining({
kind: "rpc_request",
args: expect.objectContaining({ label: expect.any(String) }),
body: [expect.objectContaining({
kind: "reboot", args: { package: "arduino_firmware" }
})]
}));
}); });
it("shows change ownership button", () => { it("shows change ownership button", () => {

View File

@ -31,8 +31,7 @@ interface TourState {
export class Tour extends React.Component<TourProps, TourState> { export class Tour extends React.Component<TourProps, TourState> {
state: TourState = { run: false, index: 0, }; state: TourState = { run: false, index: 0, };
// tslint:disable-next-line:no-any // broken typing callback = ({ action, index, step, type }: CBData) => {
callback: any = ({ action, index, step, type }: CBData) => {
console.log("Tour debug:", step.target, type, action); console.log("Tour debug:", step.target, type, action);
tourPageNavigation(step.target); tourPageNavigation(step.target);
if (type === "step:after") { if (type === "step:after") {

View File

@ -9,7 +9,6 @@ import { mount } from "enzyme";
import { Logs } from "../index"; import { Logs } from "../index";
import { ToolTips } from "../../constants"; import { ToolTips } from "../../constants";
import { TaggedLog, Dictionary } from "farmbot"; import { TaggedLog, Dictionary } from "farmbot";
import { bot } from "../../__test_support__/fake_state/bot";
import { NumericSetting } from "../../session_keys"; import { NumericSetting } from "../../session_keys";
import { fakeLog } from "../../__test_support__/fake_state/resources"; import { fakeLog } from "../../__test_support__/fake_state/resources";
import { LogsProps } from "../interfaces"; import { LogsProps } from "../interfaces";
@ -27,7 +26,6 @@ describe("<Logs />", () => {
const fakeProps = (): LogsProps => { const fakeProps = (): LogsProps => {
return { return {
logs: fakeLogs(), logs: fakeLogs(),
bot,
timeOffset: 0, timeOffset: 0,
dispatch: jest.fn(), dispatch: jest.fn(),
sourceFbosConfig: jest.fn(), sourceFbosConfig: jest.fn(),

View File

@ -1,10 +1,9 @@
import { TaggedLog, ConfigurationName, ALLOWED_MESSAGE_TYPES } from "farmbot"; import { TaggedLog, ConfigurationName, ALLOWED_MESSAGE_TYPES } from "farmbot";
import { BotState, SourceFbosConfig } from "../devices/interfaces"; import { SourceFbosConfig } from "../devices/interfaces";
import { GetWebAppConfigValue } from "../config_storage/actions"; import { GetWebAppConfigValue } from "../config_storage/actions";
export interface LogsProps { export interface LogsProps {
logs: TaggedLog[]; logs: TaggedLog[];
bot: BotState;
timeOffset: number; timeOffset: number;
dispatch: Function; dispatch: Function;
sourceFbosConfig: SourceFbosConfig; sourceFbosConfig: SourceFbosConfig;

View File

@ -28,7 +28,6 @@ export function mapStateToProps(props: Everything): LogsProps {
dispatch: props.dispatch, dispatch: props.dispatch,
sourceFbosConfig: sourceFbosConfigValue(fbosConfig, hardware.configuration), sourceFbosConfig: sourceFbosConfigValue(fbosConfig, hardware.configuration),
logs: takeSortedLogs(250, props.resources.index), logs: takeSortedLogs(250, props.resources.index),
bot: props.bot,
timeOffset: maybeGetTimeOffset(props.resources.index), timeOffset: maybeGetTimeOffset(props.resources.index),
getConfigValue: getWebAppConfigValue(() => props), getConfigValue: getWebAppConfigValue(() => props),
}; };