Merge changes

pull/1534/head
AscendFB 2019-10-25 12:28:58 +02:00
parent 6f620d174e
commit 6fd340f12b
51 changed files with 1281 additions and 203 deletions

View File

@ -70,7 +70,7 @@ GEM
url
coderay (1.1.2)
concurrent-ruby (1.1.5)
crass (1.0.4)
crass (1.0.5)
database_cleaner (1.7.0)
declarative (0.0.10)
declarative-option (0.1.0)
@ -106,15 +106,15 @@ GEM
railties (>= 3.2, < 6.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.32.1)
google-api-client (0.33.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.10.0)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.10)
google-cloud-core (1.3.1)
signet (~> 0.12)
google-cloud-core (1.3.2)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
@ -140,7 +140,7 @@ GEM
json (2.2.0)
jsonapi-renderer (0.2.2)
jwt (2.2.1)
loofah (2.3.0)
loofah (2.3.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)

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

@ -24,10 +24,12 @@ class LogService < AbstractServiceRunner
def maybe_deliver(data)
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 +43,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,23 @@
# 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"
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 }
puts json.merge(other_stuff).to_json
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,9 +114,10 @@ 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))
if (throttled_until.nil? || end_t > throttled_until)
reload.update_attributes!(throttled_until: end_t,
throttled_at: Time.now)
refresh_cache

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

@ -759,6 +759,12 @@ export namespace Content {
export const NO_GROUPS =
trim(`Press "+" to add a group.`);
export const NO_WEEDS =
trim(`Press "+" to add a weed.`);
export const NO_ZONES =
trim(`Press "+" to add a zone.`);
export const ENTER_CROP_SEARCH_TERM =
trim(`Search for a crop to add to your garden.`);

View File

@ -80,16 +80,6 @@
background-color: darken($dark-blue, 5%) !important;
}
}
&.blue2 {
background-color: $blue2;
box-shadow: 0 2px 0px 0px $blue2dark;
&:focus,
&:hover,
&.active {
background-color: $blue2medium !important;
}
}
&.red {
background-color: $red;
box-shadow: 0 2px 0px 0px darken($red, 12%);
@ -177,6 +167,69 @@
background-color: darken($pink, 5%) !important;
}
}
&.panel-green {
background-color: $panel_green;
box-shadow: 0 2px 0px 0px darken($panel_green, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_green, 5%) !important;
}
}
&.panel-yellow {
background-color: $panel_yellow;
box-shadow: 0 2px 0px 0px darken($panel_yellow, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_yellow, 5%) !important;
}
}
&.panel-gray {
background-color: $panel_gray;
box-shadow: 0 2px 0px 0px darken($panel_gray, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_gray, 5%) !important;
}
}
&.panel-blue {
background-color: $panel_blue;
box-shadow: 0 2px 0px 0px darken($panel_blue, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_blue, 5%) !important;
}
}
&.panel-brown {
background-color: $panel_brown;
box-shadow: 0 2px 0px 0px darken($panel_brown, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_brown, 5%) !important;
}
}
&.panel-teal {
background-color: $panel_teal;
box-shadow: 0 2px 0px 0px darken($panel_teal, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_teal, 5%) !important;
}
}
&.panel-red {
background-color: $panel_red;
box-shadow: 0 2px 0px 0px darken($panel_red, 12%);
&:focus,
&:hover,
&.active {
background-color: darken($panel_red, 5%) !important;
}
}
&.zoom {
padding: 8px 12px;
&.zoom-out {

View File

@ -13,9 +13,6 @@ $darker_gray: #182026;
$black: #000;
$light_blue: #cdf;
$blue: #a4c2f4;
$blue2: #026365;
$blue2medium: #025758;
$blue2dark: #013738;
$dark_blue: #37d;
$light_cyan: #d0e0e3;
$cyan: #45818e;
@ -124,10 +121,6 @@ $panel_light_red: #fff7f6;
background: $yellow !important;
}
.blue2 {
background: $blue2 !important;
}
.blue,
.info,
.saucer-info {

View File

@ -65,9 +65,6 @@
&.blue-panel {
background-color: $panel_light_blue;
}
&.blue2-panel {
background-color: $panel_light_blue;
}
&.teal-panel {
background-color: $panel_light_teal;
}
@ -92,7 +89,7 @@
background-color: $magenta;
}
&.gray-panel {
background-color: $panel_medium_light_gray;
background-color: $panel_gray;
}
&.yellow-panel {
background-color: $panel_yellow;
@ -100,9 +97,6 @@
&.blue-panel {
background-color: $panel_blue;
}
&.blue2-panel {
background-color: $panel_blue;
}
&.teal-panel {
background-color: $panel_teal;
}
@ -493,3 +487,23 @@
min-width: 7rem;
}
}
.weeds-inventory-panel,
.zones-inventory-panel,
.group-detail-panel,
.groups-panel {
.panel-content {
max-height: calc(100vh - 19rem);
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 5rem;
}
}
.weeds-inventory-panel,
.zones-inventory-panel,
.groups-panel {
.panel-content {
padding: 0;
}
}

View File

