Merge pull request #1116 from FarmBot/staging
v7.2.1 - Happy Hibiscus Recovery Releasepull/1119/head
commit
73c35ed4f7
|
@ -2,12 +2,15 @@ class SendNervesHubInfoJob < ApplicationJob
|
|||
queue_as :default
|
||||
|
||||
def perform(device_id:, serial_number:, tags:)
|
||||
device = Device.find(device_id)
|
||||
DeviceSerialNumber.transaction do
|
||||
device.update_attributes!(serial_number: serial_number)
|
||||
resp_data = NervesHub.create_or_update(serial_number, tags)
|
||||
certs = NervesHub.sign_device(resp_data.fetch(:identifier))
|
||||
Transport.current.amqp_send(certs.to_json, device_id, "nerves_hub")
|
||||
end
|
||||
device = Device.find(device_id)
|
||||
resp_data = NervesHub.create_or_update(serial_number, tags)
|
||||
certs = NervesHub.sign_device(resp_data.fetch(:identifier))
|
||||
Transport.current.amqp_send(certs.to_json, device_id, "nerves_hub")
|
||||
rescue => error
|
||||
NervesHub.report_problem({ error: error,
|
||||
device_id: device_id,
|
||||
serial_number: serial_number,
|
||||
tags: tags, })
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
|
|
@ -298,4 +298,9 @@ private
|
|||
def self.current_ca_file
|
||||
@current_ca_file ||= (try_env_ca_file || try_file_ca_file || nil)
|
||||
end
|
||||
|
||||
WHOOPS = "🚨 NervesHub Anomaly Detected! 🚨"
|
||||
def self.report_problem(payload = {})
|
||||
Rollbar.error(WHOOPS, payload)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,7 +32,6 @@ class Device < ApplicationRecord
|
|||
has_many :diagnostic_dumps, dependent: :destroy
|
||||
has_many :fragments, dependent: :destroy
|
||||
has_one :fbos_config, dependent: :destroy
|
||||
has_one :device_serial_number, dependent: :destroy
|
||||
has_many :in_use_tools
|
||||
has_many :in_use_points
|
||||
has_many :users
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
class DeviceSerialNumber < ApplicationRecord
|
||||
belongs_to :device
|
||||
# DO NOT USE THIS TABLE. IT IS DEPRECATED. DESTROY FEB 2019.
|
||||
end
|
|
@ -13,7 +13,15 @@ class FbosConfig < ApplicationRecord
|
|||
|
||||
def sync_nerves
|
||||
serial = device.serial_number
|
||||
return unless serial
|
||||
unless serial
|
||||
# This feature can be removed in May '19
|
||||
# It is used to repair data damage on
|
||||
# production during the initial nervehub
|
||||
# deployment.
|
||||
problem = "Device #{device.id} missing serial"
|
||||
NervesHub.report_problem({ problem: problem })
|
||||
return
|
||||
end
|
||||
self.delay.push_changes_to_nerves_hub(serial, update_channel)
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module DeviceCerts
|
|||
SendNervesHubInfoJob.perform_later(device_id: device.id,
|
||||
serial_number: serial_number,
|
||||
tags: tags)
|
||||
return {}
|
||||
return device.update_attributes!(serial_number: serial_number) && device
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
class DeprecateDeviceSerialNumberTable < ActiveRecord::Migration[5.2]
|
||||
|
||||
unless Kernel.const_defined?("DeviceSerialNumber")
|
||||
# Shim so that legacy users don't crash when (up|down)grading
|
||||
class DeviceSerialNumber
|
||||
def self.preload(*x)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def change
|
||||
DeviceSerialNumber.preload(:devices) do |x|
|
||||
x.device
|
||||
.update_attributes!(serial_number: x.serial_number)
|
||||
x.device.update_attributes!(serial_number: x.serial_number)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class RemoveDeviceSerialNumberTable < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
drop_table :device_serial_numbers
|
||||
end
|
||||
end
|
|
@ -14,7 +14,6 @@ if Rails.env == "development"
|
|||
[
|
||||
Sensor,
|
||||
Peripheral,
|
||||
DeviceSerialNumber,
|
||||
Log,
|
||||
PinBinding,
|
||||
Point,
|
||||
|
|
|
@ -153,38 +153,6 @@ CREATE SEQUENCE public.delayed_jobs_id_seq
|
|||
ALTER SEQUENCE public.delayed_jobs_id_seq OWNED BY public.delayed_jobs.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: device_serial_numbers; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.device_serial_numbers (
|
||||
id bigint NOT NULL,
|
||||
device_id bigint,
|
||||
serial_number character varying(16) NOT NULL,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: device_serial_numbers_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.device_serial_numbers_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: device_serial_numbers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.device_serial_numbers_id_seq OWNED BY public.device_serial_numbers.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: devices; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -385,8 +353,7 @@ CREATE TABLE public.farmware_installations (
|
|||
url character varying,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL,
|
||||
package character varying(80),
|
||||
package_error character varying
|
||||
package character varying(80)
|
||||
);
|
||||
|
||||
|
||||
|
@ -1580,13 +1547,6 @@ ALTER TABLE ONLY public.arg_sets ALTER COLUMN id SET DEFAULT nextval('public.arg
|
|||
ALTER TABLE ONLY public.delayed_jobs ALTER COLUMN id SET DEFAULT nextval('public.delayed_jobs_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: device_serial_numbers id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.device_serial_numbers ALTER COLUMN id SET DEFAULT nextval('public.device_serial_numbers_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: devices id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -1850,14 +1810,6 @@ ALTER TABLE ONLY public.delayed_jobs
|
|||
ADD CONSTRAINT delayed_jobs_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: device_serial_numbers device_serial_numbers_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.device_serial_numbers
|
||||
ADD CONSTRAINT device_serial_numbers_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: devices devices_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2151,13 +2103,6 @@ CREATE INDEX index_arg_sets_on_fragment_id ON public.arg_sets USING btree (fragm
|
|||
CREATE INDEX index_arg_sets_on_node_id ON public.arg_sets USING btree (node_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_device_serial_numbers_on_device_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_device_serial_numbers_on_device_id ON public.device_serial_numbers USING btree (device_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_devices_on_mounted_tool_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2720,14 +2665,6 @@ ALTER TABLE ONLY public.edge_nodes
|
|||
ADD CONSTRAINT fk_rails_c86213fd78 FOREIGN KEY (sequence_id) REFERENCES public.sequences(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: device_serial_numbers fk_rails_d052988096; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.device_serial_numbers
|
||||
ADD CONSTRAINT fk_rails_d052988096 FOREIGN KEY (device_id) REFERENCES public.devices(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: points fk_rails_d6f3cdbe9a; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2878,6 +2815,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||
('20190103211708'),
|
||||
('20190103213956'),
|
||||
('20190108211419'),
|
||||
('20190209133811');
|
||||
('20190209133811'),
|
||||
('20190212215842');
|
||||
|
||||
|
||||
|
|
|
@ -873,6 +873,9 @@ ul {
|
|||
label {
|
||||
margin-top: 5px;
|
||||
}
|
||||
input {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.release-notes-button {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
z-index: 3;
|
||||
background: $black;
|
||||
opacity: 0.9;
|
||||
min-height: 3.25rem;
|
||||
.bp3-collapse {
|
||||
width: 100% !important;
|
||||
overflow-y: scroll !important;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
}
|
||||
|
||||
.widget-header {
|
||||
height: 3.3rem;
|
||||
min-height: 3.3rem;
|
||||
background: $dark_gray;
|
||||
letter-spacing: .05rem;
|
||||
padding: .75rem 1rem;
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
import { isString, isFunction } from "lodash";
|
||||
import { repeatOptions } from "../map_state_to_props_add_edit";
|
||||
import { SpecialStatus, VariableDeclaration } from "farmbot";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { success, error, warning } from "farmbot-toastr";
|
||||
import moment from "moment";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { history } from "../../../history";
|
||||
|
@ -336,8 +336,8 @@ describe("<FarmEventForm/>", () => {
|
|||
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
|
||||
const i = instance(p);
|
||||
await i.commitViewModel(moment("2017-06-22T05:00:00.000Z"));
|
||||
expect(error).toHaveBeenCalledWith(expect.stringContaining(
|
||||
"This Farm Event does not appear to have a valid run time"), "Warning");
|
||||
expect(warning).toHaveBeenCalledWith(expect.stringContaining(
|
||||
"Nothing to run."), "Warning");
|
||||
});
|
||||
|
||||
it("rejects start time: add with unsupported OS", () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import moment from "moment";
|
||||
import { t } from "i18next";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { success, error, warning } from "farmbot-toastr";
|
||||
import {
|
||||
TaggedFarmEvent, SpecialStatus, TaggedSequence, TaggedRegimen,
|
||||
VariableDeclaration
|
||||
|
@ -333,7 +333,7 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
|
||||
/** Use the next item run time to display toast messages and return to
|
||||
* the form if necessary. */
|
||||
nextRunTimeActions = (editFEPath: string, now = moment()) => {
|
||||
nextRunTimeActions = (now = moment()): boolean => {
|
||||
const nextRun = this.nextItemTime(this.props.farmEvent.body, now);
|
||||
if (nextRun) {
|
||||
const nextRunText = this.props.autoSyncEnabled
|
||||
|
@ -343,36 +343,36 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
you must first SYNC YOUR DEVICE. If you do not sync, the event will
|
||||
not run.`.replace(/\s+/g, " "), { timeFromNow: nextRun.from(now) });
|
||||
success(nextRunText);
|
||||
return true;
|
||||
} else {
|
||||
history.push(editFEPath);
|
||||
error(t(Content.INVALID_RUN_TIME), t("Warning"));
|
||||
warning(t("All items scheduled before the start time. Nothing to run."),
|
||||
t("Warning"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Once saved, if
|
||||
* - Regimen Farm Event:
|
||||
* * Return to calendar view.
|
||||
* * If scheduled for today, warn about the possibility of missing tasks.
|
||||
* * Display the start time difference from now and maybe prompt to sync.
|
||||
* * Return to calendar view if items exist to be run past the start time.
|
||||
* - Sequence Farm Event:
|
||||
* * Determine the time for the next item to be run.
|
||||
* * Return to calendar view only if more items exist to be run.
|
||||
* * Display the next item run time.
|
||||
* * If auto-sync is disabled, prompt the user to sync.
|
||||
* * Return to calendar view only if more items exist to be run.
|
||||
*/
|
||||
commitViewModel = (now = moment()) => {
|
||||
if (this.maybeRejectStartTime(this.updatedFarmEvent)) {
|
||||
return startTimeWarning();
|
||||
}
|
||||
this.dispatch(overwrite(this.props.farmEvent, this.updatedFarmEvent));
|
||||
const editFEPath = window.location.pathname;
|
||||
this.dispatch(save(this.props.farmEvent.uuid))
|
||||
.then(() => {
|
||||
this.setState({ specialStatusLocal: SpecialStatus.SAVED });
|
||||
history.push("/app/designer/farm_events");
|
||||
this.dispatch(maybeWarnAboutMissedTasks(this.props.farmEvent,
|
||||
() => alert(t(Content.REGIMEN_TODAY_SKIPPED_ITEM_RISK)), now));
|
||||
this.nextRunTimeActions(editFEPath, now);
|
||||
const itemsScheduled = this.nextRunTimeActions(now);
|
||||
if (itemsScheduled) { history.push("/app/designer/farm_events"); }
|
||||
})
|
||||
.catch(() => {
|
||||
error(t("Unable to save farm event."));
|
||||
|
|
|
@ -6,16 +6,16 @@ import * as React from "react";
|
|||
import { mount } from "enzyme";
|
||||
import {
|
||||
seqDropDown,
|
||||
initialValue,
|
||||
InnerIf,
|
||||
IfParams,
|
||||
IfBlockDropDownHandler,
|
||||
LHSOptions
|
||||
LHSOptions,
|
||||
ThenElseParams
|
||||
} from "../index";
|
||||
import {
|
||||
buildResourceIndex, FAKE_RESOURCES
|
||||
} from "../../../../__test_support__/resource_index_builder";
|
||||
import { Execute, If, TaggedSequence } from "farmbot";
|
||||
import { Execute, If, TaggedSequence, VariableDeclaration } from "farmbot";
|
||||
import { overwrite } from "../../../../api/crud";
|
||||
import {
|
||||
fakeSensor, fakePeripheral
|
||||
|
@ -59,13 +59,6 @@ describe("seqDropDown()", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("initialValue()", () => {
|
||||
it("returns dropdown initial value", () => {
|
||||
const item = initialValue(execute, fakeResourceIndex);
|
||||
expect(item).toEqual({ label: fakeName, value: fakeId });
|
||||
});
|
||||
});
|
||||
|
||||
describe("LHSOptions()", () => {
|
||||
it("returns positions and pins", () => {
|
||||
const s = fakeSensor();
|
||||
|
@ -106,8 +99,13 @@ describe("<InnerIf />", () => {
|
|||
});
|
||||
|
||||
describe("IfBlockDropDownHandler()", () => {
|
||||
const fakeThenElseProps = (thenElseKey: "_then" | "_else"): ThenElseParams => ({
|
||||
...fakeProps(),
|
||||
thenElseKey,
|
||||
});
|
||||
|
||||
it("onChange()", () => {
|
||||
const { onChange } = IfBlockDropDownHandler(fakeProps(), "_else");
|
||||
const { onChange } = IfBlockDropDownHandler(fakeThenElseProps("_else"));
|
||||
|
||||
onChange(expectedItem);
|
||||
expect(overwrite).toHaveBeenCalledWith(
|
||||
|
@ -135,16 +133,32 @@ describe("IfBlockDropDownHandler()", () => {
|
|||
});
|
||||
|
||||
it("selectedItem()", () => {
|
||||
const p = fakeProps();
|
||||
const p = fakeThenElseProps("_then");
|
||||
p.currentStep.args._then = execute;
|
||||
const { selectedItem } = IfBlockDropDownHandler(p, "_then");
|
||||
const { selectedItem } = IfBlockDropDownHandler(p);
|
||||
const item = selectedItem();
|
||||
expect(item).toEqual(expectedItem);
|
||||
});
|
||||
|
||||
it("selectedItem(): null", () => {
|
||||
const { selectedItem } = IfBlockDropDownHandler(fakeProps(), "_then");
|
||||
const { selectedItem } = IfBlockDropDownHandler(fakeThenElseProps("_then"));
|
||||
const item = selectedItem();
|
||||
expect(item).toEqual({ label: "None", value: "" });
|
||||
});
|
||||
|
||||
it("edits declarations", () => {
|
||||
const declaration: VariableDeclaration = {
|
||||
kind: "variable_declaration",
|
||||
args: {
|
||||
label: "label", data_value: {
|
||||
kind: "coordinate", args: { x: 0, y: 0, z: 0 }
|
||||
}
|
||||
}
|
||||
};
|
||||
const p = fakeThenElseProps("_then");
|
||||
const { assignVariable } =
|
||||
IfBlockDropDownHandler(p);
|
||||
assignVariable([])(declaration);
|
||||
expect(p.currentStep.args._then.body).toEqual([declaration]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as React from "react";
|
||||
import { Else } from "../else";
|
||||
import { ThenElse } from "../then_else";
|
||||
import { mount } from "enzyme";
|
||||
import { fakeSequence } from "../../../../__test_support__/fake_state/resources";
|
||||
import { If } from "farmbot/dist";
|
||||
import { IfParams } from "../index";
|
||||
import { ThenElseParams } from "../index";
|
||||
import { emptyState } from "../../../../resources/reducer";
|
||||
|
||||
describe("<Else/>", () => {
|
||||
function fakeProps(): IfParams {
|
||||
describe("<ThenElse/>", () => {
|
||||
function fakeProps(): ThenElseParams {
|
||||
const currentStep: If = {
|
||||
kind: "_if",
|
||||
args: {
|
||||
|
@ -25,11 +25,21 @@ describe("<Else/>", () => {
|
|||
index: 0,
|
||||
resources: emptyState().index,
|
||||
confirmStepDeletion: false,
|
||||
thenElseKey: "_then",
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<Else {...fakeProps()} />);
|
||||
it("renders 'then'", () => {
|
||||
const wrapper = mount(<ThenElse {...fakeProps()} />);
|
||||
["THEN", "Execute Sequence"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
expect(wrapper.find("button").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("renders 'else'", () => {
|
||||
const p = fakeProps();
|
||||
p.thenElseKey = "_else";
|
||||
const wrapper = mount(<ThenElse {...p} />);
|
||||
["ELSE", "Execute Sequence"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
expect(wrapper.find("button").length).toEqual(1);
|
|
@ -1,37 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { Then } from "../then";
|
||||
import { mount } from "enzyme";
|
||||
import { fakeSequence } from "../../../../__test_support__/fake_state/resources";
|
||||
import { If } from "farmbot/dist";
|
||||
import { IfParams } from "../index";
|
||||
import { emptyState } from "../../../../resources/reducer";
|
||||
|
||||
describe("<Then/>", () => {
|
||||
function fakeProps(): IfParams {
|
||||
const currentStep: If = {
|
||||
kind: "_if",
|
||||
args: {
|
||||
lhs: "pin0",
|
||||
op: "is",
|
||||
rhs: 0,
|
||||
_then: { kind: "nothing", args: {} },
|
||||
_else: { kind: "nothing", args: {} }
|
||||
}
|
||||
};
|
||||
return {
|
||||
currentSequence: fakeSequence(),
|
||||
currentStep,
|
||||
dispatch: jest.fn(),
|
||||
index: 0,
|
||||
resources: emptyState().index,
|
||||
confirmStepDeletion: false,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<Then {...fakeProps()} />);
|
||||
["THEN", "Execute Sequence"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
expect(wrapper.find("button").length).toEqual(1);
|
||||
});
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { IfParams, seqDropDown, IfBlockDropDownHandler } from "./index";
|
||||
import { t } from "i18next";
|
||||
import { Row, Col, FBSelect } from "../../../ui/index";
|
||||
|
||||
export function Else(props: IfParams) {
|
||||
const { onChange, selectedItem } = IfBlockDropDownHandler(props, "_else");
|
||||
return <Row>
|
||||
<Col xs={12} md={12}>
|
||||
<h4>{t("ELSE...")}</h4>
|
||||
</Col>
|
||||
<Col xs={12} md={12}>
|
||||
<label>{t("Execute Sequence")}</label>
|
||||
<FBSelect
|
||||
key={JSON.stringify(props.currentSequence)}
|
||||
allowEmpty={true}
|
||||
list={seqDropDown(props.resources)}
|
||||
placeholder="Sequence..."
|
||||
onChange={onChange}
|
||||
selectedItem={selectedItem()} />
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { DropDownItem, NULL_CHOICE } from "../../../ui/index";
|
||||
import { TaggedSequence } from "farmbot";
|
||||
import { TaggedSequence, VariableDeclaration } from "farmbot";
|
||||
import { If, Execute, Nothing } from "farmbot/dist";
|
||||
import { ResourceIndex } from "../../../resources/interfaces";
|
||||
import { selectAllSequences, findSequenceById } from "../../../resources/selectors";
|
||||
import { isRecursive } from "../index";
|
||||
import { If_ } from "./if";
|
||||
import { Then } from "./then";
|
||||
import { Else } from "./else";
|
||||
import { ThenElse } from "./then_else";
|
||||
import { defensiveClone } from "../../../util";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
import { ToolTips } from "../../../constants";
|
||||
|
@ -18,6 +17,7 @@ import {
|
|||
} from "../pin_and_peripheral_support";
|
||||
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
|
||||
import { isNumber, isString } from "lodash";
|
||||
import { addOrEditVarDeclaration } from "../../locals_list/declaration_support";
|
||||
|
||||
export interface IfParams {
|
||||
currentSequence: TaggedSequence;
|
||||
|
@ -29,6 +29,10 @@ export interface IfParams {
|
|||
confirmStepDeletion: boolean;
|
||||
}
|
||||
|
||||
export interface ThenElseParams extends IfParams {
|
||||
thenElseKey: "_then" | "_else";
|
||||
}
|
||||
|
||||
export type Operator = "lhs"
|
||||
| "op"
|
||||
| "rhs"
|
||||
|
@ -67,23 +71,6 @@ export function seqDropDown(i: ResourceIndex) {
|
|||
return results;
|
||||
}
|
||||
|
||||
export function initialValue(input: Execute | Nothing, index: ResourceIndex) {
|
||||
switch (input.kind) {
|
||||
case "execute":
|
||||
const id = input.args.sequence_id;
|
||||
const seq = findSequenceById(index, id).body;
|
||||
if (isNumber(seq.id)) {
|
||||
return { label: seq.name, value: seq.id };
|
||||
} else {
|
||||
throw new Error("Failed seq id type assertion.");
|
||||
}
|
||||
case "nothing":
|
||||
return { label: t("None"), value: 0 };
|
||||
default:
|
||||
throw new Error("Only _else or _then");
|
||||
}
|
||||
}
|
||||
|
||||
export function InnerIf(props: IfParams) {
|
||||
const {
|
||||
index,
|
||||
|
@ -103,17 +90,17 @@ export function InnerIf(props: IfParams) {
|
|||
dispatch={dispatch}
|
||||
index={index}
|
||||
confirmStepDeletion={confirmStepDeletion}>
|
||||
{recursive && (
|
||||
{recursive &&
|
||||
<span>
|
||||
<i className="fa fa-exclamation-triangle"></i>
|
||||
{t("Recursive condition.")}
|
||||
</span>
|
||||
)}
|
||||
}
|
||||
</StepHeader>
|
||||
<StepContent className={className}>
|
||||
<If_ {...props} />
|
||||
<Then {...props} />
|
||||
<Else {...props} />
|
||||
<ThenElse thenElseKey={"_then"} {...props} />
|
||||
<ThenElse thenElseKey={"_else"} {...props} />
|
||||
</StepContent>
|
||||
</StepWrapper>;
|
||||
}
|
||||
|
@ -121,13 +108,12 @@ export function InnerIf(props: IfParams) {
|
|||
/** Creates a function that can be used in the `onChange` event of a _else or
|
||||
* _then block in the sequence editor.
|
||||
*/
|
||||
export let IfBlockDropDownHandler = (props: IfParams,
|
||||
key: "_else" | "_then") => {
|
||||
export let IfBlockDropDownHandler = (props: ThenElseParams) => {
|
||||
|
||||
const { dispatch, index } = props;
|
||||
const { dispatch, index, thenElseKey } = props;
|
||||
const step = props.currentStep;
|
||||
const sequence = props.currentSequence;
|
||||
const block = step.args[key];
|
||||
const block = step.args[thenElseKey];
|
||||
const selectedItem = () => {
|
||||
if (block.kind === "nothing") {
|
||||
return NULL_CHOICE;
|
||||
|
@ -145,7 +131,7 @@ export let IfBlockDropDownHandler = (props: IfParams,
|
|||
function overwriteStep(input: Execute | Nothing) {
|
||||
const update = defensiveClone(step);
|
||||
const nextSequence = defensiveClone(sequence).body;
|
||||
update.args[key] = input;
|
||||
update.args[thenElseKey] = input;
|
||||
(nextSequence.body || [])[index] = update;
|
||||
dispatch(overwrite(sequence, nextSequence));
|
||||
}
|
||||
|
@ -159,5 +145,18 @@ export let IfBlockDropDownHandler = (props: IfParams,
|
|||
}
|
||||
}
|
||||
|
||||
return { onChange, selectedItem };
|
||||
const sequenceId = selectedItem().value;
|
||||
const calleeUuid = sequenceId ?
|
||||
findSequenceById(props.resources, sequenceId).uuid : undefined;
|
||||
const calledSequenceVariableData = calleeUuid ?
|
||||
props.resources.sequenceMetas[calleeUuid] : undefined;
|
||||
|
||||
/** Replaces the execute step body with a new array of declarations. */
|
||||
const assignVariable = (declarations: VariableDeclaration[]) =>
|
||||
(variable: VariableDeclaration) => {
|
||||
block.body = addOrEditVarDeclaration(declarations, variable);
|
||||
overwriteStep(block);
|
||||
};
|
||||
|
||||
return { onChange, selectedItem, calledSequenceVariableData, assignVariable };
|
||||
};
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { IfParams, seqDropDown, IfBlockDropDownHandler } from "./index";
|
||||
import { t } from "i18next";
|
||||
import { Row, Col, FBSelect } from "../../../ui/index";
|
||||
|
||||
export function Then(props: IfParams) {
|
||||
const { onChange, selectedItem } = IfBlockDropDownHandler(props, "_then");
|
||||
return <Row>
|
||||
<Col xs={12} md={12}>
|
||||
<h4>{t("THEN...")}</h4>
|
||||
</Col>
|
||||
<Col xs={12} md={12}>
|
||||
<label>{t("Execute Sequence")}</label>
|
||||
<FBSelect
|
||||
key={JSON.stringify(props.currentSequence)}
|
||||
allowEmpty={true}
|
||||
list={seqDropDown(props.resources)}
|
||||
placeholder="Sequence..."
|
||||
onChange={onChange}
|
||||
selectedItem={selectedItem()} />
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import * as React from "react";
|
||||
import { ThenElseParams, seqDropDown, IfBlockDropDownHandler } from "./index";
|
||||
import { t } from "i18next";
|
||||
import { Row, Col, FBSelect } from "../../../ui";
|
||||
import { LocalsList } from "../../locals_list/locals_list";
|
||||
import { AllowedDeclaration } from "../../locals_list/locals_list_support";
|
||||
|
||||
export function ThenElse(props: ThenElseParams) {
|
||||
const {
|
||||
onChange, selectedItem, calledSequenceVariableData, assignVariable
|
||||
} = IfBlockDropDownHandler(props);
|
||||
const { body } = props.currentStep.args[props.thenElseKey];
|
||||
return <Row>
|
||||
<Col xs={12} md={12}>
|
||||
<h4>{props.thenElseKey === "_then" ? t("THEN...") : t("ELSE...")}</h4>
|
||||
</Col>
|
||||
<Col xs={12} md={12}>
|
||||
<label>{t("Execute Sequence")}</label>
|
||||
<FBSelect
|
||||
key={JSON.stringify(props.currentSequence)}
|
||||
allowEmpty={true}
|
||||
list={seqDropDown(props.resources)}
|
||||
placeholder="Sequence..."
|
||||
onChange={onChange}
|
||||
selectedItem={selectedItem()} />
|
||||
{!!calledSequenceVariableData &&
|
||||
<Col xs={12}>
|
||||
<LocalsList
|
||||
declarations={body}
|
||||
variableData={calledSequenceVariableData}
|
||||
sequenceUuid={props.currentSequence.uuid}
|
||||
resources={props.resources}
|
||||
onChange={assignVariable(body || [])}
|
||||
locationDropdownKey={JSON.stringify(props.currentSequence)}
|
||||
allowedDeclarations={AllowedDeclaration.identifier}
|
||||
shouldDisplay={props.shouldDisplay || (() => false)} />
|
||||
</Col>}
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
|
@ -34,7 +34,6 @@ describe Api::DeviceCertsController do
|
|||
post_args = ["/orgs/farmbot/devices/456/certificates/sign",
|
||||
post_data,
|
||||
{"Content-Type"=>"application/json"}]
|
||||
old_count = DeviceSerialNumber.count
|
||||
|
||||
# Setup wiring ===========================================================
|
||||
NervesHub.set_conn(conn)
|
||||
|
@ -57,8 +56,8 @@ describe Api::DeviceCertsController do
|
|||
post :create, body: payl.to_json, params: {format: :json}
|
||||
end
|
||||
expect(response.status).to eq(200)
|
||||
expect(json).to eq({})
|
||||
expect(DeviceSerialNumber.count).to eq(old_count)
|
||||
expect(json).to be_kind_of(Hash)
|
||||
expect(json.fetch(:id)).to eq(device.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
FactoryBot.define do
|
||||
factory :device_serial_number do
|
||||
device { nil }
|
||||
serial_number { raise "Stop using this model" }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe SendNervesHubInfoJob do
|
||||
let(:device) { FactoryBot.create(:device) }
|
||||
|
||||
it "handles failure" do
|
||||
params = { device_id: device.id,
|
||||
serial_number: "xyz",
|
||||
tags: [],
|
||||
error: StandardError.new("Hello!"), }
|
||||
not_work = \
|
||||
receive(:create_or_update).with(any_args).and_raise(params.fetch(:error))
|
||||
expect(NervesHub).to not_work
|
||||
old_logger = ActiveJob::Base.logger
|
||||
ActiveJob::Base.logger = Logger.new(nil)
|
||||
expect do
|
||||
SendNervesHubInfoJob.perform_now(**params.except(:error))
|
||||
end.to raise_error(params.fetch(:error))
|
||||
ActiveJob::Base.logger = old_logger
|
||||
end
|
||||
end
|
|
@ -1,17 +1,31 @@
|
|||
require 'spec_helper'
|
||||
require "spec_helper"
|
||||
|
||||
describe FbosConfig do
|
||||
let(:device) { FactoryBot.create(:device) }
|
||||
let(:config) { FbosConfig.create!(device: device) }
|
||||
let(:device) { FactoryBot.create(:device) }
|
||||
let(:config) { FbosConfig.create!(device: device) }
|
||||
|
||||
it 'triggers callbacks' do
|
||||
conn = double("Create a cert", :ca_file= => nil,
|
||||
:cert_store => nil,
|
||||
:cert_store= => nil,
|
||||
:use_ssl => nil,
|
||||
:use_ssl= => nil,
|
||||
:cert= => nil,
|
||||
:key= => nil)
|
||||
def fake_conn(desc)
|
||||
double(desc, :ca_file= => nil,
|
||||
:cert_store => nil,
|
||||
:cert_store= => nil,
|
||||
:use_ssl => nil,
|
||||
:use_ssl= => nil,
|
||||
:cert= => nil,
|
||||
:key= => nil)
|
||||
end
|
||||
|
||||
it "notifies us of broken production data" do
|
||||
# Remove this test by May 2019.
|
||||
config.device.update_attributes!(serial_number: nil)
|
||||
conn = fake_conn("Report broke data")
|
||||
NervesHub.set_conn(conn)
|
||||
problem = "Device #{device.id} missing serial"
|
||||
expect(NervesHub).to receive(:report_problem).with({ problem: problem })
|
||||
config.sync_nerves
|
||||
end
|
||||
|
||||
it "triggers callbacks" do
|
||||
conn = fake_conn("Create a cert")
|
||||
NervesHub.set_conn(conn)
|
||||
url = "/orgs/farmbot/devices/#{device.serial_number}"
|
||||
resp = StubResp.new("200", { "data" => { "tags": [] } }.to_json)
|
||||
|
|
|
@ -33,7 +33,7 @@ describe DeviceCerts::Create do
|
|||
result = DeviceCerts::Create.run!(tags: tags,
|
||||
device: device,
|
||||
serial_number: ser)
|
||||
expect(result).to eq({})
|
||||
expect(result).to eq(device)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue