release channel UI update and cleanup

pull/1557/head
gabrielburnworth 2019-11-01 14:25:39 -07:00
parent 8eaf643de2
commit c4025dab40
13 changed files with 128 additions and 183 deletions

View File

@ -237,6 +237,25 @@ fieldset {
}
}
.os-release-channel {
width: 100%;
margin-bottom: 0.5rem;
label {
vertical-align: bottom;
margin-bottom: 0.75rem;
margin-right: 1rem;
}
.filter-search {
display: inline;
span {
width: 40%;
}
.bp3-popover-wrapper {
display: inline;
}
}
}
.all-content-wrapper {
margin: 0 auto;
padding: 11rem 3rem 0;

View File

@ -7,7 +7,7 @@ import {
} from "./interfaces";
import { Thunk } from "../redux/interfaces";
import {
McuParams, Configuration, TaggedFirmwareConfig, ParameterApplication,
McuParams, TaggedFirmwareConfig, ParameterApplication,
ALLOWED_PIN_MODES,
FirmwareHardware
} from "farmbot";
@ -381,11 +381,11 @@ export function updateMCU(key: ConfigKey, val: string) {
}
/** Update FBOS setting. */
export function updateConfig(config: Configuration) {
export function updateConfig(config: Partial<FbosConfig>) {
return function (dispatch: Function, getState: () => Everything) {
const fbosConfig = getFbosConfig(getState().resources.index);
if (fbosConfig) {
dispatch(edit(fbosConfig, config as Partial<FbosConfig>));
dispatch(edit(fbosConfig, config));
dispatch(apiSave(fbosConfig.uuid));
}
};

View File

@ -1,36 +1,49 @@
jest.mock("../../actions", () => ({ settingToggle: jest.fn() }));
import * as React from "react";
import { mount } from "enzyme";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions";
import { bot } from "../../../__test_support__/fake_state/bot";
import { BooleanMCUInputGroupProps } from "../interfaces";
describe("BooleanMCUInputGroup", () => {
const fakeProps = (): BooleanMCUInputGroupProps => ({
sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }),
dispatch: jest.fn(),
tooltip: "Tooltip",
name: "Name",
x: "encoder_invert_x",
y: "encoder_invert_y",
z: "encoder_invert_z",
});
enum Buttons { xAxis, yAxis, zAxis }
it("triggers callbacks", () => {
const dispatch = jest.fn();
const el = mount(<BooleanMCUInputGroup
sourceFwConfig={(x) => {
return { value: bot.hardware.mcu_params[x], consistent: true };
}}
dispatch={dispatch}
tooltip={"Tooltip"}
name={"Name"}
x={"encoder_invert_x"}
y={"encoder_invert_y"}
z={"encoder_enabled_z"} />);
enum Buttons { xAxis, yAxis, zAxis }
const xEl = el.find(ToggleButton).at(Buttons.xAxis);
const yEl = el.find(ToggleButton).at(Buttons.yAxis);
const zEl = el.find(ToggleButton).at(Buttons.zAxis);
xEl.simulate("click");
expect(settingToggle)
.toHaveBeenCalledWith("encoder_invert_x", expect.any(Function), undefined);
yEl.simulate("click");
expect(settingToggle)
.toHaveBeenCalledWith("encoder_invert_y", expect.any(Function), undefined);
zEl.simulate("click");
expect(settingToggle)
.toHaveBeenCalledWith("encoder_enabled_z", expect.any(Function), undefined);
const wrapper = mount(<BooleanMCUInputGroup {...fakeProps()} />);
const xAxisButton = wrapper.find(ToggleButton).at(Buttons.xAxis);
const yAxisButton = wrapper.find(ToggleButton).at(Buttons.yAxis);
const zAxisButton = wrapper.find(ToggleButton).at(Buttons.zAxis);
xAxisButton.simulate("click");
expect(settingToggle).toHaveBeenLastCalledWith("encoder_invert_x",
expect.any(Function), undefined);
yAxisButton.simulate("click");
expect(settingToggle).toHaveBeenLastCalledWith("encoder_invert_y",
expect.any(Function), undefined);
zAxisButton.simulate("click");
expect(settingToggle).toHaveBeenLastCalledWith("encoder_invert_z",
expect.any(Function), undefined);
});
it("displays gray toggles", () => {
const p = fakeProps();
p.grayscale = { x: true, y: false, z: false };
const wrapper = mount(<BooleanMCUInputGroup {...p} />);
const xAxisButton = wrapper.find(ToggleButton).at(Buttons.xAxis);
expect(xAxisButton.props().grayscale).toBeTruthy();
const yAxisButton = wrapper.find(ToggleButton).at(Buttons.yAxis);
expect(yAxisButton.props().grayscale).toBeFalsy();
});
});

View File

@ -8,23 +8,12 @@ describe("<SendDiagnosticReport/>", () => {
dispatch: jest.fn(),
diagnostics: [fakeDiagnosticDump()],
expanded: true,
shouldDisplay: jest.fn(() => true),
botOnline: true,
});
it("renders", () => {
const p = fakeProps();
p.shouldDisplay = jest.fn(() => true);
const wrapper = render(<SendDiagnosticReport {...p} />);
expect(wrapper.text()).toContain("DIAGNOSTIC CHECK");
expect(p.shouldDisplay).toHaveBeenCalled();
});
it("doesn't render", () => {
const p = fakeProps();
p.shouldDisplay = jest.fn(() => false);
const wrapper = render(<SendDiagnosticReport {...p} />);
expect(wrapper.text()).toEqual("");
expect(p.shouldDisplay).toHaveBeenCalled();
});
});

View File

@ -165,7 +165,6 @@ export class FarmbotOsSettings
<SendDiagnosticReport
diagnostics={this.props.diagnostics}
expanded={this.props.bot.controlPanelState.diagnostic_dumps}
shouldDisplay={this.props.shouldDisplay}
botOnline={isBotOnline(sync_status, botToMqttStatus)}
dispatch={this.props.dispatch} />
</MustBeOnline>

View File

@ -34,7 +34,7 @@ describe("<AutoUpdateRow/>", () => {
p.sourceFbosConfig = () => ({ value: 0, consistent: true });
const wrapper = mount(<AutoUpdateRow {...p} />);
wrapper.find("button").first().simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: 1 });
expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: true });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
@ -43,7 +43,7 @@ describe("<AutoUpdateRow/>", () => {
p.sourceFbosConfig = () => ({ value: 1, consistent: true });
const wrapper = mount(<AutoUpdateRow {...p} />);
wrapper.find("button").first().simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: 0 });
expect(edit).toHaveBeenCalledWith(fakeConfig, { os_auto_update: false });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
});

View File

@ -1,11 +1,9 @@
jest.mock("../../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
jest.mock("../../../actions", () => ({ updateConfig: jest.fn() }));
import * as React from "react";
import {
FbosDetails, colorFromTemp, betaReleaseOptIn, colorFromThrottle, ThrottleType
FbosDetails, colorFromTemp, colorFromThrottle, ThrottleType,
BetaReleaseOptInButtonProps, BetaReleaseOptIn,
} from "../fbos_details";
import { shallow, mount } from "enzyme";
import { bot } from "../../../../__test_support__/fake_state/bot";
@ -15,8 +13,8 @@ import { fakeState } from "../../../../__test_support__/fake_state";
import {
buildResourceIndex, fakeDevice
} from "../../../../__test_support__/resource_index_builder";
import { edit, save } from "../../../../api/crud";
import { fakeTimeSettings } from "../../../../__test_support__/fake_time_settings";
import { updateConfig } from "../../../actions";
describe("<FbosDetails/>", () => {
const fakeConfig = fakeFbosConfig();
@ -56,7 +54,7 @@ describe("<FbosDetails/>", () => {
"Firmware commit", "fakeFwCo",
"FAKETARGET CPU temperature", "48.3", "C",
"WiFi strength", "-49dBm",
"Beta release Opt-In",
"OS release channel",
"Uptime", "0 seconds",
"Memory usage", "0MB",
"Disk usage", "0%",
@ -88,31 +86,6 @@ describe("<FbosDetails/>", () => {
expect(wrapper.find("a").length).toEqual(0);
});
it("toggles os beta opt in setting on", () => {
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: false, consistent: true });
const wrapper = mount(<FbosDetails {...p} />);
window.confirm = jest.fn();
wrapper.find("button").simulate("click");
expect(window.confirm).toHaveBeenCalledWith(
expect.stringContaining("you sure?"));
expect(edit).not.toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
window.confirm = () => true;
wrapper.find("button").simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { beta_opt_in: true });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
it("toggles os beta opt in setting off", () => {
bot.hardware.configuration.beta_opt_in = true;
const wrapper = mount(<FbosDetails {...fakeProps()} />);
window.confirm = () => false;
wrapper.find("button").simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { beta_opt_in: false });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
it("displays N/A when wifi strength value is undefined", () => {
const p = fakeProps();
p.botInfoSettings.wifi_level = undefined;
@ -204,53 +177,33 @@ describe("<FbosDetails/>", () => {
});
});
describe("betaReleaseOptIn()", () => {
it("uses `beta_opt_in`: beta enabled", () => {
const result = betaReleaseOptIn({
sourceFbosConfig: () => ({ value: true, consistent: true }),
shouldDisplay: () => false
});
expect(result).toEqual({
betaOptIn: { consistent: true, value: true },
betaOptInValue: true,
update: { beta_opt_in: false }
});
describe("<BetaReleaseOptIn />", () => {
const fakeProps = (): BetaReleaseOptInButtonProps => ({
dispatch: jest.fn(),
sourceFbosConfig: () => ({ value: true, consistent: true }),
});
it("uses `beta_opt_in`: beta disabled", () => {
const result = betaReleaseOptIn({
sourceFbosConfig: () => ({ value: false, consistent: true }),
shouldDisplay: () => false
});
expect(result).toEqual({
betaOptIn: { consistent: true, value: false },
betaOptInValue: false,
update: { beta_opt_in: true }
});
it("changes to beta channel", () => {
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: "stable", consistent: true });
const wrapper = shallow(<BetaReleaseOptIn {...p} />);
window.confirm = jest.fn();
wrapper.find("FBSelect").simulate("change", { label: "", value: "" });
expect(window.confirm).toHaveBeenCalledWith(
expect.stringContaining("you sure?"));
expect(updateConfig).not.toHaveBeenCalled();
window.confirm = () => true;
wrapper.find("FBSelect").simulate("change", { label: "", value: "beta" });
expect(updateConfig).toHaveBeenCalledWith({ update_channel: "beta" });
});
it("uses `update_channel`: beta enabled", () => {
const result = betaReleaseOptIn({
sourceFbosConfig: () => ({ value: "beta", consistent: true }),
shouldDisplay: () => true
});
expect(result).toEqual({
betaOptIn: { consistent: true, value: true },
betaOptInValue: true,
update: { update_channel: "stable" }
});
});
it("uses `update_channel`: beta disabled", () => {
const result = betaReleaseOptIn({
sourceFbosConfig: () => ({ value: "stable", consistent: true }),
shouldDisplay: () => true
});
expect(result).toEqual({
betaOptIn: { consistent: true, value: false },
betaOptInValue: false,
update: { update_channel: "beta" }
});
it("changes to stable channel", () => {
const p = fakeProps();
p.sourceFbosConfig = () => ({ value: "beta", consistent: true });
const wrapper = shallow(<BetaReleaseOptIn {...p} />);
window.confirm = () => false;
wrapper.find("FBSelect").simulate("change", { label: "", value: "stable" });
expect(updateConfig).toHaveBeenCalledWith({ update_channel: "stable" });
});
});

View File

@ -40,10 +40,10 @@ describe("<PowerAndReset/>", () => {
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
["Power and Reset", "Restart", "Shutdown", "Factory Reset",
"Automatic Factory Reset", "Connection Attempt Period"]
"Automatic Factory Reset", "Connection Attempt Period", "Change Ownership"]
.map(string => expect(wrapper.text().toLowerCase())
.toContain(string.toLowerCase()));
["Restart Firmware", "Change Ownership"]
["Restart Firmware"]
.map(string => expect(wrapper.text().toLowerCase())
.not.toContain(string.toLowerCase()));
});
@ -92,7 +92,6 @@ describe("<PowerAndReset/>", () => {
it("shows change ownership button", () => {
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
p.shouldDisplay = () => true;
const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("Change Ownership".toLowerCase());

View File

@ -23,10 +23,9 @@ export function AutoUpdateRow(props: AutoUpdateRowProps) {
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent}
toggleAction={() => {
const newOsAutoUpdateNum = !osAutoUpdate.value ? 1 : 0;
props.dispatch(updateConfig({ os_auto_update: newOsAutoUpdateNum }));
}} />
toggleAction={() => props.dispatch(updateConfig({
os_auto_update: !osAutoUpdate.value
}))} />
</Col>
</Row>;
}

View File

@ -1,11 +1,10 @@
import * as React from "react";
import { Saucer } from "../../../ui/index";
import { ToggleButton } from "../../../controls/toggle_button";
import { Saucer, FBSelect } from "../../../ui";
import { updateConfig } from "../../actions";
import { last, isNumber, isString } from "lodash";
import { Content } from "../../../constants";
import { FbosDetailsProps } from "./interfaces";
import { SourceFbosConfig, ShouldDisplay, Feature } from "../../interfaces";
import { SourceFbosConfig } from "../../interfaces";
import { ConfigurationName } from "farmbot";
import { t } from "../../../i18next_wrapper";
import { LastSeen } from "./last_seen_row";
@ -13,6 +12,7 @@ import { Popover } from "@blueprintjs/core";
import moment from "moment";
import { timeFormatString } from "../../../util";
import { TimeSettings } from "../../../interfaces";
import { StringConfigKey } from "farmbot/dist/resources/configs/fbos";
/** Return an indicator color for the given temperature (C). */
export const colorFromTemp = (temp: number | undefined): string => {
@ -210,54 +210,31 @@ const UptimeDisplay = ({ uptime_sec }: UptimeDisplayProps): JSX.Element => {
return <p><b>{t("Uptime")}: </b>{convertUptime(uptime_sec)}</p>;
};
interface BetaReleaseOptInParams {
sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay;
}
/** Generate params for BetaReleaseOptInButton. */
export const betaReleaseOptIn = (
{ sourceFbosConfig, shouldDisplay }: BetaReleaseOptInParams
) => {
if (shouldDisplay(Feature.use_update_channel)) {
const betaOptIn = sourceFbosConfig("update_channel" as ConfigurationName);
const betaOptInValue = betaOptIn.value !== "stable";
return {
betaOptIn: { value: betaOptInValue, consistent: true }, betaOptInValue,
update: { update_channel: betaOptInValue ? "stable" : "beta" }
};
} else {
const betaOptIn = sourceFbosConfig("beta_opt_in");
const betaOptInValue = betaOptIn.value;
return {
betaOptIn, betaOptInValue,
update: { beta_opt_in: !betaOptInValue }
};
}
};
interface BetaReleaseOptInButtonProps {
export interface BetaReleaseOptInButtonProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay;
}
/** Label and toggle button for opting in to FBOS beta releases. */
const BetaReleaseOptInButton = (
{ dispatch, sourceFbosConfig, shouldDisplay }: BetaReleaseOptInButtonProps
export const BetaReleaseOptIn = (
{ dispatch, sourceFbosConfig }: BetaReleaseOptInButtonProps
): JSX.Element => {
const { betaOptIn, betaOptInValue, update } =
betaReleaseOptIn({ sourceFbosConfig, shouldDisplay });
return <fieldset>
<label style={{ marginTop: "0.75rem" }}>
{t("Beta release Opt-In")}
const betaOptIn = sourceFbosConfig("update_channel" as ConfigurationName).value;
return <fieldset className={"os-release-channel"}>
<label>
{t("OS release channel")}
</label>
<ToggleButton
toggleValue={betaOptInValue}
dim={!betaOptIn.consistent}
toggleAction={() =>
(betaOptInValue || confirm(Content.OS_BETA_RELEASES)) &&
dispatch(updateConfig(update))} />
<FBSelect
selectedItem={{ label: t("" + betaOptIn), value: "" + betaOptIn }}
onChange={ddi =>
(ddi.value == "stable" || confirm(Content.OS_BETA_RELEASES)) &&
dispatch(updateConfig({ ["update_channel" as StringConfigKey]: ddi.value }))}
list={[
{ label: t("stable"), value: "stable" },
{ label: t("beta"), value: "beta" },
{ label: t("staging"), value: "staging" },
{ label: t("qa"), value: "qa" },
]} />
</fieldset>;
};
@ -300,10 +277,8 @@ export function FbosDetails(props: FbosDetailsProps) {
<WiFiStrengthDisplay
wifiStrength={wifi_level} wifiStrengthPercent={wifi_level_percent} />
<VoltageDisplay chip={target} throttled={throttled} />
<BetaReleaseOptInButton
dispatch={props.dispatch}
shouldDisplay={props.shouldDisplay}
sourceFbosConfig={props.sourceFbosConfig} />
<BetaReleaseOptIn
dispatch={props.dispatch} sourceFbosConfig={props.sourceFbosConfig} />
{last_ota_checkup && <p><b>{t("Last checked for updates")}: </b>
{reformatDatetime(last_ota_checkup, props.timeSettings)}</p>}
{last_ota && <p><b>{t("Last updated")}: </b>

View File

@ -48,7 +48,7 @@ export function PowerAndReset(props: PowerAndResetProps) {
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />
{shouldDisplay(Feature.change_ownership) && botOnline &&
{botOnline &&
<Popover position={Position.BOTTOM_LEFT}>
<p className={"release-notes-button"}>
{t("Change Ownership")}&nbsp;

View File

@ -3,7 +3,7 @@ import { Row, Col } from "../../ui";
import { ColWidth } from "./farmbot_os_settings";
import { Collapse } from "@blueprintjs/core";
import { Header } from "./hardware_settings/header";
import { ShouldDisplay, Feature } from "../interfaces";
import { Feature } from "../interfaces";
import { TaggedDiagnosticDump } from "farmbot";
import { DiagnosticDumpRow } from "./diagnostic_dump_row";
import { requestDiagnostic } from "../actions";
@ -13,13 +13,12 @@ import { t } from "../../i18next_wrapper";
export interface DiagReportProps {
dispatch: Function;
expanded: boolean;
shouldDisplay: ShouldDisplay;
diagnostics: TaggedDiagnosticDump[];
botOnline: boolean;
}
export class SendDiagnosticReport extends React.Component<DiagReportProps, {}> {
show = () => {
render() {
return <section>
<div style={{ fontSize: "1px" }}>
<Header
@ -57,11 +56,4 @@ export class SendDiagnosticReport extends React.Component<DiagReportProps, {}> {
</Collapse>
</section>;
}
noShow = () => <div />;
render() {
const show = this.props.shouldDisplay(Feature.diagnostic_dumps);
return (show ? this.show : this.noShow)();
}
}

View File

@ -4,8 +4,9 @@ jest.mock("../../../history", () => ({
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
}));
let mockGardenOpen = true;
jest.mock("../../saved_gardens/saved_gardens", () => ({
savedGardenOpen: () => true,
savedGardenOpen: () => mockGardenOpen,
}));
import {
@ -337,12 +338,18 @@ describe("getMode()", () => {
expect(getMode()).toEqual(Mode.addPlant);
mockPath = "/app/designer/move_to";
expect(getMode()).toEqual(Mode.moveTo);
mockPath = "/app/designer/points";
expect(getMode()).toEqual(Mode.points);
mockPath = "/app/designer/points/add";
expect(getMode()).toEqual(Mode.createPoint);
mockPath = "/app/designer/saved_gardens";
mockGardenOpen = true;
expect(getMode()).toEqual(Mode.templateView);
mockPath = "/app/designer/groups/1";
expect(getMode()).toEqual(Mode.addPointToGroup);
mockPath = "";
mockGardenOpen = false;
expect(getMode()).toEqual(Mode.none);
});
});