Merge conflict

pull/1540/head
Rick Carlino 2019-10-28 08:12:34 -05:00
commit bd87b5b198
48 changed files with 436 additions and 132 deletions

View File

@ -57,6 +57,7 @@ module Api
status
status_v8
sync
telemetry
\\#
\\*
).map { |x| x + "(\\.|\\z)" }.join("|")

View File

@ -7,7 +7,7 @@ class AmqpLogParser < Mutations::Command
TOO_OLD = "fbos version is out of date"
DISCARD = "message type field is not the kind that gets saved in the DB"
NOT_HASH = "logs must be a hash"
NOT_JSON = "Invalid JSON. Use a JSON validator."
# I keep a Ruby copy of the JSON here for reference.
# This is what a log will look like after JSON.parse()
EXAMPLE_JSON = {
@ -75,6 +75,8 @@ class AmqpLogParser < Mutations::Command
def set_payload!
# Parse from string to a Ruby hash (JSON)
@output.payload = JSON.parse(payload)
rescue JSON::ParserError
add_error :json, :not_json, NOT_JSON
end
def log

View File

@ -23,11 +23,16 @@ class LogService < AbstractServiceRunner
end
def maybe_deliver(data)
return unless data.valid?
violation = THROTTLE_POLICY.is_throttled(data.device_id)
ok = data.valid? && !violation
if violation
return warn_user(data, violation)
end
data.device.auto_sync_transaction do
ok ? deliver(data) : warn_user(data, violation)
deliver(data)
end
end
@ -41,6 +46,6 @@ class LogService < AbstractServiceRunner
end
def warn_user(data, violation)
data.device.maybe_throttle(violation)
violation && data.device.maybe_throttle(violation)
end
end

View File

@ -0,0 +1,32 @@
# A singleton that runs on a separate process than the web server.
# Listens to *ALL* incoming logs and stores them to the DB.
# Also handles throttling.
class TelemetryService < AbstractServiceRunner
MESSAGE = "TELEMETRY MESSAGE FROM %s"
FAILURE = "FAILED TELEMETRY MESSAGE FROM %s"
THROTTLE_POLICY = ThrottlePolicy.new({
ThrottlePolicy::TimePeriod.new(1.minute) => 25,
ThrottlePolicy::TimePeriod.new(1.hour) => 250,
ThrottlePolicy::TimePeriod.new(1.day) => 1500,
})
def process(delivery_info, payload)
device_key = delivery_info
.routing_key
.split(".")[1]
json = JSON.parse(payload)
other_stuff = { device: device_key,
is_telemetry: true,
message: MESSAGE % device_key }
THROTTLE_POLICY.track(device_key)
violation = THROTTLE_POLICY.is_throttled(device_key)
unless violation
puts json.merge(other_stuff).to_json
end
rescue JSON::ParserError
puts ({ device: device_key,
is_telemetry: true,
bad_json: payload,
message: FAILURE % device_key }).to_json
end
end

View File

@ -19,9 +19,9 @@ class ThrottlePolicy
def is_throttled(unique_id)
rules
.map do |rule|
is_violation = rule.time_period.usage_count_for(unique_id) > rule.limit
is_violation ? Violation.new(rule) : nil
end
is_violation = rule.time_period.usage_count_for(unique_id) > rule.limit
is_violation ? Violation.new(rule) : nil
end
.compact
.max
end

View File

@ -114,10 +114,11 @@ class Device < ApplicationRecord
# Sets the `throttled_until` and `throttled_at` fields if unpopulated or
# the throttle time period increases. Notifies user of cooldown period.
def maybe_throttle(violation)
return unless violation
end_t = violation.ends_at
# Some log validation errors will result in until_time being `nil`.
if (violation && (throttled_until.nil? || end_t > throttled_until))
reload.update!(throttled_until: end_t,
if (throttled_until.nil? || end_t > throttled_until)
reload.update_attributes!(throttled_until: end_t,
throttled_at: Time.now)
refresh_cache
cooldown = end_t.in_time_zone(self.timezone || "UTC").strftime("%I:%M%p")

View File

@ -42,6 +42,14 @@ class Transport
.bind("amq.topic", routing_key: "bot.*.logs")
end
def telemetry_channel
@telemetry_channel ||= self
.connection
.create_channel
.queue("api_telemetry_workers")
.bind("amq.topic", routing_key: "bot.*.telemetry")
end
def resource_channel
@resource_channel ||= self
.connection

View File

@ -17,7 +17,7 @@ export function Apology(_: {}) {
<h1>Page Error</h1>
<span>
We can't render this part of the page due to an unrecoverable error.
Here are some thing you can try:
Here are some things you can try:
</span>
<ol>
<li>

View File

@ -194,6 +194,15 @@
background-color: darken($panel_gray, 5%) !important;
}
}
&.panel-light-gray {
background-color: $panel_medium_light_gray;
box-shadow: 0 2px 0px 0px darken($panel_medium_light_gray, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_medium_light_gray, 5%) !important;
}
}
&.panel-blue {
background-color: $panel_blue;
box-shadow: 0 2px 0px 0px darken($panel_blue, 12%);
@ -230,6 +239,24 @@
background-color: darken($panel_red, 5%) !important;
}
}
&.panel-magenta {
background-color: $magenta;
box-shadow: 0 2px 0px 0px darken($magenta, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($magenta, 5%) !important;
}
}
&.panel-cyan {
background-color: $cyan;
box-shadow: 0 2px 0px 0px darken($cyan, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($cyan, 5%) !important;
}
}
&.zoom {
padding: 8px 12px;
&.zoom-out {

View File

@ -43,6 +43,7 @@ $panel_light_green: #f3f9f1;
$panel_yellow: #e99d18;
$panel_light_yellow: #fffaf0;
$panel_gray: #92a7b3;
$panel_medium_light_gray: #e6e6e6;
$panel_light_gray: #f9fbfc;
$panel_blue: #026365;
$panel_light_blue: #f0f8f8;

View File

@ -181,14 +181,11 @@
transition: all 0.2s ease;
}
}
%search-item {
.plant-search-item,
.group-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
&:hover,
&.hovered {
background: darken($light_green, 10%);
transition: background 0.2s ease;
}
img {
margin: 0 1rem 0 0;
height: 4rem;
@ -196,18 +193,6 @@
}
}
.plant-search-item {
@extend %search-item;
}
.group-search-item {
@extend %search-item;
&:hover,
&.hovered {
background: #d7eaea;
}
}
%panel-item-base {
text-align: right;
font-size: 1rem;
@ -244,11 +229,6 @@
.point-search-item {
cursor: pointer;
padding: 0.5rem 1rem;
&:hover,
&.hovered {
background: darken($light_brown, 10%);
transition: background 0.2s ease;
}
.saucer {
display: inline-block;
margin: 0 1rem 0 0;

View File

@ -44,32 +44,93 @@
.panel-container {
overflow: hidden;
div[class*="search-item"] {
&:hover,
&.hovered {
transition: background 0.2s ease;
}
}
&.green-panel {
background-color: $panel_light_green;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($panel_light_green, 10%);
}
}
}
&.cyan-panel {
background-color: $light_cyan;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($light_cyan, 10%);
}
}
}
&.brown-panel {
background-color: $panel_light_brown;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($panel_light_brown, 10%);
}
}
}
&.magenta-panel {
background-color: $light_magenta;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($light_magenta, 10%);
}
}
}
&.light-gray-panel,
&.gray-panel {
background-color: $panel_light_gray;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($panel_light_gray, 10%);
}
}
}
&.yellow-panel {
background-color: $panel_light_yellow;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($panel_light_yellow, 10%);
}
}
}
&.blue-panel {
background-color: $panel_light_blue;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($panel_light_blue, 10%);
}
}
}
&.teal-panel {
background-color: $panel_light_teal;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($panel_light_teal, 10%);
}
}
}
&.red-panel {
background-color: $panel_light_red;
div[class*="search-item"] {
&:hover,
&.hovered {
background: darken($panel_light_red, 10%);
}
}
}
}
@ -91,6 +152,9 @@
&.gray-panel {
background-color: $panel_gray;
}
&.light-gray-panel{
background-color: $panel_medium_light_gray;
}
&.yellow-panel {
background-color: $panel_yellow;
}
@ -144,13 +208,21 @@
.panel-title {
height: 50px;
.back-arrow {
&.black-text{
color: $medium_gray;
}
float: left;
text-align: center;
font-size: 1.8rem;
width: 50px;
line-height: 50px;
&:hover {
color: $white;
&.black-text{
color: $darker_gray !important;
}
&.white-text{
color: $white;
}
}
}
.title {
@ -220,12 +292,18 @@
position: absolute;
z-index: 9;
width: 100%;
background: $gray;
background: $panel_medium_light_gray;
padding: 0.5rem;
button {
margin: 0.5rem;
float: left;
}
.buttonrow{
label {
min-width: -webkit-fill-available;
margin-bottom: 0px;
margin-left: .5rem;
}
.buttonrow {
float:left;
}
}
@ -234,7 +312,7 @@
padding-right: 0;
padding-left: 0;
padding-bottom: 5rem;
max-height: calc(100vh - 10rem);
max-height: calc(100vh - 13rem);
overflow-y: auto;
overflow-x: hidden;
.plant-search-item,

View File

@ -17,6 +17,7 @@ import {
} from "../plants/designer_panel";
import { variableList } from "../../sequences/locals_list/variable_support";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
interface State {
uuid: string;
@ -85,10 +86,10 @@ export class RawAddFarmEvent
}
placeholderTemplate(children: React.ReactChild | React.ReactChild[]) {
return <DesignerPanel panelName={"add-farm-event"} panelColor={"yellow"}>
return <DesignerPanel panelName={"add-farm-event"} panel={Panel.FarmEvents}>
<DesignerPanelHeader
panelName={"add-farm-event"}
panelColor={"yellow"}
panel={Panel.FarmEvents}
title={t("No Executables")} />
<DesignerPanelContent panelName={"add-farm-event"}>
<label>

View File

@ -45,6 +45,7 @@ import {
} from "../../sequences/locals_list/locals_list_support";
import { t } from "../../i18next_wrapper";
import { TimeSettings } from "../../interfaces";
import { Panel } from "../panel_header";
type FormEvent = React.SyntheticEvent<HTMLInputElement>;
export const NEVER: TimeUnit = "never";
@ -467,10 +468,10 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
render() {
const { farmEvent } = this.props;
return <DesignerPanel panelName={"add-farm-event"} panelColor={"yellow"}>
return <DesignerPanel panelName={"add-farm-event"} panel={Panel.FarmEvents}>
<DesignerPanelHeader
panelName={"add-farm-event"}
panelColor={"yellow"}
panel={Panel.FarmEvents}
title={this.props.title}
onBack={!farmEvent.body.id ? () =>
// Throw out unsaved farmevents.

View File

@ -150,7 +150,7 @@ export class PureFarmEvents
};
render() {
return <DesignerPanel panelName={"farm-event"} panelColor={"yellow"}>
return <DesignerPanel panelName={"farm-event"} panel={Panel.FarmEvents}>
<DesignerNavTabs />
{this.props.timezoneIsSet ? this.normalContent() : this.tzwarning()}
</DesignerPanel>;

View File

@ -17,6 +17,7 @@ import {
import { t } from "../i18next_wrapper";
import { isBotOnline } from "../devices/must_be_online";
import { getStatus } from "../connectivity/reducer_support";
import { PanelColor } from "./panel_header";
export function mapStateToProps(props: Everything): MoveToProps {
const botToMqttStatus = getStatus(props.bot.connectivity.uptime["bot.mqtt"]);
@ -113,10 +114,10 @@ export class RawMoveTo extends React.Component<MoveToProps, {}> {
}
render() {
return <DesignerPanel panelName={"move-to"} panelColor={"gray"}>
return <DesignerPanel panelName={"move-to"} panelColor={PanelColor.gray}>
<DesignerPanelHeader
panelName={"move-to"}
panelColor={"gray"}
panelColor={PanelColor.gray}
title={t("Move to location")}
backTo={"/app/designer/plants"}
description={Content.MOVE_MODE_DESCRIPTION} />

View File

@ -19,17 +19,30 @@ export enum Panel {
type Tabs = keyof typeof Panel;
export const TAB_COLOR: { [key in Panel]: string } = {
[Panel.Map]: "gray",
[Panel.Plants]: "green",
[Panel.FarmEvents]: "yellow",
[Panel.SavedGardens]: "green",
[Panel.Tools]: "gray",
[Panel.Settings]: "gray",
[Panel.Points]: "teal",
[Panel.Groups]: "blue",
[Panel.Weeds]: "red",
[Panel.Zones]: "brown",
export enum PanelColor {
green = "green",
cyan = "cyan",
brown = "brown",
magenta = "magenta",
gray = "gray",
lightGray = "light-gray",
yellow = "yellow",
blue = "blue",
teal = "teal",
red = "red",
}
export const TAB_COLOR: { [key in Panel]: PanelColor } = {
[Panel.Map]: PanelColor.gray,
[Panel.Plants]: PanelColor.green,
[Panel.FarmEvents]: PanelColor.yellow,
[Panel.SavedGardens]: PanelColor.green,
[Panel.Tools]: PanelColor.gray,
[Panel.Settings]: PanelColor.gray,
[Panel.Points]: PanelColor.teal,
[Panel.Groups]: PanelColor.blue,
[Panel.Weeds]: PanelColor.red,
[Panel.Zones]: PanelColor.brown,
};
const iconFile = (icon: string) => `/app-resources/img/icons/${icon}.svg`;

View File

@ -0,0 +1,17 @@
import * as React from "react";
import { mount } from "enzyme";
import { DesignerPanel, DesignerPanelHeader } from "../designer_panel";
describe("<DesignerPanel />", () => {
it("renders default panel", () => {
const wrapper = mount(<DesignerPanel panelName={"test-panel"} />);
expect(wrapper.find("div").hasClass("gray-panel")).toBeTruthy();
});
});
describe("<DesignerPanelHeader />", () => {
it("renders default panel header", () => {
const wrapper = mount(<DesignerPanelHeader panelName={"test-panel"} />);
expect(wrapper.find("div").hasClass("gray-panel")).toBeTruthy();
});
});

View File

@ -4,9 +4,11 @@ import {
RawWeeds as Weeds, WeedsProps, mapStateToProps
} from "../weeds_inventory";
import { fakeState } from "../../../__test_support__/fake_state";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
describe("<Weeds> />", () => {
const fakeProps = (): WeedsProps => ({
points: [],
dispatch: jest.fn(),
});
@ -21,6 +23,17 @@ describe("<Weeds> />", () => {
{ currentTarget: { value: "0" } });
expect(wrapper.state().searchTerm).toEqual("0");
});
it("filters points", () => {
const p = fakeProps();
p.points = [fakePoint(), fakePoint()];
p.points[0].body.name = "weed 0";
p.points[1].body.name = "weed 1";
const wrapper = mount(<Weeds {...p} />);
wrapper.setState({ searchTerm: "0" });
expect(wrapper.text()).toContain("weed 0");
expect(wrapper.text()).not.toContain("weed 1");
});
});
describe("mapStateToProps()", () => {

View File

@ -9,6 +9,7 @@ import { getCropHeaderProps, searchForCurrentCrop } from "./crop_info";
import { DesignerPanel, DesignerPanelHeader } from "./designer_panel";
import { OFSearch } from "../util";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
export const mapStateToProps = (props: Everything): AddPlantProps =>
({
@ -52,12 +53,10 @@ export class RawAddPlant extends React.Component<AddPlantProps, {}> {
const { crop, result, basePath, backgroundURL } =
getCropHeaderProps({ cropSearchResults });
const panelName = "add-plant";
return <DesignerPanel
panelName={panelName}
panelColor={"green"}>
return <DesignerPanel panelName={panelName} panel={Panel.Plants}>
<DesignerPanelHeader
panelName={panelName}
panelColor={"green"}
panel={Panel.Plants}
title={result.crop.name}
style={{ background: backgroundURL }}
descriptionElement={

View File

@ -22,6 +22,7 @@ import {
} from "./designer_panel";
import { parseIntInput } from "../../util";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
export function mapStateToProps(props: Everything): CreatePointsProps {
const { position } = props.bot.hardware.location_data;
@ -233,10 +234,10 @@ export class RawCreatePoints
</Row>
render() {
return <DesignerPanel panelName={"point-creation"} panelColor={"teal"}>
return <DesignerPanel panelName={"point-creation"} panel={Panel.Points}>
<DesignerPanelHeader
panelName={"point-creation"}
panelColor={"teal"}
panel={Panel.Points}
title={t("Create point")}
backTo={"/app/designer/points"}
description={Content.CREATE_POINTS_DESCRIPTION} />

View File

@ -14,6 +14,7 @@ import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent, DesignerPanelTop
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
export function mapStateToProps(props: Everything): CropCatalogProps {
const { cropSearchQuery, cropSearchInProgress, cropSearchResults
@ -59,13 +60,13 @@ export class RawCropCatalog extends React.Component<CropCatalogProps, {}> {
}
render() {
return <DesignerPanel panelName={"crop-catalog"} panelColor={"green"}>
return <DesignerPanel panelName={"crop-catalog"} panel={Panel.Plants}>
<DesignerPanelHeader
panelName={"crop-catalog"}
panelColor={"green"}
panel={Panel.Plants}
title={t("Choose a crop")}
backTo={"/app/designer/plants"} />
<DesignerPanelTop>
<DesignerPanelTop panel={Panel.Plants}>
<div className="thin-search">
<input
autoFocus={true}

View File

@ -23,6 +23,7 @@ import {
} from "../../ui/empty_state_wrapper";
import { startCase, isArray, chain, isNumber } from "lodash";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
interface InfoFieldProps {
title: string;
@ -239,10 +240,10 @@ export class RawCropInfo extends React.Component<CropInfoProps, {}> {
const { crop, result, basePath, backgroundURL } =
getCropHeaderProps({ cropSearchResults });
const panelName = "crop-info";
return <DesignerPanel panelName={panelName} panelColor={"green"}>
return <DesignerPanel panelName={panelName} panel={Panel.Plants}>
<DesignerPanelHeader
panelName={panelName}
panelColor={"green"}
panel={Panel.Plants}
title={result.crop.name}
backTo={basePath}
onBack={this.clearCropSearchResults(crop)}

View File

@ -2,27 +2,31 @@ import * as React from "react";
import { history as routeHistory } from "../../history";
import { last, trim } from "lodash";
import { Link } from "../../link";
import { Panel, TAB_COLOR } from "../panel_header";
import { Panel, TAB_COLOR, PanelColor } from "../panel_header";
import { t } from "../../i18next_wrapper";
interface DesignerPanelProps {
panelName: string;
panelColor: string;
panel?: Panel;
panelColor?: PanelColor;
children?: React.ReactNode;
}
export const DesignerPanel = (props: DesignerPanelProps) =>
<div
export const DesignerPanel = (props: DesignerPanelProps) => {
const color = props.panel ? TAB_COLOR[props.panel] : props.panelColor;
return <div
className={[
"panel-container",
`${props.panelColor}-panel`,
`${color || PanelColor.gray}-panel`,
`${props.panelName}-panel`].join(" ")}>
{props.children}
</div>;
};
interface DesignerPanelHeaderProps {
panelName: string;
panelColor: string;
panel?: Panel;
panelColor?: PanelColor;
title?: string;
blackText?: boolean;
description?: string;
@ -39,32 +43,37 @@ const backToText = (to: string | undefined): string => {
return s ? ` ${t("to")} ${s}` : "";
};
export const DesignerPanelHeader = (props: DesignerPanelHeaderProps) =>
<div className={`panel-header ${props.panelColor}-panel`}
export const DesignerPanelHeader = (props: DesignerPanelHeaderProps) => {
const color = props.panel ? TAB_COLOR[props.panel] : props.panelColor;
const textColor = props.blackText ? "black" : "white";
return <div className={`panel-header ${color || PanelColor.gray}-panel`}
style={props.style || {}}>
<p className="panel-title">
<i className={`fa fa-arrow-left back-arrow ${props.blackText ? "black" : "white"}-text`}
<i className={`fa fa-arrow-left back-arrow ${textColor}-text`}
title={t("go back") + backToText(props.backTo)}
onClick={() => {
props.backTo ? routeHistory.push(props.backTo) : history.back();
props.onBack && props.onBack();
}} />
{props.title &&
<span className={`title ${props.blackText ? "black" : "white"}-text`}
>{t(props.title)}</span>}
<span className={`title ${textColor}-text`}>
{t(props.title)}
</span>}
{props.children}
</p>
{(props.description || props.descriptionElement) &&
<div
className={`panel-header-description ${props.panelName}-description ${props.blackText ? "black" : "white"}-text`}>
className={trim(`panel-header-description ${props.panelName}-description
${textColor}-text`)}>
{props.description && t(props.description)}
{props.descriptionElement}
</div>}
</div>;
};
interface DesignerPanelTopProps {
panel?: Panel;
panel: Panel;
linkTo?: string;
title?: string;
children?: React.ReactNode;
@ -82,7 +91,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
</div>
{props.linkTo &&
<Link to={props.linkTo}>
<div className={`fb-button panel-${TAB_COLOR[props.panel || Panel.Plants]}`}>
<div className={`fb-button panel-${TAB_COLOR[props.panel]}`}>
<i className="fa fa-plus" title={props.title} />
</div>
</Link>}

View File

@ -11,6 +11,7 @@ import { isString, isUndefined } from "lodash";
import { history, getPathArray } from "../../history";
import { destroy, edit, save } from "../../api/crud";
import { BooleanSetting } from "../../session_keys";
import { Panel } from "../panel_header";
export class RawPlantInfo extends React.Component<EditPlantInfoProps, {}> {
get templates() { return isString(this.props.openedSavedGarden); }
@ -40,10 +41,10 @@ export class RawPlantInfo extends React.Component<EditPlantInfoProps, {}> {
default = (plant_info: TaggedPlant) => {
const info = formatPlantInfo(plant_info);
return <DesignerPanel panelName={"plant-info"} panelColor={"green"}>
return <DesignerPanel panelName={"plant-info"} panel={Panel.Plants}>
<DesignerPanelHeader
panelName={"plant-info"}
panelColor={"green"}
panel={Panel.Plants}
title={`${t("Edit")} ${info.name}`}
backTo={"/app/designer/plants"}
onBack={unselectPlant(this.props.dispatch)}>

View File

@ -41,7 +41,7 @@ export class RawPlants extends React.Component<PlantInventoryProps, State> {
this.setState({ searchTerm: currentTarget.value })
render() {
return <DesignerPanel panelName={"plant-inventory"} panelColor={"green"}>
return <DesignerPanel panelName={"plant-inventory"} panel={Panel.Plants}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Plants}

View File

@ -10,6 +10,7 @@ import { TaggedPoint, Vector3 } from "farmbot";
import { maybeFindPointById } from "../../resources/selectors";
import { DeleteButton } from "../../controls/pin_form_fields";
import { getDevice } from "../../device";
import { Panel } from "../panel_header";
export const moveToPoint =
(body: Vector3) => () => getDevice().moveAbsolute(body);
@ -66,10 +67,10 @@ export class RawEditPoint extends React.Component<EditPointProps, {}> {
};
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"plant-info"} panelColor={"teal"}>
return <DesignerPanel panelName={"plant-info"} panel={Panel.Points}>
<DesignerPanelHeader
panelName={"plant-info"}
panelColor={"teal"}
panel={Panel.Points}
title={`${t("Edit")} ${point.body.name}`}
backTo={"/app/designer/points"}>
</DesignerPanelHeader>

View File

@ -26,7 +26,8 @@ interface PointsState {
export function mapStateToProps(props: Everything): PointsProps {
return {
points: selectAllGenericPointers(props.resources.index)
.filter(x => !x.body.discarded_at),
.filter(x => !x.body.discarded_at)
.filter(x => !x.body.name.toLowerCase().includes("weed")),
dispatch: props.dispatch,
};
}
@ -39,7 +40,7 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
}
render() {
return <DesignerPanel panelName={"point-inventory"} panelColor={"teal"}>
return <DesignerPanel panelName={"point-inventory"} panel={Panel.Points}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Points}

View File

@ -13,6 +13,7 @@ import {
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
import { createGroup } from "../point_groups/actions";
import { PanelColor } from "../panel_header";
export function mapStateToProps(props: Everything) {
return {
@ -46,9 +47,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
confirm(t("Are you sure you want to delete {{length}} plants?",
{ length: plantUUIDs.length }))) {
plantUUIDs.map(uuid => {
this
.props
.dispatch(destroy(uuid, true))
this.props.dispatch(destroy(uuid, true))
.then(() => { }, () => { });
});
history.push("/app/designer/plants");
@ -84,15 +83,15 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
</div>;
render() {
const { selected, plants, dispatch } = this.props;
const selectedPlantData = selected ? selected.map(uuid => {
return plants.filter(p => { return p.uuid == uuid; })[0];
}) : undefined;
const { plants, dispatch } = this.props;
const selectedPlantData =
this.selected.map(uuid => plants.filter(p => p.uuid == uuid)[0]);
return <DesignerPanel panelName={"plant-selection"} panelColor={"gray"}>
return <DesignerPanel panelName={"plant-selection"}
panelColor={PanelColor.lightGray}>
<DesignerPanelHeader
panelName={"plant-selection"}
panelColor={"gray"}
panelColor={PanelColor.lightGray}
blackText={true}
title={t("{{length}} plants selected",
{ length: this.selected.length })}

View File

@ -5,6 +5,7 @@ import {
} from "../plants/designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
export interface AddWeedProps {
dispatch: Function;
@ -20,12 +21,12 @@ export const mapStateToProps = (props: Everything): AddWeedProps => ({
export class RawAddWeed extends React.Component<AddWeedProps, AddWeedState> {
state: AddWeedState = {};
render() {
return <DesignerPanel panelName={"add-weed"} panelColor={"red"}>
return <DesignerPanel panelName={"add-weed"} panel={Panel.Weeds}>
<DesignerPanelHeader
panelName={"weeds"}
title={t("Add new weed")}
backTo={"/app/designer/weeds"}
panelColor={"red"} />
panel={Panel.Weeds} />
<DesignerPanelContent panelName={"add-weed"}>
</DesignerPanelContent>
</DesignerPanel>;

View File

@ -8,6 +8,7 @@ import { history, getPathArray } from "../../history";
import { Everything } from "../../interfaces";
import { TaggedPoint } from "farmbot";
import { maybeFindPointById } from "../../resources/selectors";
import { Panel } from "../panel_header";
export interface EditWeedProps {
dispatch: Function;
@ -33,10 +34,10 @@ export class RawEditWeed extends React.Component<EditWeedProps, {}> {
}
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"weed-info"} panelColor={"red"}>
return <DesignerPanel panelName={"weed-info"} panel={Panel.Weeds}>
<DesignerPanelHeader
panelName={"weed-info"}
panelColor={"red"}
panel={Panel.Weeds}
title={`${t("Edit")} ${point.body.name}`}
backTo={"/app/designer/points"}>
</DesignerPanelHeader>

View File

@ -10,8 +10,12 @@ import {
DesignerPanel, DesignerPanelContent, DesignerPanelTop
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
import { TaggedGenericPointer } from "farmbot";
import { selectAllGenericPointers } from "../../resources/selectors";
import { PointInventoryItem } from "./point_inventory_item";
export interface WeedsProps {
points: TaggedGenericPointer[];
dispatch: Function;
}
@ -20,6 +24,9 @@ interface WeedsState {
}
export const mapStateToProps = (props: Everything): WeedsProps => ({
points: selectAllGenericPointers(props.resources.index)
.filter(x => !x.body.discarded_at)
.filter(x => x.body.name.toLowerCase().includes("weed")),
dispatch: props.dispatch,
});
@ -31,7 +38,7 @@ export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
}
render() {
return <DesignerPanel panelName={"weeds-inventory"} panelColor={"red"}>
return <DesignerPanel panelName={"weeds-inventory"} panel={Panel.Weeds}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Weeds}
@ -42,11 +49,20 @@ export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
</DesignerPanelTop>
<DesignerPanelContent panelName={"weeds-inventory"}>
<EmptyStateWrapper
notEmpty={[].length > 0}
notEmpty={this.props.points.length > 0}
graphic={EmptyStateGraphic.weeds}
title={t("No weeds yet.")}
text={Content.NO_WEEDS}
colorScheme={"weeds"}>
{this.props.points
.filter(p => p.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.map(p => {
return <PointInventoryItem
key={p.uuid}
tpp={p}
dispatch={this.props.dispatch} />;
})}
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;

View File

@ -70,11 +70,11 @@ export class GroupDetailActive
}
render() {
return <DesignerPanel panelName={"group-detail"} panelColor={"blue"}>
return <DesignerPanel panelName={"group-detail"} panel={Panel.Groups}>
<DesignerPanelHeader
onBack={this.saveGroup}
panelName={Panel.Groups}
panelColor={"blue"}
panel={Panel.Groups}
title={t("Edit Group")}
backTo={"/app/designer/groups"}>
</DesignerPanelHeader>

View File

@ -39,7 +39,7 @@ export class RawGroupListPanel extends React.Component<GroupListPanelProps, Stat
navigate = (id: number) => history.push(`/app/designer/groups/${id}`);
render() {
return <DesignerPanel panelName={"groups"} panelColor={"blue"}>
return <DesignerPanel panelName={"groups"} panel={Panel.Groups}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Groups}

View File

@ -16,7 +16,7 @@ import {
DesignerPanel,
DesignerPanelContent
} from "../plants/designer_panel";
import { DesignerNavTabs } from "../panel_header";
import { DesignerNavTabs, Panel } from "../panel_header";
import { t } from "../../i18next_wrapper";
import { EmptyStateWrapper, EmptyStateGraphic } from "../../ui/empty_state_wrapper";
@ -40,7 +40,7 @@ export class RawSavedGardens extends React.Component<SavedGardensProps, {}> {
}
render() {
return <DesignerPanel panelName={"saved-garden"} panelColor={"green"}>
return <DesignerPanel panelName={"saved-garden"} panel={Panel.SavedGardens}>
<DesignerNavTabs />
<DesignerPanelContent panelName={"saved-garden"}
className={"with-nav"}>

View File

@ -13,7 +13,7 @@ import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { BooleanSetting, NumericSetting } from "../session_keys";
import { resetVirtualTrail } from "./map/layers/farmbot/bot_trail";
import { MapSizeInputs } from "./map_size_setting";
import { DesignerNavTabs } from "./panel_header";
import { DesignerNavTabs, Panel } from "./panel_header";
import { isUndefined } from "lodash";
export const mapStateToProps = (props: Everything): DesignerSettingsProps => ({
@ -32,7 +32,7 @@ export class RawDesignerSettings
render() {
const { getConfigValue, dispatch } = this.props;
const settingsProps = { getConfigValue, dispatch };
return <DesignerPanel panelName={"settings"} panelColor={"gray"}>
return <DesignerPanel panelName={"settings"} panel={Panel.Settings}>
<DesignerNavTabs />
<DesignerPanelContent panelName={"settings"}>
{DESIGNER_SETTINGS(settingsProps).map(setting =>

View File

@ -8,6 +8,7 @@ import { t } from "../../i18next_wrapper";
import { SaveBtn } from "../../ui";
import { SpecialStatus } from "farmbot";
import { initSave } from "../../api/crud";
import { Panel } from "../panel_header";
export interface AddToolProps {
dispatch: Function;
@ -24,12 +25,12 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({
export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
state: AddToolState = { toolName: "" };
render() {
return <DesignerPanel panelName={"tool"} panelColor={"gray"}>
return <DesignerPanel panelName={"tool"} panel={Panel.Tools}>
<DesignerPanelHeader
panelName={"tool"}
title={t("Add new tool")}
backTo={"/app/designer/tools"}
panelColor={"gray"} />
panel={Panel.Tools} />
<DesignerPanelContent panelName={"tools"}>
<label>{t("Tool Name")}</label>
<input

View File

@ -11,6 +11,7 @@ import { maybeFindToolById } from "../../resources/selectors";
import { SaveBtn } from "../../ui";
import { edit } from "../../api/crud";
import { history } from "../../history";
import { Panel } from "../panel_header";
export interface EditToolProps {
findTool(id: string): TaggedTool | undefined;
@ -40,12 +41,12 @@ export class RawEditTool extends React.Component<EditToolProps, EditToolState> {
}
default = (tool: TaggedTool) =>
<DesignerPanel panelName={"tool"} panelColor={"gray"}>
<DesignerPanel panelName={"tool"} panel={Panel.Tools}>
<DesignerPanelHeader
panelName={"tool"}
title={`${t("Edit")} ${tool.body.name}`}
backTo={"/app/designer/tools"}
panelColor={"gray"} />
panel={Panel.Tools} />
<DesignerPanelContent panelName={"tools"}>
<label>{t("Tool Name")}</label>
<input

View File

@ -48,9 +48,7 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
render() {
const panelName = "tools";
return <DesignerPanel
panelName={panelName}
panelColor={"gray"}>
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Tools}

View File

@ -5,6 +5,7 @@ import {
} from "../plants/designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
export interface AddZoneProps {
dispatch: Function;
@ -20,12 +21,12 @@ export const mapStateToProps = (props: Everything): AddZoneProps => ({
export class RawAddZone extends React.Component<AddZoneProps, AddZoneState> {
state: AddZoneState = {};
render() {
return <DesignerPanel panelName={"add-zone"} panelColor={"brown"}>
return <DesignerPanel panelName={"add-zone"} panel={Panel.Zones}>
<DesignerPanelHeader
panelName={"add-zone"}
title={t("Add new zone")}
backTo={"/app/designer/zones"}
panelColor={"brown"} />
panel={Panel.Zones} />
<DesignerPanelContent panelName={"add-zone"}>
</DesignerPanelContent>
</DesignerPanel>;

View File

@ -6,6 +6,7 @@ import {
import { t } from "../../i18next_wrapper";
import { history, getPathArray } from "../../history";
import { Everything } from "../../interfaces";
import { Panel } from "../panel_header";
export interface EditZoneProps {
dispatch: Function;
@ -31,10 +32,10 @@ export class RawEditZone extends React.Component<EditZoneProps, {}> {
}
default = () => {
return <DesignerPanel panelName={"zone-info"} panelColor={"brown"}>
return <DesignerPanel panelName={"zone-info"} panel={Panel.Zones}>
<DesignerPanelHeader
panelName={"zone-info"}
panelColor={"brown"}
panel={Panel.Zones}
title={`${t("Edit")} zone`}
backTo={"/app/designer/zones"}>
</DesignerPanelHeader>

View File

@ -31,7 +31,7 @@ export class RawZones extends React.Component<ZonesProps, ZonesState> {
}
render() {
return <DesignerPanel panelName={"zones-inventory"} panelColor={"red"}>
return <DesignerPanel panelName={"zones-inventory"} panel={Panel.Zones}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Zones}

View File

@ -101,6 +101,7 @@ export const LocationForm =
width={props.width}
onChange={props.onChange} />
<DefaultValueForm
key={props.locationDropdownKey}
variableNode={celeryNode}
resources={resources}
onChange={props.onChange} />

View File

@ -24,6 +24,7 @@ class RabbitWorker
loop do
ThreadsWait.all_waits([
thread { TelemetryService.new.go!(t.telemetry_channel) },
thread { LogService.new.go!(t.log_channel) },
thread { Resources::Service.new.go!(t.resource_channel) },
])

View File

@ -205,6 +205,8 @@ describe Api::RmqUtilsController do
".status.*",
".status",
".sync.*",
".telemetry.*",
".telemetry",
".sync",
".status_v8.*",
".status_v8"].map { |x| expect(random_channel(x).match(r)).to be }

View File

@ -2,18 +2,20 @@ require "spec_helper"
# require_relative "../../lib/log_service"
describe LogService do
normal_payl = {
z: 0,
y: 0,
x: 0,
type: "info",
major_version: 6,
message: "HQ FarmBot TEST 123 Pin 13 is 0",
created_at: 1512585641,
channels: [],
}.to_json
normal_hash = ->() do
return {
z: 0,
y: 0,
x: 0,
type: "info",
major_version: 8,
message: "HQ FarmBot TEST 123 Pin 13 is 0",
created_at: 1512585641,
channels: [],
}
end
FakeDeliveryInfo = Struct.new(:routing_key, :device)
normal_payl = normal_hash[].to_json
let!(:device) { FactoryBot.create(:device) }
let!(:device_id) { device.id }
@ -31,6 +33,12 @@ describe LogService do
expect(calls).to include(["amq.topic", { routing_key: "bot.*.logs" }])
end
it "has a telemetry_channel" do
calls = Transport.current.telemetry_channel.calls[:bind]
call = ["amq.topic", { :routing_key => "bot.*.telemetry" }]
expect(calls).to include(call)
end
it "has a resource_channel" do
calls = Transport.current.resource_channel.calls[:bind]
expect(calls).to include([
@ -54,9 +62,43 @@ describe LogService do
LogService.new.warn_user(data, time)
end
it "triggers a throttle" do
tp = LogService::THROTTLE_POLICY
ls = LogService.new
data = AmqpLogParser::DeliveryInfo.new
data.device_id = FactoryBot.create(:device).id
violation = ThrottlePolicy::Violation.new(Object.new)
allow(ls).to receive(:deliver)
expect(ls).to receive(:warn_user)
expect(tp).to receive(:is_throttled)
.with(data.device_id)
.and_return(violation)
ls.maybe_deliver(data)
end
it "handles bad params" do
expect do
LogService.new.process(fake_delivery_info, {})
end.to raise_error(Mutations::ValidationException)
end
it "handles malformed params" do
expect do
LogService.new.process(fake_delivery_info, "}}{{")
end.to raise_error(Mutations::ValidationException)
end
it "does not save `fun`, `debug` or `nil` logs" do
["fun", "debug", nil].map do |type|
Log.destroy_all
j = normal_hash[].merge(type: type).to_json
LogService.new.process(fake_delivery_info, j)
if Log.count != 0
opps = "Expected there to be no #{type.inspect} logs. " \
"There are, though. -RC"
fail(opps)
end
expect(Log.count).to be 0
end
end
end

View File

@ -0,0 +1,42 @@
require "spec_helper"
describe TelemetryService do
it "handles malformed JSON" do
ts = TelemetryService.new
routing_key = "bot.device_123.telemetry"
payload = "}"
expected = "{\"device\":\"device_123\"," \
"\"is_telemetry\":true,\"bad_json\":\"}\"," \
"\"message\":\"FAILED TELEMETRY MESSAGE " \
"FROM device_123\"}\n"
delivery_info =
FakeDeliveryInfo.new(routing_key, payload)
expect do
ts.process(delivery_info, payload)
end.to output(expected).to_stdout
end
it "parses telemetry from the device" do
ts = TelemetryService.new
routing_key = "bot.device_123.telemetry"
payload = {
foo: "bar",
# I'm putting this key here to make sure
# bots cannot change their `device_id` /
# spoof teleemetry of other bots.
device: "device_456",
}.to_json
expected = [
"{\"foo\":\"bar\"," \
"\"device\":\"device_123\"," \
"\"is_telemetry\":true," \
"\"message\":\"TELEMETRY MESSAGE " \
"FROM device_123\"}\n",
].join("")
delivery_info =
FakeDeliveryInfo.new(routing_key, payload)
expect do
ts.process(delivery_info, payload)
end.to output(expected).to_stdout
end
end

View File

@ -171,3 +171,5 @@ class NiceResponse
end
end
end
FakeDeliveryInfo = Struct.new(:routing_key, :device)