misc updates

pull/1720/head
gabrielburnworth 2020-02-26 10:10:59 -08:00
parent 49fdced812
commit 9bd98aca1e
58 changed files with 424 additions and 168 deletions

View File

@ -10,6 +10,7 @@ import { Move } from "./move/move";
import { BooleanSetting } from "../session_keys";
import { SensorReadings } from "./sensor_readings/sensor_readings";
import { isBotOnline } from "../devices/must_be_online";
import { hasSensors } from "../devices/components/firmware_hardware_support";
/** Controls page. */
export class RawControls extends React.Component<Props, {}> {
@ -24,7 +25,8 @@ export class RawControls extends React.Component<Props, {}> {
}
get hideSensors() {
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors);
return this.props.getWebAppConfigVal(BooleanSetting.hide_sensors)
|| !hasSensors(this.props.firmwareHardware);
}
move = () => <Move

View File

@ -7,7 +7,7 @@ import { AxisInputBoxGroup } from "../axis_input_box_group";
import { GetWebAppBool } from "./interfaces";
import { BooleanSetting } from "../../session_keys";
import { t } from "../../i18next_wrapper";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
import { FirmwareHardware } from "farmbot";
export interface BotPositionRowsProps {
@ -34,12 +34,12 @@ export const BotPositionRows = (props: BotPositionRowsProps) => {
<AxisDisplayGroup
position={locationData.position}
label={t("Motor Coordinates (mm)")} />
{!isExpressBoard(props.firmwareHardware) &&
{hasEncoders(props.firmwareHardware) &&
getValue(BooleanSetting.scaled_encoders) &&
<AxisDisplayGroup
position={locationData.scaled_encoders}
label={t("Scaled Encoder (mm)")} />}
{!isExpressBoard(props.firmwareHardware) &&
{hasEncoders(props.firmwareHardware) &&
getValue(BooleanSetting.raw_encoders) &&
<AxisDisplayGroup
position={locationData.raw_encoders}

View File

@ -6,7 +6,7 @@ import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { DevSettings } from "../../account/dev/dev_support";
import { t } from "../../i18next_wrapper";
import { FirmwareHardware } from "farmbot";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
import { hasEncoders } from "../../devices/components/firmware_hardware_support";
export const moveWidgetSetting =
(toggle: ToggleWebAppBool, getValue: GetWebAppBool) =>
@ -36,7 +36,7 @@ export const MoveWidgetSettingsMenu = (
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
{!isExpressBoard(firmwareHardware) &&
{hasEncoders(firmwareHardware) &&
<div className="display-encoder-data">
<p>{t("Display Encoder Data")}</p>
<Setting

View File

@ -88,4 +88,14 @@ describe("<Peripherals />", () => {
clickButton(wrapper, 3, "stock");
expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds);
});
it("hides stock button", () => {
const p = fakeProps();
p.firmwareHardware = "none";
const wrapper = mount(<Peripherals {...p} />);
wrapper.setState({ isEditing: true });
const btn = wrapper.find("button").at(3);
expect(btn.text().toLowerCase()).toContain("stock");
expect(btn.props().hidden).toBeTruthy();
});
});

View File

@ -108,7 +108,7 @@ export class Peripherals
<i className="fa fa-plus" />
</button>
<button
hidden={!isEditing}
hidden={!isEditing || this.props.firmwareHardware == "none"}
className="fb-button green"
type="button"
onClick={() => this.stockPeripherals.map(p =>

View File

@ -72,6 +72,7 @@ describe("<Sensors />", () => {
expect(wrapper.text().toLowerCase()).toContain("stock sensors");
wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "stock sensors");
expect(wrapper.find("button").at(3).props().hidden).toBeFalsy();
expect(p.dispatch).toHaveBeenCalledTimes(2);
});
@ -79,6 +80,18 @@ describe("<Sensors />", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("stock sensors");
const btn = wrapper.find("button").at(3);
expect(btn.text().toLowerCase()).toContain("stock");
expect(btn.props().hidden).toBeTruthy();
});
it("hides stock button", () => {
const p = fakeProps();
p.firmwareHardware = "none";
const wrapper = mount(<Sensors {...p} />);
wrapper.setState({ isEditing: true });
const btn = wrapper.find("button").at(3);
expect(btn.text().toLowerCase()).toContain("stock");
expect(btn.props().hidden).toBeTruthy();
});
});

View File

@ -10,7 +10,6 @@ import { saveAll, init } from "../../api/crud";
import { ToolTips } from "../../constants";
import { uniq } from "lodash";
import { t } from "../../i18next_wrapper";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
export class Sensors extends React.Component<SensorsProps, SensorState> {
constructor(props: SensorsProps) {
@ -80,15 +79,14 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
onClick={() => this.newSensor()}>
<i className="fa fa-plus" />
</button>
{!isExpressBoard(this.props.firmwareHardware) &&
<button
hidden={!isEditing}
className="fb-button green"
type="button"
onClick={this.stockSensors}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
{t("Stock sensors")}
</button>}
<button
hidden={!isEditing || this.props.firmwareHardware == "none"}
className="fb-button green"
type="button"
onClick={this.stockSensors}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
{t("Stock sensors")}
</button>
</WidgetHeader>
<WidgetBody>
{this.showPins()}

View File

@ -958,3 +958,10 @@
margin-right: 1.5rem;
&:hover { color: $white; }
}
.desktop-hide {
display: none !important;
@media screen and (max-width: 1075px) {
display: block !important;
}
}

View File

@ -407,6 +407,18 @@ a {
}
}
.load-progress-bar-wrapper {
position: absolute;
top: 3.2rem;
bottom: 0;
right: 0;
width: 100%;
height: 1px;
.load-progress-bar {
height: 100%;
}
}
.firmware-setting-export-menu {
button {
margin-bottom: 1rem;
@ -1654,3 +1666,9 @@ textarea:focus {
background-color: transparent;
box-shadow: none;
}
.read-only-icon {
margin: 9px 0px 0px 9px;
float: right;
box-sizing: inherit;
}

View File

@ -322,6 +322,9 @@
border-left: 4px solid transparent;
&.active {
border-left: 4px solid $dark_gray;
p {
font-weight: bold;
}
}
.fa-chevron-down, .fa-chevron-right {
position: absolute;
@ -330,11 +333,11 @@
font-size: 1.1rem;
}
.folder-settings-icon,
.fa-bars {
.fa-arrows-v {
position: absolute;
right: 0;
}
.fa-bars, .fa-ellipsis-v {
.fa-arrows-v, .fa-ellipsis-v {
display: none;
}
.fa-ellipsis-v {
@ -342,8 +345,14 @@
display: block;
}
}
@media screen and (max-width: 450px) {
.fa-arrows-v, .fa-ellipsis-v {
display: block;
margin-right: 0.5rem;
}
}
&:hover {
.fa-bars, .fa-ellipsis-v {
.fa-arrows-v, .fa-ellipsis-v {
display: block;
}
}
@ -367,7 +376,7 @@
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.2rem;
font-weight: bold;
font-weight: normal;
width: 75%;
padding: 0.5rem;
padding-left: 0;

View File

@ -12,6 +12,8 @@ import { clickButton } from "../../../__test_support__/helpers";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { Color } from "../../../ui";
describe("<HardwareSettings />", () => {
const fakeProps = (): HardwareSettingsProps => ({
@ -68,4 +70,41 @@ describe("<HardwareSettings />", () => {
const wrapper = shallow(<HardwareSettings {...p} />);
expect(wrapper.html()).toContain("fa-download");
});
it("shows setting load progress", () => {
type ConsistencyLookup = Record<keyof FirmwareConfig, boolean>;
const consistent: Partial<ConsistencyLookup> =
({ id: false, encoder_invert_x: true, encoder_enabled_y: false });
const consistencyLookup = consistent as ConsistencyLookup;
const p = fakeProps();
const fakeConfig: Partial<FirmwareConfig> =
({ id: 0, encoder_invert_x: 1, encoder_enabled_y: 0 });
p.firmwareConfig = fakeConfig as FirmwareConfig;
p.sourceFwConfig = x =>
({ value: p.firmwareConfig?.[x], consistent: consistencyLookup[x] });
const wrapper = mount(<HardwareSettings {...p} />);
const barStyle = wrapper.find(".load-progress-bar").props().style;
expect(barStyle?.background).toEqual(Color.white);
expect(barStyle?.width).toEqual("50%");
});
it("shows setting load progress: 0%", () => {
const p = fakeProps();
p.firmwareConfig = fakeFirmwareConfig().body;
p.sourceFwConfig = () => ({ value: 0, consistent: false });
const wrapper = mount(<HardwareSettings {...p} />);
const barStyle = wrapper.find(".load-progress-bar").props().style;
expect(barStyle?.width).toEqual("0%");
expect(barStyle?.background).toEqual(Color.darkGray);
});
it("shows setting load progress: 100%", () => {
const p = fakeProps();
p.firmwareConfig = fakeFirmwareConfig().body;
p.sourceFwConfig = () => ({ value: 0, consistent: true });
const wrapper = mount(<HardwareSettings {...p} />);
const barStyle = wrapper.find(".load-progress-bar").props().style;
expect(barStyle?.width).toEqual("100%");
expect(barStyle?.background).toEqual(Color.darkGray);
});
});

View File

@ -16,15 +16,31 @@ export const getFwHardwareValue =
return isFwHardwareValue(value) ? value : undefined;
};
const TMC_BOARDS = ["express_k10", "farmduino_k15"];
const NO_BUTTONS = ["arduino", "farmduino", "none"];
const EXPRESS_BOARDS = ["express_k10"];
const NO_SENSORS = [...EXPRESS_BOARDS];
const NO_ENCODERS = [...EXPRESS_BOARDS];
const NO_TOOLS = [...EXPRESS_BOARDS];
const NO_TMC = ["arduino", "farmduino", "farmduino_k14"];
export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) =>
!!(firmwareHardware && TMC_BOARDS.includes(firmwareHardware));
!firmwareHardware || !NO_TMC.includes(firmwareHardware);
export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) =>
!!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware));
export const hasButtons = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_BUTTONS.includes(firmwareHardware);
export const hasEncoders = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_ENCODERS.includes(firmwareHardware);
export const hasSensors = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_SENSORS.includes(firmwareHardware);
export const hasUTM = (firmwareHardware: FirmwareHardware | undefined) =>
!firmwareHardware || !NO_TOOLS.includes(firmwareHardware);
export const getBoardIdentifier =
(firmwareVersion: string | undefined): string =>
firmwareVersion ? firmwareVersion.split(".")[3] : "undefined";

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
import { HardwareSettingsProps } from "../interfaces";
import { Widget, WidgetHeader, WidgetBody, Color } from "../../ui/index";
import { HardwareSettingsProps, SourceFwConfig } from "../interfaces";
import { isBotOnline } from "../must_be_online";
import { ToolTips } from "../../constants";
import { DangerZone } from "./hardware_settings/danger_zone";
@ -19,6 +19,8 @@ import { t } from "../../i18next_wrapper";
import { PinBindings } from "./hardware_settings/pin_bindings";
import { ErrorHandling } from "./hardware_settings/error_handling";
import { maybeOpenPanel } from "./maybe_highlight";
import type { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import type { McuParamName } from "farmbot";
export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> {
@ -36,7 +38,10 @@ export class HardwareSettings extends
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
const commonProps = { dispatch, controlPanelState };
return <Widget className="hardware-widget">
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS} />
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>
<SettingLoadProgress firmwareConfig={firmwareConfig}
sourceFwConfig={sourceFwConfig} />
</WidgetHeader>
<WidgetBody>
<button
className={"fb-button gray no-float"}
@ -78,8 +83,33 @@ export class HardwareSettings extends
onReset={MCUFactoryReset}
botDisconnected={botDisconnected} />
<PinBindings {...commonProps}
resources={resources} />
resources={resources}
firmwareHardware={firmwareHardware} />
</WidgetBody>
</Widget>;
}
}
interface SettingLoadProgressProps {
sourceFwConfig: SourceFwConfig;
firmwareConfig: FirmwareConfig | undefined;
}
const UNTRACKED_KEYS: (keyof FirmwareConfig)[] = [
"id", "created_at", "updated_at", "device_id", "api_migrated",
"param_config_ok", "param_test", "param_use_eeprom", "param_version",
];
/** Track firmware configuration adoption by FarmBot OS. */
const SettingLoadProgress = (props: SettingLoadProgressProps) => {
const keys = Object.keys(props.firmwareConfig || {})
.filter((k: keyof FirmwareConfig) => !UNTRACKED_KEYS.includes(k));
const loadedKeys = keys.filter((key: McuParamName) =>
props.sourceFwConfig(key).consistent);
const progress = loadedKeys.length / keys.length * 100;
const color = [0, 100].includes(progress) ? Color.darkGray : Color.white;
return <div className={"load-progress-bar-wrapper"}>
<div className={"load-progress-bar"}
style={{ width: `${progress}%`, background: color }} />
</div>;
};

View File

@ -12,6 +12,7 @@ describe("<PinBindings />", () => {
dispatch: jest.fn(),
controlPanelState: panelState(),
resources: buildResourceIndex([]).index,
firmwareHardware: undefined,
});
it("shows pin binding labels", () => {

View File

@ -5,7 +5,7 @@ import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { EncodersProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { isExpressBoard } from "../firmware_hardware_support";
import { hasEncoders } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
export function Encoders(props: EncodersProps) {
@ -18,23 +18,23 @@ export function Encoders(props: EncodersProps) {
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value
};
const isExpress = isExpressBoard(firmwareHardware);
const showEncoders = hasEncoders(firmwareHardware);
return <Highlight className={"section"}
settingName={DeviceSetting.encoders}>
<Header
expanded={encoders}
title={isExpress
title={!showEncoders
? DeviceSetting.stallDetection
: DeviceSetting.encoders}
panel={"encoders"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders}>
<BooleanMCUInputGroup
label={isExpress
label={!showEncoders
? DeviceSetting.enableStallDetection
: DeviceSetting.enableEncoders}
tooltip={isExpress
tooltip={!showEncoders
? ToolTips.ENABLE_STALL_DETECTION
: ToolTips.ENABLE_ENCODERS}
x={"encoder_enabled_x"}
@ -42,7 +42,7 @@ export function Encoders(props: EncodersProps) {
z={"encoder_enabled_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
{isExpress &&
{!showEncoders &&
<NumericMCUInputGroup
label={DeviceSetting.stallSensitivity}
tooltip={ToolTips.STALL_SENSITIVITY}
@ -52,7 +52,7 @@ export function Encoders(props: EncodersProps) {
gray={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
{showEncoders &&
<BooleanMCUInputGroup
label={DeviceSetting.useEncodersForPositioning}
tooltip={ToolTips.ENCODER_POSITIONING}
@ -62,7 +62,7 @@ export function Encoders(props: EncodersProps) {
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
{showEncoders &&
<BooleanMCUInputGroup
label={DeviceSetting.invertEncoders}
tooltip={ToolTips.INVERT_ENCODERS}
@ -74,7 +74,7 @@ export function Encoders(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
label={DeviceSetting.maxMissedSteps}
tooltip={isExpress
tooltip={!showEncoders
? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION
: ToolTips.MAX_MISSED_STEPS_ENCODERS}
x={"encoder_missed_steps_max_x"}
@ -92,7 +92,7 @@ export function Encoders(props: EncodersProps) {
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
{!isExpress &&
{showEncoders &&
<NumericMCUInputGroup
label={DeviceSetting.encoderScaling}
tooltip={ToolTips.ENCODER_SCALING}

View File

@ -9,7 +9,7 @@ import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { calculateScale } from "./motors";
import { isExpressBoard } from "../firmware_hardware_support";
import { hasEncoders } from "../firmware_hardware_support";
import { getDevice } from "../../../device";
import { commandErr } from "../../actions";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
@ -44,7 +44,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
type={"find_home"}
title={DeviceSetting.homing}
axisTitle={t("FIND HOME")}
toolTip={isExpressBoard(firmwareHardware)
toolTip={!hasEncoders(firmwareHardware)
? ToolTips.HOMING_STALL_DETECTION
: ToolTips.HOMING_ENCODERS}
action={axis => getDevice()
@ -56,7 +56,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
type={"calibrate"}
title={DeviceSetting.calibration}
axisTitle={t("CALIBRATE")}
toolTip={isExpressBoard(firmwareHardware)
toolTip={!hasEncoders(firmwareHardware)
? ToolTips.CALIBRATION_STALL_DETECTION
: ToolTips.CALIBRATION_ENCODERS}
action={axis => getDevice().calibrate({ axis })
@ -74,7 +74,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
botDisconnected={botDisconnected} />
<BooleanMCUInputGroup
label={DeviceSetting.findHomeOnBoot}
tooltip={isExpressBoard(firmwareHardware)
tooltip={!hasEncoders(firmwareHardware)
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
disable={disabled}

View File

@ -9,7 +9,7 @@ import { Highlight } from "../maybe_highlight";
export function PinBindings(props: PinBindingsProps) {
const { pin_bindings } = props.controlPanelState;
const { dispatch, resources } = props;
const { dispatch, resources, firmwareHardware } = props;
return <Highlight className={"section"}
settingName={DeviceSetting.pinBindings}>
@ -19,7 +19,8 @@ export function PinBindings(props: PinBindingsProps) {
panel={"pin_bindings"}
dispatch={dispatch} />
<Collapse isOpen={!!pin_bindings}>
<PinBindingsContent dispatch={dispatch} resources={resources} />
<PinBindingsContent dispatch={dispatch} resources={resources}
firmwareHardware={firmwareHardware} />
</Collapse>
</Highlight>;
}

View File

@ -109,6 +109,7 @@ export interface PinBindingsProps {
dispatch: Function;
controlPanelState: ControlPanelState;
resources: ResourceIndex;
firmwareHardware: FirmwareHardware | undefined;
}
export interface DangerZoneProps {

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { mount } from "enzyme";
import {
ConnectivityDiagram,
ConnectivityDiagramProps,
@ -83,9 +82,9 @@ describe("getTextPosition()", () => {
describe("nodeLabel()", () => {
it("renders", () => {
const label = mount(nodeLabel("Top Node", "top" as DiagramNodes));
expect(label.text()).toEqual("Top Node");
expect(label.props())
const label = svgMount(nodeLabel("Top Node", "top" as DiagramNodes));
expect(label.find("text").text()).toEqual("Top Node");
expect(label.find("text").props())
.toEqual({ children: "Top Node", textAnchor: "middle", x: 0, y: -75 });
});
});

View File

@ -46,6 +46,7 @@ describe("<PinBindingsContent/>", () => {
return {
dispatch: jest.fn(),
resources: resources,
firmwareHardware: undefined,
};
}

View File

@ -1,25 +1,37 @@
jest.mock("../../../api/crud", () => ({
initSave: jest.fn(),
}));
jest.mock("../../../api/crud", () => ({ initSave: jest.fn() }));
import * as React from "react";
import { mount } from "enzyme";
import { StockPinBindingsButton } from "../tagged_pin_binding_init";
import {
StockPinBindingsButton, StockPinBindingsButtonProps
} from "../tagged_pin_binding_init";
import { initSave } from "../../../api/crud";
import { stockPinBindings } from "../list_and_label_support";
describe("<StockPinBindingsButton />", () => {
const fakeProps = () => ({
shouldDisplay: () => false,
const fakeProps = (): StockPinBindingsButtonProps => ({
dispatch: jest.fn(),
firmwareHardware: undefined,
});
it("adds bindings", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
const wrapper = mount(<StockPinBindingsButton {...p} />);
const wrapper = mount(<StockPinBindingsButton {...fakeProps()} />);
wrapper.find("button").simulate("click");
stockPinBindings.map(body =>
expect(initSave).toHaveBeenCalledWith("PinBinding", body));
});
it("is hidden", () => {
const p = fakeProps();
p.firmwareHardware = "arduino";
const wrapper = mount(<StockPinBindingsButton {...p} />);
expect(wrapper.find("button").props().hidden).toBeTruthy();
});
it("is not hidden", () => {
const p = fakeProps();
p.firmwareHardware = "farmduino_k14";
const wrapper = mount(<StockPinBindingsButton {...p} />);
expect(wrapper.find("button").props().hidden).toBeFalsy();
});
});

View File

@ -3,10 +3,12 @@ import {
PinBindingType,
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
import { FirmwareHardware } from "farmbot";
export interface PinBindingsContentProps {
dispatch: Function;
resources: ResourceIndex;
firmwareHardware: FirmwareHardware | undefined;
}
export interface PinBindingListItems {

View File

@ -68,12 +68,13 @@ const PinBindingsListHeader = () =>
</Row>;
export const PinBindingsContent = (props: PinBindingsContentProps) => {
const { dispatch, resources } = props;
const { dispatch, resources, firmwareHardware } = props;
const pinBindings = apiPinBindings(resources);
return <div className="pin-bindings">
<Row>
<StockPinBindingsButton dispatch={dispatch} />
<StockPinBindingsButton
dispatch={dispatch} firmwareHardware={firmwareHardware} />
<Popover
position={Position.RIGHT_TOP}
interactionKind={PopoverInteractionKind.HOVER}

View File

@ -8,6 +8,8 @@ import { PinBindingListItems } from "./interfaces";
import { stockPinBindings } from "./list_and_label_support";
import { initSave } from "../../api/crud";
import { t } from "../../i18next_wrapper";
import { FirmwareHardware } from "farmbot";
import { hasButtons } from "../components/firmware_hardware_support";
/** Return the correct Pin Binding resource according to binding type. */
export const pinBindingBody =
@ -34,15 +36,21 @@ export const pinBindingBody =
return body;
};
export interface StockPinBindingsButtonProps {
dispatch: Function;
firmwareHardware: FirmwareHardware | undefined;
}
/** Add default pin bindings. */
export const StockPinBindingsButton = ({ dispatch }: { dispatch: Function }) =>
export const StockPinBindingsButton = (props: StockPinBindingsButtonProps) =>
<div className="stock-pin-bindings-button">
<button
className="fb-button green"
hidden={!hasButtons(props.firmwareHardware)}
onClick={() => stockPinBindings.map(binding =>
dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
props.dispatch(initSave("PinBinding", pinBindingBody(binding))))}>
<i className="fa fa-plus" />
{t("v1.4 Stock Bindings")}
{t("Stock Bindings")}
</button>
</div>;

View File

@ -0,0 +1,19 @@
import { Plant } from "../plant";
describe("Plant()", () => {
it("returns defaults", () => {
expect(Plant({})).toEqual({
created_at: "",
id: undefined,
meta: {},
name: "Untitled Plant",
openfarm_slug: "not-set",
plant_stage: "planned",
pointer_type: "Plant",
radius: 25,
x: 0,
y: 0,
z: 0,
});
});
});

View File

@ -94,6 +94,19 @@ describe("designer reducer", () => {
});
});
it("uses current point color", () => {
const action: ReduxAction<CurrentPointPayl> = {
type: Actions.SET_CURRENT_POINT_DATA,
payload: { cx: 10, cy: 20, r: 30 }
};
const state = oldState();
state.currentPoint = { cx: 0, cy: 0, r: 0, color: "red" };
const newState = designer(state, action);
expect(newState.currentPoint).toEqual({
cx: 10, cy: 20, r: 30, color: "red"
});
});
it("sets opened saved garden", () => {
const payload = "savedGardenUuid";
const action: ReduxAction<string | undefined> = {

View File

@ -90,7 +90,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
{!props.noIcon &&
<i className="fa fa-search"></i>}
<i className="fa fa-search" />}
<ErrorBoundary>
{props.children}
</ErrorBoundary>

View File

@ -23,7 +23,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
<DesignerPanelHeader
panelName={panelName}
panel={Panel.FarmEvents}
title={t("Edit Event")} />
title={t("Edit event")} />
<DesignerPanelContent panelName={panelName}>
<EditFEForm farmEvent={fe}
deviceTimezone={this.props.deviceTimezone}
@ -31,7 +31,7 @@ export class RawEditFarmEvent extends React.Component<AddEditFarmEventProps, {}>
executableOptions={this.props.executableOptions}
dispatch={this.props.dispatch}
findExecutable={this.props.findExecutable}
title={t("Edit Event")}
title={t("Edit event")}
deleteBtn={true}
timeSettings={this.props.timeSettings}
autoSyncEnabled={this.props.autoSyncEnabled}

View File

@ -41,6 +41,7 @@ import {
} from "../../sequences/locals_list/locals_list_support";
import { t } from "../../i18next_wrapper";
import { TimeSettings } from "../../interfaces";
import { ErrorBoundary } from "../../error_boundary";
export const NEVER: TimeUnit = "never";
/** Separate each of the form fields into their own interface. Recombined later
@ -360,19 +361,24 @@ export class EditFEForm extends React.Component<EditFEProps, EditFEFormState> {
render() {
const { farmEvent } = this.props;
return <div className="edit-farm-event-form">
<FarmEventForm
isRegimen={this.isReg}
fieldGet={this.fieldGet}
fieldSet={this.fieldSet}
timeSettings={this.props.timeSettings}
executableOptions={this.props.executableOptions}
executableSet={this.executableSet}
executableGet={this.executableGet}
dispatch={this.props.dispatch}
specialStatus={farmEvent.specialStatus || this.state.specialStatusLocal}
onSave={() => this.commitViewModel()}>
<this.LocalsList />
</FarmEventForm>
<ErrorBoundary>
<FarmEventForm
isRegimen={this.isReg}
fieldGet={this.fieldGet}
fieldSet={this.fieldSet}
timeSettings={this.props.timeSettings}
executableOptions={this.props.executableOptions}
executableSet={this.executableSet}
executableGet={this.executableGet}
dispatch={this.props.dispatch}
specialStatus={farmEvent.specialStatus
|| this.state.specialStatusLocal}
onSave={() => this.commitViewModel()}>
<ErrorBoundary>
<this.LocalsList />
</ErrorBoundary>
</FarmEventForm>
</ErrorBoundary>
<FarmEventDeleteButton
hidden={!this.props.deleteBtn}
farmEvent={this.props.farmEvent}

View File

@ -111,7 +111,7 @@ export class PureFarmEvents
<input
value={this.state.searchTerm}
onChange={e => this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search events...")} />
placeholder={t("Search your events...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"farm-event"}>
<div className="farm-events">

View File

@ -1,5 +1,4 @@
import * as React from "react";
import { mount } from "enzyme";
import { MapImage, MapImageProps } from "../map_image";
import { SpecialStatus } from "farmbot";
import { cloneDeep } from "lodash";
@ -7,6 +6,9 @@ import { trim } from "../../../../../util";
import {
fakeMapTransformProps
} from "../../../../../__test_support__/map_transform_props";
import { svgMount } from "../../../../../__test_support__/svg_mount";
const NOT_DISPLAYED = "<svg><image></image></svg>";
describe("<MapImage />", () => {
const fakeProps = (): MapImageProps => {
@ -37,28 +39,28 @@ describe("<MapImage />", () => {
};
it("doesn't render image", () => {
const wrapper = mount(<MapImage {...fakeProps()} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...fakeProps()} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
it("renders pre-calibration preview", () => {
const p = fakeProps();
p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
const wrapper = mount(<MapImage {...p} />);
wrapper.setState({ width: 100, height: 100 });
const wrapper = svgMount(<MapImage {...p} />);
wrapper.find(MapImage).setState({ width: 100, height: 100 });
expect(wrapper.html()).toContain("image_url");
});
it("gets image size", () => {
const p = fakeProps();
p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
const wrapper = mount<MapImage>(<MapImage {...p} />);
expect(wrapper.state()).toEqual({ width: 0, height: 0 });
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.find(MapImage).state()).toEqual({ width: 0, height: 0 });
const img = new Image();
img.width = 100;
img.height = 200;
wrapper.instance().imageCallback(img)();
expect(wrapper.state()).toEqual({ width: 100, height: 200 });
wrapper.find<MapImage>(MapImage).instance().imageCallback(img)();
expect(wrapper.find(MapImage).state()).toEqual({ width: 100, height: 200 });
});
interface ExpectedData {
@ -83,8 +85,8 @@ describe("<MapImage />", () => {
expectedData: ExpectedData,
extra?: ExtraTranslationData) => {
it(`renders image: INPUT_SET_${num}`, () => {
const wrapper = mount(<MapImage {...inputData[num]} />);
wrapper.setState({ width: 480, height: 640 });
const wrapper = svgMount(<MapImage {...inputData[num]} />);
wrapper.find(MapImage).setState({ width: 480, height: 640 });
expect(wrapper.find("image").props()).toEqual({
xlinkHref: "image_url",
x: 0,
@ -183,21 +185,21 @@ describe("<MapImage />", () => {
it("doesn't render placeholder image", () => {
const p = INPUT_SET_1;
p.image && (p.image.body.attachment_url = "/placehold.");
const wrapper = mount(<MapImage {...p} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
it("doesn't render image taken at different height than calibration", () => {
const p = INPUT_SET_1;
p.image && (p.image.body.meta.z = 100);
const wrapper = mount(<MapImage {...p} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
it("doesn't render images that are not adjusted for camera rotation", () => {
const p = INPUT_SET_1;
p.image && (p.image.body.meta.name = "na");
const wrapper = mount(<MapImage {...p} />);
expect(wrapper.html()).toEqual("<image></image>");
const wrapper = svgMount(<MapImage {...p} />);
expect(wrapper.html()).toEqual(NOT_DISPLAYED);
});
});

View File

@ -93,11 +93,15 @@ interface NavTabProps {
linkTo: string;
title: string;
icon?: string;
desktopHide?: boolean;
}
const NavTab = (props: NavTabProps) =>
<Link to={props.linkTo} style={{ flex: 0.3 }}
className={getCurrentTab() === props.panel ? "active" : ""}>
className={[
getCurrentTab() === props.panel ? "active" : "",
props.desktopHide ? "desktop-hide" : "",
].join(" ")}>
<img {...common}
src={TAB_ICON[props.panel]} title={props.title} />
</Link>;
@ -109,7 +113,7 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
<div className="panel-tabs">
<NavTab panel={Panel.Map}
linkTo={"/app/designer"}
title={t("Map")} />
title={t("Map")} desktopHide={true} />
<NavTab
panel={Panel.Plants}
linkTo={"/app/designer/plants"}

View File

@ -87,7 +87,7 @@ export class GroupDetailActive
onBack={this.saveGroup}
panelName={Panel.Groups}
panel={Panel.Groups}
title={t("Edit Group")}
title={t("Edit group")}
backTo={"/app/designer/groups"} />
<DesignerPanelContent
panelName={"groups"}>

View File

@ -6,6 +6,7 @@ jest.mock("../../../farmware/weed_detector/actions", () => ({
let mockPath = "/app/designer/points/add";
jest.mock("../../../history", () => ({
history: { push: jest.fn() },
push: jest.fn(),
getPathArray: () => mockPath.split("/"),
}));

View File

@ -23,8 +23,9 @@ import {
import { parseIntInput } from "../../util";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
import { getPathArray } from "../../history";
import { history, getPathArray } from "../../history";
import { ListItem } from "../plants/plant_panel";
import { success } from "../../toast/toast";
export function mapStateToProps(props: Everything): CreatePointsProps {
const { position } = props.bot.hardware.location_data;
@ -176,9 +177,13 @@ export class RawCreatePoints
radius: this.attr("r"),
};
this.props.dispatch(initSave("Point", body));
success(this.panel == "weeds"
? t("Weed created.")
: t("Point created."));
this.cancel();
this.loadDefaultPoint();
history.push(`/app/designer/${this.panel}`);
}
PointProperties = () =>
<ul>
<li>

View File

@ -47,7 +47,7 @@ export class PointInventoryItem extends
{label}
</span>
<p className="point-search-item-info">
{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}
<i>{`(${point.x}, ${point.y}) ⌀${point.radius * 2}`}</i>
</p>
</div>;
}

View File

@ -80,7 +80,7 @@ export class RawEditGarden extends React.Component<EditGardenProps, {}> {
<DesignerPanelHeader
panelName={"saved-garden"}
panel={Panel.SavedGardens}
title={t("Edit Garden")}
title={t("Edit garden")}
backTo={"/app/designer/gardens"} />
<DesignerPanelContent panelName={"saved-garden-edit"}>
{savedGarden

View File

@ -14,7 +14,7 @@ export const GardenInfo = (props: SavedGardenInfoProps) => {
onClick={() => dispatch(openSavedGarden(savedGarden.uuid))}>
<Col>
<span>{savedGarden.body.name}</span>
<p>{props.plantTemplateCount} {t("plants")}</p>
<p><i>{props.plantTemplateCount} {t("plants")}</i></p>
</Col>
</div>;
};

View File

@ -193,7 +193,7 @@ describe("<Tools />", () => {
p.isActive = () => true;
p.device.body.mounted_tool_id = undefined;
const wrapper = mount(<Tools {...p} />);
expect(wrapper.text().toLowerCase()).toContain("active");
expect(wrapper.text().toLowerCase()).toContain("in slot");
});
it("displays tool as mounted", () => {

View File

@ -288,7 +288,7 @@ export const ToolSlotInventoryItem = (props: ToolSlotInventoryItemProps) => {
</Col>
<Col xs={4} className={"tool-slot-position-info"}>
<p className="tool-slot-position">
{botPositionLabel({ x, y, z }, gantry_mounted)}
<i>{botPositionLabel({ x, y, z }, gantry_mounted)}</i>
</p>
</Col>
</Row>
@ -303,7 +303,7 @@ interface ToolInventoryItemProps {
}
const ToolInventoryItem = (props: ToolInventoryItemProps) => {
const activeText = props.active ? t("active") : t("inactive");
const activeText = props.active ? t("in slot") : t("inactive");
return <div className={"tool-search-item"}
onClick={() => history.push(`/app/designer/tools/${props.toolId}`)}>
<Row>
@ -315,7 +315,7 @@ const ToolInventoryItem = (props: ToolInventoryItemProps) => {
</Col>
<Col xs={3}>
<p className="tool-status">
{props.mounted ? t("mounted") : activeText}
<i>{props.mounted ? t("mounted") : activeText}</i>
</p>
</Col>
</Row>

View File

@ -294,14 +294,14 @@ describe("<FolderListItem />", () => {
it("starts sequence move", () => {
const p = fakeProps();
const wrapper = shallow(<FolderListItem {...p} />);
wrapper.find(".fa-bars").simulate("mouseDown");
wrapper.find(".fa-arrows-v").simulate("mouseDown");
expect(p.startSequenceMove).toHaveBeenCalledWith(p.sequence.uuid);
});
it("toggles sequence move", () => {
const p = fakeProps();
const wrapper = shallow(<FolderListItem {...p} />);
wrapper.find(".fa-bars").simulate("mouseUp");
wrapper.find(".fa-arrows-v").simulate("mouseUp");
expect(p.toggleSequenceMove).toHaveBeenCalledWith(p.sequence.uuid);
});
});

View File

@ -75,7 +75,7 @@ export const FolderListItem = (props: FolderItemProps) => {
<div className="sequence-list-item-icons">
{props.inUse &&
<i className="in-use fa fa-hdd-o" title={t(Content.IN_USE)} />}
<i className="fa fa-bars"
<i className="fa fa-arrows-v"
onMouseDown={() => props.startSequenceMove(sequence.uuid)}
onMouseUp={() => props.toggleSequenceMove(sequence.uuid)} />
</div>
@ -328,7 +328,7 @@ export const FolderPanelTop = (props: FolderPanelTopProps) =>
value={props.searchTerm || ""}
onChange={e => updateSearchTerm(e.currentTarget.value)}
type="text"
placeholder={t("Search sequences")} />
placeholder={t("Search sequences...")} />
</div>
</div>
<ToggleFolderBtn

View File

@ -40,13 +40,14 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => {
noFolder: (localMetaAttributes[PARENTLESS] || {}).sequences || []
};
const index = folders.map(setDefaultParentId).reduce(addToIndex, emptyIndex);
const childrenOf = (i: number) => sortBy(index[i] || [], (x) => x.name.toLowerCase());
const childrenOf = (i: number) =>
sortBy(index[i] || [], (x) => x.name.toLowerCase());
const terminal = (x: FolderNode): FolderNodeTerminal => ({
...x,
kind: "terminal",
content: (localMetaAttributes[x.id] || {}).sequences || [],
open: true,
open: false,
editing: false,
// children: [],
...(localMetaAttributes[x.id] || {})
@ -55,7 +56,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => {
const medial = (x: FolderNode): FolderNodeMedial => ({
...x,
kind: "medial",
open: true,
open: false,
editing: false,
children: childrenOf(x.id).map(terminal),
content: (localMetaAttributes[x.id] || {}).sequences || [],
@ -67,7 +68,7 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => {
return output.folders.push({
...root,
kind: "initial",
open: true,
open: false,
editing: false,
children,
content: (localMetaAttributes[root.id] || {}).sequences || [],

View File

@ -7,7 +7,7 @@ import { selectAllTools } from "../resources/selectors";
import { store } from "../redux/store";
import { getFbosConfig } from "../resources/getters";
import {
isExpressBoard, getFwHardwareValue
getFwHardwareValue, hasUTM
} from "../devices/components/firmware_hardware_support";
export enum Tours {
@ -25,26 +25,26 @@ export const tourNames = () => [
const hasTools = () =>
selectAllTools(store.getState().resources.index).length > 0;
const isExpress = () =>
isExpressBoard(getFwHardwareValue(
const noUTM = () =>
!hasUTM(getFwHardwareValue(
getFbosConfig(store.getState().resources.index)));
const toolsStep = () => hasTools()
? [{
target: ".tools",
content: isExpress()
content: noUTM()
? t(TourContent.ADD_SEED_CONTAINERS)
: t(TourContent.ADD_TOOLS),
title: isExpress()
title: noUTM()
? t("Add seed containers")
: t("Add tools and seed containers"),
}]
: [{
target: ".tools",
content: isExpress()
content: noUTM()
? t(TourContent.ADD_SEED_CONTAINERS_AND_SLOTS)
: t(TourContent.ADD_TOOLS_AND_SLOTS),
title: isExpress()
title: noUTM()
? t("Add seed containers and slots")
: t("Add tools and slots"),
}];

View File

@ -1,7 +1,7 @@
const mockStorj: Dictionary<number | boolean> = {};
import * as React from "react";
import { mount } from "enzyme";
import { mount, shallow } from "enzyme";
import { RawLogs as Logs } from "../index";
import { ToolTips } from "../../constants";
import { TaggedLog, Dictionary } from "farmbot";
@ -172,4 +172,12 @@ describe("<Logs />", () => {
wrapper.setState({ markdown: false });
expect(wrapper.html()).not.toContain("<code>message</code>");
});
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow<Logs>(<Logs {...p} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "one" } });
expect(wrapper.state().searchTerm).toEqual("one");
});
});

View File

@ -9,7 +9,8 @@ const logTypes = MESSAGE_TYPES;
describe("<LogsFilterMenu />", () => {
const fakeState: LogsState = {
autoscroll: true, markdown: false, success: 1, busy: 1, warn: 1,
autoscroll: true, markdown: false, searchTerm: "",
success: 1, busy: 1, warn: 1,
error: 1, info: 1, fun: 1, debug: 1, assertion: 1,
};
@ -24,7 +25,7 @@ describe("<LogsFilterMenu />", () => {
const wrapper = mount(<LogsFilterMenu {...fakeProps()} />);
logTypes.filter(x => x !== "assertion").map(string =>
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
["autoscroll", "markdown"].map(string =>
["autoscroll", "markdown", "searchTerm"].map(string =>
expect(wrapper.text().toLowerCase()).not.toContain(string));
});
@ -34,7 +35,7 @@ describe("<LogsFilterMenu />", () => {
const wrapper = mount(<LogsFilterMenu {...p} />);
logTypes.map(string =>
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
["autoscroll", "markdown"].map(string =>
["autoscroll", "markdown", "searchTerm"].map(string =>
expect(wrapper.text().toLowerCase()).not.toContain(string));
});

View File

@ -0,0 +1,20 @@
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { bySearchTerm } from "../logs_table";
import { fakeLog } from "../../../__test_support__/fake_state/resources";
describe("bySearchTerm()", () => {
it("includes log", () => {
const log = fakeLog();
log.body.message = "include this log";
const result = bySearchTerm("include", fakeTimeSettings())(log);
expect(result).toBeTruthy();
});
it("excludes log", () => {
const log = fakeLog();
log.body.created_at = undefined;
log.body.message = "exclude this log";
const result = bySearchTerm("include", fakeTimeSettings())(log);
expect(result).toBeFalsy();
});
});

View File

@ -28,7 +28,7 @@ const menuSort = (a: string, b: string) =>
export const filterStateKeys =
(state: LogsState, shouldDisplay: ShouldDisplay) =>
Object.keys(state)
.filter(key => !["autoscroll", "markdown"].includes(key))
.filter(key => !["autoscroll", "markdown", "searchTerm"].includes(key))
.filter(key => shouldDisplay(Feature.assertion_block)
|| key !== "assertion");

View File

@ -3,7 +3,7 @@ import { TaggedLog, ALLOWED_MESSAGE_TYPES } from "farmbot";
import { LogsState, LogsTableProps, Filters } from "../interfaces";
import { formatLogTime } from "../index";
import { Classes } from "@blueprintjs/core";
import { isNumber, startCase } from "lodash";
import { isNumber, startCase, some } from "lodash";
import { t } from "../../i18next_wrapper";
import { TimeSettings } from "../../interfaces";
import { UUID } from "../../resources/interfaces";
@ -81,6 +81,7 @@ export const LogsTable = (props: LogsTableProps) => {
</thead>
<tbody>
{filterByVerbosity(getFilterLevel(props.state), props.logs)
.filter(bySearchTerm(props.state.searchTerm, props.timeSettings))
.map((log: TaggedLog) =>
<LogsRow
key={log.uuid}
@ -114,3 +115,18 @@ export const filterByVerbosity =
return displayLog;
});
};
export const bySearchTerm =
(searchTerm: string, timeSettings: TimeSettings) =>
(log: TaggedLog) => {
const { x, y, z, created_at, message, type } = log.body;
const displayedTime = formatLogTime(created_at || NaN, timeSettings);
const displayedPosition = xyzTableEntry(x, y, z);
const lowerSearchTerm = searchTerm.toLowerCase();
return some([message, type]
.map(string => string.toLowerCase().includes(lowerSearchTerm))
.concat([
displayedTime.toLowerCase().includes(lowerSearchTerm),
displayedPosition.includes(lowerSearchTerm),
]));
};

View File

@ -49,6 +49,7 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
fun: this.initialize(NumericSetting.fun_log, 1),
debug: this.initialize(NumericSetting.debug_log, 1),
assertion: this.initialize(NumericSetting.assertion_log, 1),
searchTerm: "",
markdown: true,
};
@ -85,13 +86,13 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
const filterBtnColor = this.filterActive ? "green" : "gray";
return <Page className="logs-page">
<Row>
<Col xs={7}>
<Col xs={6}>
<h3>
<i>{t("Logs")}</i>
</h3>
<ToolTip helpText={ToolTips.LOGS} />
</Col>
<Col xs={5}>
<Col xs={6}>
<div className={"settings-menu-button"}>
<Popover position={Position.TOP_RIGHT}>
<i className="fa fa-gear" />
@ -121,6 +122,19 @@ export class RawLogs extends React.Component<LogsProps, Partial<LogsState>> {
</div>
</Col>
</Row>
<Row>
<Col xs={12} md={5} lg={4}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search" />
<input
onChange={e =>
this.setState({ searchTerm: e.currentTarget.value })}
placeholder={t("Search logs...")} />
</div>
</div>
</Col>
</Row>
<Row>
<LogsTable logs={this.props.logs}
dispatch={this.props.dispatch}

View File

@ -16,6 +16,7 @@ export type Filters = Record<ALLOWED_MESSAGE_TYPES, number>;
export interface LogsState extends Filters {
autoscroll: boolean;
searchTerm: string;
markdown: boolean;
}

View File

@ -9,23 +9,23 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
return <div className="nav-additional-menu">
<div>
<Link to="/app/account" onClick={props.close("accountMenuOpen")}>
<i className="fa fa-cog"></i>
<i className="fa fa-cog" />
{t("Account Settings")}
</Link>
</div>
<div>
<Link to="/app/logs" onClick={props.close("accountMenuOpen")}>
<i className="fa fa-list"></i>
<i className="fa fa-list" />
{t("Logs")}
</Link>
</div>
<Link to="/app/help" onClick={props.close("accountMenuOpen")}>
<i className="fa fa-question-circle"></i>
<i className="fa fa-question-circle" />
{t("Help")}
</Link>
<div>
<a onClick={props.logout}>
<i className="fa fa-sign-out"></i>
<i className="fa fa-sign-out" />
{t("Logout")}
</a>
</div>

View File

@ -3,6 +3,7 @@ import { store } from "../redux/store";
import { warning } from "../toast/toast";
import React from "react";
import { appIsReadonly } from "./app_is_read_only";
import { t } from "../i18next_wrapper";
export const readOnlyInterceptor = (config: AxiosRequestConfig) => {
const method = (config.method || "get").toLowerCase();
@ -10,7 +11,7 @@ export const readOnlyInterceptor = (config: AxiosRequestConfig) => {
if (relevant && appIsReadonly(store.getState().resources.index)) {
if (!(config.url || "").includes("web_app_config")) {
warning("Refusing to modify data in read-only mode");
warning(t("Refusing to modify data in read-only mode"));
return Promise.reject(config);
}
}
@ -18,19 +19,12 @@ export const readOnlyInterceptor = (config: AxiosRequestConfig) => {
return Promise.resolve(config);
};
const MOVE_ME_ELSEWHERE: React.CSSProperties = {
float: "right",
boxSizing: "inherit",
margin: "9px 0px 0px 9px"
};
export const ReadOnlyIcon = (p: { locked: boolean }) => {
if (p.locked) {
return <div className="fa-stack fa-lg" style={MOVE_ME_ELSEWHERE}>
<i className="fa fa-pencil fa-stack-1x"></i>
<i className="fa fa-ban fa-stack-2x fa-rotate-90 text-danger"></i>
return <div className=" read-only-icon fa-stack fa-lg">
<i className="fa fa-pencil fa-stack-1x" />
<i className="fa fa-ban fa-stack-2x fa-rotate-90 text-danger" />
</div>;
} else {
return <div />;
}

View File

@ -42,8 +42,7 @@ export class RawRegimens extends React.Component<Props, {}> {
<Row>
<LeftPanel
className={`regimen-list-panel ${activeClasses}`}
title={t("Regimens")}
helpText={t(ToolTips.REGIMEN_LIST)}>
title={t("Regimens")}>
<RegimensList
usageStats={this.props.regimenUsageStats}
dispatch={this.props.dispatch}

View File

@ -18,10 +18,10 @@ const RegimenListHeader = (props: RegimenListHeaderProps) =>
<div className={"panel-top with-button"}>
<div className="thin-search-wrapper">
<div className="text-input-wrapper">
<i className="fa fa-search"></i>
<i className="fa fa-search" />
<input
onChange={props.onChange}
placeholder={t("Search Regimens...")} />
placeholder={t("Search regimens...")} />
</div>
</div>
<AddRegimen dispatch={props.dispatch} length={props.regimenCount} />

View File

@ -11,8 +11,6 @@ import {
isTaggedGenericPointer,
isTaggedSavedGarden,
isTaggedFolder,
isTaggedPoint,
isTaggedPointGroup,
} from "./tagged_resources";
import {
ResourceName,
@ -127,20 +125,6 @@ export function maybeFindGenericPointerById(index: ResourceIndex, id: number) {
if (resource && isTaggedGenericPointer(resource)) { return resource; }
}
/** Unlike other findById methods, this one allows undefined (missed) values */
export function maybeFindPointById(index: ResourceIndex, id: number) {
const uuid = index.byKindAndId[joinKindAndId("Point", id)];
const resource = index.references[uuid || "nope"];
if (resource && isTaggedPoint(resource)) { return resource; }
}
/** Unlike other findById methods, this one allows undefined (missed) values */
export function maybeFindGroupById(index: ResourceIndex, id: number) {
const uuid = index.byKindAndId[joinKindAndId("PointGroup", id)];
const resource = index.references[uuid || "nope"];
if (resource && isTaggedPointGroup(resource)) { return resource; }
}
/** Unlike other findById methods, this one allows undefined (missed) values */
export function maybeFindSavedGardenById(index: ResourceIndex, id: number) {
const uuid = index.byKindAndId[joinKindAndId("SavedGarden", id)];

View File

@ -100,7 +100,7 @@ export function InnerIf(props: IfParams) {
confirmStepDeletion={confirmStepDeletion}>
{recursive &&
<span>
<i className="fa fa-exclamation-triangle"></i>
<i className="fa fa-exclamation-triangle" />
&nbsp;{t("Recursive condition.")}
</span>
}

View File

@ -10,6 +10,6 @@ export function BackArrow(props: BackArrowProps) {
};
return <a onClick={onClick} className="back-arrow">
<i className="fa fa-arrow-left"></i>
<i className="fa fa-arrow-left" />
</a>;
}