access API FBOS config storage

pull/647/head
gabrielburnworth 2018-01-27 01:29:13 -08:00
parent 867d1714aa
commit 2765d345ac
55 changed files with 915 additions and 303 deletions

View File

@ -0,0 +1,12 @@
import { ControlPanelState } from "../devices/interfaces";
export const panelState = (): ControlPanelState => {
return {
homing_and_calibration: false,
motors: false,
encoders_and_endstops: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false
};
};

View File

@ -3,7 +3,7 @@ import { buildResourceIndex } from "../resource_index_builder";
import {
TaggedFarmEvent, TaggedSequence, TaggedRegimen, TaggedImage,
TaggedTool, TaggedToolSlotPointer, TaggedUser, TaggedWebcamFeed,
TaggedPlantPointer, TaggedGenericPointer, TaggedPeripheral
TaggedPlantPointer, TaggedGenericPointer, TaggedPeripheral, TaggedFbosConfig
} from "../../resources/tagged_resources";
import { ExecutableType } from "../../farm_designer/interfaces";
import { fakeResource } from "../fake_resource";
@ -136,3 +136,25 @@ export function fakePeripheral(): TaggedPeripheral {
pin: 1
});
}
export function fakeFbosConfig(): TaggedFbosConfig {
return fakeResource("FbosConfig", {
id: 1,
device_id: 1,
created_at: "",
updated_at: "",
auto_sync: false,
beta_opt_in: false,
disable_factory_reset: false,
firmware_input_log: false,
firmware_output_log: false,
sequence_body_log: false,
sequence_complete_log: false,
sequence_init_log: false,
network_not_found_timer: 0,
firmware_hardware: "arduino",
api_migrated: false,
os_auto_update: false,
arduino_debug_messages: false
});
}

View File

@ -10,6 +10,7 @@ import "../controls/interfaces";
import "../controls/peripherals/interfaces";
import "../controls/webcam/interfaces";
import "../devices/components/interfaces";
import "../devices/components/fbos_settings/interfaces";
import "../devices/interfaces";
import "../draggable/interfaces";
import "../farm_designer/farm_events/calendar/interfaces";

View File

@ -115,8 +115,10 @@ export class API {
get logsPath() { return `${this.baseUrl}/api/logs/`; }
/** /api/webcam_feed */
get webcamFeedPath() { return `${this.baseUrl}/api/webcam_feeds/`; }
/** /api/webcam_feed */
/** /api/web_app_config */
get webAppConfigPath() { return `${this.baseUrl}/api/web_app_config/`; }
/** /api/fbos_config */
get fbosConfigPath() { return `${this.baseUrl}/api/fbos_config/`; }
/** /api/users/verify/:token */
verificationPath = (token: string) => ("/api/users/verify/" + token);
}

View File