@ -870,10 +870,7 @@ ul {
p,
h5,
a {
color: $panel_gray;
}
.empty-state-graphic {
filter: saturate(0);
color: $panel_teal;
}
}
&.tools {
@ -890,6 +887,20 @@ ul {
color: $panel_blue;
}
}
&.weeds {
p,
h5,
a {
color: $panel_red;
}
}
&.zones {
p,
h5,
a {
color: $panel_brown;
}
}
}
.farmware-selection-panel {

View File

@ -36,6 +36,7 @@ input:not([role="combobox"]) {
background-color: $white !important;
color: $red;
}
-moz-appearance: textfield;
}
.input-error-wrapper {

View File

@ -48,7 +48,7 @@ describe("<DesignerNavTabs />", () => {
mockPath = "/app/designer/points";
mockDev = true;
const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("gray-panel")).toBeTruthy();
expect(wrapper.hasClass("teal-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active");
});
@ -56,7 +56,23 @@ describe("<DesignerNavTabs />", () => {
mockPath = "/app/designer/groups";
mockDev = true;
const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("blue2-panel")).toBeTruthy();
expect(wrapper.hasClass("blue-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active");
});
it("renders for weeds", () => {
mockPath = "/app/designer/weeds";
mockDev = true;
const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("red-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active");
});
it("renders for zones", () => {
mockPath = "/app/designer/zones";
mockDev = true;
const wrapper = shallow(<DesignerNavTabs />);
expect(wrapper.hasClass("brown-panel")).toBeTruthy();
expect(wrapper.html()).toContain("active");
});

View File

@ -13,6 +13,8 @@ export enum Panel {
Settings = "Settings",
Points = "Points",
Groups = "Groups",
Weeds = "Weeds",
Zones = "Zones",
}
type Tabs = keyof typeof Panel;
@ -24,8 +26,10 @@ export const TAB_COLOR: { [key in Panel]: string } = {
[Panel.SavedGardens]: "green",
[Panel.Tools]: "gray",
[Panel.Settings]: "gray",
[Panel.Points]: "gray",
[Panel.Groups]: "blue2",
[Panel.Points]: "teal",
[Panel.Groups]: "blue",
[Panel.Weeds]: "red",
[Panel.Zones]: "brown",
};
const iconFile = (icon: string) => `/app-resources/img/icons/${icon}.svg`;
@ -39,6 +43,8 @@ export const TAB_ICON: { [key in Panel]: string } = {
[Panel.Settings]: iconFile("settings"),
[Panel.Points]: iconFile("point"),
[Panel.Groups]: iconFile("groups"),
[Panel.Weeds]: iconFile("weeds"),
[Panel.Zones]: iconFile("zones"),
};
const getCurrentTab = (): Tabs => {
@ -57,6 +63,10 @@ const getCurrentTab = (): Tabs => {
return Panel.Points;
} else if (pathArray.includes("groups")) {
return Panel.Groups;
} else if (pathArray.includes("weeds")) {
return Panel.Weeds;
} else if (pathArray.includes("zones")) {
return Panel.Zones;
} else {
return Panel.Plants;
}
@ -103,11 +113,21 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
panel={Panel.FarmEvents}
linkTo={"/app/designer/events"}
title={t("Events")} />
{DevSettings.futureFeaturesEnabled() &&
<NavTab
panel={Panel.Zones}
linkTo={"/app/designer/zones"}
title={t("Zones")} />}
{DevSettings.futureFeaturesEnabled() &&
<NavTab
panel={Panel.Points}
linkTo={"/app/designer/points"}
title={t("Points")} />}
{DevSettings.futureFeaturesEnabled() &&
<NavTab
panel={Panel.Weeds}
linkTo={"/app/designer/weeds"}
title={t("Weeds")} />}
{DevSettings.futureFeaturesEnabled() &&
<NavTab
panel={Panel.Tools}

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { mount } from "enzyme";
import {
RawAddWeed as AddWeed, AddWeedProps, mapStateToProps
} from "../weeds_add";
import { fakeState } from "../../../__test_support__/fake_state";
describe("<AddWeed />", () => {
const fakeProps = (): AddWeedProps => ({
dispatch: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<AddWeed {...fakeProps()} />);
expect(wrapper.text()).toContain("Add");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const props = mapStateToProps(state);
expect(props.dispatch).toEqual(expect.any(Function));
});
});

View File

@ -0,0 +1,50 @@
let mockPath = "/app/designer/weeds/1";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
history: { push: jest.fn() }
}));
import * as React from "react";
import { mount } from "enzyme";
import {
RawEditWeed as EditWeed, EditWeedProps, mapStateToProps
} from "../weeds_edit";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { fakeState } from "../../../__test_support__/fake_state";
import {
buildResourceIndex
} from "../../../__test_support__/resource_index_builder";
describe("<EditWeed />", () => {
const fakeProps = (): EditWeedProps => ({
dispatch: jest.fn(),
findPoint: () => undefined,
});
it("redirects", () => {
mockPath = "/app/designer/weeds";
const wrapper = mount(<EditWeed {...fakeProps()} />);
expect(wrapper.text()).toContain("Redirecting...");
});
it("renders", () => {
mockPath = "/app/designer/weeds/1";
const p = fakeProps();
const point = fakePoint();
point.body.id = 1;
p.findPoint = () => point;
const wrapper = mount(<EditWeed {...p} />);
expect(wrapper.text().toLowerCase()).toContain("edit");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const point = fakePoint();
point.body.id = 1;
state.resources = buildResourceIndex([point]);
const props = mapStateToProps(state);
expect(props.findPoint(1)).toEqual(point);
});
});

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import {
RawWeeds as Weeds, WeedsProps, mapStateToProps
} from "../weeds_inventory";
import { fakeState } from "../../../__test_support__/fake_state";
describe("<Weeds> />", () => {
const fakeProps = (): WeedsProps => ({
dispatch: jest.fn(),
});
it("renders no points", () => {
const wrapper = mount(<Weeds {...fakeProps()} />);
expect(wrapper.text()).toContain("No weeds yet.");
});
it("changes search term", () => {
const wrapper = shallow<Weeds>(<Weeds {...fakeProps()} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
expect(wrapper.state().searchTerm).toEqual("0");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const props = mapStateToProps(state);
expect(props.dispatch).toEqual(expect.any(Function));
});
});

View File

@ -233,10 +233,10 @@ export class RawCreatePoints
</Row>
render() {
return <DesignerPanel panelName={"point-creation"} panelColor={"brown"}>
return <DesignerPanel panelName={"point-creation"} panelColor={"teal"}>
<DesignerPanelHeader
panelName={"point-creation"}
panelColor={"brown"}
panelColor={"teal"}
title={t("Create point")}
backTo={"/app/designer/points"}
description={Content.CREATE_POINTS_DESCRIPTION} />

View File

@ -87,7 +87,7 @@ export const DesignerPanelTop = (props: DesignerPanelTopProps) => {
</div>
{props.linkTo &&
<Link to={props.linkTo}>
<div className={`fb-button ${TAB_COLOR[props.panel || Panel.Plants]}`}>
<div className={`fb-button panel-${TAB_COLOR[props.panel || Panel.Plants]}`}>
<i className="fa fa-plus" title={props.title} />
</div>
</Link>}

View File

@ -66,10 +66,10 @@ export class RawEditPoint extends React.Component<EditPointProps, {}> {
};
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"plant-info"} panelColor={"green"}>
return <DesignerPanel panelName={"plant-info"} panelColor={"teal"}>
<DesignerPanelHeader
panelName={"plant-info"}
panelColor={"gray"}
panelColor={"teal"}
title={`${t("Edit")} ${point.body.name}`}
backTo={"/app/designer/points"}>
</DesignerPanelHeader>

View File

@ -39,7 +39,7 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
}
render() {
return <DesignerPanel panelName={"point-inventory"} panelColor={"brown"}>
return <DesignerPanel panelName={"point-inventory"} panelColor={"teal"}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Points}
@ -51,7 +51,7 @@ export class RawPoints extends React.Component<PointsProps, PointsState> {
<DesignerPanelContent panelName={"point"}>
<EmptyStateWrapper
notEmpty={this.props.points.length > 0}
graphic={EmptyStateGraphic.no_crop_results}
graphic={EmptyStateGraphic.points}
title={t("No points yet.")}
text={Content.NO_POINTS}
colorScheme={"points"}>

View File

@ -39,6 +39,8 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
}
}
get selected() { return this.props.selected || []; }
destroySelected = (plantUUIDs: string[] | undefined) => {
if (plantUUIDs && plantUUIDs.length > 0 &&
confirm(t("Are you sure you want to delete {{length}} plants?",
@ -53,14 +55,6 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
}
}
SelectedNotUndef = (input?: string[] | undefined) => {
if (input !== undefined) {
return input;
} else {
return [];
}
}
ActionButtons = () =>
<div className="panel-action-buttons">
<div className="buttonrow">
@ -82,7 +76,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
</button>
<button className="fb-button dark-blue"
onClick={() => this.props.dispatch(createGroup({
points: this.SelectedNotUndef(this.props.selected)
points: this.selected
}))}>
{t("Create group")}
</button>
@ -101,7 +95,7 @@ export class RawSelectPlants extends React.Component<SelectPlantsProps, {}> {
panelColor={"gray"}
blackText={true}
title={t("{{length}} plants selected",
{ length: this.SelectedNotUndef(selected).length })}
{ length: this.selected.length })}
backTo={"/app/designer/plants"}
description={Content.BOX_SELECT_DESCRIPTION} />
<this.ActionButtons />

View File

@ -0,0 +1,35 @@
import React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
} from "../plants/designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
export interface AddWeedProps {
dispatch: Function;
}
export interface AddWeedState {
}
export const mapStateToProps = (props: Everything): AddWeedProps => ({
dispatch: props.dispatch,
});
export class RawAddWeed extends React.Component<AddWeedProps, AddWeedState> {
state: AddWeedState = {};
render() {
return <DesignerPanel panelName={"add-weed"} panelColor={"red"}>
<DesignerPanelHeader
panelName={"weeds"}
title={t("Add new weed")}
backTo={"/app/designer/weeds"}
panelColor={"red"} />
<DesignerPanelContent panelName={"add-weed"}>
</DesignerPanelContent>
</DesignerPanel>;
}
}
export const AddWeed = connect(mapStateToProps)(RawAddWeed);

View File

@ -0,0 +1,53 @@
import * as React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
import { history, getPathArray } from "../../history";
import { Everything } from "../../interfaces";
import { TaggedPoint } from "farmbot";
import { maybeFindPointById } from "../../resources/selectors";
export interface EditWeedProps {
dispatch: Function;
findPoint(id: number): TaggedPoint | undefined;
}
export const mapStateToProps = (props: Everything): EditWeedProps => ({
dispatch: props.dispatch,
findPoint: id => maybeFindPointById(props.resources.index, id),
});
export class RawEditWeed extends React.Component<EditWeedProps, {}> {
get stringyID() { return getPathArray()[4] || ""; }
get point() {
if (this.stringyID) {
return this.props.findPoint(parseInt(this.stringyID));
}
}
fallback = () => {
history.push("/app/designer/weeds");
return <span>{t("Redirecting...")}</span>;
}
default = (point: TaggedPoint) => {
return <DesignerPanel panelName={"weed-info"} panelColor={"red"}>
<DesignerPanelHeader
panelName={"weed-info"}
panelColor={"red"}
title={`${t("Edit")} ${point.body.name}`}
backTo={"/app/designer/points"}>
</DesignerPanelHeader>
<DesignerPanelContent panelName={"weed-info"}>
</DesignerPanelContent>
</DesignerPanel>;
}
render() {
return this.point ? this.default(this.point) : this.fallback();
}
}
export const EditWeed = connect(mapStateToProps)(RawEditWeed);

