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
node_js:
- 8.9.4
- 8.11.3
cache:
yarn: true
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_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)

View File

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

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[0]}@#{y[3]}"}
.sort
.reject { |x| x.include?("router") }
# puts "Making sure that type checks pass WITHOUT any upgrades"
tc_ok = type_check

View File

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

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.
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"

View File

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

View File

@ -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": {

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,
danger_zone: 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,
"power_and_reset": false,
"pin_guard": false,
"diagnostic_dumps": false
},
"hardware": {
"gpio_registry": {},

View File

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

View File

@ -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();

View File

@ -8,13 +8,14 @@ import { DeviceAccountSettings } from "../devices/interfaces";
interface DataDumpExport { device?: DeviceAccountSettings; }
type Response = AxiosResponse<DataDumpExport | undefined>;
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 =

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 {
/** 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`;

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(); }),
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();

View File

@ -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", () => {

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) {
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 => {
return {
account: fakeResource("Device", { id: 0, name: "", tz_offset_hrs: 0 }),
diagnostics: [],
dispatch: jest.fn(),
bot,
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 { 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} />
<SendDiagnosticReport
diagnostics={this.props.diagnostics}
expanded={this.props.bot.controlPanelState.diagnostic_dumps}
shouldDisplay={this.props.shouldDisplay}
dispatch={this.props.dispatch} />
</MustBeOnline>
</WidgetBody>
</form>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <h4 onClick={() => dispatch(toggleControlPanel(name))}>
{t(title)}
<span className="icon-toggle">

View File

@ -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} />
<Collapse isOpen={!!homing_and_calibration}>
<HomingRow hardware={hardware} botDisconnected={botDisconnected} />
<CalibrationRow hardware={hardware} botDisconnected={botDisconnected} />

View File

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

View File

@ -15,7 +15,7 @@ export function PinGuard(props: PinGuardProps) {
return <section>
<Header
bool={pin_guard}
expanded={pin_guard}
title={t("Pin Guard")}
name={"pin_guard"}
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.");
function ago(input: string) {
export function ago(input: string) {
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 { resetConnectionInfo } from "./actions";
import { PinBindings } from "./components/pin_bindings";
import { selectAllDiagnosticDumps } from "../resources/selectors";
@connect(mapStateToProps)
export class Devices extends React.Component<Props, {}> {
@ -58,6 +59,7 @@ export class Devices extends React.Component<Props, {}> {
<Row>
<Col xs={12} sm={6}>
<FarmbotOsSettings
diagnostics={selectAllDiagnosticDumps(this.props.resources)}
account={this.props.deviceAccount}
dispatch={this.props.dispatch}
bot={this.props.bot}

View File

@ -11,7 +11,8 @@ import {
TaggedImage,
TaggedPeripheral,
TaggedDevice,
TaggedSensor
TaggedSensor,
TaggedDiagnosticDump
} from "../resources/tagged_resources";
import { ResourceIndex } from "../resources/interfaces";
import { TaggedUser } from "../resources/tagged_resources";
@ -64,6 +65,7 @@ export enum Feature {
jest_feature = "jest_feature", // for tests
backscheduled_regimens = "backscheduled_regimens",
endstop_invert = "endstop_invert",
diagnostic_dumps = "diagnostic_dumps"
}
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
export type MinOsFeatureLookup = Partial<Record<Feature, string>>;
@ -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;
}

View File

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

View File

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

View File

@ -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<TaggedDiagnosticDump>(i, "DiagnosticDump");
export const selectAllRegimens = (i: ResourceIndex) => findAll<TaggedRegimen>(i, "Regimen");
export const selectAllSensors = (i: ResourceIndex) => findAll<TaggedSensor>(i, "Sensor");
export const selectAllPinBindings =

View File

@ -30,6 +30,7 @@ export type ResourceName =
| "Crop"
| "Device"
| "DeviceConfig"
| "DiagnosticDump"
| "FarmEvent"
| "FarmwareInstallation"
| "FbosConfig"
@ -95,6 +96,7 @@ export interface Resource<T extends ResourceName, U extends object>
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)) {

View File

@ -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[]>("PinBinding", API.current.pinBindingPath);
fetch<SavedGarden[]>("SavedGarden", API.current.savedGardensPath);
fetch<PlantTemplate[]>("PlantTemplate", API.current.plantTemplatePath);
fetch<DiagnosticDump[]>("DiagnosticDump", API.current.diagnosticDumpsPath);
}

View File

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