From 110d7acd87edd758a429fbfe26acdf0205cb82d8 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Mon, 18 Jun 2018 16:12:00 -0500 Subject: [PATCH] Diagnostic Dumps - RC 1 (#889) Add API side diagnostic reporting. --- .travis.yml | 2 +- .../api/diagnostic_dumps_controller.rb | 28 ++++++++ app/models/celery_script_settings_bag.rb | 3 +- app/models/device.rb | 35 +++++----- app/models/diagnostic_dump.rb | 3 + app/mutations/diagnostic_dumps/create.rb | 25 +++++++ app/serializers/diagnostic_dump_serializer.rb | 5 ++ auto_upgrade.rb | 2 +- config/routes.rb | 1 + .../20180615153318_create_diagnostic_dumps.rb | 16 +++++ db/schema.rb | 17 ++++- latest_corpus.rb | 2 +- package.json | 20 +++--- .../diagnostic_dumps_controller_spec.rb | 52 ++++++++++++++ spec/factories/diagnostic_dumps.rb | 12 ++++ .../__test_support__/control_panel_state.ts | 3 +- webpack/__test_support__/fake_state/bot.ts | 1 + .../__test_support__/fake_state/resources.ts | 20 +++++- .../__tests__/request_account_exports_test.ts | 12 +++- webpack/account/request_account_export.ts | 11 +-- webpack/api/__tests__/api_test.ts | 29 ++++++++ webpack/api/api.ts | 6 +- webpack/css/global.scss | 18 +++++ webpack/devices/__tests__/actions_test.ts | 10 ++- webpack/devices/__tests__/reducer_test.ts | 8 ++- webpack/devices/actions.ts | 5 ++ .../__tests__/diagnostic_dump_row_test.tsx | 27 ++++++++ .../__tests__/farmbot_os_settings_test.tsx | 1 + .../__tests__/send_diagnostic_report_test.tsx | 32 +++++++++ .../components/diagnostic_dump_row.tsx | 46 +++++++++++++ .../components/farmbot_os_settings.tsx | 7 ++ .../fbos_settings/power_and_reset.tsx | 2 +- .../__tests__/header_test.tsx | 2 +- .../hardware_settings/danger_zone.tsx | 2 +- .../encoders_and_endstops.tsx | 2 +- .../components/hardware_settings/header.tsx | 6 +- .../homing_and_calibration.tsx | 2 +- .../components/hardware_settings/motors.tsx | 2 +- .../hardware_settings/pin_guard.tsx | 2 +- .../components/send_diagnostic_report.tsx | 63 +++++++++++++++++ .../devices/connectivity/status_checks.tsx | 2 +- webpack/devices/devices.tsx | 2 + webpack/devices/interfaces.ts | 6 +- webpack/devices/reducer.ts | 1 + webpack/resources/reducer.ts | 5 +- webpack/resources/selectors_by_kind.ts | 3 + webpack/resources/tagged_resources.ts | 17 +++++ webpack/sync/actions.ts | 4 +- yarn.lock | 69 +++++++++++-------- 49 files changed, 563 insertions(+), 88 deletions(-) create mode 100644 app/controllers/api/diagnostic_dumps_controller.rb create mode 100644 app/models/diagnostic_dump.rb create mode 100644 app/mutations/diagnostic_dumps/create.rb create mode 100644 app/serializers/diagnostic_dump_serializer.rb create mode 100644 db/migrate/20180615153318_create_diagnostic_dumps.rb create mode 100644 spec/controllers/api/diagnostic_dumps/diagnostic_dumps_controller_spec.rb create mode 100644 spec/factories/diagnostic_dumps.rb create mode 100644 webpack/api/__tests__/api_test.ts create mode 100644 webpack/devices/components/__tests__/diagnostic_dump_row_test.tsx create mode 100644 webpack/devices/components/__tests__/send_diagnostic_report_test.tsx create mode 100644 webpack/devices/components/diagnostic_dump_row.tsx create mode 100644 webpack/devices/components/send_diagnostic_report.tsx diff --git a/.travis.yml b/.travis.yml index 2d6594346..aa26a6dd8 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: -- 8.9.4 +- 8.11.3 cache: yarn: true directories: diff --git a/app/controllers/api/diagnostic_dumps_controller.rb b/app/controllers/api/diagnostic_dumps_controller.rb new file mode 100644 index 000000000..fb45e5930 --- /dev/null +++ b/app/controllers/api/diagnostic_dumps_controller.rb @@ -0,0 +1,28 @@ +module Api + class DiagnosticDumpsController < Api::AbstractController + + def index + render json: diagnostic_dumps + end + + def create + Rollbar.info("Device #{current_device.id} created a diagnostic") + mutate DiagnosticDumps::Create.run(raw_json, device: current_device) + end + + def destroy + diagnostic_dump.destroy! + render json: "" + end + +private + + def diagnostic_dumps + current_device.diagnostic_dumps + end + + def diagnostic_dump + @diagnostic_dump ||= diagnostic_dumps.find(params[:id]) + end + end +end diff --git a/app/models/celery_script_settings_bag.rb b/app/models/celery_script_settings_bag.rb index 6a94a3655..75e8b7ddf 100644 --- a/app/models/celery_script_settings_bag.rb +++ b/app/models/celery_script_settings_bag.rb @@ -18,7 +18,7 @@ module CeleryScriptSettingsBag install_farmware update_farmware take_photo zero install_first_party_farmware remove_farmware find_home register_gpio unregister_gpio - set_servo_angle change_ownership) + set_servo_angle change_ownership dump_info) ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware) ALLOWED_CHAGES = %w(add remove update) RESOURCE_NAME = %w(images plants regimens peripherals @@ -227,6 +227,7 @@ module CeleryScriptSettingsBag .node(:parameter_declaration, [:label, :data_type], []) .node(:set_servo_angle, [:pin_number, :pin_value], []) .node(:change_ownership, [], [:pair]) + .node(:dump_info, [], []) .node(:install_first_party_farmware, []) ANY_ARG_NAME = Corpus.as_json[:args].pluck("name").map(&:to_s) diff --git a/app/models/device.rb b/app/models/device.rb index 54a41e584..531a542e0 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -12,24 +12,25 @@ class Device < ApplicationRecord "Resuming log storage." CACHE_KEY = "devices.%s" - has_many :device_configs, dependent: :destroy - has_many :farm_events, dependent: :destroy + has_many :device_configs, dependent: :destroy + has_many :farm_events, dependent: :destroy has_many :farmware_installations, dependent: :destroy - has_many :images, dependent: :destroy - has_many :logs, dependent: :destroy - has_many :peripherals, dependent: :destroy - has_many :pin_bindings, dependent: :destroy - has_many :plant_templates, dependent: :destroy - has_many :points, dependent: :destroy - has_many :regimens, dependent: :destroy - has_many :saved_gardens, dependent: :destroy - has_many :sensor_readings, dependent: :destroy - has_many :sensors, dependent: :destroy - has_many :sequences, dependent: :destroy - has_many :token_issuances, dependent: :destroy - has_many :tools, dependent: :destroy - has_many :webcam_feeds, dependent: :destroy - has_one :fbos_config, dependent: :destroy + has_many :images, dependent: :destroy + has_many :logs, dependent: :destroy + has_many :peripherals, dependent: :destroy + has_many :pin_bindings, dependent: :destroy + has_many :plant_templates, dependent: :destroy + has_many :points, dependent: :destroy + has_many :regimens, dependent: :destroy + has_many :saved_gardens, dependent: :destroy + has_many :sensor_readings, dependent: :destroy + has_many :sensors, dependent: :destroy + has_many :sequences, dependent: :destroy + has_many :token_issuances, dependent: :destroy + has_many :tools, dependent: :destroy + has_many :webcam_feeds, dependent: :destroy + has_many :diagnostic_dumps, dependent: :destroy + has_one :fbos_config, dependent: :destroy has_many :in_use_tools has_many :in_use_points diff --git a/app/models/diagnostic_dump.rb b/app/models/diagnostic_dump.rb new file mode 100644 index 000000000..38601231b --- /dev/null +++ b/app/models/diagnostic_dump.rb @@ -0,0 +1,3 @@ +class DiagnosticDump < ApplicationRecord + belongs_to :device +end diff --git a/app/mutations/diagnostic_dumps/create.rb b/app/mutations/diagnostic_dumps/create.rb new file mode 100644 index 000000000..3aa2fb082 --- /dev/null +++ b/app/mutations/diagnostic_dumps/create.rb @@ -0,0 +1,25 @@ +module DiagnosticDumps + class Create < Mutations::Command + required do + model :device, class: Device + string :fbos_version + string :fbos_commit + string :firmware_commit + string :network_interface + string :fbos_dmesg_dump + string :firmware_state + end + + def execute + DiagnosticDump + .create!(device: device, + ticket_identifier: rand(36**5).to_s(36), + fbos_version: fbos_version, + fbos_commit: fbos_commit, + firmware_commit: firmware_commit, + network_interface: network_interface, + fbos_dmesg_dump: fbos_dmesg_dump, + firmware_state: firmware_state,) + end + end +end diff --git a/app/serializers/diagnostic_dump_serializer.rb b/app/serializers/diagnostic_dump_serializer.rb new file mode 100644 index 000000000..4760453c5 --- /dev/null +++ b/app/serializers/diagnostic_dump_serializer.rb @@ -0,0 +1,5 @@ +class DiagnosticDumpSerializer < ActiveModel::Serializer + attributes :id, :device_id, :ticket_identifier, :fbos_commit, :fbos_version, + :firmware_commit, :firmware_state, :network_interface, + :fbos_dmesg_dump, :created_at, :updated_at +end diff --git a/auto_upgrade.rb b/auto_upgrade.rb index d9ece5f1b..bc86598b4 100644 --- a/auto_upgrade.rb +++ b/auto_upgrade.rb @@ -43,7 +43,7 @@ DEPS = `yarn outdated` .map{|y| y.split } .map{|y| "#{y[0]}@#{y[3]}"} .sort - + .reject { |x| x.include?("router") } # puts "Making sure that type checks pass WITHOUT any upgrades" tc_ok = type_check diff --git a/config/routes.rb b/config/routes.rb index 14fa50ba0..6a4ed7643 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ FarmBot::Application.routes.draw do namespace :api, defaults: {format: :json}, constraints: { format: "json" } do # Standard API Resources: { + diagnostic_dumps: [:create, :destroy, :index], farm_events: [:create, :destroy, :index, :update], farmware_installations: [:create, :destroy, :index], images: [:create, :destroy, :index, :show], diff --git a/db/migrate/20180615153318_create_diagnostic_dumps.rb b/db/migrate/20180615153318_create_diagnostic_dumps.rb new file mode 100644 index 000000000..458742815 --- /dev/null +++ b/db/migrate/20180615153318_create_diagnostic_dumps.rb @@ -0,0 +1,16 @@ +class CreateDiagnosticDumps < ActiveRecord::Migration[5.2] + def change + create_table :diagnostic_dumps do |t| + t.references :device, foreign_key: true, null: false + t.string :ticket_identifier, null: false, unique: true + t.string :fbos_commit, null: false + t.string :fbos_version, null: false + t.string :firmware_commit, null: false + t.string :firmware_state, null: false + t.string :network_interface, null: false + t.text :fbos_dmesg_dump, null: false + t.timestamps + + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4b596c733..e53ae864a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_06_09_144559) do +ActiveRecord::Schema.define(version: 2018_06_15_153318) do # These are extensions that must be enabled in order to support this database enable_extension "hstore" @@ -53,6 +53,20 @@ ActiveRecord::Schema.define(version: 2018_06_09_144559) do t.index ["timezone"], name: "index_devices_on_timezone" end + create_table "diagnostic_dumps", force: :cascade do |t| + t.bigint "device_id", null: false + t.string "ticket_identifier", null: false + t.string "fbos_commit", null: false + t.string "fbos_version", null: false + t.string "firmware_commit", null: false + t.string "firmware_state", null: false + t.string "network_interface", null: false + t.text "fbos_dmesg_dump", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["device_id"], name: "index_diagnostic_dumps_on_device_id" + end + create_table "edge_nodes", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -478,6 +492,7 @@ ActiveRecord::Schema.define(version: 2018_06_09_144559) do end add_foreign_key "device_configs", "devices" + add_foreign_key "diagnostic_dumps", "devices" add_foreign_key "edge_nodes", "sequences" add_foreign_key "farmware_installations", "devices" add_foreign_key "peripherals", "devices" diff --git a/latest_corpus.rb b/latest_corpus.rb index cac82184f..dd2e6cb0e 100755 --- a/latest_corpus.rb +++ b/latest_corpus.rb @@ -83,7 +83,7 @@ class CorpusEmitter end end - HASH = JSON.load(open("http://localhost:3000/api/corpuses/3")).deep_symbolize_keys + HASH = JSON.load(open("http://localhost:3000/api/corpus")).deep_symbolize_keys ARGS = {} HASH[:args].map{ |x| CSArg.new(x) }.each{|x| ARGS[x.name] = x} NODES = HASH[:nodes].map { |x| CSNode.new(x) } diff --git a/package.json b/package.json index 1537e78ae..75897c777 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ "@types/fastclick": "^1.0.28", "@types/history": "^4.6.1", "@types/i18next": "^8.4.2", - "@types/jest": "23.0.2", + "@types/jest": "23.1.0", "@types/lodash": "4.14.109", "@types/markdown-it": "^0.0.4", "@types/moxios": "^0.4.5", - "@types/node": "10.3.2", + "@types/node": "10.3.3", "@types/react": "16.3.14", "@types/react-color": "2.13.5", "@types/react-dom": "16.0.5", @@ -51,11 +51,11 @@ "css-loader": "0.28.11", "enzyme": "^3.1.0", "enzyme-adapter-react-16": "^1.1.0", - "farmbot": "6.0.0-rc2", + "farmbot": "6.0.1", "farmbot-toastr": "^1.0.3", "fastclick": "^1.0.6", "file-loader": "1.1.11", - "i18next": "11.3.2", + "i18next": "11.3.3", "imports-loader": "0.8.0", "jest": "23.1.0", "json-loader": "0.5.7", @@ -67,14 +67,14 @@ "node-sass": "4.9.0", "optimize-css-assets-webpack-plugin": "4.0.2", "raf": "^3.4.0", - "react": "16.4", + "react": "16.4.1", "react-addons-css-transition-group": "^15.6.2", "react-addons-test-utils": "^15.6.2", "react-color": "2.14.1", - "react-dom": "16.4", + "react-dom": "16.4.1", "react-redux": "^5.0.6", "react-router": "^3", - "react-test-renderer": "16.4.0", + "react-test-renderer": "16.4.1", "react-transition-group": "^2.3.1", "redux": "^3.7.2", "redux-immutable-state-invariant": "^2.1.0", @@ -87,7 +87,7 @@ "ts-lint": "^4.5.1", "ts-loader": "^4.4.1", "tslint": "5.10.0", - "typescript": "2.9.1", + "typescript": "2.9.2", "url-loader": "1.0.1", "webpack": "4.12.0", "webpack-uglify-js-plugin": "1.1.9", @@ -96,8 +96,8 @@ "yarn": "^1.6.0" }, "devDependencies": { - "jscpd": "0.6.19", - "webpack-cli": "3.0.4", + "jscpd": "0.6.21", + "webpack-cli": "3.0.8", "webpack-notifier": "^1.5.0" }, "jest": { diff --git a/spec/controllers/api/diagnostic_dumps/diagnostic_dumps_controller_spec.rb b/spec/controllers/api/diagnostic_dumps/diagnostic_dumps_controller_spec.rb new file mode 100644 index 000000000..e2f10c85a --- /dev/null +++ b/spec/controllers/api/diagnostic_dumps/diagnostic_dumps_controller_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Api::DiagnosticDumpsController do + let(:device) { FactoryBot.create(:device) } + let(:user) { FactoryBot.create(:user, device: device) } + + include Devise::Test::ControllerHelpers + + it 'lists all diagnostics' do + sign_in user + DiagnosticDump.destroy_all + device_config = FactoryBot.create_list(:diagnostic_dump, 3, device: device) + get :index + expect(json.length).to eq(3) + expect(json.pluck(:device_id).uniq).to eq([user.device.id]) + end + + it 'creates a dump' do + sign_in user + DiagnosticDump.destroy_all + b4 = DiagnosticDump.count + input = { + fbos_version: "123_fbos_version", + fbos_commit: "123_fbos_commit", + firmware_commit: "123_firmware_commit", + network_interface: "123_network_interface", + fbos_dmesg_dump: "123_fbos_dmesg_dump", + firmware_state: "123_firmware_state", + } + post :create, body: input.to_json + expect(response.status).to eq(200) + expect(DiagnosticDump.count).to be > b4 + expect(DiagnosticDump.last.device).to eq(device) + expect(json[:fbos_version]).to eq("123_fbos_version") + expect(json[:fbos_commit]).to eq("123_fbos_commit") + expect(json[:firmware_commit]).to eq("123_firmware_commit") + expect(json[:network_interface]).to eq("123_network_interface") + expect(json[:fbos_dmesg_dump]).to eq("123_fbos_dmesg_dump") + expect(json[:firmware_state]).to eq("123_firmware_state") + expect(json[:ticket_identifier].length).to eq(5) + end + + it 'deletes' do + sign_in user + # DiagnosticDump.destroy_all + device_config = FactoryBot.create(:diagnostic_dump, device: device) + id = device_config.id + delete :destroy, params: { id: device_config.id } + expect(response.status).to be(200) + expect(DiagnosticDump.exists?(id)).to be false + end +end diff --git a/spec/factories/diagnostic_dumps.rb b/spec/factories/diagnostic_dumps.rb new file mode 100644 index 000000000..a4511131d --- /dev/null +++ b/spec/factories/diagnostic_dumps.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :diagnostic_dump do + device + fbos_version "123_fbos_version" + fbos_commit "123_fbos_commit" + firmware_commit "123_firmware_commit" + network_interface "123_network_interface" + fbos_dmesg_dump "123_fbos_dmesg_dump" + firmware_state "123_firmware_state" + ticket_identifier { rand(36**5).to_s(36) } + end +end diff --git a/webpack/__test_support__/control_panel_state.ts b/webpack/__test_support__/control_panel_state.ts index 13eddf61a..b38c276ac 100644 --- a/webpack/__test_support__/control_panel_state.ts +++ b/webpack/__test_support__/control_panel_state.ts @@ -7,6 +7,7 @@ export const panelState = (): ControlPanelState => { encoders_and_endstops: false, danger_zone: false, power_and_reset: false, - pin_guard: false + pin_guard: false, + diagnostic_dumps: false }; }; diff --git a/webpack/__test_support__/fake_state/bot.ts b/webpack/__test_support__/fake_state/bot.ts index 671520ad9..ab200f4e8 100644 --- a/webpack/__test_support__/fake_state/bot.ts +++ b/webpack/__test_support__/fake_state/bot.ts @@ -10,6 +10,7 @@ export let bot: Everything["bot"] = { "danger_zone": false, "power_and_reset": false, "pin_guard": false, + "diagnostic_dumps": false }, "hardware": { "gpio_registry": {}, diff --git a/webpack/__test_support__/fake_state/resources.ts b/webpack/__test_support__/fake_state/resources.ts index 40f6b8c6c..cd5a4a932 100644 --- a/webpack/__test_support__/fake_state/resources.ts +++ b/webpack/__test_support__/fake_state/resources.ts @@ -8,7 +8,8 @@ import { TaggedSensor, TaggedFirmwareConfig, TaggedPinBinding, - TaggedLog + TaggedLog, + TaggedDiagnosticDump } from "../../resources/tagged_resources"; import { ExecutableType } from "../../farm_designer/interfaces"; import { fakeResource } from "../fake_resource"; @@ -117,6 +118,23 @@ export function fakePlant(): TaggedPlantPointer { }); } +export function fakeDiagnosticDump(): TaggedDiagnosticDump { + const string = "----PLACEHOLDER DIAG STUFF ---"; + return fakeResource("DiagnosticDump", { + id: idCounter++, + device_id: 123, + ticket_identifier: string, + fbos_commit: string, + fbos_version: string, + firmware_commit: string, + firmware_state: string, + network_interface: string, + fbos_dmesg_dump: string, + created_at: string, + updated_at: string, + }); +} + export function fakePoint(): TaggedGenericPointer { return fakeResource("Point", { id: idCounter++, diff --git a/webpack/account/__tests__/request_account_exports_test.ts b/webpack/account/__tests__/request_account_exports_test.ts index 06a8eff3c..961a0fe54 100644 --- a/webpack/account/__tests__/request_account_exports_test.ts +++ b/webpack/account/__tests__/request_account_exports_test.ts @@ -10,12 +10,22 @@ jest.mock("axios", import { API } from "../../api"; import { Content } from "../../constants"; -import { requestAccountExport } from "../request_account_export"; +import { requestAccountExport, generateFilename } from "../request_account_export"; import { success } from "farmbot-toastr"; import axios from "axios"; +import { fakeDevice } from "../../__test_support__/resource_index_builder"; API.setBaseUrl("http://www.foo.bar"); +describe("generateFilename", () => { + it("generates a filename", () => { + const device = fakeDevice().body; + device.name = "FOO"; + device.id = 123; + const result = generateFilename({ device }); + expect(result).toEqual("export_foo_123.json"); + }); +}); describe("requestAccountExport", () => { it("pops toast on completion (when API has email support)", async () => { await requestAccountExport(); diff --git a/webpack/account/request_account_export.ts b/webpack/account/request_account_export.ts index cb9ceb3e2..a2492136f 100644 --- a/webpack/account/request_account_export.ts +++ b/webpack/account/request_account_export.ts @@ -8,13 +8,14 @@ import { DeviceAccountSettings } from "../devices/interfaces"; interface DataDumpExport { device?: DeviceAccountSettings; } type Response = AxiosResponse; -function generateFilename({ device }: DataDumpExport): string { - const name = (device && device.name + "_" + device.id) || "farmbot"; +export function generateFilename({ device }: DataDumpExport): string { + let name: string; + name = device ? (device.name + "_" + device.id) : "farmbot"; return `export_${name}.json`.toLowerCase(); } // Thanks, @KOL - https://stackoverflow.com/a/19328891/1064917 -function handleNow(data: DataDumpExport) { +export function jsonDownload(data: object, fname = generateFilename(data)) { // When email is not available on the API (self hosted). // Will synchronously load backup over the wire (slow) const a = document.createElement("a"); @@ -24,7 +25,7 @@ function handleNow(data: DataDumpExport) { blob = new Blob([json], { type: "octet/stream" }), url = window.URL.createObjectURL(blob); a.href = url; - a.download = generateFilename(data); + a.download = fname; a.click(); window.URL.revokeObjectURL(url); return a; @@ -32,7 +33,7 @@ function handleNow(data: DataDumpExport) { const ok = (resp: Response) => { const { data } = resp; - return data ? handleNow(data) : success(t(Content.EXPORT_SENT)); + return data ? jsonDownload(data) : success(t(Content.EXPORT_SENT)); }; export const requestAccountExport = diff --git a/webpack/api/__tests__/api_test.ts b/webpack/api/__tests__/api_test.ts new file mode 100644 index 000000000..3ad4e71c8 --- /dev/null +++ b/webpack/api/__tests__/api_test.ts @@ -0,0 +1,29 @@ +import { API } from "../api"; + +describe("API", () => { + type L = typeof location; + const fakeLocation = (input: Partial) => input as L; + it("requires initialization", () => { + expect(() => API.current).toThrow(); + const BASE = "http://localhost:3000"; + API.setBaseUrl(BASE); + [ + [API.current.pointSearchPath, BASE + "/api/points/search"], + [API.current.sensorReadingPath, BASE + "/api/sensor_readings"], + [API.current.deviceConfigPath, BASE + "/api/device_configs"], + [API.current.plantTemplatePath, BASE + "/api/plant_templates"], + [API.current.diagnosticDumpsPath, BASE + "/api/diagnostic_dumps"], + [API.current.farmwareInstallationPath, BASE + "/api/farmware_installations"], + ].map(x => expect(x[0]).toEqual(x[1])); + }); + + it("infers the correct port", () => { + const xmp: [string, L][] = [ + ["3000", fakeLocation({ port: "3808" })], + ["1234", fakeLocation({ port: "1234" })], + ["80", fakeLocation({ port: undefined })], + ["443", fakeLocation({ port: undefined, origin: "https://x.y.z" })], + ]; + xmp.map(x => expect(API.inferPort(x[1])).toEqual(x[0])); + }); +}); diff --git a/webpack/api/api.ts b/webpack/api/api.ts index 2e9509343..2124949b1 100644 --- a/webpack/api/api.ts +++ b/webpack/api/api.ts @@ -17,7 +17,7 @@ interface UrlInfo { export class API { /** Guesses the most appropriate API port based on a number of environment * factors such as hostname and protocol (HTTP vs. HTTPS). */ - static inferPort(): string { + static inferPort(location = window.location): string { // ATTEMPT 1: Most devs running a webpack server on localhost // run the API on port 3000. @@ -115,7 +115,7 @@ export class API { /** /api/points/ */ get pointsPath() { return `${this.baseUrl}/api/points/`; } /** /api/points/search */ - get pointSearchPath() { return `${this.pointsPath}/search/`; } + get pointSearchPath() { return `${this.pointsPath}search`; } /** Rather than returning ALL logs, returns a filtered subset. * /api/logs/search */ get filteredLogsPath() { return `${this.baseUrl}/api/logs/search`; } @@ -145,6 +145,8 @@ export class API { get exportDataPath() { return `${this.baseUrl}/api/export_data`; } /** /api/plant_templates/:id */ get plantTemplatePath() { return `${this.baseUrl}/api/plant_templates`; } + /** /api/diagnostic_dumps/:id */ + get diagnosticDumpsPath() { return `${this.baseUrl}/api/diagnostic_dumps`; } /** /api/farmware_installations/:id */ get farmwareInstallationPath() { return `${this.baseUrl}/api/farmware_installations`; diff --git a/webpack/css/global.scss b/webpack/css/global.scss index 193eb0743..1828d1416 100644 --- a/webpack/css/global.scss +++ b/webpack/css/global.scss @@ -778,3 +778,21 @@ ul { } } } + +a.panel-link { + // TODO: Might need to move this to a better place. CC: @gabrielBurnworth + // + // PROBLEM: links in the device panel are invisible. + // SOLUTION: Add {color: $dark_gray} + // PROBLEM 2: This rule probably does not belong at the bottom of + // global.scss + color: $dark_gray; + + &:visited { + color: $dark_gray; + }; + + &:active { + color: $dark_gray; + }; +} diff --git a/webpack/devices/__tests__/actions_test.ts b/webpack/devices/__tests__/actions_test.ts index ac1d13e9a..b38115286 100644 --- a/webpack/devices/__tests__/actions_test.ts +++ b/webpack/devices/__tests__/actions_test.ts @@ -13,7 +13,8 @@ const mockDevice = { home: jest.fn(() => { return Promise.resolve(); }), sync: jest.fn(() => { return Promise.resolve(); }), readStatus: jest.fn(() => Promise.resolve()), - updateConfig: jest.fn(() => Promise.resolve()) + updateConfig: jest.fn(() => Promise.resolve()), + dumpInfo: jest.fn(() => Promise.resolve()), }; jest.mock("../../device", () => ({ @@ -174,6 +175,13 @@ describe("MCUFactoryReset()", function () { }); }); +describe("requestDiagnostic", () => { + it("requests that FBOS build a diagnostic report", () => { + actions.requestDiagnostic(); + expect(mockDevice.dumpInfo).toHaveBeenCalled(); + }); +}); + describe("settingToggle()", () => { beforeEach(function () { jest.clearAllMocks(); diff --git a/webpack/devices/__tests__/reducer_test.ts b/webpack/devices/__tests__/reducer_test.ts index 1bc983d0a..184e85941 100644 --- a/webpack/devices/__tests__/reducer_test.ts +++ b/webpack/devices/__tests__/reducer_test.ts @@ -45,8 +45,12 @@ describe("botRedcuer", () => { type: Actions.BULK_TOGGLE_CONTROL_PANEL, payload: true }); - _.values(_.omit(state.controlPanelState, "power_and_reset")) - .map(value => expect(value).toBeTruthy()); + + const bulkToggable = + _.omit(state.controlPanelState, "power_and_reset", "diagnostic_dumps"); + _.values(bulkToggable).map(value => { + expect(value).toBeTruthy(); + }); }); it("fetches OS update info", () => { diff --git a/webpack/devices/actions.ts b/webpack/devices/actions.ts index 6e4073ef6..b923983c2 100644 --- a/webpack/devices/actions.ts +++ b/webpack/devices/actions.ts @@ -140,6 +140,11 @@ export function execSequence(sequence: Sequence) { } } +export function requestDiagnostic() { + const noun = "Diagnostic Request"; + return getDevice().dumpInfo().then(commandOK(noun), commandErr(noun)); +} + export let saveAccountChanges: Thunk = function (_dispatch, getState) { return save(getDeviceAccountSettings(getState().resources.index)); }; diff --git a/webpack/devices/components/__tests__/diagnostic_dump_row_test.tsx b/webpack/devices/components/__tests__/diagnostic_dump_row_test.tsx new file mode 100644 index 000000000..523e817d5 --- /dev/null +++ b/webpack/devices/components/__tests__/diagnostic_dump_row_test.tsx @@ -0,0 +1,27 @@ +jest.mock("../../../account/request_account_export", () => { + return { jsonDownload: jest.fn() }; +}); + +jest.mock("../../../api/crud", () => { + return { destroy: jest.fn() }; +}); +import * as React from "react"; +import { mount } from "enzyme"; +import { DiagnosticDumpRow } from "../diagnostic_dump_row"; +import { fakeDiagnosticDump } from "../../../__test_support__/fake_state/resources"; +import { jsonDownload } from "../../../account/request_account_export"; +import { destroy } from "../../../api/crud"; + +describe("", () => { + it("renders a single diagnostic dump", () => { + const dispatch = jest.fn(); + const diag = fakeDiagnosticDump(); + diag.body.ticket_identifier = "0000"; + const el = mount(); + expect(el.text()).toContain("0000"); + el.find("a").first().simulate("click"); + expect(jsonDownload).toHaveBeenCalledWith(diag.body, "farmbot_diagnostics_0000.json"); + el.find("button.red").first().simulate("click"); + expect(destroy).toHaveBeenCalledWith(diag.uuid); + }); +}); diff --git a/webpack/devices/components/__tests__/farmbot_os_settings_test.tsx b/webpack/devices/components/__tests__/farmbot_os_settings_test.tsx index dde8b33ab..ddfd95802 100644 --- a/webpack/devices/components/__tests__/farmbot_os_settings_test.tsx +++ b/webpack/devices/components/__tests__/farmbot_os_settings_test.tsx @@ -26,6 +26,7 @@ describe("", () => { const fakeProps = (): FarmbotOsProps => { return { account: fakeResource("Device", { id: 0, name: "", tz_offset_hrs: 0 }), + diagnostics: [], dispatch: jest.fn(), bot, botToMqttLastSeen: "", diff --git a/webpack/devices/components/__tests__/send_diagnostic_report_test.tsx b/webpack/devices/components/__tests__/send_diagnostic_report_test.tsx new file mode 100644 index 000000000..41303f6e5 --- /dev/null +++ b/webpack/devices/components/__tests__/send_diagnostic_report_test.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { render } from "enzyme"; +import { SendDiagnosticReport } from "../send_diagnostic_report"; +import { fakeDiagnosticDump } from "../../../__test_support__/fake_state/resources"; + +describe("", () => { + it("renders", () => { + const dispatch = jest.fn(); + const shouldDisplay = jest.fn(() => true); + const fake = fakeDiagnosticDump(); + const el = render(); + expect(el.text()).toContain("DIAGNOSTIC CHECK"); + expect(shouldDisplay).toHaveBeenCalled(); + }); + + it("doesn't render", () => { + const dispatch = jest.fn(); + const shouldDisplay = jest.fn(() => false); + const fake = fakeDiagnosticDump(); + const el = render(); + expect(el.text()).toEqual(""); + expect(shouldDisplay).toHaveBeenCalled(); + }); +}); diff --git a/webpack/devices/components/diagnostic_dump_row.tsx b/webpack/devices/components/diagnostic_dump_row.tsx new file mode 100644 index 000000000..3c6432f83 --- /dev/null +++ b/webpack/devices/components/diagnostic_dump_row.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { Row, Col } from "../../ui"; +import { TaggedDiagnosticDump } from "../../resources/tagged_resources"; +import { jsonDownload } from "../../account/request_account_export"; +import { destroy } from "../../api/crud"; +import { ago } from "../connectivity/status_checks"; + +export interface Props { + diag: TaggedDiagnosticDump; + dispatch: Function; +} + +export class DiagnosticDumpRow extends React.Component { + get ticket() { return this.props.diag.body.ticket_identifier; } + + get age() { return ago(this.props.diag.body.created_at); } + + destroy = () => this.props.dispatch(destroy(this.props.diag.uuid)); + + download = (e: React.MouseEvent<{}>) => { + e.preventDefault(); + const { body } = this.props.diag; + const { ticket_identifier } = body; + const fileName = `farmbot_diagnostics_${ticket_identifier}.json`; + jsonDownload(body, fileName); + } + + render() { + return + + + + + + + + Download diagnostic report {this.ticket} (Saved {this.age}) + + + ; + } +} diff --git a/webpack/devices/components/farmbot_os_settings.tsx b/webpack/devices/components/farmbot_os_settings.tsx index 5bee890f2..f6b140e4c 100644 --- a/webpack/devices/components/farmbot_os_settings.tsx +++ b/webpack/devices/components/farmbot_os_settings.tsx @@ -22,6 +22,8 @@ import { AutoUpdateRow } from "./fbos_settings/auto_update_row"; import { AutoSyncRow } from "./fbos_settings/auto_sync_row"; import { isUndefined } from "lodash"; import { PowerAndReset } from "./fbos_settings/power_and_reset"; +import { SendDiagnosticReport } from "./send_diagnostic_report"; + import axios from "axios"; export enum ColWidth { @@ -166,6 +168,11 @@ export class FarmbotOsSettings sourceFbosConfig={sourceFbosConfig} shouldDisplay={this.props.shouldDisplay} botOnline={botOnline} /> + diff --git a/webpack/devices/components/fbos_settings/power_and_reset.tsx b/webpack/devices/components/fbos_settings/power_and_reset.tsx index 14e83ce4b..7d8391b71 100644 --- a/webpack/devices/components/fbos_settings/power_and_reset.tsx +++ b/webpack/devices/components/fbos_settings/power_and_reset.tsx @@ -15,7 +15,7 @@ export function PowerAndReset(props: PowerAndResetProps) { return
diff --git a/webpack/devices/components/hardware_settings/__tests__/header_test.tsx b/webpack/devices/components/hardware_settings/__tests__/header_test.tsx index ae5ed06b8..dd84bb77c 100644 --- a/webpack/devices/components/hardware_settings/__tests__/header_test.tsx +++ b/webpack/devices/components/hardware_settings/__tests__/header_test.tsx @@ -7,7 +7,7 @@ describe("
", () => { const fn = jest.fn(); const el = shallow(
); expect(el.text()).toContain("FOO"); diff --git a/webpack/devices/components/hardware_settings/danger_zone.tsx b/webpack/devices/components/hardware_settings/danger_zone.tsx index 85cbe48e6..ff0962a71 100644 --- a/webpack/devices/components/hardware_settings/danger_zone.tsx +++ b/webpack/devices/components/hardware_settings/danger_zone.tsx @@ -13,7 +13,7 @@ export function DangerZone(props: DangerZoneProps) { return
diff --git a/webpack/devices/components/hardware_settings/encoders_and_endstops.tsx b/webpack/devices/components/hardware_settings/encoders_and_endstops.tsx index 0fa61fa69..ea5809b6e 100644 --- a/webpack/devices/components/hardware_settings/encoders_and_endstops.tsx +++ b/webpack/devices/components/hardware_settings/encoders_and_endstops.tsx @@ -21,7 +21,7 @@ export function EncodersAndEndStops(props: EncodersProps) { return
diff --git a/webpack/devices/components/hardware_settings/header.tsx b/webpack/devices/components/hardware_settings/header.tsx index 156abd247..b125a3f77 100644 --- a/webpack/devices/components/hardware_settings/header.tsx +++ b/webpack/devices/components/hardware_settings/header.tsx @@ -7,12 +7,12 @@ interface Props { dispatch: Function; name: keyof ControlPanelState; title: string; - bool: boolean; + expanded: boolean; } export let Header = (props: Props) => { - const { dispatch, name, title, bool } = props; - const icon_string = bool ? "minus" : "plus"; + const { dispatch, name, title, expanded } = props; + const icon_string = expanded ? "minus" : "plus"; return

dispatch(toggleControlPanel(name))}> {t(title)} diff --git a/webpack/devices/components/hardware_settings/homing_and_calibration.tsx b/webpack/devices/components/hardware_settings/homing_and_calibration.tsx index a01788a1a..2578595c3 100644 --- a/webpack/devices/components/hardware_settings/homing_and_calibration.tsx +++ b/webpack/devices/components/hardware_settings/homing_and_calibration.tsx @@ -36,7 +36,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) { title={t("Homing and Calibration")} name={"homing_and_calibration"} dispatch={dispatch} - bool={homing_and_calibration} /> + expanded={homing_and_calibration} /> diff --git a/webpack/devices/components/hardware_settings/motors.tsx b/webpack/devices/components/hardware_settings/motors.tsx index 32c0bded3..10778ec07 100644 --- a/webpack/devices/components/hardware_settings/motors.tsx +++ b/webpack/devices/components/hardware_settings/motors.tsx @@ -25,7 +25,7 @@ export function Motors(props: MotorsProps) { return
diff --git a/webpack/devices/components/hardware_settings/pin_guard.tsx b/webpack/devices/components/hardware_settings/pin_guard.tsx index 457ea5c06..8d1b451f9 100644 --- a/webpack/devices/components/hardware_settings/pin_guard.tsx +++ b/webpack/devices/components/hardware_settings/pin_guard.tsx @@ -15,7 +15,7 @@ export function PinGuard(props: PinGuardProps) { return
diff --git a/webpack/devices/components/send_diagnostic_report.tsx b/webpack/devices/components/send_diagnostic_report.tsx new file mode 100644 index 000000000..758fce567 --- /dev/null +++ b/webpack/devices/components/send_diagnostic_report.tsx @@ -0,0 +1,63 @@ +import * as React from "react"; +import { Row, Col } from "../../ui"; +import { ColWidth } from "./farmbot_os_settings"; +import { t } from "i18next"; +import { Collapse } from "@blueprintjs/core"; +import { Header } from "./hardware_settings/header"; +import { ShouldDisplay, Feature } from "../interfaces"; +import { TaggedDiagnosticDump } from "../../resources/tagged_resources"; +import { DiagnosticDumpRow } from "./diagnostic_dump_row"; +import { requestDiagnostic } from "../actions"; + +export interface DiagReportProps { + dispatch: Function; + expanded: boolean; + shouldDisplay: ShouldDisplay; + diagnostics: TaggedDiagnosticDump[]; +} + +export class SendDiagnosticReport extends React.Component{ + show = () => { + return
+
+
+
+ + + + + + +

...

+ + + + +
+ {this.props.diagnostics.map(d => { + return ; + })} +
+
; + } + + noShow = () =>
; + + render() { + const show = this.props.shouldDisplay(Feature.diagnostic_dumps); + return (show ? this.show : this.noShow)(); + } +} diff --git a/webpack/devices/connectivity/status_checks.tsx b/webpack/devices/connectivity/status_checks.tsx index ba2c7d26a..e65c54e6a 100644 --- a/webpack/devices/connectivity/status_checks.tsx +++ b/webpack/devices/connectivity/status_checks.tsx @@ -10,7 +10,7 @@ const SIX_HOURS = HOUR * 6; const NOT_SEEN = t("No messages seen yet."); -function ago(input: string) { +export function ago(input: string) { return moment(new Date(input)).fromNow(); } diff --git a/webpack/devices/devices.tsx b/webpack/devices/devices.tsx index d42dcbab9..f92a367e5 100644 --- a/webpack/devices/devices.tsx +++ b/webpack/devices/devices.tsx @@ -13,6 +13,7 @@ import { Diagnosis, DiagnosisName } from "./connectivity/diagnosis"; import { StatusRowProps } from "./connectivity/connectivity_row"; import { resetConnectionInfo } from "./actions"; import { PinBindings } from "./components/pin_bindings"; +import { selectAllDiagnosticDumps } from "../resources/selectors"; @connect(mapStateToProps) export class Devices extends React.Component { @@ -58,6 +59,7 @@ export class Devices extends React.Component { >; @@ -148,6 +150,7 @@ export interface CalibrationButtonProps { export interface FarmbotOsProps { bot: BotState; + diagnostics: TaggedDiagnosticDump[]; account: TaggedDevice; botToMqttStatus: NetworkState; botToMqttLastSeen: string; @@ -221,4 +224,5 @@ export interface ControlPanelState { danger_zone: boolean; power_and_reset: boolean; pin_guard: boolean; + diagnostic_dumps: boolean; } diff --git a/webpack/devices/reducer.ts b/webpack/devices/reducer.ts index e35d66272..e335afead 100644 --- a/webpack/devices/reducer.ts +++ b/webpack/devices/reducer.ts @@ -26,6 +26,7 @@ export let initialState = (): BotState => ({ danger_zone: false, power_and_reset: false, pin_guard: false, + diagnostic_dumps: false }, hardware: { gpio_registry: {}, diff --git a/webpack/resources/reducer.ts b/webpack/resources/reducer.ts index 66198f49f..0468c1612 100644 --- a/webpack/resources/reducer.ts +++ b/webpack/resources/reducer.ts @@ -79,7 +79,8 @@ export function emptyState(): RestResources { DeviceConfig: [], PinBinding: [], PlantTemplate: [], - SavedGarden: [] + SavedGarden: [], + DiagnosticDump: [] }, byKindAndId: {}, references: {} @@ -110,6 +111,7 @@ export let resourceReducer = generateReducer switch (resource.kind) { case "Crop": case "Device": + case "DiagnosticDump": case "FarmEvent": case "FarmwareInstallation": case "FbosConfig": @@ -144,6 +146,7 @@ export let resourceReducer = generateReducer switch (resource.kind) { case "Crop": case "Device": + case "DiagnosticDump": case "FarmEvent": case "FarmwareInstallation": case "FbosConfig": diff --git a/webpack/resources/selectors_by_kind.ts b/webpack/resources/selectors_by_kind.ts index f6fa96f39..33de393ab 100644 --- a/webpack/resources/selectors_by_kind.ts +++ b/webpack/resources/selectors_by_kind.ts @@ -20,6 +20,7 @@ import { TaggedFirmwareConfig, TaggedToolSlotPointer, TaggedPinBinding, + TaggedDiagnosticDump, } from "./tagged_resources"; import { sortResourcesById, betterCompact, bail } from "../util"; import { error } from "farmbot-toastr"; @@ -79,6 +80,8 @@ export const selectAllToolSlots = (i: ResourceIndex): TaggedToolSlotPointer[] => })); }; +export const selectAllDiagnosticDumps = + (i: ResourceIndex) => findAll(i, "DiagnosticDump"); export const selectAllRegimens = (i: ResourceIndex) => findAll(i, "Regimen"); export const selectAllSensors = (i: ResourceIndex) => findAll(i, "Sensor"); export const selectAllPinBindings = diff --git a/webpack/resources/tagged_resources.ts b/webpack/resources/tagged_resources.ts index c74190268..f6c398c00 100644 --- a/webpack/resources/tagged_resources.ts +++ b/webpack/resources/tagged_resources.ts @@ -30,6 +30,7 @@ export type ResourceName = | "Crop" | "Device" | "DeviceConfig" + | "DiagnosticDump" | "FarmEvent" | "FarmwareInstallation" | "FbosConfig" @@ -95,6 +96,7 @@ export interface Resource export type TaggedResource = | TaggedCrop | TaggedDevice + | TaggedDiagnosticDump | TaggedFarmEvent | TaggedFarmwareInstallation | TaggedFbosConfig @@ -132,6 +134,7 @@ export type TaggedSensorReading = Resource<"SensorReading", SensorReading>; export type TaggedSensor = Resource<"Sensor", Sensor>; export type TaggedSavedGarden = Resource<"SavedGarden", SavedGarden>; export type TaggedPlantTemplate = Resource<"PlantTemplate", PlantTemplate>; +export type TaggedDiagnosticDump = Resource<"DiagnosticDump", DiagnosticDump>; type PointUnion = GenericPointer | PlantPointer | ToolSlotPointer; @@ -147,6 +150,20 @@ export type TaggedWebcamFeed = Resource<"WebcamFeed", WebcamFeed>; export type TaggedFarmwareInstallation = Resource<"FarmwareInstallation", FarmwareInstallation>; +export interface DiagnosticDump { + id: number; + device_id: number; + ticket_identifier: string; + fbos_commit: string; + fbos_version: string; + firmware_commit: string; + firmware_state: string; + network_interface: string; + fbos_dmesg_dump: string; + created_at: string; + updated_at: string; +} + /** Spot check to be certain a TaggedResource is what it says it is. */ export function sanityCheck(x: object): x is TaggedResource { if (isTaggedResource(x)) { diff --git a/webpack/sync/actions.ts b/webpack/sync/actions.ts index b1acf15f3..a98b42158 100644 --- a/webpack/sync/actions.ts +++ b/webpack/sync/actions.ts @@ -8,7 +8,7 @@ import { Peripheral } from "../controls/peripherals/interfaces"; import { FarmEvent, SavedGarden, PlantTemplate } from "../farm_designer/interfaces"; import { Image } from "../farmware/images/interfaces"; import { DeviceAccountSettings } from "../devices/interfaces"; -import { ResourceName } from "../resources/tagged_resources"; +import { ResourceName, DiagnosticDump } from "../resources/tagged_resources"; import { User } from "../auth/interfaces"; import { WebcamFeed } from "../controls/interfaces"; import { WebAppConfig } from "../config_storage/web_app_configs"; @@ -61,5 +61,5 @@ export function fetchSyncData(dispatch: Function) { fetch("PinBinding", API.current.pinBindingPath); fetch("SavedGarden", API.current.savedGardensPath); fetch("PlantTemplate", API.current.plantTemplatePath); - + fetch("DiagnosticDump", API.current.diagnosticDumpsPath); } diff --git a/yarn.lock b/yarn.lock index 3dc65ff5d..977bc9929 100644 --- a/yarn.lock +++ b/yarn.lock @@ -84,9 +84,9 @@ version "8.4.3" resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-8.4.3.tgz#9136a9551bf5bf7169aa9f3125c1743f1f8dd6de" -"@types/jest@23.0.2": - version "23.0.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.0.2.tgz#f03f9e6dd2206cc2a2e8fd033161b0b7cf905db6" +"@types/jest@23.1.0": + version "23.1.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.1.0.tgz#8054dd838ba23dc331794d26456b86c7e50bf0f6" "@types/lodash@4.14.109": version "4.14.109" @@ -106,9 +106,9 @@ version "10.1.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.2.tgz#1b928a0baa408fc8ae3ac012cc81375addc147c6" -"@types/node@10.3.2": - version "10.3.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.2.tgz#3840ec6c12556fdda6e0e6d036df853101d732a4" +"@types/node@10.3.3": + version "10.3.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.3.tgz#8798d9e39af2fa604f715ee6a6b19796528e46c3" "@types/react-color@2.13.5": version "2.13.5" @@ -2392,9 +2392,9 @@ farmbot-toastr@^1.0.0, farmbot-toastr@^1.0.3: farmbot-toastr "^1.0.0" typescript "^2.3.4" -farmbot@6.0.0-rc2: - version "6.0.0-rc2" - resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-6.0.0-rc2.tgz#614e418de2264d8366ef1e8fd83121fc18f40356" +farmbot@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-6.0.1.tgz#46629b90e942141a6bacf6008059afac9928764a" dependencies: mqtt "2.15.0" @@ -3090,9 +3090,9 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" -i18next@11.3.2: - version "11.3.2" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.3.2.tgz#4a1a7bb14383ba6aed4abca139b03681fc96e023" +i18next@11.3.3: + version "11.3.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.3.3.tgz#a6ca3c2a93237c94e242bda7df3411588ac37ea1" iconv-lite@0.4.19: version "0.4.19" @@ -4049,9 +4049,9 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" -jscpd@0.6.19: - version "0.6.19" - resolved "https://registry.yarnpkg.com/jscpd/-/jscpd-0.6.19.tgz#c2ef022db2e06c41768cc3b4fbd7ec97e7117e8c" +jscpd@0.6.21: + version "0.6.21" + resolved "https://registry.yarnpkg.com/jscpd/-/jscpd-0.6.21.tgz#bf4f0be95526146412738657aa96c9d03930fa4b" dependencies: blamer "^0.1.9" bluebird "^3.0.5" @@ -5806,9 +5806,9 @@ react-day-picker@^7.0.7: dependencies: prop-types "^15.6.1" -react-dom@16.4: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e" +react-dom@16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -5819,6 +5819,10 @@ react-is@^16.4.0: version "16.4.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf" +react-is@^16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e" + react-popper@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.8.3.tgz#0f73333137c9fb0af6ec4074d2d0585a0a0461e1" @@ -5858,7 +5862,16 @@ react-router@^3: prop-types "^15.5.6" warning "^3.0.0" -react-test-renderer@16.4.0, react-test-renderer@^16.0.0-0: +react-test-renderer@16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70" + dependencies: + fbjs "^0.8.16" + object-assign "^4.1.1" + prop-types "^15.6.0" + react-is "^16.4.1" + +react-test-renderer@^16.0.0-0: version "16.4.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.0.tgz#0dbe0e24263e94e1830c7afb1f403707fad313a3" dependencies: @@ -5885,9 +5898,9 @@ react-transition-group@^2.3.1: loose-envify "^1.3.1" prop-types "^15.6.1" -react@16.4: - version "16.4.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585" +react@16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -7074,9 +7087,9 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1.tgz#fdb19d2c67a15d11995fd15640e373e09ab09961" +typescript@2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" typescript@^2.0.9, typescript@^2.3.4: version "2.8.3" @@ -7341,9 +7354,9 @@ webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" -webpack-cli@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.0.4.tgz#55d6ad2cdd608de8c0f757dde5bc4bf5bd2dec68" +webpack-cli@3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.0.8.tgz#90eddcf04a4bfc31aa8c0edc4c76785bc4f1ccd9" dependencies: chalk "^2.4.1" cross-spawn "^6.0.5"