Diagnostic Dumps - RC 1 (#889)

Add API side diagnostic reporting.
pull/891/head
Rick Carlino 2018-06-18 16:12:00 -05:00 committed by GitHub
parent d96b9bd467
commit 110d7acd87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 563 additions and 88 deletions

View File

@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- 8.9.4 - 8.11.3
cache: cache:
yarn: true yarn: true
directories: directories:

View File

@ -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

View File

@ -18,7 +18,7 @@ module CeleryScriptSettingsBag
install_farmware update_farmware take_photo zero install_farmware update_farmware take_photo zero
install_first_party_farmware remove_farmware install_first_party_farmware remove_farmware
find_home register_gpio unregister_gpio 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_PACKAGES = %w(farmbot_os arduino_firmware)
ALLOWED_CHAGES = %w(add remove update) ALLOWED_CHAGES = %w(add remove update)
RESOURCE_NAME = %w(images plants regimens peripherals RESOURCE_NAME = %w(images plants regimens peripherals
@ -227,6 +227,7 @@ module CeleryScriptSettingsBag
.node(:parameter_declaration, [:label, :data_type], []) .node(:parameter_declaration, [:label, :data_type], [])
.node(:set_servo_angle, [:pin_number, :pin_value], []) .node(:set_servo_angle, [:pin_number, :pin_value], [])
.node(:change_ownership, [], [:pair]) .node(:change_ownership, [], [:pair])
.node(:dump_info, [], [])
.node(:install_first_party_farmware, []) .node(:install_first_party_farmware, [])
ANY_ARG_NAME = Corpus.as_json[:args].pluck("name").map(&:to_s) ANY_ARG_NAME = Corpus.as_json[:args].pluck("name").map(&:to_s)

View File

@ -12,24 +12,25 @@ class Device < ApplicationRecord
"Resuming log storage." "Resuming log storage."
CACHE_KEY = "devices.%s" CACHE_KEY = "devices.%s"
has_many :device_configs, dependent: :destroy has_many :device_configs, dependent: :destroy
has_many :farm_events, dependent: :destroy has_many :farm_events, dependent: :destroy
has_many :farmware_installations, dependent: :destroy has_many :farmware_installations, dependent: :destroy
has_many :images, dependent: :destroy has_many :images, dependent: :destroy
has_many :logs, dependent: :destroy has_many :logs, dependent: :destroy
has_many :peripherals, dependent: :destroy has_many :peripherals, dependent: :destroy
has_many :pin_bindings, dependent: :destroy has_many :pin_bindings, dependent: :destroy
has_many :plant_templates, dependent: :destroy has_many :plant_templates, dependent: :destroy
has_many :points, dependent: :destroy has_many :points, dependent: :destroy
has_many :regimens, dependent: :destroy has_many :regimens, dependent: :destroy
has_many :saved_gardens, dependent: :destroy has_many :saved_gardens, dependent: :destroy
has_many :sensor_readings, dependent: :destroy has_many :sensor_readings, dependent: :destroy
has_many :sensors, dependent: :destroy has_many :sensors, dependent: :destroy
has_many :sequences, dependent: :destroy has_many :sequences, dependent: :destroy
has_many :token_issuances, dependent: :destroy has_many :token_issuances, dependent: :destroy
has_many :tools, dependent: :destroy has_many :tools, dependent: :destroy
has_many :webcam_feeds, dependent: :destroy has_many :webcam_feeds, dependent: :destroy
has_one :fbos_config, dependent: :destroy has_many :diagnostic_dumps, dependent: :destroy
has_one :fbos_config, dependent: :destroy
has_many :in_use_tools has_many :in_use_tools
has_many :in_use_points has_many :in_use_points

View File

@ -0,0 +1,3 @@
class DiagnosticDump < ApplicationRecord
belongs_to :device
end

View File

@ -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

View File

@ -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

View File

@ -43,7 +43,7 @@ DEPS = `yarn outdated`
.map{|y| y.split } .map{|y| y.split }
.map{|y| "#{y[0]}@#{y[3]}"} .map{|y| "#{y[0]}@#{y[3]}"}
.sort .sort
.reject { |x| x.include?("router") }
# puts "Making sure that type checks pass WITHOUT any upgrades" # puts "Making sure that type checks pass WITHOUT any upgrades"
tc_ok = type_check tc_ok = type_check

View File

@ -2,6 +2,7 @@ FarmBot::Application.routes.draw do
namespace :api, defaults: {format: :json}, constraints: { format: "json" } do namespace :api, defaults: {format: :json}, constraints: { format: "json" } do
# Standard API Resources: # Standard API Resources:
{ {
diagnostic_dumps: [:create, :destroy, :index],
farm_events: [:create, :destroy, :index, :update], farm_events: [:create, :destroy, :index, :update],
farmware_installations: [:create, :destroy, :index], farmware_installations: [:create, :destroy, :index],
images: [:create, :destroy, :index, :show], images: [:create, :destroy, :index, :show],

View File

@ -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

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "hstore" enable_extension "hstore"
@ -53,6 +53,20 @@ ActiveRecord::Schema.define(version: 2018_06_09_144559) do
t.index ["timezone"], name: "index_devices_on_timezone" t.index ["timezone"], name: "index_devices_on_timezone"
end 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| create_table "edge_nodes", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@ -478,6 +492,7 @@ ActiveRecord::Schema.define(version: 2018_06_09_144559) do
end end
add_foreign_key "device_configs", "devices" add_foreign_key "device_configs", "devices"
add_foreign_key "diagnostic_dumps", "devices"
add_foreign_key "edge_nodes", "sequences" add_foreign_key "edge_nodes", "sequences"
add_foreign_key "farmware_installations", "devices" add_foreign_key "farmware_installations", "devices"
add_foreign_key "peripherals", "devices" add_foreign_key "peripherals", "devices"

View File

@ -83,7 +83,7 @@ class CorpusEmitter
end end
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 = {} ARGS = {}
HASH[:args].map{ |x| CSArg.new(x) }.each{|x| ARGS[x.name] = x} HASH[:args].map{ |x| CSArg.new(x) }.each{|x| ARGS[x.name] = x}
NODES = HASH[:nodes].map { |x| CSNode.new(x) } NODES = HASH[:nodes].map { |x| CSNode.new(x) }

View File

@ -34,11 +34,11 @@
"@types/fastclick": "^1.0.28", "@types/fastclick": "^1.0.28",
"@types/history": "^4.6.1", "@types/history": "^4.6.1",
"@types/i18next": "^8.4.2", "@types/i18next": "^8.4.2",
"@types/jest": "23.0.2", "@types/jest": "23.1.0",
"@types/lodash": "4.14.109", "@types/lodash": "4.14.109",
"@types/markdown-it": "^0.0.4", "@types/markdown-it": "^0.0.4",
"@types/moxios": "^0.4.5", "@types/moxios": "^0.4.5",
"@types/node": "10.3.2", "@types/node": "10.3.3",
"@types/react": "16.3.14", "@types/react": "16.3.14",
"@types/react-color": "2.13.5", "@types/react-color": "2.13.5",
"@types/react-dom": "16.0.5", "@types/react-dom": "16.0.5",
@ -51,11 +51,11 @@
"css-loader": "0.28.11", "css-loader": "0.28.11",
"enzyme": "^3.1.0", "enzyme": "^3.1.0",
"enzyme-adapter-react-16": "^1.1.0", "enzyme-adapter-react-16": "^1.1.0",
"farmbot": "6.0.0-rc2", "farmbot": "6.0.1",
"farmbot-toastr": "^1.0.3", "farmbot-toastr": "^1.0.3",
"fastclick": "^1.0.6", "fastclick": "^1.0.6",
"file-loader": "1.1.11", "file-loader": "1.1.11",
"i18next": "11.3.2", "i18next": "11.3.3",
"imports-loader": "0.8.0", "imports-loader": "0.8.0",
"jest": "23.1.0", "jest": "23.1.0",
"json-loader": "0.5.7", "json-loader": "0.5.7",
@ -67,14 +67,14 @@
"node-sass": "4.9.0", "node-sass": "4.9.0",
"optimize-css-assets-webpack-plugin": "4.0.2", "optimize-css-assets-webpack-plugin": "4.0.2",
"raf": "^3.4.0", "raf": "^3.4.0",
"react": "16.4", "react": "16.4.1",
"react-addons-css-transition-group": "^15.6.2", "react-addons-css-transition-group": "^15.6.2",
"react-addons-test-utils": "^15.6.2", "react-addons-test-utils": "^15.6.2",
"react-color": "2.14.1", "react-color": "2.14.1",
"react-dom": "16.4", "react-dom": "16.4.1",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-router": "^3", "react-router": "^3",
"react-test-renderer": "16.4.0", "react-test-renderer": "16.4.1",
"react-transition-group": "^2.3.1", "react-transition-group": "^2.3.1",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-immutable-state-invariant": "^2.1.0", "redux-immutable-state-invariant": "^2.1.0",
@ -87,7 +87,7 @@
"ts-lint": "^4.5.1", "ts-lint": "^4.5.1",
"ts-loader": "^4.4.1", "ts-loader": "^4.4.1",
"tslint": "5.10.0", "tslint": "5.10.0",
"typescript": "2.9.1", "typescript": "2.9.2",
"url-loader": "1.0.1", "url-loader": "1.0.1",
"webpack": "4.12.0", "webpack": "4.12.0",
"webpack-uglify-js-plugin": "1.1.9", "webpack-uglify-js-plugin": "1.1.9",
@ -96,8 +96,8 @@
"yarn": "^1.6.0" "yarn": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
"jscpd": "0.6.19", "jscpd": "0.6.21",
"webpack-cli": "3.0.4", "webpack-cli": "3.0.8",
"webpack-notifier": "^1.5.0" "webpack-notifier": "^1.5.0"
}, },
"jest": { "jest": {

View File

@ -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

View File

@ -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

View File

@ -7,6 +7,7 @@ export const panelState = (): ControlPanelState => {
encoders_and_endstops: false, encoders_and_endstops: false,
danger_zone: false, danger_zone: false,
power_and_reset: false, power_and_reset: false,
pin_guard: false pin_guard: false,
diagnostic_dumps: false
}; };
}; };

View File

@ -10,6 +10,7 @@ export let bot: Everything["bot"] = {
"danger_zone": false, "danger_zone": false,
"power_and_reset": false, "power_and_reset": false,
"pin_guard": false, "pin_guard": false,
"diagnostic_dumps": false
}, },
"hardware": { "hardware": {
"gpio_registry": {}, "gpio_registry": {},

View File

@ -8,7 +8,8 @@ import {
TaggedSensor, TaggedSensor,
TaggedFirmwareConfig, TaggedFirmwareConfig,
TaggedPinBinding, TaggedPinBinding,
TaggedLog TaggedLog,
TaggedDiagnosticDump
} from "../../resources/tagged_resources"; } from "../../resources/tagged_resources";
import { ExecutableType } from "../../farm_designer/interfaces"; import { ExecutableType } from "../../farm_designer/interfaces";
import { fakeResource } from "../fake_resource"; 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 { export function fakePoint(): TaggedGenericPointer {
return fakeResource("Point", { return fakeResource("Point", {
id: idCounter++, id: idCounter++,

View File

@ -10,12 +10,22 @@ jest.mock("axios",
import { API } from "../../api"; import { API } from "../../api";
import { Content } from "../../constants"; import { Content } from "../../constants";
import { requestAccountExport } from "../request_account_export"; import { requestAccountExport, generateFilename } from "../request_account_export";
import { success } from "farmbot-toastr"; import { success } from "farmbot-toastr";
import axios from "axios"; import axios from "axios";
import { fakeDevice } from "../../__test_support__/resource_index_builder";
API.setBaseUrl("http://www.foo.bar"); 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", () => { describe("requestAccountExport", () => {
it("pops toast on completion (when API has email support)", async () => { it("pops toast on completion (when API has email support)", async () => {
await requestAccountExport(); await requestAccountExport();

View File

@ -8,13 +8,14 @@ import { DeviceAccountSettings } from "../devices/interfaces";
interface DataDumpExport { device?: DeviceAccountSettings; } interface DataDumpExport { device?: DeviceAccountSettings; }
type Response = AxiosResponse<DataDumpExport | undefined>; type Response = AxiosResponse<DataDumpExport | undefined>;
function generateFilename({ device }: DataDumpExport): string { export function generateFilename({ device }: DataDumpExport): string {
const name = (device && device.name + "_" + device.id) || "farmbot"; let name: string;
name = device ? (device.name + "_" + device.id) : "farmbot";
return `export_${name}.json`.toLowerCase(); return `export_${name}.json`.toLowerCase();
} }
// Thanks, @KOL - https://stackoverflow.com/a/19328891/1064917 // 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). // When email is not available on the API (self hosted).
// Will synchronously load backup over the wire (slow) // Will synchronously load backup over the wire (slow)
const a = document.createElement("a"); const a = document.createElement("a");
@ -24,7 +25,7 @@ function handleNow(data: DataDumpExport) {
blob = new Blob([json], { type: "octet/stream" }), blob = new Blob([json], { type: "octet/stream" }),
url = window.URL.createObjectURL(blob); url = window.URL.createObjectURL(blob);
a.href = url; a.href = url;
a.download = generateFilename(data); a.download = fname;
a.click(); a.click();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
return a; return a;
@ -32,7 +33,7 @@ function handleNow(data: DataDumpExport) {
const ok = (resp: Response) => { const ok = (resp: Response) => {
const { data } = resp; const { data } = resp;
return data ? handleNow(data) : success(t(Content.EXPORT_SENT)); return data ? jsonDownload(data) : success(t(Content.EXPORT_SENT));
}; };
export const requestAccountExport = export const requestAccountExport =

View File

@ -0,0 +1,29 @@
import { API } from "../api";
describe("API", () => {
type L = typeof location;
const fakeLocation = (input: Partial<L>) => 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]));
});
});

View File

@ -17,7 +17,7 @@ interface UrlInfo {
export class API { export class API {
/** Guesses the most appropriate API port based on a number of environment /** Guesses the most appropriate API port based on a number of environment
* factors such as hostname and protocol (HTTP vs. HTTPS). */ * 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 // ATTEMPT 1: Most devs running a webpack server on localhost
// run the API on port 3000. // run the API on port 3000.
@ -115,7 +115,7 @@ export class API {
/** /api/points/ */ /** /api/points/ */
get pointsPath() { return `${this.baseUrl}/api/points/`; } get pointsPath() { return `${this.baseUrl}/api/points/`; }
/** /api/points/search */ /** /api/points/search */
get pointSearchPath() { return `${this.pointsPath}/search/`; } get pointSearchPath() { return `${this.pointsPath}search`; }
/** Rather than returning ALL logs, returns a filtered subset. /** Rather than returning ALL logs, returns a filtered subset.
* /api/logs/search */ * /api/logs/search */
get filteredLogsPath() { return `${this.baseUrl}/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`; } get exportDataPath() { return `${this.baseUrl}/api/export_data`; }
/** /api/plant_templates/:id */ /** /api/plant_templates/:id */
get plantTemplatePath() { return `${this.baseUrl}/api/plant_templates`; } get plantTemplatePath() { return `${this.baseUrl}/api/plant_templates`; }
/** /api/diagnostic_dumps/:id */
get diagnosticDumpsPath() { return `${this.baseUrl}/api/diagnostic_dumps`; }
/** /api/farmware_installations/:id */ /** /api/farmware_installations/:id */
get farmwareInstallationPath() { get farmwareInstallationPath() {
return `${this.baseUrl}/api/farmware_installations`; return `${this.baseUrl}/api/farmware_installations`;

View File

@ -778,3 +778,21 @@ ul {
} }
} }
} }
a.panel-link {
// TODO: Might need to move this to a better place. CC: @gabrielBurnworth
//
// PROBLEM: <a> 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;
};
}

View File

@ -13,7 +13,8 @@ const mockDevice = {
home: jest.fn(() => { return Promise.resolve(); }), home: jest.fn(() => { return Promise.resolve(); }),
sync: jest.fn(() => { return Promise.resolve(); }), sync: jest.fn(() => { return Promise.resolve(); }),
readStatus: jest.fn(() => 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", () => ({ 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()", () => { describe("settingToggle()", () => {
beforeEach(function () { beforeEach(function () {
jest.clearAllMocks(); jest.clearAllMocks();

View File

@ -45,8 +45,12 @@ describe("botRedcuer", () => {
type: Actions.BULK_TOGGLE_CONTROL_PANEL, type: Actions.BULK_TOGGLE_CONTROL_PANEL,
payload: true 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", () => { it("fetches OS update info", () => {

View File

@ -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) { export let saveAccountChanges: Thunk = function (_dispatch, getState) {
return save(getDeviceAccountSettings(getState().resources.index)); return save(getDeviceAccountSettings(getState().resources.index));
}; };

View File

@ -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("<DiagnosticDumpRow/>", () => {
it("renders a single diagnostic dump", () => {
const dispatch = jest.fn();
const diag = fakeDiagnosticDump();
diag.body.ticket_identifier = "0000";
const el = mount(<DiagnosticDumpRow dispatch={dispatch} diag={diag} />);
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);
});
});

View File

@ -26,6 +26,7 @@ describe("<FarmbotOsSettings/>", () => {
const fakeProps = (): FarmbotOsProps => { const fakeProps = (): FarmbotOsProps => {
return { return {
account: fakeResource("Device", { id: 0, name: "", tz_offset_hrs: 0 }), account: fakeResource("Device", { id: 0, name: "", tz_offset_hrs: 0 }),
diagnostics: [],
dispatch: jest.fn(), dispatch: jest.fn(),
bot, bot,
botToMqttLastSeen: "", botToMqttLastSeen: "",

View File

@ -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("<SendDiagnosticReport/>", () => {
it("renders", () => {
const dispatch = jest.fn();
const shouldDisplay = jest.fn(() => true);
const fake = fakeDiagnosticDump();
const el = render(<SendDiagnosticReport
diagnostics={[fake]}
expanded={true}
dispatch={dispatch}
shouldDisplay={shouldDisplay} />);
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(<SendDiagnosticReport
diagnostics={[fake]}
expanded={true}
dispatch={dispatch}
shouldDisplay={shouldDisplay} />);
expect(el.text()).toEqual("");
expect(shouldDisplay).toHaveBeenCalled();
});
});

View File

@ -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<Props, {}> {
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 <Row>
<Col xs={1}>
<span>
<button
className="red fb-button del-button"
onClick={this.destroy}>
<i className="fa fa-times" />
</button>
</span>
</Col>
<Col xs={11}>
<a onClick={this.download} className="panel-link">
Download diagnostic report {this.ticket} (Saved {this.age})
</a>
</Col>
</Row >;
}
}

View File

@ -22,6 +22,8 @@ import { AutoUpdateRow } from "./fbos_settings/auto_update_row";
import { AutoSyncRow } from "./fbos_settings/auto_sync_row"; import { AutoSyncRow } from "./fbos_settings/auto_sync_row";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { PowerAndReset } from "./fbos_settings/power_and_reset"; import { PowerAndReset } from "./fbos_settings/power_and_reset";
import { SendDiagnosticReport } from "./send_diagnostic_report";
import axios from "axios"; import axios from "axios";
export enum ColWidth { export enum ColWidth {
@ -166,6 +168,11 @@ export class FarmbotOsSettings
sourceFbosConfig={sourceFbosConfig} sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay} shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline} /> botOnline={botOnline} />
<SendDiagnosticReport
diagnostics={this.props.diagnostics}
expanded={this.props.bot.controlPanelState.diagnostic_dumps}
shouldDisplay={this.props.shouldDisplay}
dispatch={this.props.dispatch} />
</MustBeOnline> </MustBeOnline>
</WidgetBody> </WidgetBody>
</form> </form>

View File

@ -15,7 +15,7 @@ export function PowerAndReset(props: PowerAndResetProps) {
return <section> return <section>
<div style={{ fontSize: "1px" }}> <div style={{ fontSize: "1px" }}>
<Header <Header
bool={power_and_reset} expanded={power_and_reset}
title={t("Power and Reset")} title={t("Power and Reset")}
name={"power_and_reset"} name={"power_and_reset"}
dispatch={dispatch} /> dispatch={dispatch} />

View File

@ -7,7 +7,7 @@ describe("<Header/>", () => {
const fn = jest.fn(); const fn = jest.fn();
const el = shallow(<Header const el = shallow(<Header
title="FOO" title="FOO"
bool={true} expanded={true}
name={"motors"} name={"motors"}
dispatch={fn} />); dispatch={fn} />);
expect(el.text()).toContain("FOO"); expect(el.text()).toContain("FOO");

View File

@ -13,7 +13,7 @@ export function DangerZone(props: DangerZoneProps) {
return <section> return <section>
<Header <Header
bool={danger_zone} expanded={danger_zone}
title={t("Danger Zone")} title={t("Danger Zone")}
name={"danger_zone"} name={"danger_zone"}
dispatch={dispatch} /> dispatch={dispatch} />

View File

@ -21,7 +21,7 @@ export function EncodersAndEndStops(props: EncodersProps) {
return <section> return <section>
<Header <Header
bool={encoders_and_endstops} expanded={encoders_and_endstops}
title={t("Encoders and Endstops")} title={t("Encoders and Endstops")}
name={"encoders_and_endstops"} name={"encoders_and_endstops"}
dispatch={dispatch} /> dispatch={dispatch} />

View File

@ -7,12 +7,12 @@ interface Props {
dispatch: Function; dispatch: Function;
name: keyof ControlPanelState; name: keyof ControlPanelState;
title: string; title: string;
bool: boolean; expanded: boolean;
} }
export let Header = (props: Props) => { export let Header = (props: Props) => {
const { dispatch, name, title, bool } = props; const { dispatch, name, title, expanded } = props;
const icon_string = bool ? "minus" : "plus"; const icon_string = expanded ? "minus" : "plus";
return <h4 onClick={() => dispatch(toggleControlPanel(name))}> return <h4 onClick={() => dispatch(toggleControlPanel(name))}>
{t(title)} {t(title)}
<span className="icon-toggle"> <span className="icon-toggle">

View File

@ -36,7 +36,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
title={t("Homing and Calibration")} title={t("Homing and Calibration")}
name={"homing_and_calibration"} name={"homing_and_calibration"}
dispatch={dispatch} dispatch={dispatch}
bool={homing_and_calibration} /> expanded={homing_and_calibration} />
<Collapse isOpen={!!homing_and_calibration}> <Collapse isOpen={!!homing_and_calibration}>
<HomingRow hardware={hardware} botDisconnected={botDisconnected} /> <HomingRow hardware={hardware} botDisconnected={botDisconnected} />
<CalibrationRow hardware={hardware} botDisconnected={botDisconnected} /> <CalibrationRow hardware={hardware} botDisconnected={botDisconnected} />

View File

@ -25,7 +25,7 @@ export function Motors(props: MotorsProps) {
return <section> return <section>
<Header <Header
bool={controlPanelState.motors} expanded={controlPanelState.motors}
title={t("Motors")} title={t("Motors")}
name={"motors"} name={"motors"}
dispatch={dispatch} /> dispatch={dispatch} />

View File

@ -15,7 +15,7 @@ export function PinGuard(props: PinGuardProps) {
return <section> return <section>
<Header <Header
bool={pin_guard} expanded={pin_guard}
title={t("Pin Guard")} title={t("Pin Guard")}
name={"pin_guard"} name={"pin_guard"}
dispatch={dispatch} /> dispatch={dispatch} />

View File

@ -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<DiagReportProps, {}>{
show = () => {
return <section>
<div style={{ fontSize: "1px" }}>
<Header
expanded={this.props.expanded}
title={t("Diagnostic Reports")}
name={Feature.diagnostic_dumps}
dispatch={this.props.dispatch} />
</div>
<Collapse isOpen={this.props.expanded}>
<Row>
<Col xs={ColWidth.label}>
<label>
{t("DIAGNOSTIC CHECK")}
</label>
</Col>
<Col xs={6}>
<p>...</p>
</Col>
<Col xs={3}>
<button
className="fb-button yellow"
onClick={requestDiagnostic}>
{t("Record Diagnostic")}
</button>
</Col>
</Row>
{this.props.diagnostics.map(d => {
return <DiagnosticDumpRow
key={d.uuid}
diag={d}
dispatch={this.props.dispatch} />;
})}
</Collapse>
</section>;
}
noShow = () => <div />;
render() {
const show = this.props.shouldDisplay(Feature.diagnostic_dumps);
return (show ? this.show : this.noShow)();
}
}

View File

@ -10,7 +10,7 @@ const SIX_HOURS = HOUR * 6;
const NOT_SEEN = t("No messages seen yet."); const NOT_SEEN = t("No messages seen yet.");
function ago(input: string) { export function ago(input: string) {
return moment(new Date(input)).fromNow(); return moment(new Date(input)).fromNow();
} }

View File

@ -13,6 +13,7 @@ import { Diagnosis, DiagnosisName } from "./connectivity/diagnosis";
import { StatusRowProps } from "./connectivity/connectivity_row"; import { StatusRowProps } from "./connectivity/connectivity_row";
import { resetConnectionInfo } from "./actions"; import { resetConnectionInfo } from "./actions";
import { PinBindings } from "./components/pin_bindings"; import { PinBindings } from "./components/pin_bindings";
import { selectAllDiagnosticDumps } from "../resources/selectors";
@connect(mapStateToProps) @connect(mapStateToProps)
export class Devices extends React.Component<Props, {}> { export class Devices extends React.Component<Props, {}> {
@ -58,6 +59,7 @@ export class Devices extends React.Component<Props, {}> {
<Row> <Row>
<Col xs={12} sm={6}> <Col xs={12} sm={6}>
<FarmbotOsSettings <FarmbotOsSettings
diagnostics={selectAllDiagnosticDumps(this.props.resources)}
account={this.props.deviceAccount} account={this.props.deviceAccount}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
bot={this.props.bot} bot={this.props.bot}

View File

@ -11,7 +11,8 @@ import {
TaggedImage, TaggedImage,
TaggedPeripheral, TaggedPeripheral,
TaggedDevice, TaggedDevice,
TaggedSensor TaggedSensor,
TaggedDiagnosticDump
} from "../resources/tagged_resources"; } from "../resources/tagged_resources";
import { ResourceIndex } from "../resources/interfaces"; import { ResourceIndex } from "../resources/interfaces";
import { TaggedUser } from "../resources/tagged_resources"; import { TaggedUser } from "../resources/tagged_resources";
@ -64,6 +65,7 @@ export enum Feature {
jest_feature = "jest_feature", // for tests jest_feature = "jest_feature", // for tests
backscheduled_regimens = "backscheduled_regimens", backscheduled_regimens = "backscheduled_regimens",
endstop_invert = "endstop_invert", endstop_invert = "endstop_invert",
diagnostic_dumps = "diagnostic_dumps"
} }
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */ /** Object fetched from FEATURE_MIN_VERSIONS_URL. */
export type MinOsFeatureLookup = Partial<Record<Feature, string>>; export type MinOsFeatureLookup = Partial<Record<Feature, string>>;
@ -148,6 +150,7 @@ export interface CalibrationButtonProps {
export interface FarmbotOsProps { export interface FarmbotOsProps {
bot: BotState; bot: BotState;
diagnostics: TaggedDiagnosticDump[];
account: TaggedDevice; account: TaggedDevice;
botToMqttStatus: NetworkState; botToMqttStatus: NetworkState;
botToMqttLastSeen: string; botToMqttLastSeen: string;
@ -221,4 +224,5 @@ export interface ControlPanelState {
danger_zone: boolean; danger_zone: boolean;
power_and_reset: boolean; power_and_reset: boolean;
pin_guard: boolean; pin_guard: boolean;
diagnostic_dumps: boolean;
} }

View File

@ -26,6 +26,7 @@ export let initialState = (): BotState => ({
danger_zone: false, danger_zone: false,
power_and_reset: false, power_and_reset: false,
pin_guard: false, pin_guard: false,
diagnostic_dumps: false
}, },
hardware: { hardware: {
gpio_registry: {}, gpio_registry: {},

View File

@ -79,7 +79,8 @@ export function emptyState(): RestResources {
DeviceConfig: [], DeviceConfig: [],
PinBinding: [], PinBinding: [],
PlantTemplate: [], PlantTemplate: [],
SavedGarden: [] SavedGarden: [],
DiagnosticDump: []
}, },
byKindAndId: {}, byKindAndId: {},
references: {} references: {}
@ -110,6 +111,7 @@ export let resourceReducer = generateReducer
switch (resource.kind) { switch (resource.kind) {
case "Crop": case "Crop":
case "Device": case "Device":
case "DiagnosticDump":
case "FarmEvent": case "FarmEvent":
case "FarmwareInstallation": case "FarmwareInstallation":
case "FbosConfig": case "FbosConfig":
@ -144,6 +146,7 @@ export let resourceReducer = generateReducer
switch (resource.kind) { switch (resource.kind) {
case "Crop": case "Crop":
case "Device": case "Device":
case "DiagnosticDump":
case "FarmEvent": case "FarmEvent":
case "FarmwareInstallation": case "FarmwareInstallation":
case "FbosConfig": case "FbosConfig":

View File

@ -20,6 +20,7 @@ import {
TaggedFirmwareConfig, TaggedFirmwareConfig,
TaggedToolSlotPointer, TaggedToolSlotPointer,
TaggedPinBinding, TaggedPinBinding,
TaggedDiagnosticDump,
} from "./tagged_resources"; } from "./tagged_resources";
import { sortResourcesById, betterCompact, bail } from "../util"; import { sortResourcesById, betterCompact, bail } from "../util";
import { error } from "farmbot-toastr"; import { error } from "farmbot-toastr";
@ -79,6 +80,8 @@ export const selectAllToolSlots = (i: ResourceIndex): TaggedToolSlotPointer[] =>
})); }));
}; };
export const selectAllDiagnosticDumps =
(i: ResourceIndex) => findAll<TaggedDiagnosticDump>(i, "DiagnosticDump");
export const selectAllRegimens = (i: ResourceIndex) => findAll<TaggedRegimen>(i, "Regimen"); export const selectAllRegimens = (i: ResourceIndex) => findAll<TaggedRegimen>(i, "Regimen");
export const selectAllSensors = (i: ResourceIndex) => findAll<TaggedSensor>(i, "Sensor"); export const selectAllSensors = (i: ResourceIndex) => findAll<TaggedSensor>(i, "Sensor");
export const selectAllPinBindings = export const selectAllPinBindings =

View File

@ -30,6 +30,7 @@ export type ResourceName =
| "Crop" | "Crop"
| "Device" | "Device"
| "DeviceConfig" | "DeviceConfig"
| "DiagnosticDump"
| "FarmEvent" | "FarmEvent"
| "FarmwareInstallation" | "FarmwareInstallation"
| "FbosConfig" | "FbosConfig"
@ -95,6 +96,7 @@ export interface Resource<T extends ResourceName, U extends object>
export type TaggedResource = export type TaggedResource =
| TaggedCrop | TaggedCrop
| TaggedDevice | TaggedDevice
| TaggedDiagnosticDump
| TaggedFarmEvent | TaggedFarmEvent
| TaggedFarmwareInstallation | TaggedFarmwareInstallation
| TaggedFbosConfig | TaggedFbosConfig
@ -132,6 +134,7 @@ export type TaggedSensorReading = Resource<"SensorReading", SensorReading>;
export type TaggedSensor = Resource<"Sensor", Sensor>; export type TaggedSensor = Resource<"Sensor", Sensor>;
export type TaggedSavedGarden = Resource<"SavedGarden", SavedGarden>; export type TaggedSavedGarden = Resource<"SavedGarden", SavedGarden>;
export type TaggedPlantTemplate = Resource<"PlantTemplate", PlantTemplate>; export type TaggedPlantTemplate = Resource<"PlantTemplate", PlantTemplate>;
export type TaggedDiagnosticDump = Resource<"DiagnosticDump", DiagnosticDump>;
type PointUnion = GenericPointer | PlantPointer | ToolSlotPointer; type PointUnion = GenericPointer | PlantPointer | ToolSlotPointer;
@ -147,6 +150,20 @@ export type TaggedWebcamFeed = Resource<"WebcamFeed", WebcamFeed>;
export type TaggedFarmwareInstallation = export type TaggedFarmwareInstallation =
Resource<"FarmwareInstallation", FarmwareInstallation>; 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. */ /** Spot check to be certain a TaggedResource is what it says it is. */
export function sanityCheck(x: object): x is TaggedResource { export function sanityCheck(x: object): x is TaggedResource {
if (isTaggedResource(x)) { if (isTaggedResource(x)) {

View File

@ -8,7 +8,7 @@ import { Peripheral } from "../controls/peripherals/interfaces";
import { FarmEvent, SavedGarden, PlantTemplate } from "../farm_designer/interfaces"; import { FarmEvent, SavedGarden, PlantTemplate } from "../farm_designer/interfaces";
import { Image } from "../farmware/images/interfaces"; import { Image } from "../farmware/images/interfaces";
import { DeviceAccountSettings } from "../devices/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 { User } from "../auth/interfaces";
import { WebcamFeed } from "../controls/interfaces"; import { WebcamFeed } from "../controls/interfaces";
import { WebAppConfig } from "../config_storage/web_app_configs"; import { WebAppConfig } from "../config_storage/web_app_configs";
@ -61,5 +61,5 @@ export function fetchSyncData(dispatch: Function) {
fetch<PinBinding[]>("PinBinding", API.current.pinBindingPath); fetch<PinBinding[]>("PinBinding", API.current.pinBindingPath);
fetch<SavedGarden[]>("SavedGarden", API.current.savedGardensPath); fetch<SavedGarden[]>("SavedGarden", API.current.savedGardensPath);
fetch<PlantTemplate[]>("PlantTemplate", API.current.plantTemplatePath); fetch<PlantTemplate[]>("PlantTemplate", API.current.plantTemplatePath);
fetch<DiagnosticDump[]>("DiagnosticDump", API.current.diagnosticDumpsPath);
} }

View File

@ -84,9 +84,9 @@
version "8.4.3" version "8.4.3"
resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-8.4.3.tgz#9136a9551bf5bf7169aa9f3125c1743f1f8dd6de" resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-8.4.3.tgz#9136a9551bf5bf7169aa9f3125c1743f1f8dd6de"
"@types/jest@23.0.2": "@types/jest@23.1.0":
version "23.0.2" version "23.1.0"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.0.2.tgz#f03f9e6dd2206cc2a2e8fd033161b0b7cf905db6" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.1.0.tgz#8054dd838ba23dc331794d26456b86c7e50bf0f6"
"@types/lodash@4.14.109": "@types/lodash@4.14.109":
version "4.14.109" version "4.14.109"
@ -106,9 +106,9 @@
version "10.1.2" version "10.1.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.2.tgz#1b928a0baa408fc8ae3ac012cc81375addc147c6" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.2.tgz#1b928a0baa408fc8ae3ac012cc81375addc147c6"
"@types/node@10.3.2": "@types/node@10.3.3":
version "10.3.2" version "10.3.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.2.tgz#3840ec6c12556fdda6e0e6d036df853101d732a4" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.3.tgz#8798d9e39af2fa604f715ee6a6b19796528e46c3"
"@types/react-color@2.13.5": "@types/react-color@2.13.5":
version "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" farmbot-toastr "^1.0.0"
typescript "^2.3.4" typescript "^2.3.4"
farmbot@6.0.0-rc2: farmbot@6.0.1:
version "6.0.0-rc2" version "6.0.1"
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-6.0.0-rc2.tgz#614e418de2264d8366ef1e8fd83121fc18f40356" resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-6.0.1.tgz#46629b90e942141a6bacf6008059afac9928764a"
dependencies: dependencies:
mqtt "2.15.0" mqtt "2.15.0"
@ -3090,9 +3090,9 @@ https-browserify@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
i18next@11.3.2: i18next@11.3.3:
version "11.3.2" version "11.3.3"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.3.2.tgz#4a1a7bb14383ba6aed4abca139b03681fc96e023" resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.3.3.tgz#a6ca3c2a93237c94e242bda7df3411588ac37ea1"
iconv-lite@0.4.19: iconv-lite@0.4.19:
version "0.4.19" version "0.4.19"
@ -4049,9 +4049,9 @@ jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
jscpd@0.6.19: jscpd@0.6.21:
version "0.6.19" version "0.6.21"
resolved "https://registry.yarnpkg.com/jscpd/-/jscpd-0.6.19.tgz#c2ef022db2e06c41768cc3b4fbd7ec97e7117e8c" resolved "https://registry.yarnpkg.com/jscpd/-/jscpd-0.6.21.tgz#bf4f0be95526146412738657aa96c9d03930fa4b"
dependencies: dependencies:
blamer "^0.1.9" blamer "^0.1.9"
bluebird "^3.0.5" bluebird "^3.0.5"
@ -5806,9 +5806,9 @@ react-day-picker@^7.0.7:
dependencies: dependencies:
prop-types "^15.6.1" prop-types "^15.6.1"
react-dom@16.4: react-dom@16.4.1:
version "16.4.0" version "16.4.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
dependencies: dependencies:
fbjs "^0.8.16" fbjs "^0.8.16"
loose-envify "^1.1.0" loose-envify "^1.1.0"
@ -5819,6 +5819,10 @@ react-is@^16.4.0:
version "16.4.0" version "16.4.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf" 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: react-popper@^0.8.2:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.8.3.tgz#0f73333137c9fb0af6ec4074d2d0585a0a0461e1" 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" prop-types "^15.5.6"
warning "^3.0.0" 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" version "16.4.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.0.tgz#0dbe0e24263e94e1830c7afb1f403707fad313a3" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.0.tgz#0dbe0e24263e94e1830c7afb1f403707fad313a3"
dependencies: dependencies:
@ -5885,9 +5898,9 @@ react-transition-group@^2.3.1:
loose-envify "^1.3.1" loose-envify "^1.3.1"
prop-types "^15.6.1" prop-types "^15.6.1"
react@16.4: react@16.4.1:
version "16.4.0" version "16.4.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585" resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
dependencies: dependencies:
fbjs "^0.8.16" fbjs "^0.8.16"
loose-envify "^1.1.0" loose-envify "^1.1.0"
@ -7074,9 +7087,9 @@ typedarray@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typescript@2.9.1: typescript@2.9.2:
version "2.9.1" version "2.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1.tgz#fdb19d2c67a15d11995fd15640e373e09ab09961" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
typescript@^2.0.9, typescript@^2.3.4: typescript@^2.0.9, typescript@^2.3.4:
version "2.8.3" version "2.8.3"
@ -7341,9 +7354,9 @@ webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
webpack-cli@3.0.4: webpack-cli@3.0.8:
version "3.0.4" version "3.0.8"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.0.4.tgz#55d6ad2cdd608de8c0f757dde5bc4bf5bd2dec68" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.0.8.tgz#90eddcf04a4bfc31aa8c0edc4c76785bc4f1ccd9"
dependencies: dependencies:
chalk "^2.4.1" chalk "^2.4.1"
cross-spawn "^6.0.5" cross-spawn "^6.0.5"