Merge branch 'staging' into cosmetic-fixes

pull/1505/head
Rick Carlino 2019-10-22 08:25:18 -05:00 committed by GitHub
commit 29ecee243c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 129 additions and 90 deletions

View File

@ -1,12 +1,10 @@
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
@ -15,7 +13,7 @@ module Api
render json: ""
end
private
private
def diagnostic_dumps
current_device.diagnostic_dumps

View File

@ -94,9 +94,6 @@ class DashboardController < ApplicationController
rescue
report = { problem: "Crashed while parsing report" }
end
# We get too many CSP reports.
# Rollbar.info("CSP Violation", report)
render json: report
end

View File

@ -89,6 +89,7 @@ class ApplicationRecord < ActiveRecord::Base
current_device.id,
chan_name,
Time.now.utc.to_i) if current_device
self
end
def manually_sync!

View File

@ -45,11 +45,9 @@ class User < ApplicationRecord
end
def self.refresh_everyones_ui
Rollbar.error("Global UI refresh triggered")
msg = {
"type" => "reload",
"commit" => (ENV["HEROKU_SLUG_COMMIT"] || "NONE").first(8)
"commit" => (ENV["HEROKU_SLUG_COMMIT"] || "NONE").first(8),
}
Transport

View File

@ -1,6 +1,7 @@
module PointGroups
class Update < Mutations::Command
include PointGroups::Helpers
BLACKLISTED_FIELDS = [:device, :point_ids, :point_group]
required do
model :device, class: Device
@ -19,11 +20,13 @@ module PointGroups
end
def execute
PointGroup.transaction do
PointGroup.auto_sync_debounce do
PointGroup.transaction do
maybe_reconcile_points
point_group.update_attributes!(update_attributes)
point_group.reload
PointGroupItem.transaction do
maybe_reconcile_points
point_group.update_attributes!(update_attributes)
point_group.reload # <= Because PointGroupItem caching?
end
end
end
end
@ -31,9 +34,7 @@ module PointGroups
private
def update_attributes
@update_attributes ||= inputs
.except(:device, :point_ids, :point_group)
.merge(updated_at: Time.now)
@update_attributes ||= inputs.except(*BLACKLISTED_FIELDS)
end
def maybe_reconcile_points

View File

@ -63,15 +63,25 @@ if Rails.env == "development"
z: rand(1...300) })
end
VEGGIES.shuffle.first(PLANT_COUNT).each do |veggie|
Plant.create(device: u.device,
x: rand(40...1500),
y: rand(40...800),
radius: rand(30...60),
name: veggie,
openfarm_slug: veggie.downcase.gsub(" ", "-"))
all_of_em = []
1.upto(8) do |n1|
1.upto(8) do |n2|
veggie = VEGGIES.sample
p = Plant.create(device: u.device,
x: n1 * 80,
y: n2 * 80,
radius: rand(20...70),
name: veggie,
openfarm_slug: veggie.downcase.gsub(" ", "-"))
all_of_em.push(p.id)
end
end
PointGroups::Create.run!(device: u.device,
name: "TEST GROUP I",
point_ids: all_of_em.sample(8),
sort_type: "random")
Device.all.map { |device| SavedGardens::Snapshot.run!(device: device) }
POINT_COUNT.times do

View File

@ -77,6 +77,16 @@ export class FarmbotOsSettings
const { bot, sourceFbosConfig, botToMqttStatus } = this.props;
const { sync_status } = bot.hardware.informational_settings;
const botOnline = isBotOnline(sync_status, botToMqttStatus);
const bootRow = <Row>
<Col xs={ColWidth.label}>
<label>
{t("BOOT SEQUENCE")}
</label>
</Col>
<Col xs={7}>
<BootSequenceSelector />
</Col>
</Row>;
return <Widget className="device-widget">
<form onSubmit={(e) => e.preventDefault()}>
<WidgetHeader title="Device">
@ -150,16 +160,7 @@ export class FarmbotOsSettings
shouldDisplay={this.props.shouldDisplay}
timeSettings={this.props.timeSettings}
sourceFbosConfig={sourceFbosConfig} />
<Row>
<Col xs={ColWidth.label}>
<label>
{t("BOOT SEQUENCE")}
</label>
</Col>
<Col xs={7}>
{this.props.shouldDisplay(Feature.boot_sequence) && <BootSequenceSelector />}
</Col>
</Row>
{this.props.shouldDisplay(Feature.boot_sequence) && bootRow}
<PowerAndReset
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}