View File

@ -0,0 +1,56 @@
import * as React from "react";
import { connect } from "react-redux";
import { Everything } from "../../interfaces";
import { DesignerNavTabs, Panel } from "../panel_header";
import {
EmptyStateWrapper, EmptyStateGraphic
} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelTop
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
export interface WeedsProps {
dispatch: Function;
}
interface WeedsState {
searchTerm: string;
}
export const mapStateToProps = (props: Everything): WeedsProps => ({
dispatch: props.dispatch,
});
export class RawWeeds extends React.Component<WeedsProps, WeedsState> {
state: WeedsState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
render() {
return <DesignerPanel panelName={"weeds-inventory"} panelColor={"red"}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Weeds}
linkTo={"/app/designer/weeds/add"}
title={t("Add weed")}>
<input type="text" onChange={this.update}
placeholder={t("Search your weeds...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"weeds-inventory"}>
<EmptyStateWrapper
notEmpty={[].length > 0}
graphic={EmptyStateGraphic.weeds}
title={t("No weeds yet.")}
text={Content.NO_WEEDS}
colorScheme={"weeds"}>
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
}
}
export const Weeds = connect(mapStateToProps)(RawWeeds);

View File

@ -70,7 +70,7 @@ export class GroupDetailActive
}
render() {
return <DesignerPanel panelName={"groups"} panelColor={"blue"}>
return <DesignerPanel panelName={"group-detail"} panelColor={"blue"}>
<DesignerPanelHeader
onBack={this.saveGroup}
panelName={Panel.Groups}

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={"blue2"}>
return <DesignerPanel panelName={"groups"} panelColor={"blue"}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Groups}

View File

@ -62,7 +62,7 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
<DesignerPanelContent panelName={"tools"}>
<EmptyStateWrapper
notEmpty={this.props.tools.length > 0}
graphic={EmptyStateGraphic.sequences}
graphic={EmptyStateGraphic.tools}
title={t("Add a tool")}
text={Content.NO_TOOLS}
colorScheme={"tools"}>

View File

@ -0,0 +1,25 @@
import * as React from "react";
import { mount } from "enzyme";
import {
RawAddZone as AddZone, AddZoneProps, mapStateToProps
} from "../add_zone";
import { fakeState } from "../../../__test_support__/fake_state";
describe("<AddZone />", () => {
const fakeProps = (): AddZoneProps => ({
dispatch: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<AddZone {...fakeProps()} />);
expect(wrapper.text()).toContain("Add");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const props = mapStateToProps(state);
expect(props.dispatch).toEqual(expect.any(Function));
});
});

View File

@ -0,0 +1,41 @@
let mockPath = "/app/designer/zones/1";
jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/")),
history: { push: jest.fn() }
}));
import * as React from "react";
import { mount } from "enzyme";
import {
RawEditZone as EditZone, EditZoneProps, mapStateToProps
} from "../edit_zone";
import { fakeState } from "../../../__test_support__/fake_state";
describe("<EditZone />", () => {
const fakeProps = (): EditZoneProps => ({
dispatch: jest.fn(),
findZone: () => undefined,
});
it("redirects", () => {
mockPath = "/app/designer/zones";
const wrapper = mount(<EditZone {...fakeProps()} />);
expect(wrapper.text()).toContain("Redirecting...");
});
it("renders", () => {
mockPath = "/app/designer/zones/1";
const p = fakeProps();
p.findZone = () => "stub zone";
const wrapper = mount(<EditZone {...p} />);
expect(wrapper.text().toLowerCase()).toContain("edit");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const props = mapStateToProps(state);
expect(props.findZone(1)).toEqual(undefined);
});
});

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import {
RawZones as Zones, ZonesProps, mapStateToProps
} from "../zones_inventory";
import { fakeState } from "../../../__test_support__/fake_state";
describe("<Zones> />", () => {
const fakeProps = (): ZonesProps => ({
dispatch: jest.fn(),
});
it("renders no zones", () => {
const wrapper = mount(<Zones {...fakeProps()} />);
expect(wrapper.text()).toContain("No zones yet.");
});
it("changes search term", () => {
const wrapper = shallow<Zones>(<Zones {...fakeProps()} />);
wrapper.find("input").first().simulate("change",
{ currentTarget: { value: "0" } });
expect(wrapper.state().searchTerm).toEqual("0");
});
});
describe("mapStateToProps()", () => {
it("returns props", () => {
const state = fakeState();
const props = mapStateToProps(state);
expect(props.dispatch).toEqual(expect.any(Function));
});
});

View File

@ -0,0 +1,35 @@
import React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelHeader
} from "../plants/designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
export interface AddZoneProps {
dispatch: Function;
}
export interface AddZoneState {
}
export const mapStateToProps = (props: Everything): AddZoneProps => ({
dispatch: props.dispatch,
});
export class RawAddZone extends React.Component<AddZoneProps, AddZoneState> {
state: AddZoneState = {};
render() {
return <DesignerPanel panelName={"add-zone"} panelColor={"brown"}>
<DesignerPanelHeader
panelName={"add-zone"}
title={t("Add new zone")}
backTo={"/app/designer/zones"}
panelColor={"brown"} />
<DesignerPanelContent panelName={"add-zone"}>
</DesignerPanelContent>
</DesignerPanel>;
}
}
export const AddZone = connect(mapStateToProps)(RawAddZone);

View File

@ -0,0 +1,51 @@
import * as React from "react";
import { connect } from "react-redux";
import {
DesignerPanel, DesignerPanelHeader, DesignerPanelContent
} from "../plants/designer_panel";
import { t } from "../../i18next_wrapper";
import { history, getPathArray } from "../../history";
import { Everything } from "../../interfaces";
export interface EditZoneProps {
dispatch: Function;
findZone(id: number): string | undefined;
}
export const mapStateToProps = (props: Everything): EditZoneProps => ({
dispatch: props.dispatch,
findZone: _id => undefined,
});
export class RawEditZone extends React.Component<EditZoneProps, {}> {
get stringyID() { return getPathArray()[4] || ""; }
get zone() {
if (this.stringyID) {
return this.props.findZone(parseInt(this.stringyID));
}
}
fallback = () => {
history.push("/app/designer/zones");
return <span>{t("Redirecting...")}</span>;
}
default = () => {
return <DesignerPanel panelName={"zone-info"} panelColor={"brown"}>
<DesignerPanelHeader
panelName={"zone-info"}
panelColor={"brown"}
title={`${t("Edit")} zone`}
backTo={"/app/designer/zones"}>
</DesignerPanelHeader>
<DesignerPanelContent panelName={"zone-info"}>
</DesignerPanelContent>
</DesignerPanel>;
}
render() {
return this.zone ? this.default() : this.fallback();
}
}
export const EditZone = connect(mapStateToProps)(RawEditZone);

View File

@ -0,0 +1,56 @@
import * as React from "react";
import { connect } from "react-redux";
import { Everything } from "../../interfaces";
import { DesignerNavTabs, Panel } from "../panel_header";
import {
EmptyStateWrapper, EmptyStateGraphic
} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import {
DesignerPanel, DesignerPanelContent, DesignerPanelTop
} from "../plants/designer_panel";
import { t } from "../../i18next_wrapper";
export interface ZonesProps {
dispatch: Function;
}
interface ZonesState {
searchTerm: string;
}
export const mapStateToProps = (props: Everything): ZonesProps => ({
dispatch: props.dispatch,
});
export class RawZones extends React.Component<ZonesProps, ZonesState> {
state: ZonesState = { searchTerm: "" };
update = ({ currentTarget }: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ searchTerm: currentTarget.value });
}
render() {
return <DesignerPanel panelName={"zones-inventory"} panelColor={"red"}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Zones}
linkTo={"/app/designer/zones/add"}
title={t("Add zone")}>
<input type="text" onChange={this.update}
placeholder={t("Search your zones...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"zones-inventory"}>
<EmptyStateWrapper
notEmpty={[].length > 0}
graphic={EmptyStateGraphic.zones}
title={t("No zones yet.")}
text={Content.NO_ZONES}
colorScheme={"zones"}>
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;
}
}
export const Zones = connect(mapStateToProps)(RawZones);

View File

@ -353,4 +353,52 @@ export const UNBOUND_ROUTES = [
getChild: () => import("./farm_designer/point_groups/group_detail"),
childKey: "GroupDetail"
}),
route({
children: true,
$: "/designer/weeds",
getModule,
key,
getChild: () => import("./farm_designer/plants/weeds_inventory"),
childKey: "Weeds"
}),
route({
children: true,
$: "/designer/weeds/add",
getModule,
key,
getChild: () => import("./farm_designer/plants/weeds_add"),
childKey: "AddWeed"
}),
route({
children: true,
$: "/designer/weeds/:point_id",
getModule,
key,
getChild: () => import("./farm_designer/plants/weeds_edit"),
childKey: "EditWeed"
}),
route({
children: true,
$: "/designer/zones",
getModule,
key,
getChild: () => import("./farm_designer/zones/zones_inventory"),
childKey: "Zones"
}),
route({
children: true,
$: "/designer/zones/add",
getModule,
key,
getChild: () => import("./farm_designer/zones/add_zone"),
childKey: "AddZone"
}),
route({
children: true,
$: "/designer/zones/:zone_id",
getModule,
key,
getChild: () => import("./farm_designer/zones/edit_zone"),
childKey: "EditZone"
}),
].concat([NOT_FOUND_ROUTE]);

View File

@ -8,7 +8,11 @@ export enum EmptyStateGraphic {
sequences = "sequences",
regimens = "regimens",
farm_events = "farm_events",
groups = "groups"
groups = "groups",
points = "points",
tools = "tools",
weeds = "weeds",
zones = "zones",
}
interface EmptyStateWrapperProps {
@ -17,7 +21,8 @@ interface EmptyStateWrapperProps {
text?: string;
textElement?: JSX.Element;
graphic: string;
colorScheme?: "plants" | "events" | "gardens" | "points" | "tools" | "groups";
colorScheme?: "plants" | "events" | "gardens" | "points" | "tools"
| "groups" | "weeds" | "zones";
children?: React.ReactNode;
}

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

@ -29,16 +29,16 @@
"@blueprintjs/datetime": "3.14.0",
"@blueprintjs/select": "3.11.1",
"@types/enzyme": "3.10.3",
"@types/jest": "24.0.18",
"@types/jest": "24.0.19",
"@types/lodash": "4.14.144",
"@types/markdown-it": "0.0.9",
"@types/moxios": "0.4.9",
"@types/node": "12.7.12",
"@types/node": "12.11.5",
"@types/promise-timeout": "1.3.0",
"@types/react": "16.9.5",
"@types/react": "16.9.9",
"@types/react-color": "3.0.1",
"@types/react-dom": "16.9.1",
"@types/react-redux": "7.1.4",
"@types/react-dom": "16.9.2",
"@types/react-redux": "7.1.5",
"axios": "0.19.0",
"boxed_value": "1.0.0",
"browser-speech": "1.1.1",
@ -46,7 +46,7 @@
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1",
"farmbot": "8.3.0-rc6",
"i18next": "17.2.0",
"i18next": "17.3.0",
"install": "0.13.0",
"lodash": "4.17.15",
"markdown-it": "10.0.0",
@ -58,18 +58,18 @@
"parcel-bundler": "1.12.4",
"promise-timeout": "1.3.0",
"raf": "3.4.1",
"react": "16.10.2",
"react": "16.11.0",
"react-addons-test-utils": "15.6.2",
"react-color": "2.17.3",
"react-dom": "16.10.2",
"react-dom": "16.11.0",
"react-joyride": "2.1.1",
"react-redux": "7.1.1",
"react-test-renderer": "16.10.2",
"react-test-renderer": "16.11.0",
"react-transition-group": "4.3.0",
"redux": "4.0.4",
"redux-immutable-state-invariant": "2.1.0",
"redux-thunk": "2.3.0",
"sass": "1.23.0",
"sass": "1.23.1",
"sass-lint": "1.13.1",
"takeme": "0.11.3",
"ts-jest": "24.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -1,119 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#C46F24;}
.st2{opacity:0.5;fill:#C46F24;}
.st3{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-miterlimit:10;}
.st4{fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st5{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st6{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st7{fill:#F15A24;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st8{fill:none;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st9{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st10{fill:#FFFFFF;}
.st11{fill:#FFFFFF;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:4.088,10.22;}
.st13{fill:#FFFFFF;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st15{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st16{fill:none;stroke:#969696;stroke-width:5;stroke-miterlimit:10;}
.st17{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st18{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st19{fill:none;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st20{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st21{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st22{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;}
.st23{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:3.2727,9.8182;}
.st24{fill:none;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st25{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st26{fill:none;stroke:#333333;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st27{fill:none;stroke:#E6E6E6;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st28{fill:#1EB287;}
.st29{fill:#DEEDCB;}
.st30{fill:#505305;}
.st31{fill:#186435;}
.st32{fill:#A44F79;}
.st33{fill:#2AB188;}
.st34{fill:#A35915;}
.st35{fill:#4D4D4D;}
.st36{fill:#F6B330;}
.st37{fill:#324872;}
.st38{fill:#2BA270;}
.st39{fill:#53A4EA;}
.st40{fill:#3BA2A0;}
.st41{fill:#1792CD;}
.st42{fill:#0C2E3D;}
.st43{fill:#35761B;}
.st44{fill:#0C6364;}
.st45{fill:#F4A519;}
.st46{opacity:0.06;fill:#3B9910;}
.st47{opacity:0.06;fill:#E56200;}
.st48{opacity:0.06;fill:#2E5799;}
.st49{opacity:0.06;fill:#007F7C;}
.st50{opacity:0.06;fill:#00B7FF;}
.st51{opacity:0.06;fill:#FF9D00;}
.st52{opacity:0.06;fill:#00CC8D;}
.st53{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-miterlimit:10;}
.st54{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st55{fill:#F15A24;}
.st56{fill:#F15A24;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st57{fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st58{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st59{opacity:0.8;enable-background:new ;}
.st60{clip-path:url(#SVGID_2_);}
.st61{clip-path:url(#SVGID_4_);}
.st62{clip-path:url(#SVGID_6_);fill:#333333;}
.st63{clip-path:url(#SVGID_8_);}
.st64{clip-path:url(#SVGID_10_);fill:#333333;}
.st65{clip-path:url(#SVGID_12_);}
.st66{clip-path:url(#SVGID_14_);}
.st67{clip-path:url(#SVGID_16_);fill:#333333;}
.st68{clip-path:url(#SVGID_18_);}
.st69{clip-path:url(#SVGID_20_);fill:#333333;}
.st70{clip-path:url(#SVGID_22_);}
.st71{clip-path:url(#SVGID_24_);fill:#333333;}
.st72{clip-path:url(#SVGID_26_);}
.st73{clip-path:url(#SVGID_28_);fill:#333333;}
.st74{clip-path:url(#SVGID_30_);}
.st75{clip-path:url(#SVGID_32_);fill:#333333;}
.st76{clip-path:url(#SVGID_34_);}
.st77{clip-path:url(#SVGID_36_);fill:#333333;}
.st78{clip-path:url(#SVGID_38_);}
.st79{clip-path:url(#SVGID_40_);fill:#333333;}
.st80{clip-path:url(#SVGID_42_);}
.st81{clip-path:url(#SVGID_44_);fill:#333333;}
.st82{clip-path:url(#SVGID_46_);}
.st83{clip-path:url(#SVGID_48_);fill:#333333;}
.st84{clip-path:url(#SVGID_50_);}
.st85{clip-path:url(#SVGID_52_);fill:#333333;}
.st86{clip-path:url(#SVGID_54_);}
.st87{clip-path:url(#SVGID_56_);fill:#333333;}
.st88{clip-path:url(#SVGID_58_);}
.st89{clip-path:url(#SVGID_60_);fill:#333333;}
.st90{clip-path:url(#SVGID_62_);}
.st91{clip-path:url(#SVGID_64_);}
.st92{clip-path:url(#SVGID_66_);enable-background:new ;}
.st93{clip-path:url(#SVGID_68_);}
.st94{clip-path:url(#SVGID_70_);}
.st95{clip-path:url(#SVGID_72_);fill:#333333;}
.st96{clip-path:url(#SVGID_74_);}
.st97{clip-path:url(#SVGID_76_);}
.st98{clip-path:url(#SVGID_78_);fill:#333333;}
.st99{clip-path:url(#SVGID_80_);}
.st100{clip-path:url(#SVGID_82_);fill:#333333;}
.st101{clip-path:url(#SVGID_84_);}
.st102{clip-path:url(#SVGID_86_);}
.st103{clip-path:url(#SVGID_88_);fill:#333333;}
.st104{clip-path:url(#SVGID_90_);}
.st105{clip-path:url(#SVGID_92_);fill:#333333;}
.st106{clip-path:url(#SVGID_94_);}
.st107{clip-path:url(#SVGID_96_);fill:#333333;}
.st108{clip-path:url(#SVGID_98_);}
.st109{clip-path:url(#SVGID_100_);fill:#333333;}
.st110{opacity:0.8;clip-path:url(#SVGID_102_);fill:#333333;}
.st3{fill:#C1631E;}
.st4{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st5{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-miterlimit:10;}
.st6{fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st7{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st8{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st9{fill:#F15A24;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st10{fill:none;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st11{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:#FFFFFF;}
.st13{fill:#FFFFFF;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:4.088,10.22;}
.st15{fill:#FFFFFF;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st16{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st17{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st18{fill:none;stroke:#969696;stroke-width:5;stroke-miterlimit:10;}
.st19{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st20{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st21{fill:none;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st22{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st23{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st24{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;}
.st25{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:3.2727,9.8182;}
.st26{fill:none;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st27{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st28{fill:none;stroke:#333333;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st29{fill:none;stroke:#E6E6E6;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st30{fill:#1EB287;}
.st31{fill:#DEEDCB;}
.st32{fill:#505305;}
.st33{fill:#186435;}
.st34{fill:#A44F79;}
.st35{fill:#2AB188;}
.st36{fill:#A35915;}
.st37{fill:#4D4D4D;}
.st38{fill:#F6B330;}
.st39{fill:#324872;}
.st40{fill:#2BA270;}
.st41{fill:#53A4EA;}
.st42{fill:#3BA2A0;}
.st43{fill:#1792CD;}
.st44{fill:#0C2E3D;}
.st45{fill:#35761B;}
.st46{fill:#0C6364;}
.st47{fill:#F4A519;}
.st48{opacity:0.06;fill:#3B9910;}
.st49{opacity:0.06;fill:#E56200;}
.st50{opacity:0.06;fill:#2E5799;}
.st51{opacity:0.06;fill:#007F7C;}
.st52{opacity:0.06;fill:#00B7FF;}
.st53{opacity:0.06;fill:#FF9D00;}
.st54{opacity:0.06;fill:#00CC8D;}
.st55{fill:#91A7B4;}
.st56{fill:#9F6300;}
.st57{fill:#07B386;}
.st58{fill:#FF4D2D;}
.st59{fill:#F9FBFC;}
.st60{fill:#FBF7F0;}
.st61{fill:#F0F8F8;}
.st62{fill:#F1FCF9;}
.st63{fill:#FFF7F6;}
.st64{fill:#AF8761;}
.st65{fill:#FFF8F3;}
.st66{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-miterlimit:10;}
.st67{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st68{fill:#F15A24;}
.st69{fill:#F15A24;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st70{fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st71{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st72{opacity:0.8;enable-background:new ;}
.st73{clip-path:url(#SVGID_2_);}
.st74{clip-path:url(#SVGID_4_);}
.st75{clip-path:url(#SVGID_6_);fill:#333333;}
.st76{clip-path:url(#SVGID_8_);}
.st77{clip-path:url(#SVGID_10_);fill:#333333;}
.st78{clip-path:url(#SVGID_12_);}
.st79{clip-path:url(#SVGID_14_);}
.st80{clip-path:url(#SVGID_16_);fill:#333333;}
.st81{clip-path:url(#SVGID_18_);}
.st82{clip-path:url(#SVGID_20_);fill:#333333;}
.st83{clip-path:url(#SVGID_22_);}
.st84{clip-path:url(#SVGID_24_);fill:#333333;}
.st85{clip-path:url(#SVGID_26_);}
.st86{clip-path:url(#SVGID_28_);fill:#333333;}
.st87{clip-path:url(#SVGID_30_);}
.st88{clip-path:url(#SVGID_32_);fill:#333333;}
.st89{clip-path:url(#SVGID_34_);}
.st90{clip-path:url(#SVGID_36_);fill:#333333;}
.st91{clip-path:url(#SVGID_38_);}
.st92{clip-path:url(#SVGID_40_);fill:#333333;}
.st93{clip-path:url(#SVGID_42_);}
.st94{clip-path:url(#SVGID_44_);fill:#333333;}
.st95{clip-path:url(#SVGID_46_);}
.st96{clip-path:url(#SVGID_48_);fill:#333333;}
.st97{clip-path:url(#SVGID_50_);}
.st98{clip-path:url(#SVGID_52_);fill:#333333;}
.st99{clip-path:url(#SVGID_54_);}
.st100{clip-path:url(#SVGID_56_);fill:#333333;}
.st101{clip-path:url(#SVGID_58_);}
.st102{clip-path:url(#SVGID_60_);fill:#333333;}
.st103{clip-path:url(#SVGID_62_);}
.st104{clip-path:url(#SVGID_64_);}
.st105{clip-path:url(#SVGID_66_);enable-background:new ;}
.st106{clip-path:url(#SVGID_68_);}
.st107{clip-path:url(#SVGID_70_);}
.st108{clip-path:url(#SVGID_72_);fill:#333333;}
.st109{clip-path:url(#SVGID_74_);}
.st110{clip-path:url(#SVGID_76_);}
.st111{clip-path:url(#SVGID_78_);fill:#333333;}
.st112{clip-path:url(#SVGID_80_);}
.st113{clip-path:url(#SVGID_82_);fill:#333333;}
.st114{clip-path:url(#SVGID_84_);}
.st115{clip-path:url(#SVGID_86_);}
.st116{clip-path:url(#SVGID_88_);fill:#333333;}
.st117{clip-path:url(#SVGID_90_);}
.st118{clip-path:url(#SVGID_92_);fill:#333333;}
.st119{clip-path:url(#SVGID_94_);}
.st120{clip-path:url(#SVGID_96_);fill:#333333;}
.st121{clip-path:url(#SVGID_98_);}
.st122{clip-path:url(#SVGID_100_);fill:#333333;}
.st123{opacity:0.8;clip-path:url(#SVGID_102_);fill:#333333;}
.st124{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st125{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st126{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st127{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st128{opacity:0.8;}
.st129{opacity:0.8;fill:#FFFFFF;}
.st130{fill:none;stroke:#FFFFFF;stroke-width:6;stroke-miterlimit:10;}
.st131{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-miterlimit:10;}
.st132{opacity:0.2;fill:url(#SVGID_103_);}
.st133{fill:#D8EAD2;}
.st134{opacity:0.2;fill:url(#SVGID_104_);}
.st135{opacity:0.06;fill:#E07127;}
.st136{fill:#FFFFFF;stroke:#00B485;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
</style>
<g id="Layer_2">
</g>
@ -122,19 +148,11 @@
<g id="Layer_4">
</g>
<g id="Layer_1">
<g>
<path d="M46.86,22.5l-3.1,0c-0.82,0-1.53-0.57-1.71-1.37c-1.47-6.45-6.53-11.65-13.14-13.17C28.09,7.78,27.5,7.05,27.5,6.2l0-3.06
c0-1.31-0.94-2.5-2.24-2.63C23.76,0.36,22.5,1.53,22.5,3v3.24c0,0.82-0.57,1.52-1.37,1.71C14.68,9.42,9.48,14.48,7.97,21.09
C7.78,21.91,7.05,22.5,6.2,22.5l-3.06,0c-1.31,0-2.5,0.94-2.63,2.24C0.36,26.24,1.53,27.5,3,27.5h3.19c0.84,0,1.58,0.58,1.76,1.4
c0.61,2.69,1.84,5.21,3.67,7.37c2.48,2.94,5.79,4.93,9.48,5.76c0.82,0.19,1.4,0.92,1.4,1.76l0,3.06c0,1.31,0.94,2.5,2.24,2.63
c1.5,0.15,2.76-1.02,2.76-2.49v-3.24c0-0.82,0.57-1.53,1.37-1.71c6.45-1.47,11.66-6.53,13.17-13.14c0.19-0.83,0.92-1.41,1.77-1.41
H47c1.47,0,2.64-1.26,2.49-2.76C49.36,23.44,48.17,22.5,46.86,22.5z M28.31,37.05c-0.91,0.25-1.81-0.41-1.81-1.36v-3.91
c0-0.83-0.67-1.5-1.5-1.5s-1.5,0.67-1.5,1.5v3.9c0,0.91-0.86,1.61-1.73,1.38c-2.46-0.65-4.65-2.03-6.32-4.01
c-1.2-1.43-2.05-3.07-2.52-4.82c-0.23-0.88,0.47-1.73,1.38-1.73h3.92c0.83,0,1.5-0.67,1.5-1.5s-0.67-1.5-1.5-1.5h-3.9
c-0.94,0-1.6-0.91-1.35-1.82c1.17-4.27,4.54-7.57,8.72-8.73c0.91-0.25,1.81,0.41,1.81,1.36v3.91c0,0.83,0.67,1.5,1.5,1.5
s1.5-0.67,1.5-1.5v-3.9c0-0.94,0.91-1.6,1.82-1.35c4.27,1.17,7.57,4.54,8.73,8.72c0.25,0.91-0.41,1.81-1.36,1.81h-3.91
c-0.83,0-1.5,0.67-1.5,1.5s0.67,1.5,1.5,1.5h3.9c0.95,0,1.6,0.91,1.35,1.82C35.86,32.59,32.5,35.89,28.31,37.05z"/>
<circle cx="25" cy="25" r="1.75"/>
</g>
<path d="M23.52,0.61c-3.43,0.46-6.45,2.65-8.02,5.72c-2.16,4.22-1.22,8.99,1.69,12.12c0.46,0.5,0.76,1.13,0.87,1.8l4.46,27.02
c0.18,1.07,0.97,1.99,2.03,2.18c1.41,0.25,2.7-0.7,2.92-2.05l4.48-27.15c0.11-0.67,0.41-1.3,0.87-1.8
c1.82-1.96,2.86-4.56,2.86-7.26C35.68,4.81,30.07-0.28,23.52,0.61z M19.3,11.19c0-3.14,2.56-5.7,5.7-5.7c3.14,0,5.7,2.56,5.7,5.7
s-2.56,5.7-5.7,5.7C21.86,16.89,19.3,14.33,19.3,11.19z M24.31,27.46l-1.14-6.88c-0.07-0.44,0.3-0.82,0.74-0.77
c0.36,0.05,0.72,0.07,1.09,0.07c0.37,0,0.73-0.03,1.09-0.07c0.44-0.06,0.81,0.33,0.74,0.77l-1.14,6.88
C25.56,28.24,24.44,28.24,24.31,27.46z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#C46F24;}
.st2{opacity:0.5;fill:#C46F24;}
.st3{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-miterlimit:10;}
.st4{fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st5{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st6{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st7{fill:#F15A24;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st8{fill:none;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st9{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st10{fill:#FFFFFF;}
.st11{fill:#FFFFFF;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:4.088,10.22;}
.st13{fill:#FFFFFF;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st15{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st16{fill:none;stroke:#969696;stroke-width:5;stroke-miterlimit:10;}
.st17{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st18{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st19{fill:none;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st20{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st21{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st22{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;}
.st23{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:3.2727,9.8182;}
.st24{fill:none;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st25{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st26{fill:none;stroke:#333333;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st27{fill:none;stroke:#E6E6E6;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st28{fill:#1EB287;}
.st29{fill:#DEEDCB;}
.st30{fill:#505305;}
.st31{fill:#186435;}
.st32{fill:#A44F79;}
.st33{fill:#2AB188;}
.st34{fill:#A35915;}
.st35{fill:#4D4D4D;}
.st36{fill:#F6B330;}
.st37{fill:#324872;}
.st38{fill:#2BA270;}
.st39{fill:#53A4EA;}
.st40{fill:#3BA2A0;}
.st41{fill:#1792CD;}
.st42{fill:#0C2E3D;}
.st43{fill:#35761B;}
.st44{fill:#0C6364;}
.st45{fill:#F4A519;}
.st46{opacity:0.06;fill:#3B9910;}
.st47{opacity:0.06;fill:#E56200;}
.st48{opacity:0.06;fill:#2E5799;}
.st49{opacity:0.06;fill:#007F7C;}
.st50{opacity:0.06;fill:#00B7FF;}
.st51{opacity:0.06;fill:#FF9D00;}
.st52{opacity:0.06;fill:#00CC8D;}
.st53{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-miterlimit:10;}
.st54{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st55{fill:#F15A24;}
.st56{fill:#F15A24;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st57{fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st58{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st59{opacity:0.8;enable-background:new ;}
.st60{clip-path:url(#SVGID_2_);}
.st61{clip-path:url(#SVGID_4_);}
.st62{clip-path:url(#SVGID_6_);fill:#333333;}
.st63{clip-path:url(#SVGID_8_);}
.st64{clip-path:url(#SVGID_10_);fill:#333333;}
.st65{clip-path:url(#SVGID_12_);}
.st66{clip-path:url(#SVGID_14_);}
.st67{clip-path:url(#SVGID_16_);fill:#333333;}
.st68{clip-path:url(#SVGID_18_);}
.st69{clip-path:url(#SVGID_20_);fill:#333333;}
.st70{clip-path:url(#SVGID_22_);}
.st71{clip-path:url(#SVGID_24_);fill:#333333;}
.st72{clip-path:url(#SVGID_26_);}
.st73{clip-path:url(#SVGID_28_);fill:#333333;}
.st74{clip-path:url(#SVGID_30_);}
.st75{clip-path:url(#SVGID_32_);fill:#333333;}
.st76{clip-path:url(#SVGID_34_);}
.st77{clip-path:url(#SVGID_36_);fill:#333333;}
.st78{clip-path:url(#SVGID_38_);}
.st79{clip-path:url(#SVGID_40_);fill:#333333;}
.st80{clip-path:url(#SVGID_42_);}
.st81{clip-path:url(#SVGID_44_);fill:#333333;}
.st82{clip-path:url(#SVGID_46_);}
.st83{clip-path:url(#SVGID_48_);fill:#333333;}
.st84{clip-path:url(#SVGID_50_);}
.st85{clip-path:url(#SVGID_52_);fill:#333333;}
.st86{clip-path:url(#SVGID_54_);}
.st87{clip-path:url(#SVGID_56_);fill:#333333;}
.st88{clip-path:url(#SVGID_58_);}
.st89{clip-path:url(#SVGID_60_);fill:#333333;}
.st90{clip-path:url(#SVGID_62_);}
.st91{clip-path:url(#SVGID_64_);}
.st92{clip-path:url(#SVGID_66_);enable-background:new ;}
.st93{clip-path:url(#SVGID_68_);}
.st94{clip-path:url(#SVGID_70_);}
.st95{clip-path:url(#SVGID_72_);fill:#333333;}
.st96{clip-path:url(#SVGID_74_);}
.st97{clip-path:url(#SVGID_76_);}
.st98{clip-path:url(#SVGID_78_);fill:#333333;}
.st99{clip-path:url(#SVGID_80_);}
.st100{clip-path:url(#SVGID_82_);fill:#333333;}
.st101{clip-path:url(#SVGID_84_);}
.st102{clip-path:url(#SVGID_86_);}
.st103{clip-path:url(#SVGID_88_);fill:#333333;}
.st104{clip-path:url(#SVGID_90_);}
.st105{clip-path:url(#SVGID_92_);fill:#333333;}
.st106{clip-path:url(#SVGID_94_);}
.st107{clip-path:url(#SVGID_96_);fill:#333333;}
.st108{clip-path:url(#SVGID_98_);}
.st109{clip-path:url(#SVGID_100_);fill:#333333;}
.st110{opacity:0.8;clip-path:url(#SVGID_102_);fill:#333333;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_3">
</g>
<g id="Layer_4">
</g>
<g id="Layer_1">
<path d="M29.58,2.46C23.56,1.23,17.43,2.43,12.3,5.82s-8.62,8.58-9.84,14.6S2.43,32.58,5.82,37.7c3.39,5.12,8.58,8.62,14.6,9.84
c1.54,0.31,3.07,0.46,4.59,0.46c10.71,0,20.32-7.53,22.53-18.42C50.07,17.15,42.01,4.99,29.58,2.46z M8.93,16.83
c0.68-1.4,1.57-2.7,2.62-3.84c3.62-3.93,8.44-5.99,13.41-5.99c1.2,0,2.42,0.12,3.62,0.37c3.52,0.72,6.59,2.43,8.99,4.78
c0.86,0.84,0.74,2.25-0.26,2.91l-3.54,2.34c-0.66,0.44-1.54,0.34-2.09-0.23c-3.52-3.67-8.7-5.55-14.38-5.1
c-0.51,0.04-0.98,0.35-1.22,0.81c-2.63,5.06-2.91,10.57-0.91,15.24c0.31,0.72,0.05,1.56-0.61,2l-3.54,2.35
c-1,0.67-2.35,0.21-2.79-0.91C6.41,26.91,6.56,21.66,8.93,16.83z M19.78,15.02c3.58,0.17,6.79,1.47,9.18,3.67
c0.61,0.56,0.51,1.56-0.18,2.02l-2.32,1.53c-0.57,0.38-1.33,0.22-1.71-0.35l-0.66-1c-0.46-0.69-1.39-0.88-2.08-0.42
c-0.69,0.46-0.88,1.39-0.42,2.08l0.66,1c0.38,0.57,0.22,1.33-0.35,1.71l-2.32,1.54c-0.69,0.46-1.65,0.16-1.93-0.62
c-1.09-3.06-1.03-6.52,0.2-9.88C18.14,15.5,18.92,14.98,19.78,15.02z M25.26,28.11l3.64,5.5c0.36,0.54-0.1,1.25-0.74,1.14
c-2.74-0.49-5.19-1.67-7.11-3.43c-0.61-0.56-0.51-1.56,0.18-2.02l2.31-1.53C24.12,27.38,24.88,27.54,25.26,28.11z M31.41,31.95
l-3.64-5.5c-0.38-0.57-0.22-1.33,0.35-1.71l2.32-1.54c0.69-0.46,1.65-0.16,1.93,0.62c0.88,2.46,1.01,5.18,0.38,7.89
C32.6,32.34,31.76,32.49,31.41,31.95z M41.84,31.37c-2.76,7.41-10.26,12.2-18.15,11.59c-4.36-0.34-8.29-2.16-11.27-5.08
c-0.86-0.84-0.75-2.26,0.26-2.92l3.54-2.35c0.66-0.44,1.54-0.34,2.09,0.23C21.49,36.15,26.02,38,31.05,38
c0.54,0,1.09-0.02,1.64-0.06c0.51-0.04,0.98-0.35,1.22-0.81c2.63-5.06,2.91-10.57,0.91-15.24c-0.31-0.72-0.05-1.56,0.61-2
l3.53-2.34c1-0.66,2.34-0.22,2.78,0.89C43.3,22.38,43.5,26.92,41.84,31.37z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#C46F24;}
.st2{opacity:0.5;fill:#C46F24;}
.st3{fill:#FFFFFF;stroke:#000000;stroke-width:7;stroke-miterlimit:10;}
.st4{fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-miterlimit:10;}
.st5{fill:#FFFFFF;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st6{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st7{fill:#F15A24;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st8{fill:none;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st9{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st10{fill:#FFFFFF;}
.st11{fill:#FFFFFF;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:4.088,10.22;}
.st13{fill:#FFFFFF;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st14{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st15{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st16{fill:none;stroke:#969696;stroke-width:5;stroke-miterlimit:10;}
.st17{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st18{fill:#B3B3B3;stroke:#4D4D4D;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st19{fill:none;stroke:#4D4D4D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st20{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st21{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st22{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;}
.st23{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:3.2727,9.8182;}
.st24{fill:none;stroke:#333333;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st25{fill:none;stroke:#333333;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st26{fill:none;stroke:#333333;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st27{fill:none;stroke:#E6E6E6;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st28{fill:#1EB287;}
.st29{fill:#DEEDCB;}
.st30{fill:#505305;}
.st31{fill:#186435;}
.st32{fill:#A44F79;}
.st33{fill:#2AB188;}
.st34{fill:#A35915;}
.st35{fill:#4D4D4D;}
.st36{fill:#F6B330;}
.st37{fill:#324872;}
.st38{fill:#2BA270;}
.st39{fill:#53A4EA;}
.st40{fill:#3BA2A0;}
.st41{fill:#1792CD;}
.st42{fill:#0C2E3D;}
.st43{fill:#35761B;}
.st44{fill:#0C6364;}
.st45{fill:#F4A519;}
.st46{opacity:0.06;fill:#3B9910;}
.st47{opacity:0.06;fill:#E56200;}
.st48{opacity:0.06;fill:#2E5799;}
.st49{opacity:0.06;fill:#007F7C;}
.st50{opacity:0.06;fill:#00B7FF;}
.st51{opacity:0.06;fill:#FF9D00;}
.st52{opacity:0.06;fill:#00CC8D;}
.st53{fill:none;stroke:#FFFFFF;stroke-width:5;stroke-linecap:round;stroke-miterlimit:10;}
.st54{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-linecap:round;stroke-miterlimit:10;}
.st55{fill:#F15A24;}
.st56{fill:#F15A24;stroke:#000000;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st57{fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:10;}
.st58{fill:#FFFFFF;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
.st59{opacity:0.8;enable-background:new ;}
.st60{clip-path:url(#SVGID_2_);}
.st61{clip-path:url(#SVGID_4_);}
.st62{clip-path:url(#SVGID_6_);fill:#333333;}
.st63{clip-path:url(#SVGID_8_);}
.st64{clip-path:url(#SVGID_10_);fill:#333333;}
.st65{clip-path:url(#SVGID_12_);}
.st66{clip-path:url(#SVGID_14_);}
.st67{clip-path:url(#SVGID_16_);fill:#333333;}
.st68{clip-path:url(#SVGID_18_);}
.st69{clip-path:url(#SVGID_20_);fill:#333333;}
.st70{clip-path:url(#SVGID_22_);}
.st71{clip-path:url(#SVGID_24_);fill:#333333;}
.st72{clip-path:url(#SVGID_26_);}
.st73{clip-path:url(#SVGID_28_);fill:#333333;}
.st74{clip-path:url(#SVGID_30_);}
.st75{clip-path:url(#SVGID_32_);fill:#333333;}
.st76{clip-path:url(#SVGID_34_);}
.st77{clip-path:url(#SVGID_36_);fill:#333333;}
.st78{clip-path:url(#SVGID_38_);}
.st79{clip-path:url(#SVGID_40_);fill:#333333;}
.st80{clip-path:url(#SVGID_42_);}
.st81{clip-path:url(#SVGID_44_);fill:#333333;}
.st82{clip-path:url(#SVGID_46_);}
.st83{clip-path:url(#SVGID_48_);fill:#333333;}
.st84{clip-path:url(#SVGID_50_);}
.st85{clip-path:url(#SVGID_52_);fill:#333333;}
.st86{clip-path:url(#SVGID_54_);}
.st87{clip-path:url(#SVGID_56_);fill:#333333;}
.st88{clip-path:url(#SVGID_58_);}
.st89{clip-path:url(#SVGID_60_);fill:#333333;}
.st90{clip-path:url(#SVGID_62_);}
.st91{clip-path:url(#SVGID_64_);}
.st92{clip-path:url(#SVGID_66_);enable-background:new ;}
.st93{clip-path:url(#SVGID_68_);}
.st94{clip-path:url(#SVGID_70_);}
.st95{clip-path:url(#SVGID_72_);fill:#333333;}
.st96{clip-path:url(#SVGID_74_);}
.st97{clip-path:url(#SVGID_76_);}
.st98{clip-path:url(#SVGID_78_);fill:#333333;}
.st99{clip-path:url(#SVGID_80_);}
.st100{clip-path:url(#SVGID_82_);fill:#333333;}
.st101{clip-path:url(#SVGID_84_);}
.st102{clip-path:url(#SVGID_86_);}
.st103{clip-path:url(#SVGID_88_);fill:#333333;}
.st104{clip-path:url(#SVGID_90_);}
.st105{clip-path:url(#SVGID_92_);fill:#333333;}
.st106{clip-path:url(#SVGID_94_);}
.st107{clip-path:url(#SVGID_96_);fill:#333333;}
.st108{clip-path:url(#SVGID_98_);}
.st109{clip-path:url(#SVGID_100_);fill:#333333;}
.st110{opacity:0.8;clip-path:url(#SVGID_102_);fill:#333333;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_3">
</g>
<g id="Layer_4">
</g>
<g id="Layer_1">
<g>
<path d="M46.15,40c-0.85-0.25-1.73-0.14-2.46,0.31c-0.38,0.23-0.69,0.54-0.93,0.9c-0.3,0.46-0.69,0.85-1.15,1.15
c-0.36,0.24-0.67,0.56-0.9,0.93c-0.45,0.73-0.56,1.61-0.31,2.46c0.36,1.24,1.58,2.11,2.96,2.11h1.91c1.71,0,3-1.38,3-3.21v-1.7
C48.25,41.58,47.39,40.36,46.15,40z"/>
<path d="M32.56,41.87h-3.15c-0.92,0-1.78,0.42-2.36,1.14c-0.57,0.73-0.78,1.67-0.55,2.59c0.31,1.31,1.59,2.26,3.03,2.26h3.15
c0.92,0,1.78-0.42,2.36-1.14c0.57-0.73,0.78-1.67,0.55-2.59C35.28,42.82,34,41.87,32.56,41.87z"/>
<path d="M19.47,41.87h-3.15c-0.92,0-1.78,0.42-2.36,1.14c-0.57,0.73-0.78,1.67-0.55,2.59c0.31,1.31,1.59,2.26,3.03,2.26h3.15
c0.92,0,1.78-0.42,2.36-1.14c0.57-0.73,0.78-1.67,0.55-2.59C22.19,42.82,20.91,41.87,19.47,41.87z"/>
<path d="M8.91,42.37c-0.46-0.3-0.85-0.69-1.15-1.15c-0.24-0.36-0.56-0.67-0.93-0.9C6.09,39.86,5.21,39.75,4.36,40
c-1.24,0.36-2.11,1.58-2.11,2.96v1.91c0,1.66,1.34,3,3,3h1.91c1.38,0,2.59-0.87,2.96-2.11c0.25-0.85,0.14-1.73-0.31-2.46
C9.58,42.92,9.27,42.6,8.91,42.37z"/>
<path d="M4.52,35.59c0.24,0.06,0.49,0.09,0.73,0.09c0.67,0,1.32-0.22,1.86-0.64c0.73-0.57,1.14-1.43,1.14-2.36v-3.27
c0-0.92-0.42-1.78-1.14-2.36s-1.67-0.78-2.59-0.56c-1.31,0.32-2.26,1.59-2.26,3.03v3.03C2.25,34,3.2,35.28,4.52,35.59z"/>
<path d="M4.52,22.5c0.24,0.06,0.49,0.09,0.73,0.09c0.67,0,1.32-0.22,1.86-0.64c0.73-0.57,1.14-1.43,1.14-2.36v-3.27
c0-0.92-0.42-1.78-1.14-2.36c-0.73-0.57-1.67-0.78-2.59-0.56C3.2,13.72,2.25,15,2.25,16.44v3.03C2.25,20.91,3.2,22.19,4.52,22.5z"
/>
<path d="M7.16,1.87H5.25c-1.66,0-3,1.34-3,3v1.91c0,1.38,0.87,2.59,2.11,2.96c0.3,0.09,0.6,0.13,0.9,0.13
c0.55,0,1.09-0.15,1.57-0.44c0.38-0.23,0.69-0.54,0.93-0.9c0.3-0.46,0.69-0.85,1.15-1.15c0.36-0.24,0.67-0.56,0.9-0.93
c0.45-0.73,0.56-1.61,0.31-2.46C9.76,2.73,8.54,1.87,7.16,1.87z"/>
<path d="M32.56,1.87h-3.15c-0.92,0-1.78,0.42-2.36,1.14c-0.57,0.73-0.78,1.67-0.55,2.59c0.31,1.31,1.59,2.26,3.03,2.26h3.15
c0.92,0,1.78-0.42,2.36-1.14c0.57-0.73,0.78-1.67,0.55-2.59C35.28,2.82,34,1.87,32.56,1.87z"/>
<path d="M19.47,1.87h-3.15c-0.92,0-1.78,0.42-2.36,1.14c-0.57,0.73-0.78,1.67-0.55,2.59c0.31,1.31,1.59,2.26,3.03,2.26h3.15
c0.92,0,1.78-0.42,2.36-1.14C22.52,6,22.72,5.05,22.5,4.13C22.19,2.82,20.91,1.87,19.47,1.87z"/>
<path d="M45.25,1.87h-1.91c-1.38,0-2.59,0.87-2.96,2.11c-0.25,0.85-0.14,1.73,0.31,2.46c0.23,0.38,0.54,0.69,0.9,0.93
c0.46,0.3,0.85,0.69,1.15,1.15c0.24,0.36,0.56,0.67,0.93,0.9c0.48,0.29,1.02,0.44,1.57,0.44c0.3,0,0.6-0.04,0.9-0.13
c1.24-0.36,2.11-1.58,2.11-2.96V4.87C48.25,3.21,46.91,1.87,45.25,1.87z"/>
<path d="M47.11,27.05c-0.73-0.57-1.67-0.77-2.59-0.56c-1.31,0.32-2.26,1.59-2.26,3.03v3.03c0,1.44,0.95,2.72,2.26,3.03
c0.24,0.06,0.49,0.09,0.73,0.09c0.67,0,1.32-0.22,1.86-0.64c0.73-0.57,1.14-1.43,1.14-2.36v-3.27
C48.25,28.48,47.84,27.63,47.11,27.05z"/>
<path d="M47.11,13.96c-0.73-0.57-1.67-0.78-2.59-0.56c-1.31,0.32-2.26,1.59-2.26,3.03v3.03c0,1.44,0.95,2.72,2.26,3.03
c0.24,0.06,0.49,0.09,0.73,0.09c0.67,0,1.32-0.22,1.86-0.64c0.73-0.57,1.14-1.43,1.14-2.36v-3.27
C48.25,15.39,47.84,14.53,47.11,13.96z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

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

@ -13,8 +13,6 @@ describe LogService do
channels: [],
}.to_json
FakeDeliveryInfo = Struct.new(:routing_key, :device)
let!(:device) { FactoryBot.create(:device) }
let!(:device_id) { device.id }
let!(:fake_delivery_info) do
@ -31,6 +29,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 +58,29 @@ 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
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)