diff --git a/Gemfile b/Gemfile index 969c90c65..22a2c8929 100755 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ gem "scenic" gem "secure_headers" gem "tzinfo" # For validation of user selected timezone names gem "valid_url" -# gem "farady", "~> 1.0.0" +gem "kaminari" group :development, :test do gem "climate_control" diff --git a/Gemfile.lock b/Gemfile.lock index 7f22fde26..f0f340215 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,6 +153,18 @@ GEM json (2.3.0) jsonapi-renderer (0.2.2) jwt (2.2.1) + kaminari (1.2.0) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.0) + kaminari-activerecord (= 1.2.0) + kaminari-core (= 1.2.0) + kaminari-actionview (1.2.0) + actionview + kaminari-core (= 1.2.0) + kaminari-activerecord (1.2.0) + activerecord + kaminari-core (= 1.2.0) + kaminari-core (1.2.0) loofah (2.4.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -320,6 +332,7 @@ DEPENDENCIES google-cloud-storage (~> 1.11) hashdiff jwt + kaminari mutations passenger pg diff --git a/app/controllers/api/abstract_controller.rb b/app/controllers/api/abstract_controller.rb index a3fdaa4b5..4b27ef6b3 100644 --- a/app/controllers/api/abstract_controller.rb +++ b/app/controllers/api/abstract_controller.rb @@ -80,6 +80,16 @@ module Api { root: false, user: current_user } end + def maybe_paginate(collection) + page = params[:page] + per = params[:per] + + if page && per + render json: collection.page(page).per(per) + else + render json: collection + end + end private def clean_expired_farm_events diff --git a/app/controllers/api/alerts_controller.rb b/app/controllers/api/alerts_controller.rb index e1c9c2cb4..41298b2a2 100644 --- a/app/controllers/api/alerts_controller.rb +++ b/app/controllers/api/alerts_controller.rb @@ -1,7 +1,7 @@ module Api class AlertsController < Api::AbstractController def index - render json: current_device.alerts + maybe_paginate current_device.alerts end def destroy diff --git a/app/controllers/api/farm_events_controller.rb b/app/controllers/api/farm_events_controller.rb index a2913cdee..f974cd613 100644 --- a/app/controllers/api/farm_events_controller.rb +++ b/app/controllers/api/farm_events_controller.rb @@ -3,7 +3,7 @@ module Api before_action :clean_expired_farm_events, only: [:index] def index - render json: current_device.farm_events + maybe_paginate current_device.farm_events end def show diff --git a/app/controllers/api/farmware_envs_controller.rb b/app/controllers/api/farmware_envs_controller.rb index 630edf7f1..9340eb72e 100644 --- a/app/controllers/api/farmware_envs_controller.rb +++ b/app/controllers/api/farmware_envs_controller.rb @@ -10,7 +10,7 @@ module Api end def index - render json: farmware_envs + maybe_paginate farmware_envs end def show diff --git a/app/controllers/api/farmware_installations_controller.rb b/app/controllers/api/farmware_installations_controller.rb index 21a67dd3a..6b59eeaf4 100644 --- a/app/controllers/api/farmware_installations_controller.rb +++ b/app/controllers/api/farmware_installations_controller.rb @@ -1,7 +1,7 @@ module Api class FarmwareInstallationsController < Api::AbstractController def index - render json: farmware_installations + maybe_paginate farmware_installations end def show diff --git a/app/controllers/api/peripherals_controller.rb b/app/controllers/api/peripherals_controller.rb index f9fb20030..d9ed7e98e 100644 --- a/app/controllers/api/peripherals_controller.rb +++ b/app/controllers/api/peripherals_controller.rb @@ -1,7 +1,7 @@ module Api class PeripheralsController < Api::AbstractController def index - render json: current_device.peripherals + maybe_paginate current_device.peripherals end def show diff --git a/app/controllers/api/pin_bindings_controller.rb b/app/controllers/api/pin_bindings_controller.rb index a50c8821d..6809b1627 100644 --- a/app/controllers/api/pin_bindings_controller.rb +++ b/app/controllers/api/pin_bindings_controller.rb @@ -1,7 +1,7 @@ module Api class PinBindingsController < Api::AbstractController def index - render json: pin_bindings + maybe_paginate pin_bindings end def show diff --git a/app/controllers/api/plant_templates_controller.rb b/app/controllers/api/plant_templates_controller.rb index 9e398e56b..0618edcc9 100644 --- a/app/controllers/api/plant_templates_controller.rb +++ b/app/controllers/api/plant_templates_controller.rb @@ -1,7 +1,7 @@ module Api class PlantTemplatesController < Api::AbstractController def index - render json: current_device.plant_templates + maybe_paginate current_device.plant_templates end def create diff --git a/app/controllers/api/point_groups_controller.rb b/app/controllers/api/point_groups_controller.rb index a6bbef56d..bd45be617 100644 --- a/app/controllers/api/point_groups_controller.rb +++ b/app/controllers/api/point_groups_controller.rb @@ -3,7 +3,7 @@ module Api before_action :clean_expired_farm_events, only: [:destroy] def index - render json: your_point_groups + maybe_paginate your_point_groups end def show diff --git a/app/controllers/api/points_controller.rb b/app/controllers/api/points_controller.rb index 99c098c7d..18d27db68 100644 --- a/app/controllers/api/points_controller.rb +++ b/app/controllers/api/points_controller.rb @@ -20,7 +20,7 @@ module Api .where("discarded_at < ?", Time.now - HARD_DELETE_AFTER) .destroy_all - render json: points(params.fetch(:filter) { "kept" }) + maybe_paginate points(params.fetch(:filter) { "kept" }) end def show diff --git a/app/controllers/api/regimens_controller.rb b/app/controllers/api/regimens_controller.rb index 9c9eea1fd..dd168cf11 100644 --- a/app/controllers/api/regimens_controller.rb +++ b/app/controllers/api/regimens_controller.rb @@ -3,7 +3,7 @@ module Api before_action :clean_expired_farm_events, only: [:destroy] def index - render json: your_regimens + maybe_paginate your_regimens end def show diff --git a/app/controllers/api/saved_gardens_controller.rb b/app/controllers/api/saved_gardens_controller.rb index f8bcbc8e1..bd689d8b8 100644 --- a/app/controllers/api/saved_gardens_controller.rb +++ b/app/controllers/api/saved_gardens_controller.rb @@ -1,7 +1,7 @@ module Api class SavedGardensController < Api::AbstractController def index - render json: current_device.saved_gardens + maybe_paginate current_device.saved_gardens end def create diff --git a/app/controllers/api/sensor_readings_controller.rb b/app/controllers/api/sensor_readings_controller.rb index 1954a04c1..a0e945873 100644 --- a/app/controllers/api/sensor_readings_controller.rb +++ b/app/controllers/api/sensor_readings_controller.rb @@ -5,7 +5,7 @@ module Api end def index - render json: readings + maybe_paginate(readings) end def show diff --git a/app/controllers/api/sensors_controller.rb b/app/controllers/api/sensors_controller.rb index 2e259dc74..aff19605e 100644 --- a/app/controllers/api/sensors_controller.rb +++ b/app/controllers/api/sensors_controller.rb @@ -1,7 +1,7 @@ module Api class SensorsController < Api::AbstractController def index - render json: current_device.sensors + maybe_paginate current_device.sensors end def show diff --git a/app/controllers/api/tools_controller.rb b/app/controllers/api/tools_controller.rb index 706d6398d..28c70c4a1 100644 --- a/app/controllers/api/tools_controller.rb +++ b/app/controllers/api/tools_controller.rb @@ -2,7 +2,7 @@ module Api class ToolsController < Api::AbstractController def index - render json: tools + maybe_paginate tools end def show diff --git a/app/controllers/api/webcam_feeds_controller.rb b/app/controllers/api/webcam_feeds_controller.rb index a22d99ebe..82ae43f80 100644 --- a/app/controllers/api/webcam_feeds_controller.rb +++ b/app/controllers/api/webcam_feeds_controller.rb @@ -7,7 +7,7 @@ module Api end def index - render json: webcams + maybe_paginate webcams end def show diff --git a/app/serializers/point_group_serializer.rb b/app/serializers/point_group_serializer.rb index 66e128aa4..f762c7e3c 100644 --- a/app/serializers/point_group_serializer.rb +++ b/app/serializers/point_group_serializer.rb @@ -4,4 +4,8 @@ class PointGroupSerializer < ApplicationSerializer def point_ids object.point_group_items.pluck(:point_id) end + + def criteria + object.criteria || PointGroup::DEFAULT_CRITERIA + end end diff --git a/db/migrate/20200204230135_add_show_zones_to_web_app_config.rb b/db/migrate/20200204230135_add_show_zones_to_web_app_config.rb new file mode 100644 index 000000000..f4a23279b --- /dev/null +++ b/db/migrate/20200204230135_add_show_zones_to_web_app_config.rb @@ -0,0 +1,8 @@ +class AddShowZonesToWebAppConfig < ActiveRecord::Migration[6.0] + def change + add_column :web_app_configs, + :show_zones, + :boolean, + default: false + end +end diff --git a/frontend/__test_support__/additional_mocks.ts b/frontend/__test_support__/additional_mocks.tsx similarity index 73% rename from frontend/__test_support__/additional_mocks.ts rename to frontend/__test_support__/additional_mocks.tsx index 6d80eed8a..6b2b3046d 100644 --- a/frontend/__test_support__/additional_mocks.ts +++ b/frontend/__test_support__/additional_mocks.tsx @@ -1,3 +1,5 @@ +import * as React from "react"; + jest.mock("browser-speech", () => ({ talk: jest.fn(), })); @@ -16,3 +18,8 @@ window.location = { pathname: "", href: "", hash: "", search: "", hostname: "", origin: "", port: "", protocol: "", host: "", }; + +jest.mock("../error_boundary", () => ({ + // tslint:disable-next-line:no-any + ErrorBoundary: (p: any) =>
{p.children}
, +})); diff --git a/frontend/__test_support__/fake_html_events.ts b/frontend/__test_support__/fake_html_events.ts new file mode 100644 index 000000000..e0aecfe1e --- /dev/null +++ b/frontend/__test_support__/fake_html_events.ts @@ -0,0 +1,36 @@ +import { DeepPartial } from "redux"; + +type DomEvent = React.SyntheticEvent; +export const inputEvent = (value: string, name?: string): DomEvent => { + const event: DeepPartial = { currentTarget: { value, name } }; + return event as DomEvent; +}; + +type ChangeEvent = React.ChangeEvent; +export const changeEvent = (value: string): ChangeEvent => { + const event: DeepPartial = { currentTarget: { value } }; + return event as ChangeEvent; +}; + +type IMGEvent = React.SyntheticEvent; +export const imgEvent = (): IMGEvent => { + const event: DeepPartial = { + currentTarget: { + getAttribute: jest.fn(), + setAttribute: jest.fn(), + } + }; + return event as IMGEvent; +}; + +type FormEvent = React.FormEvent; +export const formEvent = (): FormEvent => { + const event: Partial = { preventDefault: jest.fn() }; + return event as FormEvent; +}; + +type DragEvent = React.DragEvent; +export const dragEvent = (key: string): DragEvent => { + const event: DeepPartial = { dataTransfer: { getData: () => key } }; + return event as DragEvent; +}; diff --git a/frontend/__test_support__/fake_input_event.ts b/frontend/__test_support__/fake_input_event.ts deleted file mode 100644 index bd742a7ca..000000000 --- a/frontend/__test_support__/fake_input_event.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DeepPartial } from "redux"; - -type DomEvent = React.SyntheticEvent; -export const inputEvent = (value: string): DomEvent => { - const event: DeepPartial = { currentTarget: { value } }; - return event as DomEvent; -}; diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts index 9a545bee4..641cba1bb 100644 --- a/frontend/__test_support__/fake_state/resources.ts +++ b/frontend/__test_support__/fake_state/resources.ts @@ -316,6 +316,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig { show_historic_points: false, time_format_24_hour: false, show_pins: false, + show_zones: false, disable_emergency_unlock_confirmation: false, map_size_x: 2900, map_size_y: 1400, @@ -459,7 +460,7 @@ export function fakePointGroup(): TaggedPointGroup { sort_type: "xy_ascending", point_ids: [], criteria: { - day: { op: ">", days: 0 }, + day: { op: "<", days: 0 }, number_eq: {}, number_gt: {}, number_lt: {}, diff --git a/frontend/__test_support__/unmock_i18next.ts b/frontend/__test_support__/mock_i18next.ts similarity index 100% rename from frontend/__test_support__/unmock_i18next.ts rename to frontend/__test_support__/mock_i18next.ts diff --git a/frontend/__tests__/error_boundary_test.tsx b/frontend/__tests__/error_boundary_test.tsx index 224731b7e..6de0190cd 100644 --- a/frontend/__tests__/error_boundary_test.tsx +++ b/frontend/__tests__/error_boundary_test.tsx @@ -1,3 +1,5 @@ +jest.unmock("../error_boundary"); + jest.mock("../util/errors.ts", () => ({ catchErrors: jest.fn() })); import * as React from "react"; diff --git a/frontend/connectivity/__tests__/ping_mqtt_test.ts b/frontend/connectivity/__tests__/ping_mqtt_test.ts index 9e0de13a1..ea00a23cb 100644 --- a/frontend/connectivity/__tests__/ping_mqtt_test.ts +++ b/frontend/connectivity/__tests__/ping_mqtt_test.ts @@ -4,8 +4,6 @@ jest.mock("../index", () => ({ dispatchQosStart: jest.fn(), pingOK: jest.fn() })); -const mockTimestamp = 0; -jest.mock("../../util", () => ({ timestamp: () => mockTimestamp })); import { readPing, diff --git a/frontend/constants.ts b/frontend/constants.ts index 8617b8829..71388071f 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -572,12 +572,6 @@ export namespace Content { export const CONFIRM_PLANT_DELETION = trim(`Show a confirmation dialog when deleting a plant.`); - export const SORT_DESCRIPTION = - trim(`When executing a sequence over a Group of locations, FarmBot will - travel to each group member in the order of the chosen sort method. - If the random option is chosen, FarmBot will travel in a random order - every time, so the ordering shown below will only be representative.`); - // Device export const NOT_HTTPS = trim(`WARNING: Sending passwords via HTTP:// is not secure.`); @@ -824,6 +818,20 @@ export namespace Content { trim(`You haven't made any sequences or regimens yet. To add an event, first create a sequence or regimen.`); + // Groups + export const SORT_DESCRIPTION = + trim(`When executing a sequence over a Group of locations, FarmBot will + travel to each group member in the order of the chosen sort method. + If the random option is chosen, FarmBot will travel in a random order + every time, so the ordering shown below will only be representative.`); + + export const CRITERIA_SELECTION_COUNT = + trim(`Criteria additions can only be removed by changing criteria. + Click and drag in the map to modify zone selection criteria. + Criteria will be applied at the time of sequence execution. The final + selection at that time may differ from the selection currently + displayed.`); + // Farmware export const NO_IMAGES_YET = trim(`You haven't yet taken any photos with your FarmBot. @@ -912,12 +920,12 @@ export namespace DiagnosticMessages { network, a firewall may be blocking port 5672. Ensure that the blue LED communications light on the FarmBot electronics box is illuminated.`); - export const WIFI_OR_CONFIG = trim(`Your browser is connected correctly, but - we have no recent record of FarmBot connecting to the internet. This usually - happens because of poor WiFi connectivity in the garden, a bad password during - configuration, a very long power outage, or blocked ports on FarmBot's local - network. Please refer IT staff to - https://software.farm.bot/docs/for-it-security-professionals`); + export const WIFI_OR_CONFIG = trim(`Your browser is connected correctly, + but we have no recent record of FarmBot connecting to the internet. + This usually happens because of poor WiFi connectivity in the garden, + a bad password during configuration, a very long power outage, or + blocked ports on FarmBot's local network. Please refer IT staff to + https://software.farm.bot/docs/for-it-security-professionals`); export const NO_WS_AVAILABLE = trim(`You are either offline, using a web browser that does not support WebSockets, or are behind a firewall that diff --git a/frontend/controls/__tests__/controls_test.tsx b/frontend/controls/__tests__/controls_test.tsx index abea6315f..15462ac06 100644 --- a/frontend/controls/__tests__/controls_test.tsx +++ b/frontend/controls/__tests__/controls_test.tsx @@ -25,6 +25,7 @@ describe("", () => { sensorReadings: [], timeSettings: fakeTimeSettings(), env: {}, + firmwareHardware: undefined, }); it("shows webcam widget", () => { diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index 7574ccd05..bcd98afa0 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -34,6 +34,7 @@ export class RawControls extends React.Component { arduinoBusy={this.arduinoBusy} botToMqttStatus={this.props.botToMqttStatus} firmwareSettings={this.props.firmwareSettings} + firmwareHardware={this.props.firmwareHardware} getWebAppConfigVal={this.props.getWebAppConfigVal} /> peripherals = () => { return Promise.resolve(); }), + moveAbsolute: jest.fn(() => Promise.resolve()), }; +jest.mock("../../../device", () => ({ getDevice: () => mockDevice })); -jest.mock("../../../device", () => ({ - getDevice: () => (mockDevice) +jest.mock("../../../config_storage/actions", () => ({ + toggleWebAppBool: jest.fn() })); -jest.mock("../../../config_storage/actions", () => { - return { - toggleWebAppBool: jest.fn() - }; -}); - import * as React from "react"; import { shallow, mount } from "enzyme"; import { BotPositionRows, BotPositionRowsProps } from "../bot_position_rows"; @@ -22,14 +17,12 @@ import { BooleanSetting } from "../../../session_keys"; describe("", () => { const mockConfig: Dictionary = {}; - function fakeProps(): BotPositionRowsProps { - return { - getValue: jest.fn(key => mockConfig[key]), - locationData: bot.hardware.location_data, - arduinoBusy: false, - firmware_version: undefined, - }; - } + const fakeProps = (): BotPositionRowsProps => ({ + getValue: jest.fn(key => mockConfig[key]), + locationData: bot.hardware.location_data, + arduinoBusy: false, + firmwareHardware: undefined, + }); it("inputs axis destination", () => { const wrapper = shallow(); @@ -38,19 +31,21 @@ describe("", () => { expect(mockDevice.moveAbsolute).toHaveBeenCalledWith("123"); }); - it("shows encoder position in steps", () => { + it("shows encoder position", () => { mockConfig[BooleanSetting.scaled_encoders] = true; + mockConfig[BooleanSetting.raw_encoders] = true; const p = fakeProps(); - p.firmware_version = undefined; + p.firmwareHardware = undefined; const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("scaled encoder (steps)"); + expect(wrapper.text().toLowerCase()).toContain("encoder"); }); - it("shows encoder position in mm", () => { + it("doesn't show encoder position", () => { mockConfig[BooleanSetting.scaled_encoders] = true; + mockConfig[BooleanSetting.raw_encoders] = true; const p = fakeProps(); - p.firmware_version = "6.0.0"; + p.firmwareHardware = "express_k10"; const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("scaled encoder (mm)"); + expect(wrapper.text().toLowerCase()).not.toContain("encoder"); }); }); diff --git a/frontend/controls/move/__tests__/move_test.tsx b/frontend/controls/move/__tests__/move_test.tsx index d9ba41196..eae4f86f4 100644 --- a/frontend/controls/move/__tests__/move_test.tsx +++ b/frontend/controls/move/__tests__/move_test.tsx @@ -33,6 +33,7 @@ describe("", () => { firmwareSettings: bot.hardware.mcu_params, getWebAppConfigVal: jest.fn((key) => (mockConfig[key])), env: {}, + firmwareHardware: undefined, }); it("has default elements", () => { diff --git a/frontend/controls/move/__tests__/settings_menu_test.tsx b/frontend/controls/move/__tests__/settings_menu_test.tsx index 8ae8e52e5..6393e2b20 100644 --- a/frontend/controls/move/__tests__/settings_menu_test.tsx +++ b/frontend/controls/move/__tests__/settings_menu_test.tsx @@ -8,7 +8,9 @@ jest.mock("../../../account/dev/dev_support", () => ({ import * as React from "react"; import { mount } from "enzyme"; import { BooleanSetting } from "../../../session_keys"; -import { moveWidgetSetting, MoveWidgetSettingsMenu } from "../settings_menu"; +import { + moveWidgetSetting, MoveWidgetSettingsMenu, MoveWidgetSettingsMenuProps +} from "../settings_menu"; describe("moveWidgetSetting()", () => { it("renders setting", () => { @@ -22,9 +24,10 @@ describe("moveWidgetSetting()", () => { }); describe("", () => { - const fakeProps = () => ({ + const fakeProps = (): MoveWidgetSettingsMenuProps => ({ toggle: jest.fn(), getValue: jest.fn(), + firmwareHardware: undefined, }); it("displays motor plot toggle", () => { @@ -35,4 +38,16 @@ describe("", () => { expect(wrapper.text()).toContain("Motor position plot"); mockDev = false; }); + + it("displays encoder toggles", () => { + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("encoder"); + }); + + it("doesn't display encoder toggles", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("encoder"); + }); }); diff --git a/frontend/controls/move/bot_position_rows.tsx b/frontend/controls/move/bot_position_rows.tsx index 4b5b6695c..6dbf1a026 100644 --- a/frontend/controls/move/bot_position_rows.tsx +++ b/frontend/controls/move/bot_position_rows.tsx @@ -2,26 +2,23 @@ import * as React from "react"; import { Row, Col } from "../../ui"; import { BotLocationData } from "../../devices/interfaces"; import { moveAbs } from "../../devices/actions"; -import { minFwVersionCheck } from "../../util"; import { AxisDisplayGroup } from "../axis_display_group"; 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 { FirmwareHardware } from "farmbot"; export interface BotPositionRowsProps { locationData: BotLocationData; getValue: GetWebAppBool; arduinoBusy: boolean; - firmware_version: string | undefined; + firmwareHardware: FirmwareHardware | undefined; } export const BotPositionRows = (props: BotPositionRowsProps) => { - const { locationData, getValue, arduinoBusy, firmware_version } = props; - const scaled_encoder_label = - minFwVersionCheck(firmware_version, "5.0.5") - ? t("Scaled Encoder (mm)") - : t("Scaled Encoder (steps)"); + const { locationData, getValue, arduinoBusy } = props; return
@@ -37,11 +34,13 @@ export const BotPositionRows = (props: BotPositionRowsProps) => { - {getValue(BooleanSetting.scaled_encoders) && + {!isExpressBoard(props.firmwareHardware) && + getValue(BooleanSetting.scaled_encoders) && } - {getValue(BooleanSetting.raw_encoders) && + label={t("Scaled Encoder (mm)")} />} + {!isExpressBoard(props.firmwareHardware) && + getValue(BooleanSetting.raw_encoders) && } diff --git a/frontend/controls/move/interfaces.ts b/frontend/controls/move/interfaces.ts index d3ab61465..6d549b46e 100644 --- a/frontend/controls/move/interfaces.ts +++ b/frontend/controls/move/interfaces.ts @@ -1,5 +1,5 @@ import { BotPosition, BotState, UserEnv } from "../../devices/interfaces"; -import { McuParams, Xyz } from "farmbot"; +import { McuParams, Xyz, FirmwareHardware } from "farmbot"; import { NetworkState } from "../../connectivity/interfaces"; import { GetWebAppConfigValue } from "../../config_storage/actions"; import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app"; @@ -15,6 +15,7 @@ export interface MoveProps { firmwareSettings: McuParams; getWebAppConfigVal: GetWebAppConfigValue; env: UserEnv; + firmwareHardware: FirmwareHardware | undefined; } export interface DirectionButtonProps { diff --git a/frontend/controls/move/move.tsx b/frontend/controls/move/move.tsx index fa9a58155..dd3916082 100644 --- a/frontend/controls/move/move.tsx +++ b/frontend/controls/move/move.tsx @@ -33,7 +33,8 @@ export class Move extends React.Component { + getValue={this.getValue} + firmwareHardware={this.props.firmwareHardware} /> @@ -53,7 +54,7 @@ export class Move extends React.Component { locationData={locationData} getValue={this.getValue} arduinoBusy={this.props.arduinoBusy} - firmware_version={informational_settings.firmware_version} /> + firmwareHardware={this.props.firmwareHardware} /> {this.props.getWebAppConfigVal(BooleanSetting.show_motor_plot) && } diff --git a/frontend/controls/move/settings_menu.tsx b/frontend/controls/move/settings_menu.tsx index 6367372c8..094ac35d2 100644 --- a/frontend/controls/move/settings_menu.tsx +++ b/frontend/controls/move/settings_menu.tsx @@ -5,6 +5,8 @@ import { ToggleWebAppBool, GetWebAppBool } from "./interfaces"; 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"; export const moveWidgetSetting = (toggle: ToggleWebAppBool, getValue: GetWebAppBool) => @@ -18,10 +20,15 @@ export const moveWidgetSetting = toggleValue={getValue(setting)} /> ; -export const MoveWidgetSettingsMenu = ({ toggle, getValue }: { - toggle: ToggleWebAppBool, - getValue: GetWebAppBool -}) => { +export interface MoveWidgetSettingsMenuProps { + toggle: ToggleWebAppBool; + getValue: GetWebAppBool; + firmwareHardware: FirmwareHardware | undefined; +} + +export const MoveWidgetSettingsMenu = ( + { toggle, getValue, firmwareHardware }: MoveWidgetSettingsMenuProps +) => { const Setting = moveWidgetSetting(toggle, getValue); return

{t("Invert Jog Buttons")}

@@ -29,13 +36,16 @@ export const MoveWidgetSettingsMenu = ({ toggle, getValue }: { -

{t("Display Encoder Data")}

- - + {!isExpressBoard(firmwareHardware) && +
+

{t("Display Encoder Data")}

+ + +
}

{t("Swap jog buttons (and rotate map)")}

diff --git a/frontend/controls/state_to_props.ts b/frontend/controls/state_to_props.ts index 49b5c2f8a..aa0c83916 100644 --- a/frontend/controls/state_to_props.ts +++ b/frontend/controls/state_to_props.ts @@ -7,12 +7,14 @@ import { maybeGetTimeSettings } from "../resources/selectors"; import { Props } from "./interfaces"; -import { validFwConfig } from "../util"; +import { validFwConfig, validFbosConfig } from "../util"; import { getWebAppConfigValue } from "../config_storage/actions"; -import { getFirmwareConfig } from "../resources/getters"; +import { getFirmwareConfig, getFbosConfig } from "../resources/getters"; import { uniq } from "lodash"; import { getStatus } from "../connectivity/reducer_support"; import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props"; +import { sourceFbosConfigValue } from "../devices/components/source_config_value"; +import { isFwHardwareValue } from "../devices/components/firmware_hardware_support"; export function mapStateToProps(props: Everything): Props { const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index)); @@ -21,6 +23,12 @@ export function mapStateToProps(props: Everything): Props { const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot); const env = getEnv(props.resources.index, shouldDisplay, props.bot); + const { configuration } = props.bot.hardware; + const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index)); + const sourceFbosConfig = sourceFbosConfigValue(fbosConfig, configuration); + const { value } = sourceFbosConfig("firmware_hardware"); + const firmwareHardware = isFwHardwareValue(value) ? value : undefined; + return { feeds: selectAllWebcamFeeds(props.resources.index), dispatch: props.dispatch, @@ -34,5 +42,6 @@ export function mapStateToProps(props: Everything): Props { sensorReadings: selectAllSensorReadings(props.resources.index), timeSettings: maybeGetTimeSettings(props.resources.index), env, + firmwareHardware, }; } diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index 4a6529760..74d11b8af 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -213,7 +213,7 @@ .groups-list-wrapper { padding: 0.5em 0em; } - .groups-delete-btn { + .group-delete-btn { float: left; margin-top: 1em; } @@ -332,6 +332,21 @@ } } +.zones-layer { + [id*="zones-1D-"] { + stroke: $black; + stroke-width: 5; + } + [id*="zones-"] { + opacity: 0.1; + &.current { + opacity: 0.25; + fill: $white; + stroke: $white; + } + } +} + .virtual-bot-trail, .virtual-peripherals { pointer-events: none; diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index f00163816..a1867cc96 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -738,6 +738,114 @@ } } +.group-detail-panel { + .panel-content { + .group-criteria { + margin-top: 1rem; + .criteria-heading { + margin-top: 0; + } + .fb-button { + margin-top: 0.5rem; + } + .group-criteria-presets { + input[type="radio"] { + width: auto; + margin-right: 1rem; + } + p { + display: inline; + text-transform: uppercase; + } + } + .criteria-string, + .criteria-pointer-type, + .criteria-plant-status, + .criteria-slug { + margin-top: 1rem; + } + .location-criteria { + .row { + margin-top: 1rem; + p { + font-size: 1.4rem; + font-weight: bold; + } + label { + margin-top: 0; + } + } + } + .day-criteria { + p { + display: inline; + vertical-align: bottom; + } + } + .string-eq-criteria { + margin-top: 1rem; + .row { + margin-top: 1rem; + } + } + .number-eq-criteria, + .number-gt-lt-criteria { + margin-top: 1rem; + .row { + margin-top: 1rem; + } + p { + text-align: center; + margin-top: 0.5rem; + } + } + .expandable-header { + margin-top: 3rem; + } + } + .criteria-point-count-breakdown { + margin-bottom: 1rem; + .manual-group-member-count, + .criteria-group-member-count { + margin-left: 2rem; + div { + display: inline; + padding: 0.25rem; + font-size: 1.2rem; + border: 1px solid $panel_light_blue; + } + p { + display: inline; + margin-left: 1rem; + } + } + .criteria-group-member-count { + div { + border: 1px solid gray; + border-radius: 5px; + } + } + } + } +} + +.zone-info-panel { + .panel-content { + .location-criteria { + .row { + margin-top: 1rem; + p { + font-size: 1.4rem; + font-weight: bold; + } + label { + margin-top: 0; + } + } + } + } +} + .weeds-inventory-panel, .zones-inventory-panel, .groups-panel { diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 04ecd0a8f..8e320263b 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -1579,7 +1579,7 @@ textarea:focus { } } table { - margin: 2rem; + margin: auto; margin-top: 3rem; width: 93%; font-size: 1.2rem; @@ -1596,6 +1596,7 @@ textarea:focus { padding-bottom: 1rem; span { display: block; + white-space: nowrap; } } } diff --git a/frontend/css/inputs.scss b/frontend/css/inputs.scss index 4e5d9d508..e31b27443 100644 --- a/frontend/css/inputs.scss +++ b/frontend/css/inputs.scss @@ -95,9 +95,9 @@ select { } } &.disabled { + pointer-events: none; button { background: darken($white, 10%) !important; - pointer-events: none; } } } diff --git a/frontend/devices/components/__tests__/firmware_hardware_support_test.ts b/frontend/devices/components/__tests__/firmware_hardware_support_test.ts index 3c3d11b64..e7f5e2a4a 100644 --- a/frontend/devices/components/__tests__/firmware_hardware_support_test.ts +++ b/frontend/devices/components/__tests__/firmware_hardware_support_test.ts @@ -2,7 +2,7 @@ import { boardType } from "../firmware_hardware_support"; describe("boardType()", () => { it("returns Farmduino", () => { - expect(boardType("5.0.3.F")).toEqual("farmduino"); + expect(boardType("5.0.3.F.extra")).toEqual("farmduino"); }); it("returns Farmduino k1.4", () => { @@ -25,6 +25,7 @@ describe("boardType()", () => { expect(boardType(undefined)).toEqual("unknown"); expect(boardType("Arduino Disconnected!")).toEqual("unknown"); expect(boardType("STUBFW")).toEqual("unknown"); + expect(boardType("0.0.0.S.STUB")).toEqual("unknown"); }); it("returns None", () => { diff --git a/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx b/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx index 71b2b1abe..410b12931 100644 --- a/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/boot_sequence_selector_test.tsx @@ -1,7 +1,13 @@ -import { sequence2ddi, mapStateToProps, RawBootSequenceSelector } from "../boot_sequence_selector"; -import { fakeSequence, fakeFbosConfig } from "../../../../__test_support__/fake_state/resources"; +import { + sequence2ddi, mapStateToProps, RawBootSequenceSelector +} from "../boot_sequence_selector"; +import { + fakeSequence, fakeFbosConfig +} from "../../../../__test_support__/fake_state/resources"; import { fakeState } from "../../../../__test_support__/fake_state"; -import { buildResourceIndex } from "../../../../__test_support__/resource_index_builder"; +import { + buildResourceIndex +} from "../../../../__test_support__/resource_index_builder"; import React from "react"; import { mount } from "enzyme"; import { FBSelect } from "../../../../ui"; diff --git a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx index 4b5bb5fd7..f7cd59033 100644 --- a/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx +++ b/frontend/devices/components/fbos_settings/__tests__/fbos_details_test.tsx @@ -37,7 +37,7 @@ describe("", () => { p.botInfoSettings.commit = "fakeCommit"; p.botInfoSettings.target = "fakeTarget"; p.botInfoSettings.node_name = "fakeName"; - p.botInfoSettings.firmware_version = "fakeFirmware"; + p.botInfoSettings.firmware_version = "0.0.0.R.ramps"; p.botInfoSettings.firmware_commit = "fakeFwCommit"; p.botInfoSettings.soc_temp = 48.3; p.botInfoSettings.wifi_level = -49; @@ -50,8 +50,9 @@ describe("", () => { "Commit", "fakeComm", "Target", "fakeTarget", "Node name", "fakeName", - "Firmware", "fakeFirmware", + "Firmware", "0.0.0 Arduino/RAMPS (Genesis v1.2)", "Firmware commit", "fakeFwCo", + "Firmware code", "0.0.0.R.ramps", "FAKETARGET CPU temperature", "48.3", "C", "WiFi strength", "-49dBm", "OS release channel", @@ -70,6 +71,20 @@ describe("", () => { expect(wrapper.text()).not.toContain("name@"); }); + it("handles missing firmware version", () => { + const p = fakeProps(); + p.botInfoSettings.firmware_version = undefined; + const wrapper = mount(); + expect(wrapper.text()).toContain("---"); + }); + + it("handles unknown firmware version", () => { + const p = fakeProps(); + p.botInfoSettings.firmware_version = "0.0.0.S.S"; + const wrapper = mount(); + expect(wrapper.text()).toContain("0.0.0"); + }); + it("displays commit link", () => { const p = fakeProps(); p.botInfoSettings.commit = "abcdefgh"; diff --git a/frontend/devices/components/fbos_settings/fbos_details.tsx b/frontend/devices/components/fbos_settings/fbos_details.tsx index d4c95198c..9070b57cf 100644 --- a/frontend/devices/components/fbos_settings/fbos_details.tsx +++ b/frontend/devices/components/fbos_settings/fbos_details.tsx @@ -13,6 +13,7 @@ import moment from "moment"; import { timeFormatString } from "../../../util"; import { TimeSettings } from "../../../interfaces"; import { StringConfigKey } from "farmbot/dist/resources/configs/fbos"; +import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support"; /** Return an indicator color for the given temperature (C). */ export const colorFromTemp = (temp: number | undefined): string => { @@ -244,6 +245,13 @@ const reformatDatetime = (datetime: string, timeSettings: TimeSettings) => .utcOffset(timeSettings.utcOffset) .format(`MMMM D, ${timeFormatString(timeSettings)}`); +const reformatFwVersion = (firmwareVersion: string | undefined): string => { + const version = firmwareVersion ? + firmwareVersion.split(".").slice(0, 3).join(".") : "none"; + const board = FIRMWARE_CHOICES_DDI[boardType(firmwareVersion)]?.label || ""; + return version == "none" ? "---" : `${version} ${board}`; +}; + /** Current technical information about FarmBot OS running on the device. */ export function FbosDetails(props: FbosDetailsProps) { const { @@ -265,9 +273,10 @@ export function FbosDetails(props: FbosDetailsProps) {

{t("Node name")}: {last((node_name || "").split("@"))}

{t("Device ID")}: {props.deviceAccount.body.id}

{isString(private_ip) &&

{t("Local IP address")}: {private_ip}

} -

{t("Firmware")}: {firmware_version}

+

{t("Firmware")}: {reformatFwVersion(firmware_version)}

+

{t("Firmware code")}: {firmware_version}

{isNumber(uptime) && } {isNumber(memory_usage) &&

{t("Memory usage")}: {memory_usage}MB

} diff --git a/frontend/devices/components/firmware_hardware_support.ts b/frontend/devices/components/firmware_hardware_support.ts index c7b3e2190..2178c1b8f 100644 --- a/frontend/devices/components/firmware_hardware_support.ts +++ b/frontend/devices/components/firmware_hardware_support.ts @@ -11,9 +11,18 @@ export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => { return !!values.includes(x as FirmwareHardware); }; +const TMC_BOARDS = ["express_k10", "farmduino_k15"]; +const EXPRESS_BOARDS = ["express_k10"]; + +export const isTMCBoard = (firmwareHardware: FirmwareHardware | undefined) => + !!(firmwareHardware && TMC_BOARDS.includes(firmwareHardware)); + +export const isExpressBoard = (firmwareHardware: FirmwareHardware | undefined) => + !!(firmwareHardware && EXPRESS_BOARDS.includes(firmwareHardware)); + export const getBoardIdentifier = (firmwareVersion: string | undefined): string => - firmwareVersion ? firmwareVersion.slice(-1) : "undefined"; + firmwareVersion ? firmwareVersion.split(".")[3] : "undefined"; export const isKnownBoard = (firmwareVersion: string | undefined): boolean => { const boardIdentifier = getBoardIdentifier(firmwareVersion); diff --git a/frontend/devices/components/hardware_settings.tsx b/frontend/devices/components/hardware_settings.tsx index 1dbbdaf44..a94d92497 100644 --- a/frontend/devices/components/hardware_settings.tsx +++ b/frontend/devices/components/hardware_settings.tsx @@ -25,7 +25,6 @@ export class HardwareSettings extends botToMqttStatus, firmwareHardware, resources } = this.props; const { informational_settings } = this.props.bot.hardware; - const firmwareVersion = informational_settings.firmware_version; const { sync_status } = informational_settings; const botDisconnected = !isBotOnline(sync_status, botToMqttStatus); return @@ -68,16 +67,15 @@ export class HardwareSettings extends botDisconnected={botDisconnected} /> + sourceFwConfig={sourceFwConfig} + firmwareHardware={firmwareHardware} /> ", () => { sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }), shouldDisplay: jest.fn(key => mockFeatures[key]), + firmwareHardware: undefined, }); - it("shows new inversion param", () => { - mockFeatures.endstop_invert = true; - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).not.toContain("invert endstops"); + it("shows encoder labels", () => { + const p = fakeProps(); + p.firmwareHardware = undefined; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("encoder"); + expect(wrapper.text().toLowerCase()).not.toContain("stall"); + }); + + it("shows stall labels", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("encoder"); + expect(wrapper.text().toLowerCase()).toContain("stall"); }); it.each<["short" | "long"]>([ diff --git a/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx b/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx index 1790fc70c..6fd42b781 100644 --- a/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/homing_and_calibration_test.tsx @@ -1,6 +1,4 @@ -jest.mock("../../../actions", () => ({ - updateMCU: jest.fn() -})); +jest.mock("../../../actions", () => ({ updateMCU: jest.fn() })); import * as React from "react"; import { mount } from "enzyme"; @@ -11,23 +9,22 @@ import { fakeFirmwareConfig } from "../../../../__test_support__/fake_state/resources"; import { error, warning } from "../../../../toast/toast"; +import { inputEvent } from "../../../../__test_support__/fake_html_events"; describe("", () => { function testAxisLengthInput( - fw: string, provided: string, expected: string | undefined) { + provided: string, expected: string | undefined) { const dispatch = jest.fn(); bot.controlPanelState.homing_and_calibration = true; - bot.hardware.informational_settings.firmware_version = fw; const result = mount( { - return { value: bot.hardware.mcu_params[x], consistent: true }; - }} + sourceFwConfig={x => ({ + value: bot.hardware.mcu_params[x], consistent: true + })} botDisconnected={false} />); - const e = { currentTarget: { value: provided } } as - React.SyntheticEvent; + const e = inputEvent(provided); const input = result.find("input").first().props(); input.onChange && input.onChange(e); input.onSubmit && input.onSubmit(e); @@ -36,20 +33,15 @@ describe("", () => { .toHaveBeenCalledWith("movement_axis_nr_steps_x", expected) : expect(updateMCU).not.toHaveBeenCalled(); } - it("short int", () => { - testAxisLengthInput("5.0.0", "100000", undefined); - expect(error) - .toHaveBeenCalledWith("Value must be less than or equal to 32000."); - }); it("long int: too long", () => { - testAxisLengthInput("6.0.0", "10000000000", undefined); + testAxisLengthInput("10000000000", undefined); expect(error) .toHaveBeenCalledWith("Value must be less than or equal to 2000000000."); }); it("long int: ok", () => { - testAxisLengthInput("6.0.0", "100000", "100000"); + testAxisLengthInput("100000", "100000"); expect(warning).not.toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); }); diff --git a/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx b/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx index 771eca78f..fb2df2174 100644 --- a/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx +++ b/frontend/devices/components/hardware_settings/__tests__/motors_test.tsx @@ -28,10 +28,8 @@ describe("", () => { controlPanelState.motors = true; return { dispatch: jest.fn(x => x(jest.fn(), () => state)), - firmwareVersion: undefined, controlPanelState, sourceFwConfig: () => ({ value: 0, consistent: true }), - isValidFwConfig: true, firmwareHardware: undefined, }; }; @@ -46,27 +44,20 @@ describe("", () => { expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); }); - it("doesn't render homing speed", () => { + it("shows TMC parameters", () => { const p = fakeProps(); - p.firmwareVersion = "4.0.0R"; - p.isValidFwConfig = false; + p.firmwareHardware = "express_k10"; const wrapper = render(); - expect(wrapper.text()).not.toContain("Homing Speed"); + expect(wrapper.text()).toContain("Stall"); + expect(wrapper.text()).toContain("Current"); }); - it("renders homing speed", () => { + it("doesn't show TMC parameters", () => { const p = fakeProps(); - p.firmwareVersion = "5.1.0R"; + p.firmwareHardware = "farmduino"; const wrapper = render(); - expect(wrapper.text()).toContain("Homing Speed"); - }); - - it("renders homing speed while disconnected", () => { - const p = fakeProps(); - p.firmwareVersion = undefined; - p.isValidFwConfig = true; - const wrapper = render(); - expect(wrapper.text()).toContain("Homing Speed"); + expect(wrapper.text()).not.toContain("Stall"); + expect(wrapper.text()).not.toContain("Current"); }); const testParamToggle = ( diff --git a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx b/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx index 558f546d6..72bdce981 100644 --- a/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx +++ b/frontend/devices/components/hardware_settings/encoders_and_endstops.tsx @@ -7,11 +7,12 @@ import { Header } from "./header"; import { Collapse } from "@blueprintjs/core"; import { Feature } from "../../interfaces"; import { t } from "../../../i18next_wrapper"; +import { isExpressBoard } from "../firmware_hardware_support"; export function EncodersAndEndStops(props: EncodersProps) { const { encoders_and_endstops } = props.controlPanelState; - const { dispatch, sourceFwConfig, shouldDisplay } = props; + const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props; const encodersDisabled = { x: !sourceFwConfig("encoder_enabled_x").value, @@ -22,36 +23,42 @@ export function EncodersAndEndStops(props: EncodersProps) { return
- - + {!isExpressBoard(firmwareHardware) && + } + {!isExpressBoard(firmwareHardware) && + } - + {!isExpressBoard(firmwareHardware) && + } + intSize={"long"} /> - + {settingType === "button" ? {children} @@ -47,15 +47,17 @@ export const calculateScale = export function Motors(props: MotorsProps) { const { - dispatch, firmwareVersion, controlPanelState, - sourceFwConfig, isValidFwConfig, firmwareHardware + dispatch, controlPanelState, sourceFwConfig, firmwareHardware } = props; const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x"); const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x"); const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err"); const scale = calculateScale(sourceFwConfig); - const isFarmduinoExpress = firmwareHardware && - firmwareHardware.includes("express"); + const encodersDisabled = { + x: !sourceFwConfig("encoder_enabled_x").value, + y: !sourceFwConfig("encoder_enabled_y").value, + z: !sourceFwConfig("encoder_enabled_z").value, + }; return
- {(minFwVersionCheck(firmwareVersion, "5.0.5") || isValidFwConfig) && - } + - {isFarmduinoExpress && + {isTMCBoard(firmwareHardware) && } - {isFarmduinoExpress && + {isExpressBoard(firmwareHardware) && } {t("Pin Number")} - +