View File

@ -71,4 +71,14 @@ describe("<GroupDetailActive/>", () => {
},
{ sort_type: "random" });
});
it("unmounts", () => {
window.clearInterval = jest.fn();
const p = fakeProps();
const el = new GroupDetailActive(p);
// tslint:disable-next-line:no-any
el.state.timerId = 123 as any;
el.componentWillUnmount && el.componentWillUnmount();
expect(clearInterval).toHaveBeenCalledWith(123);
});
});

View File

@ -9,8 +9,6 @@ import {
import { TaggedPointGroup } from "farmbot";
import { DeleteButton } from "../../controls/pin_form_fields";
import { save, edit } from "../../api/crud";
import { Dictionary } from "lodash";
import { OFIcon } from "../../open_farm/cached_crop";
import { TaggedPlant } from "../map/interfaces";
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
@ -22,7 +20,7 @@ interface GroupDetailActiveProps {
plants: TaggedPlant[];
}
type State = Dictionary<OFIcon | undefined>;
type State = { timerId?: ReturnType<typeof setInterval> };
export class GroupDetailActive
extends React.Component<GroupDetailActiveProps, State> {
@ -61,6 +59,16 @@ export class GroupDetailActive
dispatch(edit(group, { sort_type }));
}
componentDidMount() {
// There are better ways to do this.
this.setState({ timerId: setInterval(this.saveGroup, 900) });
}
componentWillUnmount() {
const { timerId } = this.state;
(typeof timerId == "number") && clearInterval(timerId);
}
render() {
return <DesignerPanel panelName={"groups"} panelColor={"blue"}>
<DesignerPanelHeader

View File

@ -5,7 +5,7 @@ import { AddButton } from "../add_button";
describe("<AddButton />", () => {
it("renders an add button when active", () => {
const props: AddButtonProps = { active: true, click: jest.fn() };
const props: AddButtonProps = { active: true, onClick: jest.fn() };
const wrapper = mount(<AddButton {...props} />);
const button = wrapper.find("button");
["green", "add"].map(klass => {
@ -13,11 +13,11 @@ describe("<AddButton />", () => {
});
expect(wrapper.find("i").hasClass("fa-plus")).toBeTruthy();
button.simulate("click");
expect(props.click).toHaveBeenCalled();
expect(props.onClick).toHaveBeenCalled();
});
it("renders a <div> when inactive", () => {
const props: AddButtonProps = { active: false, click: jest.fn() };
const props: AddButtonProps = { active: false, onClick: jest.fn() };
const wrapper = mount(<AddButton {...props} />);
expect(wrapper.html()).toEqual("<div></div>");
});

View File

@ -7,6 +7,7 @@ import {
} from "../../../__test_support__/resource_index_builder";
import { Actions } from "../../../constants";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { AddButton } from "../add_button";
describe("<BulkScheduler />", () => {
const weeks = [{
@ -102,4 +103,12 @@ describe("<BulkScheduler />", () => {
expect(change).toThrowError("WARNING: Not a sequence UUID.");
expect(p.dispatch).not.toHaveBeenCalled();
});
it("commits bulk editor", () => {
const p = fakeProps();
p.dispatch = jest.fn();
const panel = shallow<BulkScheduler>(<BulkScheduler {...p} />);
panel.find(AddButton).first().simulate("click");
expect(p.dispatch).toHaveBeenCalledWith(expect.any(Function));
});
});

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { AddButtonProps } from "./interfaces";
export function AddButton({ active, click }: AddButtonProps) {
export function AddButton({ active, onClick: click }: AddButtonProps) {
if (!active) { return <div />; }
return <button
className="fb-button green add"

View File

@ -70,7 +70,7 @@ export class BulkScheduler extends React.Component<BulkEditorProps, {}> {
return <div className="bulk-scheduler-content">
<AddButton
active={active}
click={() => dispatch(commitBulkEditor())} />
onClick={() => dispatch(commitBulkEditor())} />
<Row>
<this.SequenceSelectBox />
<this.TimeSelection />

View File

@ -46,7 +46,7 @@ export interface ToggleDayParams {
export interface AddButtonProps {
active: boolean;
click: React.EventHandler<React.FormEvent<{}>>;
onClick: React.EventHandler<React.FormEvent<{}>>;
}
export interface SequenceListProps {

View File

@ -39,6 +39,7 @@ describe("<LocalsList/>", () => {
onChange: jest.fn(),
shouldDisplay: jest.fn(),
allowedVariableNodes: AllowedVariableNodes.parameter,
customFilterRule: undefined
};
};

View File

@ -34,6 +34,7 @@ describe("<LocationForm/>", () => {
onChange: jest.fn(),
shouldDisplay: jest.fn(),
allowedVariableNodes: AllowedVariableNodes.parameter,
customFilterRule: undefined
});
it("renders correct UI components", () => {
@ -44,7 +45,7 @@ describe("<LocationForm/>", () => {
expect(selects.length).toBe(1);
const select = selects.first().props();
const choices = locationFormList(p.resources, [PARENT("")], true);
const choices = locationFormList(p.resources, [PARENT("")]);
const actualLabels = select.list.map(x => x.label).sort();
const expectedLabels = choices.map(x => x.label).sort();
const diff = difference(actualLabels, expectedLabels);
@ -116,7 +117,6 @@ describe("<LocationForm/>", () => {
it("shows groups in dropdown", () => {
const p = fakeProps();
p.shouldDisplay = () => true;
p.hideGroups = false;
const wrapper = shallow(<LocationForm {...p} />);
expect(wrapper.find(FBSelect).first().props().list).toContainEqual({
headingId: "Coordinate",

View File

@ -6,7 +6,7 @@ import { LocationForm } from "./location_form";
import {
SequenceMeta, determineVector, determineDropdown
} from "../../resources/sequence_meta";
import { Help } from "../../ui";
import { Help, DropDownItem } from "../../ui";
import { ToolTips } from "../../constants";
import { t } from "../../i18next_wrapper";
import { Position } from "@blueprintjs/core";
@ -17,6 +17,9 @@ export interface DefaultValueFormProps {
onChange: (v: ParameterDeclaration) => void;
}
export const NO_GROUPS =
(d: DropDownItem) => (d.headingId != "PointGroup");
export const DefaultValueForm = (props: DefaultValueFormProps) => {
if (props.variableNode.kind === "parameter_declaration") {
return <div className="default-value-form">
@ -32,8 +35,8 @@ export const DefaultValueForm = (props: DefaultValueFormProps) => {
shouldDisplay={() => true}
allowedVariableNodes={AllowedVariableNodes.variable}
hideTypeLabel={true}
hideGroups={true}
onChange={change(props.onChange, props.variableNode)} />
onChange={change(props.onChange, props.variableNode)}
customFilterRule={NO_GROUPS} />
</div>;
} else {
return <div />;

View File

@ -46,21 +46,21 @@ export const LocalsList = (props: LocalsListProps) => {
// Show default values for parameters as a fallback if not in Sequence header
.map(v => v && props.bodyVariables && isParameterDeclaration(v.celeryNode)
? convertFormVariable(v, props.resources) : v))
.map(variable =>
<LocationForm
key={variable.celeryNode.args.label}
locationDropdownKey={props.locationDropdownKey}
bodyVariables={props.bodyVariables}
variable={variable}
sequenceUuid={props.sequenceUuid}
resources={props.resources}
shouldDisplay={props.shouldDisplay}
hideVariableLabel={Object.values(props.variableData || {}).length < 2}
allowedVariableNodes={props.allowedVariableNodes}
collapsible={props.collapsible}
collapsed={props.collapsed}
toggleVarShow={props.toggleVarShow}
onChange={props.onChange} />)}
.map(variable => <LocationForm
key={variable.celeryNode.args.label}
locationDropdownKey={props.locationDropdownKey}
bodyVariables={props.bodyVariables}
variable={variable}
sequenceUuid={props.sequenceUuid}
resources={props.resources}
shouldDisplay={props.shouldDisplay}
hideVariableLabel={Object.values(props.variableData || {}).length < 2}
allowedVariableNodes={props.allowedVariableNodes}
collapsible={props.collapsible}
collapsed={props.collapsed}
toggleVarShow={props.toggleVarShow}
onChange={props.onChange}
customFilterRule={props.customFilterRule} />)}
</div>;
};

View File

@ -8,6 +8,7 @@ import {
} from "../../resources/interfaces";
import { SequenceMeta } from "../../resources/sequence_meta";
import { ShouldDisplay } from "../../devices/interfaces";
import { DropDownItem } from "../../ui";
export type VariableNode =
ParameterDeclaration | VariableDeclaration | ParameterApplication;
@ -49,13 +50,13 @@ interface CommonProps {
* chooses between reassignment vs. creation for new variables,
* and determines which variables to display in the form. */
allowedVariableNodes: AllowedVariableNodes;
/** Do not show `groups` as an option. Eg: Don't allow the user to pick
* "group123" in the sequence editor header. */
hideGroups?: boolean;
/** Add ability to collapse the form content. */
collapsible?: boolean;
collapsed?: boolean;
toggleVarShow?: () => void;
/** Optional filter to allow removal of arbitrary dropdown items.
* Return `false` to omit an item from display. */
customFilterRule?: (ddi: DropDownItem) => boolean;
}
export interface LocalsListProps extends CommonProps {

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { Row, Col, FBSelect, DropDownItem } from "../../ui";
import { Row, Col, FBSelect } from "../../ui";
import { locationFormList, NO_VALUE_SELECTED_DDI } from "./location_form_list";
import { convertDDItoVariable } from "../locals_list/handle_select";
import {
@ -38,8 +38,6 @@ const maybeUseStepData = ({ resources, bodyVariables, variable, uuid }: {
return variable;
};
const hideGroups = (x: DropDownItem) => x.headingId !== "PointGroup";
const allowAll = (_: unknown) => true;
/**
* Form with an "import from" dropdown and coordinate input boxes.
* Can be used to set a specific value, import a value, or declare a variable.
@ -57,8 +55,9 @@ export const LocationForm =
const variableListItems = displayVariables ? [PARENT(determineVarDDILabel({
label: "parent", resources, uuid: sequenceUuid, forceExternal: headerForm
}))] : [];
const list = locationFormList(resources, variableListItems)
.filter(props.hideGroups ? hideGroups : allowAll);
const unfiltered = locationFormList(resources, variableListItems);
const list = props.customFilterRule ?
unfiltered.filter(props.customFilterRule) : unfiltered;
/** Variable name. */
const { label } = celeryNode.args;
if (variable.default) {

View File

@ -66,27 +66,22 @@ export const groups2Ddi = (groups: TaggedPointGroup[]): DropDownItem[] => {
/** Location selection menu items. */
export function locationFormList(resources: ResourceIndex,
additionalItems: DropDownItem[], displayGroups?: boolean): DropDownItem[] {
additionalItems: DropDownItem[]): DropDownItem[] {
const points = selectAllActivePoints(resources)
.filter(x => x.body.pointer_type !== "ToolSlot");
const plantDDI = points2ddi(points, "Plant");
const genericPointerDDI = points2ddi(points, "GenericPointer");
const toolDDI = activeToolDDIs(resources);
const output = [COORDINATE_DDI()]
return [COORDINATE_DDI()]
.concat(additionalItems)
.concat(heading("Tool"))
.concat(toolDDI)
.concat(heading("Plant"))
.concat(plantDDI)
.concat(heading("GenericPointer"))
.concat(genericPointerDDI);
if (displayGroups) {
return output
.concat(heading("PointGroup"))
.concat(groups2Ddi(selectAllPointGroups(resources)));
} else {
return output;
}
.concat(genericPointerDDI)
.concat(heading("PointGroup"))
.concat(groups2Ddi(selectAllPointGroups(resources)));
}
/** Create drop down item with label; i.e., "Point/Plant (1, 2, 3)" */

View File

@ -29,6 +29,7 @@ import {
import { BooleanSetting } from "../session_keys";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { isUndefined } from "lodash";
import { NO_GROUPS } from "./locals_list/default_value_form";
export const onDrop =
(dispatch1: Function, sequence: TaggedSequence) =>
@ -210,7 +211,8 @@ const SequenceHeader = (props: SequenceHeaderProps) => {
collapsible={true}
collapsed={props.variablesCollapsed}
toggleVarShow={props.toggleVarShow}
shouldDisplay={props.shouldDisplay} />
shouldDisplay={props.shouldDisplay}
customFilterRule={NO_GROUPS} />
</div>;
};

View File

@ -118,8 +118,7 @@ export class RefactoredExecuteBlock
onChange={assignVariable(this.props)(currentStep.body || [])}
locationDropdownKey={JSON.stringify(currentSequence)}
allowedVariableNodes={AllowedVariableNodes.identifier}
shouldDisplay={this.props.shouldDisplay}
hideGroups={false} />
shouldDisplay={this.props.shouldDisplay} />
</Col>}
</Row>
</StepContent>

View File

@ -22,6 +22,7 @@ import { MoveAbsoluteWarning } from "./tile_move_absolute_conflict_check";
import { t } from "../../i18next_wrapper";
import { Collapse } from "@blueprintjs/core";
import { ExpandableHeader } from "../../ui/expandable_header";
import { NO_GROUPS } from "../locals_list/default_value_form";
export class TileMoveAbsolute extends React.Component<StepParams, MoveAbsState> {
state: MoveAbsState = {
@ -96,8 +97,8 @@ export class TileMoveAbsolute extends React.Component<StepParams, MoveAbsState>
hideHeader={true}
locationDropdownKey={JSON.stringify(this.props.currentSequence)}
allowedVariableNodes={AllowedVariableNodes.identifier}
hideGroups={true}
width={3} />
width={3}
customFilterRule={NO_GROUPS} />
SpeedInput = () =>
<Col xs={3}>

View File

@ -29,6 +29,7 @@ describe Api::PointGroupsController do
new_point_ids = rando_points + dont_delete
payload = { name: "new name",
point_ids: new_point_ids }
Transport.current.connection.clear!
put :update, body: payload.to_json, format: :json, params: { id: pg.id }
expect(response.status).to eq(200)
expect(PointGroupItem.exists?(do_delete)).to be false
@ -36,5 +37,11 @@ describe Api::PointGroupsController do
expect(json[:point_ids].count).to eq(new_point_ids.count)
expect(json.fetch(:name)).to eq "new name"
expect(new_point_ids.to_set).to eq(json.fetch(:point_ids).to_set)
calls = Transport.current.connection.calls.fetch(:publish)
expect(calls.length).to eq(1) # Don't echo!
call1 = calls.first
expect(call1.last.fetch(:routing_key)).to include(".sync.PointGroup.")
json2 = JSON.parse(call1.first, symbolize_names: true).fetch(:body)
expect(json).to eq(json2)
end
end

View File

@ -15,11 +15,9 @@ describe User do
describe ".refresh_everyones_ui" do
it "Sends a message over AMQP" do
expect(Rollbar).to receive(:error).with("Global UI refresh triggered")
get_msg = receive(:raw_amqp_send)
.with({
"type" => "reload", "commit" => "NONE",
}.to_json, Api::RmqUtilsController::PUBLIC_BROADCAST)
get_msg = receive(:raw_amqp_send).with({
"type" => "reload", "commit" => "NONE",
}.to_json, Api::RmqUtilsController::PUBLIC_BROADCAST)
expect(Transport.current).to get_msg
User.refresh_everyones_ui
end