add force unlock config

pull/1158/head
gabrielburnworth 2019-04-17 12:31:18 -07:00
parent cb44640ca5
commit 412a46415c
13 changed files with 79 additions and 11 deletions

View File

@ -0,0 +1,9 @@
class AddDisableEmergencyUnlockConfirmationToWebAppConfig < ActiveRecord::Migration[5.2]
safety_assured
def change
add_column :web_app_configs,
:disable_emergency_unlock_confirmation,
:boolean,
default: false
end
end

View File

@ -310,6 +310,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
show_historic_points: false,
time_format_24_hour: false,
show_pins: false,
disable_emergency_unlock_confirmation: false,
});
}

View File

@ -4,7 +4,7 @@ describe("fetchLabFeatures", () => {
Object.defineProperty(window.location, "reload", { value: jest.fn() });
it("basically just initializes stuff", () => {
const val = fetchLabFeatures(jest.fn());
expect(val.length).toBe(9);
expect(val.length).toBe(10);
expect(val[0].value).toBeFalsy();
const { callback } = val[0];
if (callback) {

View File

@ -82,7 +82,15 @@ export const fetchLabFeatures =
description: t(Content.TIME_FORMAT_24_HOUR),
storageKey: BooleanSetting.time_format_24_hour,
value: false,
}
},
{
name: t("Confirm emergency unlock"),
description: t(Content.EMERGENCY_UNLOCK_CONFIRM_CONFIG),
confirmationMessage: t(Content.CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE),
storageKey: BooleanSetting.disable_emergency_unlock_confirmation,
value: false,
displayInvert: true,
},
].map(fetchSettingValue(getConfigValue)));
/** Always allow toggling from true => false (deactivate).

View File

@ -464,6 +464,15 @@ export namespace Content {
export const SHOW_PINS =
trim(`Show raw pin lists in Read Sensor and Control Peripheral steps.`);
export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
trim(`Confirm when unlocking FarmBot after an emergency stop.`);
export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
trim(`Warning! When disabled, clicking the UNLOCK button will immediately
unlock FarmBot instead of confirming that it is safe to do so.
As a result, double-clicking the E-STOP button may not stop FarmBot.
Are you sure you want to disable this feature?`);
// Device
export const NOT_HTTPS =
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);

View File

@ -37,7 +37,10 @@ export class Move extends React.Component<MoveProps, {}> {
toggle={this.toggle}
getValue={this.getValue} />
</Popover>
<EStopButton bot={this.props.bot} />
<EStopButton
bot={this.props.bot}
forceUnlock={this.getValue(
BooleanSetting.disable_emergency_unlock_confirmation)} />
</WidgetHeader>
<WidgetBody>
<MustBeOnline

View File

@ -116,9 +116,9 @@ export function emergencyLock() {
.then(commandOK(noun), commandErr(noun));
}
export function emergencyUnlock() {
export function emergencyUnlock(force = false) {
const noun = "Emergency unlock";
if (confirm(t(`Are you sure you want to unlock the device?`))) {
if (force || confirm(t(`Are you sure you want to unlock the device?`))) {
getDevice()
.emergencyUnlock()
.then(commandOK(noun), commandErr(noun));

View File

@ -1,3 +1,6 @@
const mockDevice = { emergencyUnlock: jest.fn(() => Promise.resolve()) };
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
import * as React from "react";
import { mount } from "enzyme";
import { EStopButton } from "../e_stop_btn";
@ -5,7 +8,7 @@ import { bot } from "../../../__test_support__/fake_state/bot";
import { EStopButtonProps } from "../../interfaces";
describe("<EStopButton />", () => {
const fakeProps = (): EStopButtonProps => ({ bot });
const fakeProps = (): EStopButtonProps => ({ bot, forceUnlock: false });
it("renders", () => {
bot.hardware.informational_settings.sync_status = "synced";
const wrapper = mount(<EStopButton {...fakeProps()} />);
@ -13,18 +16,45 @@ describe("<EStopButton />", () => {
expect(wrapper.find("button").hasClass("red")).toBeTruthy();
});
it("grayed out", () => {
it("is grayed out when offline", () => {
bot.hardware.informational_settings.sync_status = undefined;
const wrapper = mount(<EStopButton {...fakeProps()} />);
expect(wrapper.text()).toEqual("E-STOP");
expect(wrapper.find("button").hasClass("pseudo-disabled")).toBeTruthy();
});
it("locked", () => {
it("shows locked state", () => {
bot.hardware.informational_settings.sync_status = "synced";
bot.hardware.informational_settings.locked = true;
const wrapper = mount(<EStopButton {...fakeProps()} />);
expect(wrapper.text()).toEqual("UNLOCK");
expect(wrapper.find("button").hasClass("yellow")).toBeTruthy();
});
it("confirms unlock", () => {
bot.hardware.informational_settings.sync_status = "synced";
bot.hardware.informational_settings.locked = true;
const p = fakeProps();
p.forceUnlock = false;
window.confirm = jest.fn(() => false);
const wrapper = mount(<EStopButton {...p} />);
expect(wrapper.text()).toEqual("UNLOCK");
wrapper.find("button").simulate("click");
expect(window.confirm).toHaveBeenCalledWith(
"Are you sure you want to unlock the device?");
expect(mockDevice.emergencyUnlock).not.toHaveBeenCalled();
});
it("doesn't confirm unlock", () => {
bot.hardware.informational_settings.sync_status = "synced";
bot.hardware.informational_settings.locked = true;
const p = fakeProps();
p.forceUnlock = true;
window.confirm = jest.fn(() => false);
const wrapper = mount(<EStopButton {...p} />);
expect(wrapper.text()).toEqual("UNLOCK");
wrapper.find("button").simulate("click");
expect(window.confirm).not.toHaveBeenCalled();
expect(mockDevice.emergencyUnlock).toHaveBeenCalled();
});
});

View File

@ -10,7 +10,9 @@ export class EStopButton extends React.Component<EStopButtonProps, {}> {
render() {
const i = this.props.bot.hardware.informational_settings;
const isLocked = !!i.locked;
const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock;
const toggleEmergencyLock = isLocked
? () => emergencyUnlock(this.props.forceUnlock)
: emergencyLock;
const color = isLocked ? "yellow" : "red";
const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : GRAY;
const emergencyLockStatusText = isLocked ? t("UNLOCK") : "E-STOP";

View File

@ -198,6 +198,7 @@ export interface McuInputBoxProps {
export interface EStopButtonProps {
bot: BotState;
forceUnlock: boolean;
}
export interface PeripheralsProps {

View File

@ -18,6 +18,7 @@ import { Connectivity } from "../devices/connectivity/connectivity";
import { connectivityData } from "../devices/connectivity/generate_data";
import { DiagnosisSaucer } from "../devices/connectivity/diagnosis";
import { maybeSetTimezone } from "../devices/timezones/guess_timezone";
import { BooleanSetting } from "../session_keys";
export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
@ -111,7 +112,10 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
{AdditionalMenu({ logout: this.logout, close })}
</Popover>
</div>
<EStopButton bot={this.props.bot} />
<EStopButton
bot={this.props.bot}
forceUnlock={!!this.props.getConfigValue(
BooleanSetting.disable_emergency_unlock_confirmation)} />
{this.syncButton()}
<div className="connection-status-popover">
<Popover position={Position.BOTTOM_RIGHT}

View File

@ -22,6 +22,7 @@ export const BooleanSetting: Record<BooleanConfigKey, BooleanConfigKey> = {
show_historic_points: "show_historic_points",
time_format_24_hour: "time_format_24_hour",
show_pins: "show_pins",
disable_emergency_unlock_confirmation: "disable_emergency_unlock_confirmation",
/** "Labs" feature names. (App preferences) */
stub_config: "stub_config",

View File

@ -43,7 +43,7 @@
"coveralls": "3.0.3",
"enzyme": "3.9.0",
"enzyme-adapter-react-16": "1.12.1",
"farmbot": "7.0.4-rc3",
"farmbot": "7.0.4",
"farmbot-toastr": "1.0.3",
"i18next": "15.0.9",
"jest": "24.7.1",