Merge conflict
commit
bd87b5b198
|
@ -57,6 +57,7 @@ module Api
|
|||
status
|
||||
status_v8
|
||||
sync
|
||||
telemetry
|
||||
\\#
|
||||
\\*
|
||||
).map { |x| x + "(\\.|\\z)" }.join("|")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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()", () => {
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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)}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"}>
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -101,6 +101,7 @@ export const LocationForm =
|
|||
width={props.width}
|
||||
onChange={props.onChange} />
|
||||
<DefaultValueForm
|
||||
key={props.locationDropdownKey}
|
||||
variableNode={celeryNode}
|
||||
resources={resources}
|
||||
onChange={props.onChange} />
|
||||
|
|
|
@ -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) },
|
||||
])
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -171,3 +171,5 @@ class NiceResponse
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
FakeDeliveryInfo = Struct.new(:routing_key, :device)
|
||||
|
|
Loading…
Reference in New Issue