@ -218,6 +218,7 @@ export function urlFor(tag: ResourceName) {
Image: API.current.imagesPath,
Log: API.current.logsPath,
WebcamFeed: API.current.webcamFeedPath,
FbosConfig: API.current.fbosConfigPath,
WebAppConfig: API.current.webAppConfigPath
};
const url = OPTIONS[tag];
@ -229,7 +230,7 @@ export function urlFor(tag: ResourceName) {
}
}
const SINGULAR_RESOURCE: ResourceName[] = ["WebAppConfig"];
const SINGULAR_RESOURCE: ResourceName[] = ["WebAppConfig", "FbosConfig"];
/** Shared functionality in create() and update(). */
function updateViaAjax(payl: AjaxUpdatePayload) {

View File

@ -7,6 +7,7 @@ const BLACKLIST: ResourceName[] = [
"WebcamFeed",
"User",
"WebAppConfig",
"FbosConfig",
];
export function maybeStartTracking(uuid: string) {

View File

@ -1,10 +1,10 @@
import { BooleanConfigKey } from "./web_app_configs";
import { BooleanConfigKey as BooleanWebAppConfigKey } from "./web_app_configs";
import { GetState } from "../redux/interfaces";
import { getWebAppConfig } from "../resources/selectors";
import { edit, save } from "../api/crud";
/** Inverts boolean config key in WebAppConfig object, stored in the API. */
export function toggleWebAppBool(key: BooleanConfigKey) {
export function toggleWebAppBool(key: BooleanWebAppConfigKey) {
return (dispatch: Function, getState: GetState) => {
const conf = getWebAppConfig(getState().resources.index);
if (conf) {

View File

@ -102,6 +102,7 @@ export interface ToggleButtonProps {
toggleValue: number | string | boolean | undefined;
disabled?: boolean | undefined;
customText?: { textFalse: string, textTrue: string };
dim?: boolean;
}
export interface WebcamFeed {

View File

@ -44,7 +44,7 @@ export class ToggleButton extends React.Component<ToggleButtonProps, {}> {
const cb = () => !this.props.disabled && this.props.toggleAction();
return <button
disabled={!!this.props.disabled}
className={this.css()}
className={this.css() + (this.props.dim ? " dim" : "")}
onClick={cb}>
{this.caption()}
</button>;

View File

@ -248,6 +248,9 @@
&:hover {
background: $green;
}
&.dim {
background: lighten($green, 20%) !important;
}
}
&.red {
text-align: right !important;
@ -263,6 +266,9 @@
left: 0.2rem;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 0px 0 rgba(0, 0, 0, 0.04), 0 4px 9px rgba(0, 0, 0, 0.13), 0 3px 3px rgba(0, 0, 0, 0.05);
}
&.dim {
background: lighten($red, 10%) !important;
}
}
}

View File

@ -28,6 +28,9 @@ input:not([role="combobox"]) {
&.day {
display: none;
}
&.dim {
background: darken($white, 2%) !important;
}
}
.day-selector-wrapper {
@ -47,6 +50,17 @@ select {
width: 100%;
}
.filter-search {
&.dim {
Button {
background: darken($white, 2%) !important;
&:hover {
background: darken($white, 2%) !important;
}
}
}
}
.filter-search-item-none::after {
content: "*";
}

View File

@ -45,6 +45,9 @@
background: $black;
.markdown p {
font-weight: 600;
code {
background: $dark_gray;
}
}
.status-ticker-created-at {
font-size: 1.2rem;

View File

@ -12,7 +12,8 @@ const mockDevice = {
togglePin: jest.fn(() => { return Promise.resolve(); }),
home: 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())
};
jest.mock("../../device", () => ({
@ -37,13 +38,14 @@ jest.mock("axios", () => ({
}));
import * as actions from "../actions";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { fakeSequence, fakeFbosConfig } from "../../__test_support__/fake_state/resources";
import { fakeState } from "../../__test_support__/fake_state";
import { changeStepSize, resetNetwork, resetConnectionInfo } from "../actions";
import { Actions } from "../../constants";
import { fakeDevice } from "../../__test_support__/resource_index_builder";
import { fakeDevice, buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { API } from "../../api/index";
import axios from "axios";
import { SpecialStatus } from "../../resources/tagged_resources";
describe("checkControllerUpdates()", function () {
beforeEach(function () {
@ -302,3 +304,36 @@ describe("fetchReleases()", () => {
});
});
});
describe("updateConfig()", () => {
beforeEach(function () {
jest.clearAllMocks();
});
it("updates config: configUpdate", () => {
const dispatch = jest.fn();
const state = fakeState();
state.resources.index = buildResourceIndex([fakeFbosConfig()]).index;
actions.updateConfig({ auto_sync: true })(dispatch, () => state);
expect(mockDevice.updateConfig).toHaveBeenCalledWith({ auto_sync: true });
expect(dispatch).not.toHaveBeenCalled();
});
it("updates config: FbosConfig", () => {
const dispatch = jest.fn(() => Promise.resolve());
const state = fakeState();
const fakeFBOSConfig = fakeFbosConfig();
fakeFBOSConfig.body.api_migrated = true;
state.resources.index = buildResourceIndex([fakeFBOSConfig]).index;
actions.updateConfig({ auto_sync: true })(dispatch, () => state);
expect(mockDevice.updateConfig).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledWith({
payload: {
specialStatus: SpecialStatus.DIRTY,
update: { auto_sync: true },
uuid: expect.stringContaining("FbosConfig")
},
type: Actions.EDIT_RESOURCE
});
});
});

View File

@ -23,7 +23,8 @@ describe("<Devices/>", () => {
deviceAccount: fakeDevice(),
images: [],
dispatch: jest.fn(),
resources: buildResourceIndex(FAKE_RESOURCES).index
resources: buildResourceIndex(FAKE_RESOURCES).index,
sourceFbosConfig: jest.fn()
});
it("renders relevant panels", () => {

View File

@ -0,0 +1,49 @@
import { fakeFbosConfig } from "../../__test_support__/fake_state/resources";
let mockFbosConfig: TaggedFbosConfig | undefined = fakeFbosConfig();
jest.mock("../../resources/selectors", () => ({
getDeviceAccountSettings: jest.fn(),
assertUuid: jest.fn(),
getFbosConfig: () => mockFbosConfig,
selectAllImages: jest.fn()
}));
import { mapStateToProps } from "../state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
import { TaggedFbosConfig } from "../../resources/tagged_resources";
describe("mapStateToProps()", () => {
it("API source of FBOS settings", () => {
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.auto_sync = true;
fakeApiConfig.body.api_migrated = true;
mockFbosConfig = fakeApiConfig;
const props = mapStateToProps(fakeState());
expect(props.sourceFbosConfig("auto_sync")).toEqual({
value: true, consistent: false
});
});
it("bot source of FBOS settings", () => {
const state = fakeState();
state.bot.hardware.configuration.auto_sync = false;
mockFbosConfig = undefined;
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("auto_sync")).toEqual({
value: false, consistent: true
});
});
it("bot source of FBOS settings: ignore API defaults", () => {
const state = fakeState();
state.bot.hardware.configuration.auto_sync = false;
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.auto_sync = true;
fakeApiConfig.body.api_migrated = false;
mockFbosConfig = fakeApiConfig;
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("auto_sync")).toEqual({
value: false, consistent: true
});
});
});

View File

@ -12,13 +12,14 @@ import { Sequence } from "../sequences/interfaces";
import { ControlPanelState } from "../devices/interfaces";
import { API } from "../api/index";
import { User } from "../auth/interfaces";
import { getDeviceAccountSettings } from "../resources/selectors";
import { getDeviceAccountSettings, getFbosConfig } from "../resources/selectors";
import { TaggedDevice } from "../resources/tagged_resources";
import { versionOK } from "./reducer";
import { HttpData, oneOf } from "../util";
import { Actions, Content } from "../constants";
import { mcuParamValidator } from "./update_interceptor";
import { pingAPI } from "../connectivity/ping_mqtt";
import { edit, save as apiSave } from "../api/crud";
const ON = 1, OFF = 0;
export type ConfigKey = keyof McuParams;
@ -277,11 +278,27 @@ export function updateMCU(key: ConfigKey, val: string) {
export function updateConfig(config: Configuration) {
const noun = "Update Config";
return function (dispatch: Function) {
getDevice()
.updateConfig(config)
.then(() => updateOK(dispatch, noun))
.catch(() => updateNO(dispatch, noun));
let useAPI = false;
return function (dispatch: Function, getState?: () => Everything) {
if (getState) {
const fbosConfig = getFbosConfig(getState().resources.index);
if (fbosConfig) {
useAPI = fbosConfig.body.api_migrated;
if (useAPI) {
const [key, value] = Object.entries(config)[0];
dispatch(edit(fbosConfig, { [key]: value }));
dispatch(apiSave(fbosConfig.uuid))
.then(() => updateOK(_.noop, noun))
.catch(() => updateNO(_.noop, noun));
}
}
}
if (!useAPI) {
getDevice()
.updateConfig(config)
.then(() => updateOK(_.noop, noun))
.catch(() => updateNO(_.noop, noun));
}
};
}

View File

@ -0,0 +1,77 @@
const mockDevice = {
updateConfig: jest.fn(() => { return Promise.resolve(); }),
};
jest.mock("../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react";
import { shallow } from "enzyme";
import { BotConfigInputBox, BotConfigInputBoxProps } from "../bot_config_input_box";
import { bot } from "../../../__test_support__/fake_state/bot";
describe("<BotConfigInputBox />", () => {
beforeEach(function () {
jest.clearAllMocks();
});
const fakeProps = (): BotConfigInputBoxProps => {
return {
setting: "network_not_found_timer",
dispatch: jest.fn(x => x()),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("renders value: number", () => {
bot.hardware.configuration.network_not_found_timer = 10;
const wrapper = shallow(<BotConfigInputBox {...fakeProps() } />);
const inputBoxProps = wrapper.find("BlurableInput").props();
expect(inputBoxProps.value).toEqual("10");
expect(inputBoxProps.className).toEqual("");
});
it("doesn't render value: string", () => {
// tslint:disable-next-line:no-any
bot.hardware.configuration.network_not_found_timer = "bad" as any;
const wrapper = shallow(<BotConfigInputBox {...fakeProps() } />);
expect(wrapper.find("BlurableInput").props().value).toEqual("");
});
it("updates value", () => {
bot.hardware.configuration.network_not_found_timer = 0;
const p = fakeProps();
const wrapper = shallow(<BotConfigInputBox {...p} />);
wrapper.find("BlurableInput")
.simulate("commit", { currentTarget: { value: "10" } });
expect(mockDevice.updateConfig)
.toHaveBeenCalledWith({ network_not_found_timer: 10 });
});
it("doesn't update value: same value", () => {
bot.hardware.configuration.network_not_found_timer = 10;
const p = fakeProps();
const wrapper = shallow(<BotConfigInputBox {...p} />);
wrapper.find("BlurableInput")
.simulate("commit", { currentTarget: { value: "10" } });
expect(mockDevice.updateConfig).not.toHaveBeenCalled();
});
it("doesn't update value: NaN", () => {
bot.hardware.configuration.network_not_found_timer = 10;
const p = fakeProps();
const wrapper = shallow(<BotConfigInputBox {...p} />);
wrapper.find("BlurableInput")
.simulate("commit", { currentTarget: { value: "x" } });
expect(mockDevice.updateConfig).not.toHaveBeenCalled();
});
it("not consistent", () => {
const p = fakeProps();
p.sourceFbosConfig = x => { return { value: 10, consistent: false }; };
const wrapper = shallow(<BotConfigInputBox {...p} />);
expect(wrapper.find("BlurableInput").props().className).toEqual("dim");
});
});

View File

@ -7,23 +7,29 @@ jest.mock("axios", () => ({
import * as React from "react";
import { FarmbotOsSettings } from "../farmbot_os_settings";
import { mount } from "enzyme";
import { mount, shallow } from "enzyme";
import { bot } from "../../../__test_support__/fake_state/bot";
import { fakeResource } from "../../../__test_support__/fake_resource";
import { FbosDetails } from "../fbos_settings/farmbot_os_row";
import { FarmbotOsProps } from "../../interfaces";
import axios from "axios";
import { FbosDetailsProps } from "../fbos_settings/interfaces";
import { Actions } from "../../../constants";
import { SpecialStatus } from "../../../resources/tagged_resources";
describe("<FarmbotOsSettings/>", () => {
function fakeProps(): FarmbotOsProps {
const fakeProps = (): FarmbotOsProps => {
return {
account: fakeResource("Device", { id: 0, name: "", tz_offset_hrs: 0 }),
dispatch: jest.fn(),
bot: bot,
bot,
botToMqttLastSeen: "",
botToMqttStatus: "up"
botToMqttStatus: "up",
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
}
};
it("renders settings", () => {
const osSettings = mount(<FarmbotOsSettings {...fakeProps() } />);
@ -50,11 +56,37 @@ describe("<FarmbotOsSettings/>", () => {
expect(osSettings.state().osReleaseNotes)
.toEqual("Could not get release notes.");
});
it("changes bot name", () => {
const p = fakeProps();
const osSettings = shallow(<FarmbotOsSettings {...p} />);
osSettings.find("input")
.simulate("change", { currentTarget: { value: "new bot name" } });
expect(p.dispatch).toHaveBeenCalledWith({
payload: {
specialStatus: SpecialStatus.DIRTY,
update: { name: "new bot name" },
uuid: expect.stringContaining("Device")
},
type: Actions.EDIT_RESOURCE
});
});
});
describe("<FbosDetails />", () => {
const fakeProps = (): FbosDetailsProps => {
return {
dispatch: jest.fn(),
bot: bot,
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("renders", () => {
const wrapper = mount(<FbosDetails {...bot} />);
const wrapper = mount(<FbosDetails {...fakeProps() } />);
["Environment: ---",
"Commit: ---",
"Target: ---",

View File

@ -1,32 +1,30 @@
import * as React from "react";
import { mount } from "enzyme";
import { HardwareSettings } from "../hardware_settings";
import { fakeState } from "../../../__test_support__/fake_state";
import { ControlPanelState } from "../../interfaces";
import { HardwareSettingsProps } from "../../interfaces";
import { Actions } from "../../../constants";
import { bot } from "../../../__test_support__/fake_state/bot";
import { panelState } from "../../../__test_support__/control_panel_state";
describe("<HardwareSettings />", () => {
beforeEach(() => {
jest.clearAllMocks();
});
function panelState(): ControlPanelState {
const fakeProps = (): HardwareSettingsProps => {
return {
homing_and_calibration: false,
motors: false,
encoders_and_endstops: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false
bot,
controlPanelState: panelState(),
botToMqttStatus: "up",
dispatch: jest.fn(),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
}
};
it("renders", () => {
const wrapper = mount(<HardwareSettings
controlPanelState={panelState()}
dispatch={jest.fn()}
bot={fakeState().bot}
botToMqttStatus={"up"} />);
const wrapper = mount(<HardwareSettings {...fakeProps() } />);
["expand all", "x axis", "motors"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
});
@ -37,16 +35,12 @@ describe("<HardwareSettings />", () => {
buttonText: string,
type: string,
payload: boolean | string) {
const dispatch = jest.fn();
const wrapper = mount(<HardwareSettings
controlPanelState={panelState()}
dispatch={dispatch}
bot={fakeState().bot}
botToMqttStatus={"up"} />);
const p = fakeProps();
const wrapper = mount(<HardwareSettings {...p} />);
const button = wrapper.find(buttonElement).at(buttonIndex);
expect(button.text().toLowerCase()).toContain(buttonText);
button.simulate("click");
expect(dispatch).toHaveBeenCalledWith({ payload, type });
expect(p.dispatch).toHaveBeenCalledWith({ payload, type });
}
it("expands all", () => {

View File

@ -0,0 +1,27 @@
import { sourceFbosConfigValue } from "../source_fbos_config_value";
import { bot } from "../../../__test_support__/fake_state/bot";
import { fakeFbosConfig } from "../../../__test_support__/fake_state/resources";
describe("sourceFbosConfigValue()", () => {
it("returns api value", () => {
const fakeConfig = fakeFbosConfig().body;
fakeConfig.auto_sync = false;
bot.hardware.configuration.auto_sync = true;
const source = sourceFbosConfigValue(fakeConfig, bot.hardware.configuration);
expect(source("auto_sync")).toEqual({ value: false, consistent: false });
});
it("returns bot value", () => {
bot.hardware.configuration.auto_sync = true;
const source = sourceFbosConfigValue(undefined, bot.hardware.configuration);
expect(source("auto_sync")).toEqual({ value: true, consistent: true });
});
it("returns api value: consistent with bot", () => {
const fakeConfig = fakeFbosConfig().body;
fakeConfig.auto_sync = true;
bot.hardware.configuration.auto_sync = true;
const source = sourceFbosConfigValue(fakeConfig, bot.hardware.configuration);
expect(source("auto_sync")).toEqual({ value: true, consistent: true });
});
});

View File

@ -1,25 +1,28 @@
import * as React from "react";
import * as _ from "lodash";
import { BlurableInput } from "../../ui/index";
import { StepsPerMMBoxProps } from "../interfaces";
import { SourceFbosConfig } from "../interfaces";
import { ConfigurationName } from "farmbot/dist";
import { updateConfig } from "../actions";
/**
* Steps per mm is not an actual Arduino command.
* We needed to fake it on the UI layer to give the appearance that the settings
* all coming from the same place.
*/
export class BotConfigInputBox extends React.Component<StepsPerMMBoxProps, {}> {
export interface BotConfigInputBoxProps {
setting: ConfigurationName;
dispatch: Function;
disabled?: boolean;
sourceFbosConfig: SourceFbosConfig;
}
get setting() { return this.props.setting; }
export class BotConfigInputBox
extends React.Component<BotConfigInputBoxProps, {}> {
get config() { return this.props.bot.hardware.configuration; }
get config() {
return this.props.sourceFbosConfig(this.props.setting);
}
change = (key: ConfigurationName, dispatch: Function) => {
return (event: React.FormEvent<HTMLInputElement>) => {
const next = parseInt(event.currentTarget.value, 10);
const current = this.config[this.setting];
const current = this.config.value;
if (!_.isNaN(next) && (next !== current)) {
dispatch(updateConfig({ [key]: next }));
}
@ -27,13 +30,15 @@ export class BotConfigInputBox extends React.Component<StepsPerMMBoxProps, {}> {
}
render() {
const hmm = this.config[this.setting];
const value = (_.isNumber(hmm) || _.isBoolean(hmm)) ? hmm.toString() : "";
const current = this.config.value;
const boxValue = (_.isNumber(current) || _.isBoolean(current))
? current.toString() : "";
return <BlurableInput
type="number"
className={!this.config.consistent ? "dim" : ""}
onCommit={this.change(this.props.setting, this.props.dispatch)}
value={value}
value={boxValue}
disabled={this.props.disabled} />;
}
}

View File

@ -91,25 +91,11 @@ export class FarmbotOsSettings
device={this.props.account} />;
}
// TODO: Delete this function on 1 Jan 2018. This is a backwards compatibility
// fix because old FBOS breaks when `auto_sync` is toggled. - RC
maybeShowAutoSync = () => {
const { auto_sync } = this.props.bot.hardware.configuration;
const isDevMode = location.host.includes("localhost"); // Enable in dev.
// Old FBOS => no auto_sync option => breaks when toggled.
const properFbosVersion = !isUndefined(auto_sync);
if (isDevMode || properFbosVersion) {
return <AutoSyncRow currentValue={!!auto_sync} />;
}
}
render() {
const { account } = this.props;
const { hardware } = this.props.bot;
const { firmware_version } = hardware.informational_settings;
const { controller_version } = hardware.informational_settings;
const { account, sourceFbosConfig } = this.props;
const {
firmware_version, controller_version, sync_status
} = this.props.bot.hardware.informational_settings;
return <Widget className="device-widget">
<form onSubmit={(e) => e.preventDefault()}>
<WidgetHeader title="Device" helpText={ToolTips.OS_SETTINGS}>
@ -149,20 +135,32 @@ export class FarmbotOsSettings
</Row>
<this.lastSeen />
<MustBeOnline
syncStatus={this.props.bot.hardware.informational_settings.sync_status}
syncStatus={sync_status}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<FarmbotOsRow
bot={this.props.bot}
controller_version={controller_version}
osReleaseNotes={this.state.osReleaseNotes} />
<AutoUpdateRow bot={this.props.bot} />
{this.maybeShowAutoSync()}
<CameraSelection env={hardware.user_env} />
<BoardType firmwareVersion={firmware_version} />
osReleaseNotes={this.state.osReleaseNotes}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
{(location.host.includes("localhost")
|| !isUndefined(sourceFbosConfig("auto_sync").value)) &&
<AutoSyncRow
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />}
<CameraSelection env={this.props.bot.hardware.user_env} />
<BoardType
firmwareVersion={firmware_version}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<PowerAndReset
bot={this.props.bot}
dispatch={this.props.dispatch} />
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
</MustBeOnline>
</WidgetBody>
</form>

View File

@ -10,16 +10,28 @@ import * as React from "react";
import { AutoSyncRow } from "../auto_sync_row";
import { mount } from "enzyme";
import { Content } from "../../../../constants";
import { AutoSyncRowProps } from "../interfaces";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<AutoSyncRow/>", () => {
const fakeProps = (): AutoSyncRowProps => {
return {
dispatch: jest.fn(x => x()),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("renders", () => {
const wrapper = mount(<AutoSyncRow currentValue={true} />);
const wrapper = mount(<AutoSyncRow {...fakeProps() } />);
["AUTO SYNC", Content.AUTO_SYNC]
.map(string => expect(wrapper.text()).toContain(string));
});
it("toggles", () => {
const wrapper = mount(<AutoSyncRow currentValue={true} />);
bot.hardware.configuration.auto_sync = true;
const wrapper = mount(<AutoSyncRow {...fakeProps() } />);
wrapper.find("button").simulate("click");
expect(mockDevice.updateConfig)
.toHaveBeenCalledWith({ auto_sync: false });

View File

@ -9,20 +9,30 @@ import * as React from "react";
import { AutoUpdateRow } from "../auto_update_row";
import { mount } from "enzyme";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { AutoUpdateRowProps } from "../interfaces";
describe("<AutoUpdateRow/>", () => {
beforeEach(function () {
jest.clearAllMocks();
});
const fakeProps = (): AutoUpdateRowProps => {
return {
dispatch: jest.fn(x => x()),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("renders", () => {
const wrapper = mount(<AutoUpdateRow bot={bot} />);
const wrapper = mount(<AutoUpdateRow {...fakeProps() } />);
expect(wrapper.text().toLowerCase()).toContain("auto update");
});
it("toggles auto-update on", () => {
bot.hardware.configuration.os_auto_update = 0;
const wrapper = mount(<AutoUpdateRow bot={bot} />);
const wrapper = mount(<AutoUpdateRow {...fakeProps() } />);
wrapper.find("button").first().simulate("click");
expect(mockDevice.updateConfig)
.toHaveBeenCalledWith({ os_auto_update: 1 });
@ -30,7 +40,7 @@ describe("<AutoUpdateRow/>", () => {
it("toggles auto-update off", () => {
bot.hardware.configuration.os_auto_update = 1;
const wrapper = mount(<AutoUpdateRow bot={bot} />);
const wrapper = mount(<AutoUpdateRow {...fakeProps() } />);
wrapper.find("button").first().simulate("click");
expect(mockDevice.updateConfig)
.toHaveBeenCalledWith({ os_auto_update: 0 });

View File

@ -15,41 +15,60 @@ jest.mock("farmbot-toastr", () => ({
import * as React from "react";
import { mount, shallow } from "enzyme";
import { BoardType } from "../board_type";
import { BoardTypeProps } from "../interfaces";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<BoardType/>", () => {
const fakeProps = (): BoardTypeProps => {
return {
firmwareVersion: "",
dispatch: jest.fn(),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("Farmduino", () => {
const wrapper = mount(<BoardType
firmwareVersion={"5.0.3.F"} />);
const p = fakeProps();
p.firmwareVersion = "5.0.3.F";
const wrapper = mount(<BoardType {...p} />);
expect(wrapper.text()).toContain("Farmduino");
});
it("Arduino/RAMPS", () => {
const wrapper = mount(<BoardType
firmwareVersion={"5.0.3.R"} />);
const p = fakeProps();
p.firmwareVersion = "5.0.3.R";
const wrapper = mount(<BoardType {...p} />);
expect(wrapper.text()).toContain("Arduino/RAMPS");
});
it("Other", () => {
const wrapper = mount(<BoardType
firmwareVersion={"4.0.2"} />);
const p = fakeProps();
p.firmwareVersion = "4.0.2";
const wrapper = mount(<BoardType {...p} />);
expect(wrapper.text()).toContain("Arduino/RAMPS");
});
it("Undefined", () => {
const wrapper = mount(<BoardType
firmwareVersion={undefined} />);
const p = fakeProps();
p.firmwareVersion = undefined;
const wrapper = mount(<BoardType {...p} />);
expect(wrapper.text()).toContain("None");
});
it("Disconnected", () => {
const wrapper = mount(<BoardType
firmwareVersion={"Arduino Disconnected!"} />);
const p = fakeProps();
p.firmwareVersion = "Arduino Disconnected!";
const wrapper = mount(<BoardType {...p} />);
expect(wrapper.text()).toContain("None");
});
it("calls updateConfig", () => {
const wrapper = shallow(<BoardType
firmwareVersion={"Arduino Disconnected!"} />);
const p = fakeProps();
p.firmwareVersion = "Arduino Disconnected!";
p.dispatch = jest.fn(x => Promise.resolve(x()));
const wrapper = shallow(<BoardType {...p} />);
wrapper.find("FBSelect").simulate("change",
{ label: "firmware_hardware", value: "farmduino" });
expect(mockDevice.updateConfig)

View File

@ -9,12 +9,23 @@ import * as React from "react";
import { FbosDetails } from "../farmbot_os_row";
import { shallow, mount } from "enzyme";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { FbosDetailsProps } from "../interfaces";
describe("<FbosDetails/>", () => {
beforeEach(function () {
jest.clearAllMocks();
});
const fakeProps = (): FbosDetailsProps => {
return {
bot,
dispatch: jest.fn(x => x()),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("renders", () => {
bot.hardware.informational_settings.env = "fakeEnv";
bot.hardware.informational_settings.commit = "fakeCommit";
@ -22,7 +33,7 @@ describe("<FbosDetails/>", () => {
bot.hardware.informational_settings.node_name = "fakeName";
bot.hardware.informational_settings.firmware_version = "fakeFirmware";
bot.hardware.informational_settings.firmware_commit = "fakeFwCommit";
const wrapper = shallow(<FbosDetails {...bot} />);
const wrapper = shallow(<FbosDetails {...fakeProps() } />);
["Environment", "fakeEnv",
"Commit", "fakeComm",
"Target", "fakeTarget",
@ -35,15 +46,16 @@ describe("<FbosDetails/>", () => {
});
it("simplifies node name", () => {
bot.hardware.informational_settings.node_name = "name@nodeName";
const wrapper = shallow(<FbosDetails {...bot} />);
const p = fakeProps();
p.bot.hardware.informational_settings.node_name = "name@nodeName";
const wrapper = shallow(<FbosDetails {...p} />);
expect(wrapper.text()).toContain("nodeName");
expect(wrapper.text()).not.toContain("name@");
});
it("toggles os beta opt in setting on", () => {
bot.hardware.configuration.beta_opt_in = false;
const wrapper = mount(<FbosDetails {...bot} />);
const wrapper = mount(<FbosDetails {...fakeProps() } />);
wrapper.find("button").simulate("click");
expect(mockDevice.updateConfig).not.toHaveBeenCalled();
window.confirm = () => true;
@ -54,7 +66,7 @@ describe("<FbosDetails/>", () => {
it("toggles os beta opt in setting off", () => {
bot.hardware.configuration.beta_opt_in = true;
const wrapper = mount(<FbosDetails {...bot} />);
const wrapper = mount(<FbosDetails {...fakeProps() } />);
window.confirm = () => false;
wrapper.find("button").simulate("click");
expect(mockDevice.updateConfig)

View File

@ -8,12 +8,25 @@ jest.mock("../../../../device", () => ({
import * as React from "react";
import { PowerAndReset } from "../power_and_reset";
import { mount } from "enzyme";
import { PowerAndResetProps } from "../interfaces";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { panelState } from "../../../../__test_support__/control_panel_state";
describe("<PowerAndReset/>", () => {
const fakeProps = (): PowerAndResetProps => {
return {
controlPanelState: panelState(),
dispatch: jest.fn(x => x()),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("open", () => {
bot.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset bot={bot} dispatch={jest.fn()} />);
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
["Power and Reset", "Restart", "Shutdown", "Factory Reset",
"Automatic Factory Reset", "Connection Attempt Period"]
.map(string => expect(wrapper.text().toLowerCase())
@ -21,8 +34,9 @@ describe("<PowerAndReset/>", () => {
});
it("closed", () => {
bot.controlPanelState.power_and_reset = false;
const wrapper = mount(<PowerAndReset bot={bot} dispatch={jest.fn()} />);
const p = fakeProps();
p.controlPanelState.power_and_reset = false;
const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("Power and Reset".toLowerCase());
expect(wrapper.text().toLowerCase())
@ -30,18 +44,20 @@ describe("<PowerAndReset/>", () => {
});
it("timer input disabled", () => {
bot.controlPanelState.power_and_reset = true;
bot.hardware.configuration.disable_factory_reset = true;
const wrapper = mount(<PowerAndReset bot={bot} dispatch={jest.fn()} />);
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.find("input").last().props().disabled).toBeTruthy();
expect(wrapper.find("label").last().props().style)
.toEqual({ color: "grey" });
});
it("toggles auto reset", () => {
bot.controlPanelState.power_and_reset = true;
bot.hardware.configuration.disable_factory_reset = false;
const wrapper = mount(<PowerAndReset bot={bot} dispatch={jest.fn()} />);
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
wrapper.find("button").at(3).simulate("click");
expect(mockDevice.updateConfig)
.toHaveBeenCalledWith({ disable_factory_reset: true });

View File

@ -4,13 +4,11 @@ import { t } from "i18next";
import { ToggleButton } from "../../../controls/toggle_button";
import { Content } from "../../../constants";
import { updateConfig } from "../../actions";
import { noop } from "lodash";
import { ColWidth } from "../farmbot_os_settings";
interface AutoSyncRowProps { currentValue: boolean; }
import { AutoSyncRowProps } from "./interfaces";
export function AutoSyncRow(props: AutoSyncRowProps) {
const auto_sync = !props.currentValue;
const autoSync = props.sourceFbosConfig("auto_sync");
return <Row>
<Col xs={ColWidth.label}>
<label>
@ -23,9 +21,11 @@ export function AutoSyncRow(props: AutoSyncRowProps) {
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={props.currentValue}
<ToggleButton
toggleValue={autoSync.value}
dim={!autoSync.consistent}
toggleAction={() => {
updateConfig({ auto_sync })(noop);
props.dispatch(updateConfig({ auto_sync: !autoSync.value }));
}} />
</Col>
</Row>;

View File

@ -1,19 +1,14 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { t } from "i18next";
import { BotState } from "../../interfaces";
import { ColWidth } from "../farmbot_os_settings";
import { ToggleButton } from "../../../controls/toggle_button";
import { updateConfig } from "../../actions";
import { noop } from "lodash";
import { Content } from "../../../constants";
interface AutoUpdateRowProps {
bot: BotState;
}
import { AutoUpdateRowProps } from "./interfaces";
export function AutoUpdateRow(props: AutoUpdateRowProps) {
const { os_auto_update } = props.bot.hardware.configuration;
const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
return <Row>
<Col xs={ColWidth.label}>
<label>
@ -26,10 +21,11 @@ export function AutoUpdateRow(props: AutoUpdateRowProps) {
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={os_auto_update}
<ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent}
toggleAction={() => {
const newOsAutoUpdateNum = !os_auto_update ? 1 : 0;
updateConfig({ os_auto_update: newOsAutoUpdateNum })(noop);
const newOsAutoUpdateNum = !osAutoUpdate.value ? 1 : 0;
props.dispatch(updateConfig({ os_auto_update: newOsAutoUpdateNum }));
}} />
</Col>
</Row>;

View File

@ -1,14 +1,11 @@
import * as React from "react";
import { Row, Col, DropDownItem, FBSelect } from "../../../ui/index";
import { t } from "i18next";
import { getDevice } from "../../../device";
import { info, error } from "farmbot-toastr";
import { info } from "farmbot-toastr";
import { FirmwareHardware } from "farmbot";
import { ColWidth } from "../farmbot_os_settings";
export interface BoardTypeProps {
firmwareVersion: string | undefined;
}
import { updateConfig } from "../../actions";
import { BoardTypeProps } from "./interfaces";
const FIRMWARE_CHOICES = [
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
@ -26,10 +23,23 @@ const FIRMWARE_CHOICES_DDI = {
}
};
export class BoardType
extends React.Component<BoardTypeProps, {}> {
interface BoardTypeState { boardType: string, sending: boolean }
getBoardType() {
export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
state = {
boardType: this.boardType,
sending: this.sending
};
componentWillReceiveProps() {
this.setState({ sending: this.sending });
}
get sending() {
return !this.props.sourceFbosConfig("firmware_hardware").consistent;
}
get boardType() {
if (this.props.firmwareVersion) {
const boardIdentifier = this.props.firmwareVersion.slice(-1);
switch (boardIdentifier) {
@ -47,9 +57,8 @@ export class BoardType
}
}
selectedBoard(): DropDownItem | undefined {
const board = this.getBoardType();
switch (board) {
get selectedBoard(): DropDownItem | undefined {
switch (this.state.boardType) {
case "Arduino/RAMPS":
case "Present":
return FIRMWARE_CHOICES_DDI["arduino"];
@ -60,19 +69,19 @@ export class BoardType
}
}
sendOffConfig = (selectedBoard: DropDownItem) => {
sendOffConfig = (selectedItem: DropDownItem) => {
// tslint:disable-next-line:no-any
const isFwHardwareValue = (x?: any): x is FirmwareHardware => {
const values: FirmwareHardware[] = ["arduino", "farmduino"];
return !!values.includes(x as FirmwareHardware);
};
const firmware_hardware = selectedBoard.value;
if (selectedBoard && isFwHardwareValue(firmware_hardware)) {
const firmware_hardware = selectedItem.value;
if (selectedItem && isFwHardwareValue(firmware_hardware)) {
info(t("Sending firmware configuration..."), t("Sending"));
getDevice()
.updateConfig({ firmware_hardware })
.catch(() => error(t("An error occurred during configuration.")));
this.props.dispatch(updateConfig({ firmware_hardware }));
this.setState({ sending: true });
this.forceUpdate();
}
}
@ -86,11 +95,10 @@ export class BoardType
<Col xs={ColWidth.description}>
<div>
<FBSelect
key={this.getBoardType()}
allowEmpty={true}
key={this.state.boardType}
extraClass={this.state.sending ? "dim" : ""}
list={FIRMWARE_CHOICES}
selectedItem={this.selectedBoard()}
placeholder={this.getBoardType()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</div>
</Col>

View File

@ -4,14 +4,14 @@ import { t } from "i18next";
import { Content } from "../../../constants";
import { factoryReset, updateConfig } from "../../actions";
import { ToggleButton } from "../../../controls/toggle_button";
import { noop } from "lodash";
import { BotConfigInputBox } from "../step_per_mm_box";
import { PowerAndResetProps } from "./power_and_reset";
import { BotConfigInputBox } from "../bot_config_input_box";
import { FactoryResetRowProps } from "./interfaces";
import { ColWidth } from "../farmbot_os_settings";
export function FactoryResetRow(props: PowerAndResetProps) {
const { disable_factory_reset } = props.bot.hardware.configuration;
const maybeDisableTimer = disable_factory_reset ? { color: "grey" } : {};
export function FactoryResetRow(props: FactoryResetRowProps) {
const { dispatch, sourceFbosConfig } = props;
const diableFactoryReset = sourceFbosConfig("disable_factory_reset");
const maybeDisableTimer = diableFactoryReset.value ? { color: "grey" } : {};
return <div>
<Row>
<Col xs={ColWidth.label}>
@ -45,11 +45,13 @@ export function FactoryResetRow(props: PowerAndResetProps) {
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={!disable_factory_reset}
<ToggleButton
toggleValue={diableFactoryReset.value}
dim={!diableFactoryReset.consistent}
toggleAction={() => {
updateConfig({
disable_factory_reset: !disable_factory_reset
})(noop);
dispatch(updateConfig({
disable_factory_reset: !diableFactoryReset.value
}));
}} />
</Col>
</Row>
@ -67,9 +69,9 @@ export function FactoryResetRow(props: PowerAndResetProps) {
<Col xs={ColWidth.button}>
<BotConfigInputBox
setting="network_not_found_timer"
bot={props.bot}
dispatch={props.dispatch}
disabled={disable_factory_reset} />
dispatch={dispatch}
disabled={!!diableFactoryReset.value}
sourceFbosConfig={sourceFbosConfig} />
</Col>
</Row >
</div >;

View File

@ -2,25 +2,20 @@ import * as React from "react";
import { Row, Col, Markdown } from "../../../ui/index";
import { t } from "i18next";
import { OsUpdateButton } from "./os_update_button";
import { BotState } from "../../interfaces";
import { Popover, Position } from "@blueprintjs/core";
import { ColWidth } from "../farmbot_os_settings";
import { ToggleButton } from "../../../controls/toggle_button";
import { updateConfig } from "../../actions";
import { noop, last } from "lodash";
import { last } from "lodash";
import { Content } from "../../../constants";
import { FbosDetailsProps, FarmbotOsRowProps } from "./interfaces";
interface FarmbotOsRowProps {
controller_version: string | undefined;
bot: BotState;
osReleaseNotes: string;
}
export function FbosDetails(bot: BotState) {
export function FbosDetails(props: FbosDetailsProps) {
const { dispatch, sourceFbosConfig } = props;
const {
env, commit, target, node_name, firmware_version, firmware_commit
} = bot.hardware.informational_settings;
const { beta_opt_in } = bot.hardware.configuration;
} = props.bot.hardware.informational_settings;
const betaOptIn = sourceFbosConfig("beta_opt_in");
const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8);
return <div>
<p><b>Environment: </b>{env}</p>
@ -34,16 +29,20 @@ export function FbosDetails(bot: BotState) {
{t("Beta release Opt-In")}
</label>
<ToggleButton
toggleValue={beta_opt_in}
toggleValue={betaOptIn.value}
dim={!betaOptIn.consistent}
toggleAction={() =>
(beta_opt_in || confirm(Content.OS_BETA_RELEASES)) &&
updateConfig({ beta_opt_in: !beta_opt_in })(noop)} />
(betaOptIn.value || confirm(Content.OS_BETA_RELEASES)) &&
dispatch(updateConfig({ beta_opt_in: !betaOptIn.value }))} />
</fieldset>
</div>;
}
export function FarmbotOsRow(props: FarmbotOsRowProps) {
const version = props.controller_version || t(" unknown (offline)");
const {
controller_version, sourceFbosConfig, dispatch, bot, osReleaseNotes
} = props;
const version = controller_version || t(" unknown (offline)");
return <Row>
<Col xs={ColWidth.label}>
<label>
@ -55,7 +54,10 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) {
<p>
{t("Version {{ version }}", { version })}
</p>
<FbosDetails {...props.bot} />
<FbosDetails
bot={bot}
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} />
</Popover>
</Col>
<Col xs={3}>
@ -66,13 +68,13 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) {
</p>
<div className="release-notes">
<Markdown>
{props.osReleaseNotes}
{osReleaseNotes}
</Markdown>
</div>
</Popover>
</Col>
<Col xs={3}>
<OsUpdateButton bot={props.bot} />
<OsUpdateButton bot={bot} />
</Col>
</Row >;
}

View File

@ -0,0 +1,42 @@
import { SourceFbosConfig, BotState, ControlPanelState } from "../../interfaces";
export interface AutoSyncRowProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface AutoUpdateRowProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface BoardTypeProps {
firmwareVersion: string | undefined;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface PowerAndResetProps {
controlPanelState: ControlPanelState;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface FactoryResetRowProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface FarmbotOsRowProps {
controller_version: string | undefined;
bot: BotState;
osReleaseNotes: string;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface FbosDetailsProps {
bot: BotState;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}

View File

@ -1,19 +1,14 @@
import * as React from "react";
import { Header } from "../hardware_settings/header";
import { Collapse } from "@blueprintjs/core";
import { BotState } from "../../interfaces";
import { RestartRow } from "./restart_row";
import { ShutdownRow } from "./shutdown_row";
import { FactoryResetRow } from "./factory_reset_row";
export interface PowerAndResetProps {
bot: BotState;
dispatch: Function;
}
import { PowerAndResetProps } from "./interfaces";
export function PowerAndReset(props: PowerAndResetProps) {
const { bot, dispatch } = props;
const { power_and_reset } = bot.controlPanelState;
const { dispatch, sourceFbosConfig } = props;
const { power_and_reset } = props.controlPanelState;
return <section>
<div style={{ fontSize: "1px" }}>
<Header
@ -25,7 +20,9 @@ export function PowerAndReset(props: PowerAndResetProps) {
<Collapse isOpen={!!power_and_reset}>
<RestartRow />
<ShutdownRow />
<FactoryResetRow bot={bot} dispatch={dispatch} />
<FactoryResetRow
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig} />
</Collapse>
</section>;
}

View File

@ -18,7 +18,7 @@ export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> {
render() {
const { bot, dispatch } = this.props;
const { bot, dispatch, sourceFbosConfig } = this.props;
const { sync_status } = this.props.bot.hardware.informational_settings;
return <Widget className="hardware-widget">
<WidgetHeader title="Hardware" helpText={ToolTips.HW_SETTINGS}>
@ -58,7 +58,8 @@ export class HardwareSettings extends
bot={bot} />
<Motors
dispatch={dispatch}
bot={bot} />
bot={bot}
sourceFbosConfig={sourceFbosConfig} />
<EncodersAndEndStops
dispatch={dispatch}
bot={bot} />

View File

@ -17,9 +17,18 @@ describe("<Motors/>", () => {
jest.clearAllMocks();
});
const fakeProps = (): MotorsProps => {
return {
dispatch: jest.fn(),
bot,
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("renders the base case", () => {
const props: MotorsProps = { dispatch: jest.fn(), bot };
const el = render(<Motors {...props} />);
const el = render(<Motors {...fakeProps() } />);
const txt = el.text();
[ // Not a whole lot to test here....
"Enable 2nd X Motor",
@ -30,26 +39,26 @@ describe("<Motors/>", () => {
});
it("doesn't render homing speed", () => {
const props: MotorsProps = { dispatch: jest.fn(), bot };
props.bot.hardware.informational_settings.firmware_version = "4.0.0R";
const wrapper = render(<Motors {...props} />);
const p = fakeProps();
p.bot.hardware.informational_settings.firmware_version = "4.0.0R";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).not.toContain("Homing Speed");
});
it("renders homing speed", () => {
const props: MotorsProps = { dispatch: jest.fn(), bot };
props.bot.hardware.informational_settings.firmware_version = "5.1.0R";
const wrapper = render(<Motors {...props} />);
const p = fakeProps();
p.bot.hardware.informational_settings.firmware_version = "5.1.0R";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).toContain("Homing Speed");
});
function testParamToggle(
description: string, parameter: McuParamName, position: number) {
it(description, () => {
bot.controlPanelState.motors = true;
bot.hardware.mcu_params[parameter] = 1;
const props: MotorsProps = { dispatch: jest.fn(), bot };
const wrapper = mount(<Motors {...props} />);
const p = fakeProps();
p.bot.controlPanelState.motors = true;
p.bot.hardware.mcu_params[parameter] = 1;
const wrapper = mount(<Motors {...p} />);
wrapper.find("button").at(position).simulate("click");
expect(mockDevice.updateMcu)
.toHaveBeenCalledWith({ [parameter]: 0 });
@ -61,10 +70,18 @@ describe("<Motors/>", () => {
});
describe("<StepsPerMmSettings/>", () => {
const fakeProps = (): MotorsProps => {
return {
dispatch: jest.fn(),
bot,
sourceFbosConfig: jest.fn()
};
};
it("renders OS settings", () => {
const props: MotorsProps = { dispatch: jest.fn(), bot };
props.bot.hardware.informational_settings.firmware_version = "4.0.0R";
const wrapper = shallow(<StepsPerMmSettings {...props} />);
const p = fakeProps();
p.bot.hardware.informational_settings.firmware_version = "4.0.0R";
const wrapper = shallow(<StepsPerMmSettings {...p} />);
const firstInputProps = wrapper.find("BotConfigInputBox")
// tslint:disable-next-line:no-any
.first().props() as any;
@ -72,9 +89,9 @@ describe("<StepsPerMmSettings/>", () => {
});
it("renders mcu settings", () => {
const props: MotorsProps = { dispatch: jest.fn(), bot };
props.bot.hardware.informational_settings.firmware_version = "5.0.5R";
const wrapper = shallow(<StepsPerMmSettings {...props} />);
const p = fakeProps();
p.bot.hardware.informational_settings.firmware_version = "5.0.5R";
const wrapper = shallow(<StepsPerMmSettings {...p} />);
const firstInputProps = wrapper.find("NumericMCUInputGroup")
.first().props();
expect(firstInputProps.x).toBe("movement_step_per_mm_x");

View File

@ -6,7 +6,7 @@ import { SpacePanelToolTip } from "../space_panel_tool_tip";
import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { BotConfigInputBox } from "../step_per_mm_box";
import { BotConfigInputBox } from "../bot_config_input_box";
import { MotorsProps } from "../interfaces";
import { Row, Col } from "../../../ui/index";
import { Header } from "./header";
@ -14,7 +14,8 @@ import { Collapse } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { minFwVersionCheck } from "../../../util";
export function StepsPerMmSettings({ dispatch, bot }: MotorsProps) {
export function StepsPerMmSettings(props: MotorsProps) {
const { dispatch, bot, sourceFbosConfig } = props;
const { firmware_version } = bot.hardware.informational_settings;
if (minFwVersionCheck(firmware_version, "5.0.5")) {
return <NumericMCUInputGroup
@ -36,27 +37,27 @@ export function StepsPerMmSettings({ dispatch, bot }: MotorsProps) {
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_x"
bot={bot}
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_y"
bot={bot}
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
<Col xs={2}>
<BotConfigInputBox
setting="steps_per_mm_z"
bot={bot}
sourceFbosConfig={sourceFbosConfig}
dispatch={dispatch} />
</Col>
</Row>;
}
}
export function Motors({ dispatch, bot }: MotorsProps) {
export function Motors(props: MotorsProps) {
const { dispatch, bot, sourceFbosConfig } = props;
const { mcu_params } = bot.hardware;
const { motors } = bot.controlPanelState;
const { firmware_version } = bot.hardware.informational_settings;
@ -131,7 +132,8 @@ export function Motors({ dispatch, bot }: MotorsProps) {
dispatch={dispatch} />
<StepsPerMmSettings
dispatch={dispatch}
bot={bot} />
bot={bot}
sourceFbosConfig={sourceFbosConfig} />
<BooleanMCUInputGroup
name={t("Always Power Motors")}
tooltip={t(ToolTips.ALWAYS_POWER_MOTORS)}

View File

@ -1,4 +1,4 @@
import { BotState } from "../interfaces";
import { BotState, SourceFbosConfig } from "../interfaces";
import { McuParamName, McuParams } from "farmbot/dist";
import { IntegerSize } from "../../util";
@ -58,6 +58,7 @@ export interface PinGuardProps {
export interface MotorsProps {
dispatch: Function;
bot: BotState;
sourceFbosConfig: SourceFbosConfig;
}
export interface EncodersProps {

View File

@ -0,0 +1,15 @@
import { FbosConfig } from "../../config_storage/fbos_configs";
import { Configuration, ConfigurationName } from "farmbot";
import { SourceFbosConfig } from "../interfaces";
export const sourceFbosConfigValue =
(apiConfig: FbosConfig | undefined, botConfig: Configuration
): SourceFbosConfig =>
(setting: ConfigurationName) => {
const apiValue = apiConfig && apiConfig[setting as keyof FbosConfig];
const botValue = botConfig[setting];
return {
value: apiConfig ? apiValue : botValue,
consistent: apiConfig ? apiValue === botValue : true
};
};

View File

@ -64,7 +64,8 @@ export class Devices extends React.Component<Props, {}> {
dispatch={this.props.dispatch}
bot={this.props.bot}
botToMqttLastSeen={botToMqttLastSeen}
botToMqttStatus={botToMqttStatus} />
botToMqttStatus={botToMqttStatus}
sourceFbosConfig={this.props.sourceFbosConfig} />
<ConnectivityPanel
status={this.props.deviceAccount.specialStatus}
onRefresh={this.refresh}
@ -82,7 +83,8 @@ export class Devices extends React.Component<Props, {}> {
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
bot={this.props.bot}
botToMqttStatus={botToMqttStatus} />
botToMqttStatus={botToMqttStatus}
sourceFbosConfig={this.props.sourceFbosConfig} />
{this.props.bot.hardware.gpio_registry &&
<PinBindings
dispatch={this.props.dispatch}

View File

@ -1,7 +1,6 @@
import { BotStateTree } from "farmbot";
import { BotStateTree, ConfigurationName } from "farmbot";
import {
McuParamName,
ConfigurationName,
Dictionary,
SyncStatus,
FarmwareManifest,
@ -30,8 +29,15 @@ export interface Props {
images: TaggedImage[];
dispatch: Function;
resources: ResourceIndex;
sourceFbosConfig: SourceFbosConfig;
}
export type SourceFbosConfig = (config: ConfigurationName) =>
{
value: boolean | number | string | undefined,
consistent: boolean
};
/** How the device is stored in the API side.
* This is what comes back from the API as JSON.
*/
@ -105,6 +111,7 @@ export interface FarmbotOsProps {
botToMqttStatus: NetworkState;
botToMqttLastSeen: string;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface FarmbotOsState {
@ -119,13 +126,6 @@ export interface CameraSelectionState {
cameraStatus: "" | "sending" | "done" | "error";
}
export interface StepsPerMMBoxProps {
bot: BotState;
setting: ConfigurationName;
dispatch: Function;
disabled?: boolean;
}
export interface McuInputBoxProps {
bot: BotState;
setting: McuParamName;
@ -166,6 +166,7 @@ export interface HardwareSettingsProps {
dispatch: Function;
botToMqttStatus: NetworkState;
bot: BotState;
sourceFbosConfig: SourceFbosConfig;
}
export interface ControlPanelState {

View File

@ -2,10 +2,16 @@ import { Everything } from "../interfaces";
import { Props } from "./interfaces";
import {
selectAllImages,
getDeviceAccountSettings
getDeviceAccountSettings,
getFbosConfig
} from "../resources/selectors";
import { sourceFbosConfigValue } from "./components/source_fbos_config_value";
export function mapStateToProps(props: Everything): Props {
const conf = getFbosConfig(props.resources.index);
const { hardware } = props.bot;
const fbosConfig = (conf && conf.body && conf.body.api_migrated)
? conf.body : undefined;
return {
userToApi: props.bot.connectivity["user.api"],
userToMqtt: props.bot.connectivity["user.mqtt"],
@ -16,5 +22,6 @@ export function mapStateToProps(props: Everything): Props {
dispatch: props.dispatch,
images: selectAllImages(props.resources.index),
resources: props.resources.index,
sourceFbosConfig: sourceFbosConfigValue(fbosConfig, hardware.configuration),
};
}

View File

@ -65,8 +65,18 @@ describe("<Logs />", () => {
});
}
const fakeProps = () => {
return {
logs: fakeLogs(),
bot,
timeOffset: 0,
dispatch: jest.fn(),
sourceFbosConfig: jest.fn()
};
};
it("renders", () => {
const wrapper = mount(<Logs logs={fakeLogs()} bot={bot} timeOffset={0} />);
const wrapper = mount(<Logs {...fakeProps() } />);
["Logs", ToolTips.LOGS, "Type", "Message", "Time", "Info",
"Fake log message 1", "Success", "Fake log message 2"]
.map(string =>
@ -77,7 +87,7 @@ describe("<Logs />", () => {
});
it("filters logs", () => {
const wrapper = mount(<Logs logs={fakeLogs()} bot={bot} timeOffset={0} />);
const wrapper = mount(<Logs {...fakeProps() } />);
wrapper.setState({ info: 0 });
expect(wrapper.text()).not.toContain("Fake log message 1");
const filterBtn = wrapper.find("button").first();
@ -86,31 +96,31 @@ describe("<Logs />", () => {
});
it("shows position", () => {
const logs = fakeLogs();
logs[0].body.meta.x = 100;
logs[1].body.meta.x = 0;
logs[1].body.meta.y = 1;
logs[1].body.meta.z = 2;
const wrapper = mount(<Logs logs={logs} bot={bot} timeOffset={0} />);
const p = fakeProps();
p.logs[0].body.meta.x = 100;
p.logs[1].body.meta.x = 0;
p.logs[1].body.meta.y = 1;
p.logs[1].body.meta.z = 2;
const wrapper = mount(<Logs {...p} />);
expect(wrapper.text()).toContain("Unknown");
expect(wrapper.text()).toContain("0, 1, 2");
});
it("shows verbosity", () => {
const logs = fakeLogs();
logs[0].body.meta.verbosity = -999;
const wrapper = mount(<Logs logs={logs} bot={bot} timeOffset={0} />);
const p = fakeProps();
p.logs[0].body.meta.verbosity = -999;
const wrapper = mount(<Logs {...p} />);
expect(wrapper.text()).toContain(-999);
});
it("loads filter setting", () => {
mockStorj[NumericSetting.warn_log] = 3;
const wrapper = mount(<Logs logs={fakeLogs()} bot={bot} timeOffset={0} />);
const wrapper = mount(<Logs {...fakeProps() } />);
expect(wrapper.state().warn).toEqual(3);
});
it("shows overall filter status", () => {
const wrapper = mount(<Logs logs={fakeLogs()} bot={bot} timeOffset={0} />);
const wrapper = mount(<Logs {...fakeProps() } />);
wrapper.setState({
success: 3, busy: 3, warn: 3, error: 3, info: 3, fun: 3, debug: 3
});
@ -121,7 +131,7 @@ describe("<Logs />", () => {
it("toggles filter", () => {
mockStorj[NumericSetting.warn_log] = 3;
const wrapper = mount(<Logs logs={fakeLogs()} bot={bot} timeOffset={0} />);
const wrapper = mount(<Logs {...fakeProps() } />);
// tslint:disable-next-line:no-any
const instance = wrapper.instance() as any;
expect(wrapper.state().warn).toEqual(3);
@ -133,7 +143,7 @@ describe("<Logs />", () => {
it("sets filter", () => {
mockStorj[NumericSetting.warn_log] = 3;
const wrapper = mount(<Logs logs={fakeLogs()} bot={bot} timeOffset={0} />);
const wrapper = mount(<Logs {...fakeProps() } />);
// tslint:disable-next-line:no-any
const instance = wrapper.instance() as any;
expect(wrapper.state().warn).toEqual(3);

View File

@ -5,6 +5,7 @@ import { TaggedLog, SpecialStatus } from "../../resources/tagged_resources";
import { Log } from "../../interfaces";
import { generateUuid } from "../../resources/util";
import { times } from "lodash";
import { fakeFbosConfig } from "../../__test_support__/fake_state/resources";
describe("mapStateToProps()", () => {
function fakeLogs(count: number): TaggedLog[] {
@ -33,4 +34,30 @@ describe("mapStateToProps()", () => {
const props = mapStateToProps(state);
expect(props.logs.length).toEqual(250);
});
it("API source of FBOS settings", () => {
const state = fakeState();
state.bot.hardware.configuration.sequence_init_log = false;
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.sequence_init_log = true;
fakeApiConfig.body.api_migrated = true;
state.resources = buildResourceIndex([fakeApiConfig]);
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("sequence_init_log")).toEqual({
value: true, consistent: false
});
});
it("bot source of FBOS settings", () => {
const state = fakeState();
state.bot.hardware.configuration.sequence_init_log = false;
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.sequence_init_log = true;
fakeApiConfig.body.api_migrated = false;
state.resources = buildResourceIndex([fakeApiConfig]);
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("sequence_init_log")).toEqual({
value: false, consistent: true
});
});
});

View File

@ -33,15 +33,25 @@ import { LogsSettingsMenu } from "../settings_menu";
import { bot } from "../../../__test_support__/fake_state/bot";
import { ConfigurationName, Dictionary } from "farmbot";
import { NumericSetting } from "../../../session_keys";
import { LogsSettingsMenuProps } from "../../interfaces";
describe("<LogsSettingsMenu />", () => {
beforeEach(() => {
jest.clearAllMocks();
});
const fakeProps = (): LogsSettingsMenuProps => {
return {
setFilterLevel: () => jest.fn(),
dispatch: jest.fn(x => x()),
sourceFbosConfig: (x) => {
return { value: bot.hardware.configuration[x], consistent: true };
}
};
};
it("renders", () => {
const wrapper = mount(<LogsSettingsMenu
bot={bot} setFilterLevel={() => jest.fn()} />);
const wrapper = mount(<LogsSettingsMenu {...fakeProps() } />);
["begin", "steps", "complete"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
});
@ -49,8 +59,7 @@ describe("<LogsSettingsMenu />", () => {
function testSettingToggle(setting: ConfigurationName, position: number) {
it("toggles setting", () => {
bot.hardware.configuration[setting] = false;
const wrapper = mount(<LogsSettingsMenu
bot={bot} setFilterLevel={() => jest.fn()} />);
const wrapper = mount(<LogsSettingsMenu {...fakeProps() } />);
wrapper.find("button").at(position).simulate("click");
expect(mockDevice.updateConfig)
.toHaveBeenCalledWith({ [setting]: true });
@ -64,9 +73,10 @@ describe("<LogsSettingsMenu />", () => {
testSettingToggle("arduino_debug_messages", 5);
it("conditionally increases filter level", () => {
const p = fakeProps();
const setFilterLevel = jest.fn();
const wrapper = mount(<LogsSettingsMenu
bot={bot} setFilterLevel={() => setFilterLevel} />);
p.setFilterLevel = () => setFilterLevel;
const wrapper = mount(<LogsSettingsMenu {...p} />);
mockStorj[NumericSetting.busy_log] = 0;
wrapper.find("button").at(0).simulate("click");
expect(setFilterLevel).toHaveBeenCalledWith(2);

View File

@ -4,27 +4,73 @@ import { Help } from "../../ui/index";
import { ToolTips } from "../../constants";
import { ToggleButton } from "../../controls/toggle_button";
import { updateConfig } from "../../devices/actions";
import { noop } from "lodash";
import {
LogSettingProps, LogsSettingsMenuProps, LogsState
} from "../interfaces";
import { Session, safeNumericSetting } from "../../session";
import { ConfigurationName } from "farmbot";
interface LogSettingRecord {
label: string;
setting: ConfigurationName;
tooltip: string;
}
const SEQUENCE_LOG_SETTINGS: LogSettingRecord[] = [
{
label: "Begin",
setting: "sequence_init_log",
tooltip: ToolTips.SEQUENCE_LOG_BEGIN
},
{
label: "Steps",
setting: "sequence_body_log",
tooltip: ToolTips.SEQUENCE_LOG_STEP
},
{
label: "Complete",
setting: "sequence_complete_log",
tooltip: ToolTips.SEQUENCE_LOG_END
}
];
const FIRMWARE_LOG_SETTINGS: LogSettingRecord[] = [
{
label: "Sent",
setting: "firmware_output_log",
tooltip: ToolTips.FIRMWARE_LOG_SENT
},
{
label: "Received",
setting: "firmware_input_log",
tooltip: ToolTips.FIRMWARE_LOG_RECEIVED
},
{
label: "Debug",
setting: "arduino_debug_messages",
tooltip: ToolTips.FIRMWARE_DEBUG_MESSAGES
},
];
const LogSetting = (props: LogSettingProps) => {
const { label, setting, toolTip, value, setFilterLevel } = props;
const { label, setting, toolTip, setFilterLevel, sourceFbosConfig } = props;
const updateMinFilterLevel = (name: keyof LogsState, level: number) => {
const currentLevel = Session.deprecatedGetNum(safeNumericSetting(name + "_log")) || 0;
const currentLevel =
Session.deprecatedGetNum(safeNumericSetting(name + "_log")) || 0;
if (currentLevel < level) { setFilterLevel(name)(level); }
};
const config = sourceFbosConfig(setting);
return <fieldset>
<label>
{t(label)}
</label>
<Help text={t(toolTip)} />
<ToggleButton toggleValue={value}
<ToggleButton
toggleValue={config.value}
dim={!config.consistent}
toggleAction={() => {
updateConfig({ [setting]: !value })(noop);
if (!value === true) {
props.dispatch(updateConfig({ [setting]: !config.value }));
if (!config.value === true) {
switch (setting) {
case "firmware_output_log":
case "firmware_input_log":
@ -47,46 +93,21 @@ const LogSetting = (props: LogSettingProps) => {
};
export const LogsSettingsMenu = (props: LogsSettingsMenuProps) => {
const { bot, setFilterLevel } = props;
const { configuration } = bot.hardware;
const { setFilterLevel, sourceFbosConfig } = props;
const LogSettingRow = (settingProps: LogSettingRecord) => {
const { label, setting, tooltip } = settingProps;
return <LogSetting
label={label}
setting={setting}
toolTip={tooltip}
setFilterLevel={setFilterLevel}
dispatch={props.dispatch}
sourceFbosConfig={sourceFbosConfig} />;
};
return <div className={"logs-settings-menu"}>
{t("Create logs for sequence:")}
<LogSetting
label={"Begin"}
setting={"sequence_init_log"}
toolTip={ToolTips.SEQUENCE_LOG_BEGIN}
value={configuration.sequence_init_log}
setFilterLevel={setFilterLevel} />
<LogSetting
label={"Steps"}
setting={"sequence_body_log"}
toolTip={ToolTips.SEQUENCE_LOG_STEP}
value={configuration.sequence_body_log}
setFilterLevel={setFilterLevel} />
<LogSetting
label={"Complete"}
setting={"sequence_complete_log"}
toolTip={ToolTips.SEQUENCE_LOG_END}
value={configuration.sequence_complete_log}
setFilterLevel={setFilterLevel} />
{SEQUENCE_LOG_SETTINGS.map(p => <LogSettingRow key={p.setting} {...p} />)}
{t("Firmware Logs:")}
<LogSetting
label={"Sent"}
setting={"firmware_output_log"}
toolTip={ToolTips.FIRMWARE_LOG_SENT}
value={configuration.firmware_output_log}
setFilterLevel={setFilterLevel} />
<LogSetting
label={"Received"}
setting={"firmware_input_log"}
toolTip={ToolTips.FIRMWARE_LOG_RECEIVED}
value={configuration.firmware_input_log}
setFilterLevel={setFilterLevel} />
<LogSetting
label={"Debug"}
setting={"arduino_debug_messages"}
toolTip={ToolTips.FIRMWARE_DEBUG_MESSAGES}
value={configuration.arduino_debug_messages}
setFilterLevel={setFilterLevel} />
{FIRMWARE_LOG_SETTINGS.map(p => <LogSettingRow key={p.setting} {...p} />)}
</div>;
};

View File

@ -89,7 +89,9 @@ export class Logs extends React.Component<LogsProps, Partial<LogsState>> {
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-gear" />
<LogsSettingsMenu
setFilterLevel={this.setFilterLevel} bot={this.props.bot} />
setFilterLevel={this.setFilterLevel}
dispatch={this.props.dispatch}
sourceFbosConfig={this.props.sourceFbosConfig} />
</Popover>
</div>
<div className={"settings-menu-button"}>

View File

@ -1,11 +1,13 @@
import { TaggedLog } from "../resources/tagged_resources";
import { BotState } from "../devices/interfaces";
import { BotState, SourceFbosConfig } from "../devices/interfaces";
import { ConfigurationName } from "farmbot";
export interface LogsProps {
logs: TaggedLog[];
bot: BotState;
timeOffset: number;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface Filters {
@ -41,11 +43,13 @@ export interface LogSettingProps {
label: string;
setting: ConfigurationName;
toolTip: string;
value: boolean | number | undefined;
setFilterLevel: SetNumSetting;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}
export interface LogsSettingsMenuProps {
bot: BotState;
setFilterLevel: SetNumSetting;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
}

View File

@ -1,10 +1,21 @@
import { Everything } from "../interfaces";
import { selectAllLogs, maybeGetTimeOffset } from "../resources/selectors";
import {
selectAllLogs, maybeGetTimeOffset, getFbosConfig
} from "../resources/selectors";
import * as _ from "lodash";
import { LogsProps } from "./interfaces";
import {
sourceFbosConfigValue
} from "../devices/components/source_fbos_config_value";
export function mapStateToProps(props: Everything): LogsProps {
const { hardware } = props.bot;
const conf = getFbosConfig(props.resources.index);
const fbosConfig = (conf && conf.body && conf.body.api_migrated)
? conf.body : undefined;
return {
dispatch: props.dispatch,
sourceFbosConfig: sourceFbosConfigValue(fbosConfig, hardware.configuration),
logs: _(selectAllLogs(props.resources.index))
.sortBy("body.created_at")
.reverse()

View File

@ -127,6 +127,7 @@ export let resourceReducer = generateReducer
case "User":
case "WebcamFeed":
case "WebAppConfig":
case "FbosConfig":
reindexResource(s.index, resource);
dontTouchThis(resource);
s.index.references[resource.uuid] = resource;
@ -154,6 +155,7 @@ export let resourceReducer = generateReducer
case "User":
case "WebcamFeed":
case "WebAppConfig":
case "FbosConfig":
case "Image":
removeFromIndex(s.index, resource);
break;

View File

@ -28,6 +28,7 @@ import {
TaggedUser,
TaggedWebcamFeed,
TaggedDevice,
TaggedFbosConfig,
TaggedWebAppConfig
} from "./tagged_resources";
import { CowardlyDictionary, betterCompact, sortResourcesById } from "../util";
@ -556,3 +557,10 @@ export function getWebAppConfig(i: ResourceIndex): TaggedWebAppConfig | undefine
return conf;
}
}
export function getFbosConfig(i: ResourceIndex): TaggedFbosConfig | undefined {
const conf = i.references[i.byKind.FbosConfig[0] || "NO"];
if (conf && conf.kind === "FbosConfig") {
return conf;
}
}

View File

@ -93,6 +93,7 @@ export type TaggedResource =
| TaggedTool
| TaggedUser
| TaggedWebcamFeed
| TaggedFbosConfig
| TaggedWebAppConfig;
export type TaggedRegimen = Resource<"Regimen", Regimen>;

View File

@ -14,6 +14,7 @@ import { HttpData } from "../util";
import { WebcamFeed } from "../controls/interfaces";
import { WebAppConfig } from "../config_storage/web_app_configs";
import { Session } from "../session";
import { FbosConfig } from "../config_storage/fbos_configs";
export interface ResourceReadyPayl {
name: ResourceName;
@ -40,6 +41,7 @@ export function fetchSyncData(dispatch: Function) {
fetch<User>("User", API.current.usersPath);
fetch<DeviceAccountSettings>("Device", API.current.devicePath);
fetch<WebcamFeed>("WebcamFeed", API.current.webcamFeedPath);
fetch<FbosConfig>("FbosConfig", API.current.fbosConfigPath);
fetch<WebAppConfig>("WebAppConfig", API.current.webAppConfigPath);
fetch<FarmEvent[]>("FarmEvent", API.current.farmEventsPath);
fetch<Image[]>("Image", API.current.imagesPath);

View File

@ -0,0 +1,51 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { FBSelect, FBSelectProps } from "../new_fb_select";
describe("<FBSelect />", () => {
const fakeProps = (): FBSelectProps => {
return {
selectedItem: undefined,
onChange: jest.fn(),
list: [{ value: "item", label: "Item" }]
};
};
it("renders", () => {
const p = fakeProps();
const wrapper = mount(<FBSelect {...p} />);
expect(wrapper.text()).toEqual("None");
});
it("renders item", () => {
const p = fakeProps();
p.selectedItem = { value: "item", label: "Item" };
const wrapper = mount(<FBSelect {...p} />);
expect(wrapper.text()).toEqual("Item");
});
it("allows empty", () => {
const p = fakeProps();
p.allowEmpty = true;
const wrapper = shallow(<FBSelect {...p} />);
// tslint:disable-next-line:no-any
expect((wrapper.find("FilterSearch").props() as any).items)
.toEqual([
{ label: "Item", value: "item" },
{ label: "None", value: "" }]);
});
it("doesn't allow empty", () => {
const wrapper = shallow(<FBSelect {...fakeProps() } />);
// tslint:disable-next-line:no-any
expect((wrapper.find("FilterSearch").props() as any).items)
.toEqual([{ label: "Item", value: "item" }]);
});
it("has extra class", () => {
const p = fakeProps();
p.extraClass = "extra";
const wrapper = mount(<FBSelect {...p} />);
expect(wrapper.find("div").first().hasClass("extra")).toBeTruthy();
});
});

View File

@ -14,6 +14,8 @@ export interface FBSelectProps {
allowEmpty?: boolean;
/** Text shown before user selection. */
placeholder?: string | undefined;
/** Extra class names to add. */
extraClass?: string;
}
export class FBSelect extends React.Component<FBSelectProps, {}> {
@ -32,7 +34,8 @@ export class FBSelect extends React.Component<FBSelectProps, {}> {
}
render() {
return <div className="filter-search">
const { extraClass } = this.props;
return <div className={`filter-search ${extraClass ? extraClass : ""}`}>
<FilterSearch
selectedItem={this.item}
items={this.list}