diff --git a/.circleci/circle_envs b/.circleci/circle_envs index d2e7cc54d..2f7477f1c 100644 --- a/.circleci/circle_envs +++ b/.circleci/circle_envs @@ -11,3 +11,4 @@ CI=true CODECLIMATE_REPO_TOKEN=2216bf71b977a85ed8da497942c34a503dbedd77da1b6b97c33551ba02ea02f8 COVERALLS_REPO_TOKEN=lEX6nkql7y2YFCcIXVq5ORvdvMtYzfZdG CODECOV_TOKEN=c0fe1e65-d284-4d58-a742-4088e88be35d +RAILS_ENV=test diff --git a/.circleci/config.yml b/.circleci/config.yml index e28a46417..6e1ad6f15 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,22 +6,35 @@ jobs: steps: - checkout - run: - name: Setup the database and (fake) secrets + name: Shuffle configs around, install Docker-Compose command: | curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-`uname -s`-`uname -m` > docker-compose chmod +x docker-compose sudo mv docker-compose /usr/local/bin mv .circleci/circle_envs .env + - run: + name: Install Ruby/JS deps + command: | sudo docker-compose run web bundle install sudo docker-compose run web npm install - sudo docker-compose run web bundle exec rails db:setup + - run: + name: Create databases and secrets + command: | + sudo docker-compose run web bundle exec rails db:create + sudo docker-compose run web bundle exec rails db:migrate sudo docker-compose run web rake keys:generate - run: - name: Run Rails and JS tests + name: Run Ruby tests command: | sudo docker-compose run web rspec spec + - run: + name: Run linters + command: | sudo docker-compose run webpack npm run tslint sudo docker-compose run webpack npm run sass-lint sudo docker-compose run webpack npm run typecheck + - run: + name: Run JS tests + command: | sudo docker-compose run webpack npm run test-slow - sudo docker-compose run webpack npm run coverage + sudo docker-compose run -e COVERALLS_REPO_TOKEN=lEX6nkql7y2YFCcIXVq5ORvdvMtYzfZdG webpack npm run coverage diff --git a/Gemfile.lock b/Gemfile.lock index b2833b23d..6bd7df6b7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,9 +157,9 @@ GEM os (>= 0.9, < 2.0) signet (~> 0.7) hashdiff (0.3.7) - hashie (3.5.7) + hashie (3.6.0) httpclient (2.8.3) - i18n (1.1.0) + i18n (1.1.1) concurrent-ruby (~> 1.0) json (2.1.0) jsonapi-renderer (0.2.0) @@ -175,9 +175,9 @@ GEM loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.0) + mail (2.7.1) mini_mime (>= 0.1.1) - marcel (0.3.2) + marcel (0.3.3) mimemagic (~> 0.3.2) memoist (0.16.0) method_source (0.9.0) @@ -281,7 +281,7 @@ GEM rspec-mocks (~> 3.8.0) rspec-core (3.8.0) rspec-support (~> 3.8.0) - rspec-expectations (3.8.1) + rspec-expectations (3.8.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.8.0) rspec-mocks (3.8.0) diff --git a/app/models/celery_script_settings_bag.rb b/app/models/celery_script_settings_bag.rb index ed3c5f665..7d275354b 100644 --- a/app/models/celery_script_settings_bag.rb +++ b/app/models/celery_script_settings_bag.rb @@ -17,7 +17,7 @@ module CeleryScriptSettingsBag # List of all celery script nodes that can be used as a varaible... ANY_VARIABLE = [:tool, :coordinate, :point, :identifier] - PLANT_STAGES = %w(planned planted harvested) + PLANT_STAGES = %w(planned planted harvested sprouted) ALLOWED_PIN_MODES = [DIGITAL = 0, ANALOG = 1] ALLOWED_RPC_NODES = %w(home emergency_lock emergency_unlock read_status sync check_updates power_off reboot toggle_pin diff --git a/app/models/global_config.rb b/app/models/global_config.rb index 4a7966a10..803a150c5 100644 --- a/app/models/global_config.rb +++ b/app/models/global_config.rb @@ -7,20 +7,25 @@ class GlobalConfig < ApplicationRecord validates_uniqueness_of :key validates_presence_of :key + # Bootstrap these values, but NEVER clobber pre-existing ones: + { + "FBOS_END_OF_LIFE_VERSION" => "6.3.0", + "MINIMUM_FBOS_VERSION" => "6.0.0", + "TOS_URL" => ENV.fetch("TOS_URL", ""), + "PRIV_URL" => ENV.fetch("PRIV_URL", "") + }.map do |(key, value)| + x = self.find_by(key: key) + self.create!(key: key, value: value) unless x + end + LONG_REVISION = ENV["BUILT_AT"] || ENV["HEROKU_SLUG_COMMIT"] || "NONE" - # Bootstrap initial defaults: + # Bootstrap these values, and ALWAYS clobber pre-existing ones: { "NODE_ENV" => Rails.env || "development", - "TOS_URL" => ENV.fetch("TOS_URL", ""), - "PRIV_URL" => ENV.fetch("PRIV_URL", ""), "LONG_REVISION" => LONG_REVISION, "SHORT_REVISION" => LONG_REVISION.first(8), - "FBOS_END_OF_LIFE_VERSION" => "0.0.0", - "MINIMUM_FBOS_VERSION" => "6.0.0" }.map do |(key, value)| - self - .find_or_create_by(key: key) - .update_attributes(key: key, value: value) + self.find_or_create_by(key: key).update_attributes(key: key, value: value) end # Memoized version of every GlobalConfig, with key/values layed out in a hash. diff --git a/db/migrate/20180925203846_change_default_api_migrated.rb b/db/migrate/20180925203846_change_default_api_migrated.rb new file mode 100644 index 000000000..fd9ba6d43 --- /dev/null +++ b/db/migrate/20180925203846_change_default_api_migrated.rb @@ -0,0 +1,6 @@ +class ChangeDefaultApiMigrated < ActiveRecord::Migration[5.2] + def change + change_column_default(:fbos_configs, :api_migrated, from: false, to: true) + change_column_default(:firmware_configs, :api_migrated, from: false, to: true) + end +end diff --git a/db/migrate/20181014221342_add_show_sensor_readings_to_web_app_configs.rb b/db/migrate/20181014221342_add_show_sensor_readings_to_web_app_configs.rb new file mode 100644 index 000000000..e662581cc --- /dev/null +++ b/db/migrate/20181014221342_add_show_sensor_readings_to_web_app_configs.rb @@ -0,0 +1,9 @@ +class AddShowSensorReadingsToWebAppConfigs < ActiveRecord::Migration[5.2] + safety_assured + def change + add_column :web_app_configs, + :show_sensor_readings, + :boolean, + default: false + end +end diff --git a/db/structure.sql b/db/structure.sql index ae4a82470..228cd2cf4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -376,7 +376,7 @@ CREATE TABLE public.fbos_configs ( sequence_init_log boolean DEFAULT false, network_not_found_timer integer, firmware_hardware character varying DEFAULT 'arduino'::character varying, - api_migrated boolean DEFAULT false, + api_migrated boolean DEFAULT true, os_auto_update boolean DEFAULT true, arduino_debug_messages boolean DEFAULT false ); @@ -499,7 +499,7 @@ CREATE TABLE public.firmware_configs ( pin_guard_5_active_state integer DEFAULT 1, pin_guard_5_pin_nr integer DEFAULT 0, pin_guard_5_time_out integer DEFAULT 60, - api_migrated boolean DEFAULT false, + api_migrated boolean DEFAULT true, movement_invert_2_endpoints_x integer DEFAULT 0, movement_invert_2_endpoints_y integer DEFAULT 0, movement_invert_2_endpoints_z integer DEFAULT 0 @@ -2389,6 +2389,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20180829211322'), ('20180910143055'), ('20180920194120'), -('20180926161918'); +('20180925203846'), +('20180926161918'), +('20181014231010'); diff --git a/docker_configs/api.Dockerfile b/docker_configs/api.Dockerfile index 9b2c2eee6..eb8762288 100644 --- a/docker_configs/api.Dockerfile +++ b/docker_configs/api.Dockerfile @@ -1,4 +1,7 @@ FROM ruby:2.5 +# WHY: We need Postgres 10, not the default (9) +RUN wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | apt-key add - +RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' RUN apt-get update -qq && apt-get install -y build-essential libpq-dev \ postgresql postgresql-contrib RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - diff --git a/package.json b/package.json index c1e0295b2..e201a6268 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "https://github.com/farmbot/farmbot-web-frontend" }, "scripts": { - "coverage": "cat **/*lcov.info | ./node_modules/coveralls/bin/coveralls.js", + "coverage": "cat **/*lcov.info | ./node_modules/coveralls/bin/coveralls.js --verbose", "clean": "rm -rf public/dist && rm -rf public/webpack", "build": "TARGET=production bundle exec rake webpack:compile", "start": "echo 'use `sudo docker-compose up` instead.'", @@ -43,6 +43,7 @@ "@types/react": "^16.4.18", "@types/react-color": "2.13.6", "@types/react-dom": "^16.0.9", + "@types/react-joyride": "^2.0.0", "@types/react-redux": "6.0.9", "axios": "^0.18.0", "boxed_value": "^1.0.0", @@ -51,7 +52,7 @@ "css-loader": "1.0.0", "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.6.0", - "farmbot": "^6.5.2", + "farmbot": "6.5.2", "farmbot-toastr": "^1.0.3", "fastclick": "^1.0.6", "file-loader": "2.0.0", @@ -71,6 +72,7 @@ "react-addons-test-utils": "^15.6.2", "react-color": "2.14.1", "react-dom": "16.5.2", + "react-joyride": "^2.0.0-15", "react-redux": "^5.0.6", "react-test-renderer": "16.5.2", "react-transition-group": "2.5.0", diff --git a/spec/controllers/api/configs/fbos_configs_controller_spec.rb b/spec/controllers/api/configs/fbos_configs_controller_spec.rb index aff21a0d5..57b9fe8ab 100644 --- a/spec/controllers/api/configs/fbos_configs_controller_spec.rb +++ b/spec/controllers/api/configs/fbos_configs_controller_spec.rb @@ -27,7 +27,7 @@ describe Api::FbosConfigsController do network_not_found_timer: nil, os_auto_update: true, firmware_hardware: "arduino", - api_migrated: false + api_migrated: true }.to_a.map do |key, value| actual = json[key] expected = value diff --git a/spec/controllers/api/points/create_spec.rb b/spec/controllers/api/points/create_spec.rb index 7d0a3af53..c91a021ba 100644 --- a/spec/controllers/api/points/create_spec.rb +++ b/spec/controllers/api/points/create_spec.rb @@ -26,16 +26,20 @@ describe Api::PointsController do time = (DateTime.now - 1.day).to_json p = { x: 23, y: 45, - name: "My Lettuce", + name: "Put me in a salad", pointer_type: "Plant", - openfarm_slug: "limestone-lettuce", - planted_at: time } + openfarm_slug: "mung-bean", + planted_at: time, + plant_stage: "sprouted" + } post :create, body: p.to_json, params: { format: :json } expect(response.status).to eq(200) plant = Plant.last - expect(plant.x).to eq(p[:x]) - expect(plant.y).to eq(p[:y]) - expect(plant.name).to eq(p[:name]) + expect(plant.x).to eq(p[:x]) + expect(plant.y).to eq(p[:y]) + expect(plant.name).to eq(p[:name]) + expect(plant.plant_stage).to eq("sprouted") + expect(p[:plant_stage]).to eq("sprouted") expect(plant.openfarm_slug).to eq(p[:openfarm_slug]) expect(plant.created_at).to be_truthy p.keys.each do |key| diff --git a/webpack/__test_support__/fake_state/resources.ts b/webpack/__test_support__/fake_state/resources.ts index aa03a5304..faa82d65c 100644 --- a/webpack/__test_support__/fake_state/resources.ts +++ b/webpack/__test_support__/fake_state/resources.ts @@ -271,6 +271,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig { show_spread: false, show_farmbot: true, show_images: false, + show_sensor_readings: false, show_plants: true, show_points: true, x_axis_inverted: false, diff --git a/webpack/__tests__/app_test.tsx b/webpack/__tests__/app_test.tsx index d5327ec8f..86d633b04 100644 --- a/webpack/__tests__/app_test.tsx +++ b/webpack/__tests__/app_test.tsx @@ -31,6 +31,7 @@ const fakeProps = (): AppProps => { xySwap: false, animate: false, getConfigValue: jest.fn(), + tour: undefined, }; }; diff --git a/webpack/account/labs/labs_features.tsx b/webpack/account/labs/labs_features.tsx index 1cd651525..a58c5d87f 100644 --- a/webpack/account/labs/labs_features.tsx +++ b/webpack/account/labs/labs_features.tsx @@ -16,7 +16,7 @@ export class LabsFeatures extends React.Component { render() { const { getConfigValue, dispatch } = this.props; - return + return diff --git a/webpack/app.tsx b/webpack/app.tsx index 034713a50..46cd73adc 100644 --- a/webpack/app.tsx +++ b/webpack/app.tsx @@ -45,6 +45,7 @@ export interface AppProps { firmwareConfig: FirmwareConfig | undefined; animate: boolean; getConfigValue: GetWebAppConfigValue; + tour: string | undefined; } function mapStateToProps(props: Everything): AppProps { @@ -66,6 +67,7 @@ function mapStateToProps(props: Everything): AppProps { firmwareConfig: validFwConfig(getFirmwareConfig(props.resources.index)), animate: !webAppConfigValue(BooleanSetting.disable_animations), getConfigValue: webAppConfigValue, + tour: props.resources.consumers.help.currentTour, }; } /** Time at which the app gives up and asks the user to refresh */ @@ -114,7 +116,8 @@ export class App extends React.Component { bot={this.props.bot} dispatch={this.props.dispatch} logs={this.props.logs} - getConfigValue={this.props.getConfigValue} /> + getConfigValue={this.props.getConfigValue} + tour={this.props.tour} /> {!syncLoaded && } {syncLoaded && this.props.children} {!(["controls", "account", "regimens"].includes(currentPage)) && diff --git a/webpack/config_storage/web_app_configs.ts b/webpack/config_storage/web_app_configs.ts index 0ea49aa62..25d5a210d 100644 --- a/webpack/config_storage/web_app_configs.ts +++ b/webpack/config_storage/web_app_configs.ts @@ -48,6 +48,7 @@ export interface WebAppConfig { home_button_homing: boolean; show_motor_plot: boolean; show_historic_points: boolean; + show_sensor_readings: boolean; } export type NumberConfigKey = "id" @@ -93,4 +94,5 @@ export type BooleanConfigKey = "confirm_step_deletion" |"xy_swap" |"home_button_homing" |"show_motor_plot" - |"show_historic_points"; + |"show_historic_points" + |"show_sensor_readings"; diff --git a/webpack/constants.ts b/webpack/constants.ts index aa04db5ae..0a17642b6 100644 --- a/webpack/constants.ts +++ b/webpack/constants.ts @@ -317,6 +317,9 @@ export namespace ToolTips { // App export const LABS = trim(`Customize your web app experience.`); + + export const TOURS = + trim(`Take a guided tour of the Web App.`); } export namespace Content { @@ -591,6 +594,55 @@ export namespace Content { trim(`Not available when device is offline.`); } +export namespace TourContent { + // Getting started + export const ADD_PLANTS = + trim(`Add plants by pressing the + button and searching for a plant, + selecting one, and dragging it into the garden.`); + + export const ADD_TOOLS = + trim(`Press edit and then the + button to add tools.`); + + export const ADD_TOOLS_SLOTS = + trim(`Add the newly created tools to the corresponding toolbay slots on + FarmBot: press edit and then + to create a toolbay slot.`); + + export const ADD_PERIPHERALS = + trim(`Press edit and then the + button to add peripherals.`); + + export const ADD_SEQUENCES = + trim(`Press the + button to add a new sequence. You will need + to create sequences to mount tools, move to the plant locations you + created in the Farm Designer, and seed/water them.`); + + export const ADD_REGIMENS = + trim(`Press the + button and add your newly created sequences to a + regimen via the scheduler. The regimen should include all actions + needed to take care of a plant over its life.`); + + export const ADD_FARM_EVENTS = + trim(`Add a farm event via the + button to schedule a sequence or + regimen in the calendar.`); + + // Monitoring + export const LOCATION_GRID = + trim(`View FarmBot's current location using the axis position display.`); + + export const VIRTUAL_FARMBOT = + trim(`Or view FarmBot's current location in the virtual garden.`); + + export const LOGS_TABLE = + trim(`View recent log messages here. More detailed log messages can be + shown by adjusting filter settings.`); + + export const PHOTOS = + trim(`View photos your FarmBot has taken here.`); + + // Fun stuff + export const APP_SETTINGS = + trim(`Toggle various settings to customize your web app experience.`); +} + export enum Actions { // Resources @@ -668,6 +720,9 @@ export enum Actions { SELECT_IMAGE = "SELECT_IMAGE", FETCH_FIRST_PARTY_FARMWARE_NAMES_OK = "FETCH_FIRST_PARTY_FARMWARE_NAMES_OK", + // App + START_TOUR = "START_TOUR", + // Network NETWORK_EDGE_CHANGE = "NETWORK_EDGE_CHANGE", RESET_NETWORK = "RESET_NETWORK", diff --git a/webpack/controls/move/move.tsx b/webpack/controls/move/move.tsx index b1fb10c8e..b06f259d1 100644 --- a/webpack/controls/move/move.tsx +++ b/webpack/controls/move/move.tsx @@ -28,7 +28,7 @@ export class Move extends React.Component { render() { const { location_data, informational_settings } = this.props.bot.hardware; const locationData = validBotLocationData(location_data); - return + return diff --git a/webpack/controls/webcam/edit.tsx b/webpack/controls/webcam/edit.tsx index 7bbbedba1..29e057069 100644 --- a/webpack/controls/webcam/edit.tsx +++ b/webpack/controls/webcam/edit.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { Widget, WidgetHeader } from "../../ui/index"; +import { Widget, WidgetHeader, WidgetBody } from "../../ui/index"; import { t } from "i18next"; import { ToolTips } from "../../constants"; import { WebcamPanelProps } from "./interfaces"; @@ -27,7 +27,7 @@ export function Edit(props: WebcamPanelProps) { const unsaved = props .feeds .filter(x => x.specialStatus === SpecialStatus.DIRTY); - return + return -
+ {rows} -
+
; } diff --git a/webpack/controls/webcam/show.tsx b/webpack/controls/webcam/show.tsx index f5fbccc90..6b23f1516 100644 --- a/webpack/controls/webcam/show.tsx +++ b/webpack/controls/webcam/show.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { Widget, WidgetHeader, FallbackImg } from "../../ui/index"; +import { Widget, WidgetHeader, FallbackImg, WidgetBody } from "../../ui/index"; import { t } from "i18next"; import { ToolTips } from "../../constants"; import { WebcamPanelProps } from "./interfaces"; @@ -53,7 +53,7 @@ export class Show extends React.Component { const title = flipper.current.name || t("Webcam Feeds"); const msg = this.getMessage(flipper.current.url); const imageClass = msg.length > 0 ? "no-flipper-image-container" : ""; - return + return -
+

{msg}

@@ -85,7 +85,7 @@ export class Show extends React.Component { {t("Next")}
-
+
; } } diff --git a/webpack/css/global.scss b/webpack/css/global.scss index be6833901..aca4b8f22 100644 --- a/webpack/css/global.scss +++ b/webpack/css/global.scss @@ -951,3 +951,9 @@ ul { } } } + +.tour-list { + margin: auto; + width: 75%; + margin-top: 1rem; +} diff --git a/webpack/extras/fallback_widget.tsx b/webpack/extras/fallback_widget.tsx index fec74c650..851294dbe 100644 --- a/webpack/extras/fallback_widget.tsx +++ b/webpack/extras/fallback_widget.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { t } from "i18next"; -import { Widget, WidgetHeader } from "../ui/index"; +import { Widget, WidgetHeader, WidgetBody } from "../ui"; /* * Widget to display if the desired widget fails to load. @@ -29,9 +29,9 @@ export class FallbackWidget extends -
+ {t("Widget load failed.")} -
+ ; } } diff --git a/webpack/farm_designer/__tests__/farm_designer_test.tsx b/webpack/farm_designer/__tests__/farm_designer_test.tsx index 01f601ab3..4a25651c7 100644 --- a/webpack/farm_designer/__tests__/farm_designer_test.tsx +++ b/webpack/farm_designer/__tests__/farm_designer_test.tsx @@ -60,6 +60,8 @@ describe("", () => { }, tzOffset: 0, getConfigValue: jest.fn(), + sensorReadings: [], + sensors: [], }; } diff --git a/webpack/farm_designer/index.tsx b/webpack/farm_designer/index.tsx index 9e3da9bd6..7f7538886 100755 --- a/webpack/farm_designer/index.tsx +++ b/webpack/farm_designer/index.tsx @@ -63,6 +63,7 @@ export class FarmDesigner extends React.Component> { show_spread: this.initializeSetting(BooleanSetting.show_spread, false), show_farmbot: this.initializeSetting(BooleanSetting.show_farmbot, true), show_images: this.initializeSetting(BooleanSetting.show_images, false), + show_sensor_readings: this.initializeSetting(BooleanSetting.show_sensor_readings, false), bot_origin_quadrant: this.getBotOriginQuadrant(), zoom_level: calcZoomLevel(getZoomLevelIndex(this.props.getConfigValue)) }; @@ -106,6 +107,7 @@ export class FarmDesigner extends React.Component> { show_spread, show_farmbot, show_images, + show_sensor_readings, bot_origin_quadrant, zoom_level } = this.state; @@ -144,6 +146,7 @@ export class FarmDesigner extends React.Component> { showSpread={show_spread} showFarmbot={show_farmbot} showImages={show_images} + showSensorReadings={show_sensor_readings} dispatch={this.props.dispatch} tzOffset={this.props.tzOffset} getConfigValue={this.props.getConfigValue} @@ -163,6 +166,7 @@ export class FarmDesigner extends React.Component> { showSpread={show_spread} showFarmbot={show_farmbot} showImages={show_images} + showSensorReadings={show_sensor_readings} selectedPlant={this.props.selectedPlant} crops={this.props.crops} dispatch={this.props.dispatch} @@ -182,7 +186,10 @@ export class FarmDesigner extends React.Component> { eStopStatus={this.props.eStopStatus} latestImages={this.props.latestImages} cameraCalibrationData={this.props.cameraCalibrationData} - getConfigValue={this.props.getConfigValue} /> + getConfigValue={this.props.getConfigValue} + sensorReadings={this.props.sensorReadings} + timeOffset={this.props.tzOffset} + sensors={this.props.sensors} />
{this.props.designer.openedSavedGarden && diff --git a/webpack/farm_designer/interfaces.ts b/webpack/farm_designer/interfaces.ts index f4566bcd0..6442d885c 100644 --- a/webpack/farm_designer/interfaces.ts +++ b/webpack/farm_designer/interfaces.ts @@ -7,6 +7,8 @@ import { TaggedRegimen, TaggedGenericPointer, TaggedImage, + TaggedSensorReading, + TaggedSensor, } from "farmbot"; import { SlotWithTool } from "../resources/interfaces"; import { BotPosition, StepsPerMmXY, BotLocationData } from "../devices/interfaces"; @@ -16,7 +18,9 @@ import { AxisNumberProperty, BotSize, TaggedPlant } from "./map/interfaces"; import { SelectionBoxData } from "./map/selection_box"; import { BooleanConfigKey } from "../config_storage/web_app_configs"; import { GetWebAppConfigValue } from "../config_storage/actions"; -import { ExecutableType, PlantPointer } from "farmbot/dist/resources/api_resources"; +import { + ExecutableType, PlantPointer +} from "farmbot/dist/resources/api_resources"; /* BotOriginQuadrant diagram @@ -42,6 +46,7 @@ export interface State extends TypeCheckerHint { show_spread: boolean; show_farmbot: boolean; show_images: boolean; + show_sensor_readings: boolean; bot_origin_quadrant: BotOriginQuadrant; zoom_level: number; } @@ -64,6 +69,8 @@ export interface Props { cameraCalibrationData: CameraCalibrationData; tzOffset: number; getConfigValue: GetWebAppConfigValue; + sensorReadings: TaggedSensorReading[]; + sensors: TaggedSensor[]; } export interface MovePlantProps { @@ -156,6 +163,7 @@ export interface GardenMapProps { showSpread: boolean | undefined; showFarmbot: boolean | undefined; showImages: boolean | undefined; + showSensorReadings: boolean | undefined; dispatch: Function; designer: DesignerState; points: TaggedGenericPointer[]; @@ -176,6 +184,9 @@ export interface GardenMapProps { latestImages: TaggedImage[]; cameraCalibrationData: CameraCalibrationData; getConfigValue: GetWebAppConfigValue; + sensorReadings: TaggedSensorReading[]; + sensors: TaggedSensor[]; + timeOffset: number; } export interface GardenMapState { diff --git a/webpack/farm_designer/map/__tests__/garden_map_legend_test.tsx b/webpack/farm_designer/map/__tests__/garden_map_legend_test.tsx index de50b5228..4d93b0865 100644 --- a/webpack/farm_designer/map/__tests__/garden_map_legend_test.tsx +++ b/webpack/farm_designer/map/__tests__/garden_map_legend_test.tsx @@ -31,6 +31,7 @@ describe("", () => { showSpread: false, showFarmbot: false, showImages: false, + showSensorReadings: false, dispatch: jest.fn(), tzOffset: 0, getConfigValue: jest.fn(), diff --git a/webpack/farm_designer/map/__tests__/garden_map_test.tsx b/webpack/farm_designer/map/__tests__/garden_map_test.tsx index 6802d8122..aecdd039f 100644 --- a/webpack/farm_designer/map/__tests__/garden_map_test.tsx +++ b/webpack/farm_designer/map/__tests__/garden_map_test.tsx @@ -37,6 +37,7 @@ function fakeProps(): GardenMapProps { showSpread: false, showFarmbot: false, showImages: false, + showSensorReadings: false, selectedPlant: fakePlant(), crops: [], dispatch: jest.fn(), @@ -92,6 +93,9 @@ function fakeProps(): GardenMapProps { calibrationZ: undefined }, getConfigValue: jest.fn(), + sensorReadings: [], + sensors: [], + timeOffset: 0, }; } diff --git a/webpack/farm_designer/map/__tests__/garden_sensor_reading_test.tsx b/webpack/farm_designer/map/__tests__/garden_sensor_reading_test.tsx new file mode 100644 index 000000000..306e59285 --- /dev/null +++ b/webpack/farm_designer/map/__tests__/garden_sensor_reading_test.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { mount, shallow } from "enzyme"; +import { + GardenSensorReading, GardenSensorReadingProps +} from "../garden_sensor_reading"; +import { + fakeSensorReading +} from "../../../__test_support__/fake_state/resources"; +import { + fakeMapTransformProps +} from "../../../__test_support__/map_transform_props"; + +describe("", () => { + const fakeProps = (): GardenSensorReadingProps => ({ + sensorReading: fakeSensorReading(), + mapTransformProps: fakeMapTransformProps(), + endTime: undefined, + timeOffset: 0, + sensorLookup: {}, + }); + + it("renders", () => { + const wrapper = mount(); + expect(wrapper.html()).toContain("sensor-reading-"); + expect(wrapper.find("circle").length).toEqual(2); + }); + + it("doesn't render", () => { + const p = fakeProps(); + p.sensorReading.body.x = undefined; + const wrapper = mount(); + expect(wrapper.find("circle").length).toEqual(0); + }); + + it("renders sensor name", () => { + const p = fakeProps(); + p.sensorLookup = { 1: "Sensor Name" }; + const wrapper = mount(); + expect(wrapper.text()).toContain("Sensor Name (pin 1)"); + }); + + it("renders analog reading", () => { + const p = fakeProps(); + p.sensorReading.body.mode = 1; + const wrapper = mount(); + expect(wrapper.text()).toContain("value 0 (analog)"); + }); + + it("calls hover", () => { + const wrapper = shallow( + ); + wrapper.find("circle").first().simulate("mouseEnter"); + expect(wrapper.find("text").props().visibility).toEqual("visible"); + expect(wrapper.state().hovered).toEqual(true); + wrapper.find("circle").first().simulate("mouseLeave"); + expect(wrapper.find("text").props().visibility).toEqual("hidden"); + expect(wrapper.state().hovered).toEqual(false); + }); +}); diff --git a/webpack/farm_designer/map/garden_map.tsx b/webpack/farm_designer/map/garden_map.tsx index 3eb39e735..efb70e206 100644 --- a/webpack/farm_designer/map/garden_map.tsx +++ b/webpack/farm_designer/map/garden_map.tsx @@ -39,6 +39,7 @@ import { Bugs, showBugs } from "./easter_eggs/bugs"; import { BooleanSetting } from "../../session_keys"; import { savedGardenOpen } from "../saved_gardens/saved_gardens"; import { unpackUUID } from "../../util"; +import { SensorReadingsLayer } from "./layers/sensor_readings_layer"; /** Garden map interaction modes. */ export enum Mode { @@ -442,6 +443,12 @@ export class GardenMap extends + { dispatch={props.dispatch} getConfigValue={getConfigValue} imageAgeInfo={props.imageAgeInfo} />} /> + {localStorage.getItem("FUTURE_FEATURES") && + } ; }; diff --git a/webpack/farm_designer/map/garden_sensor_reading.tsx b/webpack/farm_designer/map/garden_sensor_reading.tsx new file mode 100644 index 000000000..e7a26503f --- /dev/null +++ b/webpack/farm_designer/map/garden_sensor_reading.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import * as moment from "moment"; +import { transformXY } from "./util"; +import { TaggedSensorReading } from "farmbot"; +import { MapTransformProps } from "./interfaces"; +import { isNumber } from "lodash"; +import { formatLogTime } from "../../logs"; +import { t } from "i18next"; +import { Color } from "../../ui"; + +const VISIBLE_PERIOD_SECONDS = 24 * 60 * 60.; + +export interface GardenSensorReadingProps { + sensorReading: TaggedSensorReading; + mapTransformProps: MapTransformProps; + endTime: string | undefined; + timeOffset: number; + sensorLookup: Record; +} + +interface GardenSensorReadingState { + hovered: boolean; +} + +export class GardenSensorReading + extends React.Component { + state: GardenSensorReadingState = { hovered: false }; + + hover = () => this.setState({ hovered: true }); + unhover = () => this.setState({ hovered: false }); + + render() { + const { + sensorReading, mapTransformProps, endTime, timeOffset, sensorLookup + } = this.props; + const { id, x, y, value, mode, created_at, pin } = sensorReading.body; + if (isNumber(x) && isNumber(y)) { + const { qx, qy } = transformXY(x, y, mapTransformProps); + const val = 255 - value / (mode === 1 ? 1024. : 1) * 255; + const age = moment(endTime).unix() - moment(created_at).unix(); + const opacity = Math.max(0, 1 - age / VISIBLE_PERIOD_SECONDS); + const sensorName = sensorLookup[pin] + ? `${sensorLookup[pin]} (${t("pin")} ${pin})` + : `${t("pin")} ${pin}`; + const modeText = mode === 0 ? t("digital") : t("analog"); + const hovered = this.state.hovered ? "visible" : "hidden"; + const textX = qx + 13; + return + + + + {sensorName} + + {`${t("value")} ${value} (${modeText})`} + + + {formatLogTime(moment(created_at).unix(), timeOffset)} + + + ; + } + return ; + } +} diff --git a/webpack/farm_designer/map/interfaces.ts b/webpack/farm_designer/map/interfaces.ts index 2b894c601..a3d3f15c5 100644 --- a/webpack/farm_designer/map/interfaces.ts +++ b/webpack/farm_designer/map/interfaces.ts @@ -40,6 +40,7 @@ export interface GardenMapLegendProps { showSpread: boolean; showFarmbot: boolean; showImages: boolean; + showSensorReadings: boolean; dispatch: Function; tzOffset: number; getConfigValue: GetWebAppConfigValue; diff --git a/webpack/farm_designer/map/layers/__tests__/sensor_readings_layer_test.tsx b/webpack/farm_designer/map/layers/__tests__/sensor_readings_layer_test.tsx new file mode 100644 index 000000000..3b616c912 --- /dev/null +++ b/webpack/farm_designer/map/layers/__tests__/sensor_readings_layer_test.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { mount } from "enzyme"; +import { + SensorReadingsLayer, SensorReadingsLayerProps +} from "../sensor_readings_layer"; +import { + fakeMapTransformProps +} from "../../../../__test_support__/map_transform_props"; +import { + fakeSensorReading, fakeSensor +} from "../../../../__test_support__/fake_state/resources"; + +describe("", () => { + const fakeProps = (): SensorReadingsLayerProps => ({ + visible: true, + sensorReadings: [fakeSensorReading()], + mapTransformProps: fakeMapTransformProps(), + timeOffset: 0, + sensors: [fakeSensor()], + }); + + it("renders", () => { + const wrapper = mount(); + expect(wrapper.html()).toContain("sensor-readings-layer"); + }); +}); diff --git a/webpack/farm_designer/map/layers/sensor_readings_layer.tsx b/webpack/farm_designer/map/layers/sensor_readings_layer.tsx new file mode 100644 index 000000000..b3e62b14f --- /dev/null +++ b/webpack/farm_designer/map/layers/sensor_readings_layer.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { TaggedSensorReading, TaggedSensor } from "farmbot"; +import { MapTransformProps } from "../interfaces"; +import { GardenSensorReading } from "../garden_sensor_reading"; +import { last } from "lodash"; + +export interface SensorReadingsLayerProps { + visible: boolean; + sensorReadings: TaggedSensorReading[]; + mapTransformProps: MapTransformProps; + timeOffset: number; + sensors: TaggedSensor[]; +} + +export function SensorReadingsLayer(props: SensorReadingsLayerProps) { + const { + visible, sensorReadings, mapTransformProps, timeOffset, sensors + } = props; + const mostRecentSensorReading = last(sensorReadings); + const sensorNameByPinLookup: { [x: number]: string } = {}; + sensors.map(x => { sensorNameByPinLookup[x.body.pin || 0] = x.body.label; }); + return + {visible && mostRecentSensorReading && + sensorReadings.map(sr => + )} + ; +} diff --git a/webpack/farm_designer/state_to_props.ts b/webpack/farm_designer/state_to_props.ts index 8828e308f..55e28d41d 100644 --- a/webpack/farm_designer/state_to_props.ts +++ b/webpack/farm_designer/state_to_props.ts @@ -8,7 +8,9 @@ import { maybeGetTimeOffset, selectAllPeripherals, getFirmwareConfig, - selectAllPlantTemplates + selectAllPlantTemplates, + selectAllSensorReadings, + selectAllSensors } from "../resources/selectors"; import * as _ from "lodash"; import { validBotLocationData, validFwConfig, unpackUUID } from "../util"; @@ -84,6 +86,13 @@ export function mapStateToProps(props: Everything): Props { calibrationZ: user_env["CAMERA_CALIBRATION_camera_z"], }; + const sensorReadings = _(selectAllSensorReadings(props.resources.index)) + .sortBy(x => x.body.created_at) + .reverse() + .take(500) + .reverse() + .value(); + return { crops: selectAllCrops(props.resources.index), dispatch: props.dispatch, @@ -102,5 +111,7 @@ export function mapStateToProps(props: Everything): Props { cameraCalibrationData, tzOffset: maybeGetTimeOffset(props.resources.index), getConfigValue, + sensorReadings, + sensors: selectAllSensors(props.resources.index), }; } diff --git a/webpack/help/__tests__/help_test.tsx b/webpack/help/__tests__/help_test.tsx new file mode 100644 index 000000000..ab966cb11 --- /dev/null +++ b/webpack/help/__tests__/help_test.tsx @@ -0,0 +1,36 @@ +jest.mock("react-redux", () => ({ connect: jest.fn() })); + +import * as React from "react"; +import { mount } from "enzyme"; +import { Help, mapStateToProps } from "../help"; +import { fakeState } from "../../__test_support__/fake_state"; +import { clickButton } from "../../__test_support__/helpers"; +import { tourNames } from "../tours"; +import { Actions } from "../../constants"; + +describe("", () => { + const fakeProps = () => ({ dispatch: jest.fn() }); + + it("renders", () => { + const wrapper = mount(); + ["help with", "start tour", "getting started"].map(string => + expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); + }); + + it("starts tour", () => { + const p = fakeProps(); + const wrapper = mount(); + clickButton(wrapper, 0, "start tour"); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.START_TOUR, + payload: tourNames()[0].name + }); + }); +}); + +describe("mapStateToProps()", () => { + it("returns props", () => { + const props = mapStateToProps(fakeState()); + expect(props).toEqual({ dispatch: expect.any(Function) }); + }); +}); diff --git a/webpack/help/__tests__/reducer_test.ts b/webpack/help/__tests__/reducer_test.ts new file mode 100644 index 000000000..2e93420df --- /dev/null +++ b/webpack/help/__tests__/reducer_test.ts @@ -0,0 +1,19 @@ +import { helpReducer, HelpState } from "../reducer"; +import { Actions } from "../../constants"; + +describe("helpReducer", () => { + const fakeState = (): HelpState => ({ + currentTour: undefined, + }); + + it("changes current tour", () => { + const oldState = fakeState(); + oldState.currentTour = "aTour"; + const newState = helpReducer(oldState, { + type: Actions.START_TOUR, + payload: undefined + }); + expect(oldState.currentTour).not.toEqual(newState.currentTour); + expect(newState.currentTour).toBeUndefined(); + }); +}); diff --git a/webpack/help/__tests__/tour_test.tsx b/webpack/help/__tests__/tour_test.tsx new file mode 100644 index 000000000..b74ef369a --- /dev/null +++ b/webpack/help/__tests__/tour_test.tsx @@ -0,0 +1,57 @@ +jest.mock("../../history", () => ({ history: { push: jest.fn() } })); + +import * as React from "react"; +import { tourNames, TOUR_STEPS } from "../tours"; +import { mount, shallow } from "enzyme"; +import { RunTour, Tour } from "../tour"; +import { history } from "../../history"; + +describe("", () => { + const EMPTY_DIV = "
"; + + it("tour is running", () => { + const wrapper = mount(); + expect(wrapper.html()).not.toBe(EMPTY_DIV); + }); + + it("tour is not running", () => { + const wrapper = mount(); + expect(wrapper.html()).toBe(EMPTY_DIV); + }); +}); + +describe("", () => { + it("ends tour", () => { + const steps = [TOUR_STEPS()[tourNames()[0].name][0]]; + const wrapper = shallow(); + // tslint:disable-next-line:no-any + const instance = wrapper.instance() as any; + instance.callback({ action: "", index: 0, step: {}, type: "tour:end" }); + expect(wrapper.state()).toEqual({ run: false, index: 0 }); + expect(history.push).toHaveBeenCalledWith("/app/help"); + }); + + it("navigates through tour: next", () => { + const steps = TOUR_STEPS()[tourNames()[0].name]; + const wrapper = shallow(); + // tslint:disable-next-line:no-any + const instance = wrapper.instance() as any; + instance.callback({ + action: "next", index: 0, step: {}, type: "step:after" + }); + expect(wrapper.state()).toEqual({ run: true, index: 1 }); + expect(history.push).toHaveBeenCalledWith("/app/tools"); + }); + + it("navigates through tour: other", () => { + const steps = [TOUR_STEPS()[tourNames()[0].name][0]]; + const wrapper = shallow(); + // tslint:disable-next-line:no-any + const instance = wrapper.instance() as any; + instance.callback({ + action: "prev", index: 9, step: {}, type: "step:after" + }); + expect(wrapper.state()).toEqual({ run: true, index: 8 }); + expect(history.push).not.toHaveBeenCalled(); + }); +}); diff --git a/webpack/help/__tests__/tours_test.ts b/webpack/help/__tests__/tours_test.ts new file mode 100644 index 000000000..2ab27cf74 --- /dev/null +++ b/webpack/help/__tests__/tours_test.ts @@ -0,0 +1,27 @@ +jest.mock("../../history", () => ({ history: { push: jest.fn() } })); + +import { tourPageNavigation } from "../tours"; +import { history } from "../../history"; + +describe("tourPageNavigation()", () => { + const testCase = (el: string) => { + tourPageNavigation(el); + expect(history.push).toHaveBeenCalled(); + }; + + it("covers all cases", () => { + testCase(".farm-designer"); + testCase(".plant-inventory-panel"); + testCase(".farm-event-panel"); + testCase(".move-widget"); + testCase(".peripherals-widget"); + testCase(".device-widget"); + testCase(".sequence-list-panel"); + testCase(".regimen-list-panel"); + testCase(".tool-list"); + testCase(".toolbay-list"); + testCase(".photos"); + testCase(".logs-table"); + testCase(".app-settings-widget"); + }); +}); diff --git a/webpack/help/help.tsx b/webpack/help/help.tsx new file mode 100644 index 000000000..6631a973a --- /dev/null +++ b/webpack/help/help.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { connect } from "react-redux"; +import { t } from "i18next"; +import { Widget, WidgetBody, WidgetHeader, Page, Col } from "../ui"; +import { ToolTips, Actions } from "../constants"; +import { Everything } from "../interfaces"; +import { tourNames } from "./tours"; + +const TourList = ({ dispatch }: { dispatch: Function }) => +
+ {tourNames().map(tour =>
+ + +
)} +
; + +export function mapStateToProps(props: Everything): { dispatch: Function } { + const { dispatch } = props; + return { dispatch }; +} + +@connect(mapStateToProps) +export class Help extends React.Component<{ dispatch: Function }, {}> { + + componentDidMount() { + this.props.dispatch({ type: Actions.START_TOUR, payload: undefined }); + } + + render() { + return + + + + + {t("What do you need help with?")} + + + + + ; + } +} diff --git a/webpack/help/reducer.ts b/webpack/help/reducer.ts new file mode 100644 index 000000000..ad4b09783 --- /dev/null +++ b/webpack/help/reducer.ts @@ -0,0 +1,16 @@ +import { generateReducer } from "../redux/generate_reducer"; +import { Actions } from "../constants"; + +export interface HelpState { + currentTour: string | undefined; +} + +export let initialState: HelpState = { + currentTour: undefined, +}; + +export let helpReducer = generateReducer(initialState) + .add(Actions.START_TOUR, (s, { payload }) => { + s.currentTour = payload; + return s; + }); diff --git a/webpack/help/tour.tsx b/webpack/help/tour.tsx new file mode 100644 index 000000000..84b6302a1 --- /dev/null +++ b/webpack/help/tour.tsx @@ -0,0 +1,77 @@ +import * as React from "react"; +import { t } from "i18next"; +import Joyride, { Step as TourStep, State as CBData } from "react-joyride"; +import { Color } from "../ui"; +import { history } from "../history"; +import { TOUR_STEPS, tourPageNavigation } from "./tours"; + +const strings = () => ({ + back: t("Back"), + close: t("Close"), + last: t("End Tour"), + next: t("Next"), + skip: t("End Tour") +}); + +// tslint:disable-next-line:no-any // broken typing +const STYLES: any = { + buttonNext: { backgroundColor: Color.green }, + buttonBack: { color: Color.darkGray } +}; + +interface TourProps { + steps: TourStep[]; +} + +interface TourState { + run: boolean; + index: number; +} + +export class Tour extends React.Component { + state: TourState = { run: false, index: 0, }; + + // tslint:disable-next-line:no-any // broken typing + callback: any = ({ action, index, step, type }: CBData) => { + console.log("Tour debug:", step.target, type, action); + tourPageNavigation(step.target); + if (type === "step:after") { + const increment = action === "prev" ? -1 : 1; + const nextStepIndex = index + increment; + this.setState({ index: nextStepIndex }); + const nextStepTarget = nextStepIndex < this.props.steps.length + ? this.props.steps[nextStepIndex].target : ""; + tourPageNavigation(nextStepTarget); + } + if (type === "tour:end") { + this.setState({ run: false }); + history.push("/app/help"); + } + }; + + componentDidMount() { + this.setState({ run: true, index: 0 }); + } + + render() { + const steps = this.props.steps.map(step => { + step.disableBeacon = true; + return step; + }); + return
+ +
; + } +} + +export const RunTour = ({ currentTour }: { currentTour: string | undefined }) => { + return currentTour ? :
; +}; diff --git a/webpack/help/tours.ts b/webpack/help/tours.ts new file mode 100644 index 000000000..c0abef253 --- /dev/null +++ b/webpack/help/tours.ts @@ -0,0 +1,125 @@ +import { history } from "../history"; +import { Step as TourStep } from "react-joyride"; +import { t } from "i18next"; +import { TourContent } from "../constants"; + +export enum Tours { + gettingStarted = "gettingStarted", + monitoring = "monitoring", + funStuff = "funStuff", +} + +export const tourNames = () => [ + { name: Tours.gettingStarted, description: t("getting started") }, + { name: Tours.monitoring, description: t("see what FarmBot is doing") }, + { name: Tours.funStuff, description: t("find new features") }, +]; + +export const TOUR_STEPS = (): { [x: string]: TourStep[] } => ({ + [Tours.gettingStarted]: [ + { + target: ".plant-inventory-panel", + content: TourContent.ADD_PLANTS, + title: t("Add plants"), + }, + { + target: ".tool-list", + content: TourContent.ADD_TOOLS, + title: t("Add tools"), + }, + { + target: ".toolbay-list", + content: TourContent.ADD_TOOLS_SLOTS, + title: t("Add tools to tool bay"), + }, + { + target: ".peripherals-widget", + content: TourContent.ADD_PERIPHERALS, + title: t("Add peripherals"), + }, + { + target: ".sequence-list-panel", + content: TourContent.ADD_SEQUENCES, + title: t("Create sequences"), + }, + { + target: ".regimen-list-panel", + content: TourContent.ADD_REGIMENS, + title: t("Create regimens"), + }, + { + target: ".farm-event-panel", + content: TourContent.ADD_FARM_EVENTS, + title: t("Create farm events"), + }, + ], + [Tours.monitoring]: [ + { + target: ".move-widget", + content: TourContent.LOCATION_GRID, + title: t("View current location"), + }, + { + target: ".farm-designer", + content: TourContent.VIRTUAL_FARMBOT, + title: t("View current location"), + }, + { + target: ".logs-table", + content: TourContent.LOGS_TABLE, + title: t("View log messages"), + }, + { + target: ".photos", + content: TourContent.PHOTOS, + title: t("Take and view photos"), + }, + ], + [Tours.funStuff]: [ + { + target: ".app-settings-widget", + content: TourContent.APP_SETTINGS, + title: t("Customize your web app experience"), + }, + ], +}); + +export const tourPageNavigation = (nextStepTarget: string | HTMLElement) => { + switch (nextStepTarget) { + case ".farm-designer": + history.push("/app/designer"); + break; + case ".plant-inventory-panel": + history.push("/app/designer/plants"); + break; + case ".farm-event-panel": + history.push("/app/designer/farm_events"); + break; + case ".move-widget": + case ".peripherals-widget": + history.push("/app/controls"); + break; + case ".device-widget": + history.push("/app/device"); + break; + case ".sequence-list-panel": + history.push("/app/sequences"); + break; + case ".regimen-list-panel": + history.push("/app/regimens"); + break; + case ".tool-list": + case ".toolbay-list": + history.push("/app/tools"); + break; + case ".photos": + history.push("/app/farmware"); + break; + case ".logs-table": + history.push("/app/logs"); + break; + case ".app-settings-widget": + history.push("/app/account"); + break; + } +}; diff --git a/webpack/nav/__tests__/nav_index_test.tsx b/webpack/nav/__tests__/nav_index_test.tsx index c8bb9ad23..53fb4889f 100644 --- a/webpack/nav/__tests__/nav_index_test.tsx +++ b/webpack/nav/__tests__/nav_index_test.tsx @@ -15,6 +15,7 @@ describe("NavBar", () => { user: taggedUser, dispatch: jest.fn(), getConfigValue: jest.fn(), + tour: undefined, }); it("has correct parent classname", () => { diff --git a/webpack/nav/additional_menu.tsx b/webpack/nav/additional_menu.tsx index e3f2c1b0c..4a71d2bbd 100644 --- a/webpack/nav/additional_menu.tsx +++ b/webpack/nav/additional_menu.tsx @@ -10,6 +10,11 @@ export const AdditionalMenu = (props: AccountMenuProps) => { {t("Account Settings")} + {localStorage.getItem("FUTURE_FEATURES") && + + + {t("Help")} + }
diff --git a/webpack/nav/interfaces.ts b/webpack/nav/interfaces.ts index 7e1984b1a..f4172dc1f 100644 --- a/webpack/nav/interfaces.ts +++ b/webpack/nav/interfaces.ts @@ -18,6 +18,7 @@ export interface NavBarProps { dispatch: Function; timeOffset: number; getConfigValue: GetWebAppConfigValue; + tour: string | undefined; } export interface NavBarState { diff --git a/webpack/resources/interfaces.ts b/webpack/resources/interfaces.ts index 1afa3c5f2..a4464d44a 100644 --- a/webpack/resources/interfaces.ts +++ b/webpack/resources/interfaces.ts @@ -11,6 +11,7 @@ import { } from "farmbot"; import { RegimenState } from "../regimens/reducer"; import { FarmwareState } from "../farmware/interfaces"; +import { HelpState } from "../help/reducer"; type UUID = string; @@ -30,6 +31,7 @@ export interface RestResources { regimens: RegimenState; farm_designer: DesignerState; farmware: FarmwareState; + help: HelpState; } } diff --git a/webpack/resources/reducer.ts b/webpack/resources/reducer.ts index d7f6d8cb9..f8d1c072b 100644 --- a/webpack/resources/reducer.ts +++ b/webpack/resources/reducer.ts @@ -32,6 +32,10 @@ import { farmwareReducer as farmware, farmwareState } from "../farmware/reducer"; +import { + helpReducer as help, + initialState as helpState +} from "../help/reducer"; import { Actions } from "../constants"; import { maybeTagSteps as dontTouchThis } from "./sequence_tagging"; import { GeneralizedError } from "./actions"; @@ -44,7 +48,8 @@ const consumerReducer = combineReducers({ regimens, sequences, farm_designer, - farmware + farmware, + help } as any); // tslint:disable-line export function emptyState(): RestResources { @@ -53,7 +58,8 @@ export function emptyState(): RestResources { sequences: sequenceState, regimens: regimenState, farm_designer: designerState, - farmware: farmwareState + farmware: farmwareState, + help: helpState, }, loaded: [], index: { @@ -97,7 +103,8 @@ const afterEach = (state: RestResources, a: ReduxAction) => { sequences: state.consumers.sequences, regimens: state.consumers.regimens, farm_designer: state.consumers.farm_designer, - farmware: state.consumers.farmware + farmware: state.consumers.farmware, + help: state.consumers.help, }, a); return state; }; diff --git a/webpack/route_config.tsx b/webpack/route_config.tsx index 61fe3b2aa..baafe318b 100644 --- a/webpack/route_config.tsx +++ b/webpack/route_config.tsx @@ -99,6 +99,12 @@ export const UNBOUND_ROUTES = [ getModule: () => import("./account"), key: "Account", }), + route({ + children: false, + $: "/help", + getModule: () => import("./help/help"), + key: "Help", + }), route({ children: false, $: "/controls", diff --git a/webpack/session_keys.ts b/webpack/session_keys.ts index bb6787c58..f0c26263d 100644 --- a/webpack/session_keys.ts +++ b/webpack/session_keys.ts @@ -12,6 +12,7 @@ export const BooleanSetting: Record = { show_spread: "show_spread", show_farmbot: "show_farmbot", show_images: "show_images", + show_sensor_readings: "show_sensor_readings", xy_swap: "xy_swap", home_button_homing: "home_button_homing", show_motor_plot: "show_motor_plot", diff --git a/webpack/tools/components/tool_list.tsx b/webpack/tools/components/tool_list.tsx index 6dd6c7408..0a301cbf2 100644 --- a/webpack/tools/components/tool_list.tsx +++ b/webpack/tools/components/tool_list.tsx @@ -26,7 +26,7 @@ export class ToolList extends React.Component { render() { const { tools, toggle } = this.props; - return + return