Misc bug fixes and refactoring (#627)
* allow external docs links * match upgrade path test cases * style updates: prefer return < over ( * refactor util * import refactor * fix sequence step scroll * refactor mocked paths * remove unnecessary prop * zoom level checks * misc fixes * sync and estop button fixes * don't sync WebAppConfigspull/628/head
parent
88b6e5625d
commit
29a14e3997
|
@ -53,7 +53,6 @@ module FarmBot
|
|||
default_src: %w(https: 'self'),
|
||||
base_uri: %w('self'),
|
||||
block_all_mixed_content: false, # :( Some webcam feeds use http://
|
||||
child_src: %w('self'),
|
||||
connect_src: ALL_LOCAL_URIS + [ENV["MQTT_HOST"],
|
||||
"api.github.com",
|
||||
"raw.githubusercontent.com",
|
||||
|
@ -68,7 +67,7 @@ module FarmBot
|
|||
fonts.gstatic.com
|
||||
),
|
||||
form_action: %w('self'),
|
||||
frame_ancestors: %w(*), # We need "*" to support webcam users.
|
||||
frame_src: %w(*), # We need "*" to support webcam users.
|
||||
img_src: %w(* data:), # We need "*" to support webcam users.
|
||||
manifest_src: %w('self'),
|
||||
media_src: %w(),
|
||||
|
@ -78,6 +77,7 @@ module FarmBot
|
|||
allow-forms
|
||||
allow-same-origin
|
||||
allow-modals
|
||||
allow-popups
|
||||
),
|
||||
plugin_types: %w(),
|
||||
script_src: %w(
|
||||
|
|
|
@ -47,9 +47,9 @@ describe SessionToken do
|
|||
expect(test_case["0.0.0"]).to eq(CalculateUpgrade::OLD_OS_URL)
|
||||
expect(test_case["5.0.5"]).to eq(CalculateUpgrade::OLD_OS_URL)
|
||||
expect(test_case["5.0.6"]).to eq(CalculateUpgrade::OLD_OS_URL)
|
||||
expect(test_case["5.0.7"]).to eq(CalculateUpgrade::MID_OS_URL)
|
||||
expect(test_case["5.0.8"]).to eq(CalculateUpgrade::MID_OS_URL)
|
||||
expect(test_case["5.1.0"]).to eq(CalculateUpgrade::OS_RELEASE)
|
||||
expect(test_case["5.0.9"]).to eq(CalculateUpgrade::MID_OS_URL)
|
||||
expect(test_case["6.0.1"]).to eq(CalculateUpgrade::OS_RELEASE)
|
||||
end
|
||||
|
||||
it "doesn't honor expired tokens" do
|
||||
|
|
|
@ -6,6 +6,11 @@ jest.mock("react-redux", () => ({
|
|||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
let mockPath = "";
|
||||
jest.mock("../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { App, AppProps } from "../app";
|
||||
import { mount } from "enzyme";
|
||||
|
@ -29,9 +34,7 @@ describe("<App />: Controls Pop-Up", () => {
|
|||
|
||||
function controlsPopUp(page: string, exists: boolean) {
|
||||
it(`doesn't render controls pop-up on ${page} page`, () => {
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/" + page, configurable: true
|
||||
});
|
||||
mockPath = "/app/" + page;
|
||||
const wrapper = mount(<App {...fakeProps() } />);
|
||||
if (exists) {
|
||||
expect(wrapper.html()).toContain("controls-popup");
|
||||
|
|
|
@ -2,13 +2,11 @@ jest.mock("fastclick", () => ({
|
|||
attach: jest.fn(),
|
||||
}));
|
||||
|
||||
import { auth } from "../__test_support__/fake_state/token";
|
||||
const mockAuth = auth;
|
||||
let mockAuth: AuthState | undefined = undefined;
|
||||
const mockClear = jest.fn();
|
||||
jest.mock("../session", () => ({
|
||||
Session: {
|
||||
fetchStoredToken: jest.fn(() => mockAuth)
|
||||
.mockImplementationOnce(() => { }),
|
||||
fetchStoredToken: jest.fn(() => mockAuth),
|
||||
deprecatedGetNum: () => undefined,
|
||||
deprecatedGetBool: () => undefined,
|
||||
getAll: () => undefined,
|
||||
|
@ -20,6 +18,8 @@ import * as React from "react";
|
|||
import { shallow } from "enzyme";
|
||||
import { RootComponent } from "../routes";
|
||||
import { store } from "../redux/store";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
import { auth } from "../__test_support__/fake_state/token";
|
||||
|
||||
describe("<RootComponent />", () => {
|
||||
beforeEach(function () {
|
||||
|
@ -27,6 +27,7 @@ describe("<RootComponent />", () => {
|
|||
});
|
||||
|
||||
it("clears session when not authorized", () => {
|
||||
mockAuth = undefined;
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/account"
|
||||
});
|
||||
|
@ -35,6 +36,7 @@ describe("<RootComponent />", () => {
|
|||
});
|
||||
|
||||
it("authorized", () => {
|
||||
mockAuth = auth;
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/account"
|
||||
});
|
||||
|
|
|
@ -1,301 +0,0 @@
|
|||
import {
|
||||
prettyPrintApiErrors,
|
||||
defensiveClone,
|
||||
getParam,
|
||||
betterCompact,
|
||||
safeStringFetch,
|
||||
oneOf,
|
||||
semverCompare,
|
||||
SemverResult,
|
||||
trim,
|
||||
bitArray,
|
||||
withTimeout,
|
||||
minFwVersionCheck,
|
||||
move,
|
||||
shortRevision,
|
||||
clampUnsignedInteger,
|
||||
isUndefined,
|
||||
IntegerSize,
|
||||
Progress,
|
||||
colors,
|
||||
randomColor
|
||||
} from "../util";
|
||||
import { times } from "lodash";
|
||||
describe("util", () => {
|
||||
describe("safeStringFetch", () => {
|
||||
const data = {
|
||||
// tslint:disable-next-line:no-null-keyword
|
||||
"null": null,
|
||||
"undefined": undefined,
|
||||
"number": 0,
|
||||
"string": "hello",
|
||||
"boolean": false,
|
||||
"other": () => { "not allowed!"; }
|
||||
};
|
||||
|
||||
it("fetches null", () => {
|
||||
expect(safeStringFetch(data, "null")).toEqual("");
|
||||
});
|
||||
|
||||
it("fetches undefined", () => {
|
||||
expect(safeStringFetch(data, "undefined")).toEqual("");
|
||||
});
|
||||
|
||||
it("fetches number", () => {
|
||||
expect(safeStringFetch(data, "number")).toEqual("0");
|
||||
});
|
||||
|
||||
it("fetches string", () => {
|
||||
expect(safeStringFetch(data, "string")).toEqual("hello");
|
||||
});
|
||||
|
||||
it("fetches boolean", () => {
|
||||
expect(safeStringFetch(data, "boolean")).toEqual("false");
|
||||
});
|
||||
|
||||
it("handles others with exception", () => {
|
||||
expect(() => safeStringFetch(data, "other")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("betterCompact", () => {
|
||||
it("removes falsy values", () => {
|
||||
const before = [{}, {}, undefined];
|
||||
const after = betterCompact(before);
|
||||
expect(after.length).toBe(2);
|
||||
expect(after).not.toContain(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("defensiveClone", () => {
|
||||
it("deep clones any serializable object", () => {
|
||||
const origin = { a: "b", c: 2, d: [{ e: { f: "g" } }] };
|
||||
const child = defensiveClone(origin);
|
||||
origin.a = "--";
|
||||
origin.c = 0;
|
||||
origin.d[0].e.f = "--";
|
||||
expect(child).not.toBe(origin);
|
||||
expect(child.a).toEqual("b");
|
||||
expect(child.c).toEqual(2);
|
||||
expect(child.d[0].e.f).toEqual("g");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getParam", () => {
|
||||
it("gets params", () => {
|
||||
Object.defineProperty(window.location, "search", {
|
||||
writable: true,
|
||||
value: "?foo=bar&baz=wow"
|
||||
});
|
||||
expect(getParam("foo")).toEqual("bar");
|
||||
expect(getParam("baz")).toEqual("wow");
|
||||
expect(getParam("blah")).toEqual("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("prettyPrintApiErrors", () => {
|
||||
it("handles properly formatted API error messages", () => {
|
||||
const result = prettyPrintApiErrors({
|
||||
response: {
|
||||
data: {
|
||||
email: "can't be blank"
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(result).toEqual("Email: can't be blank");
|
||||
});
|
||||
});
|
||||
|
||||
describe("oneOf()", () => {
|
||||
it("determines matches", () => {
|
||||
expect(oneOf(["foo"], "foobar")).toBeTruthy();
|
||||
expect(oneOf(["foo", "baz"], "foo bar baz")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("determines non-matches", () => {
|
||||
expect(oneOf(["foo"], "QMMADSDASDASD")).toBeFalsy();
|
||||
expect(oneOf(["foo", "baz"], "nothing to see here.")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("semver compare", () => {
|
||||
it("knows when RIGHT_IS_GREATER", () => {
|
||||
expect(semverCompare("3.1.6", "4.0.0"))
|
||||
.toBe(SemverResult.RIGHT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("2.1.6", "4.1.0"))
|
||||
.toBe(SemverResult.RIGHT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("4.1.6", "5.1.9"))
|
||||
.toBe(SemverResult.RIGHT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("1.1.9", "2.0.2"))
|
||||
.toBe(SemverResult.RIGHT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("", "1.0.0"))
|
||||
.toBe(SemverResult.RIGHT_IS_GREATER);
|
||||
});
|
||||
|
||||
it("knows when LEFT_IS_GREATER", () => {
|
||||
expect(semverCompare("4.0.0", "3.1.6"))
|
||||
.toBe(SemverResult.LEFT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("4.1.0", "2.1.6"))
|
||||
.toBe(SemverResult.LEFT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("5.1.9", "4.1.6"))
|
||||
.toBe(SemverResult.LEFT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("2.0.2", "1.1.9"))
|
||||
.toBe(SemverResult.LEFT_IS_GREATER);
|
||||
|
||||
expect(semverCompare("1.0.0", ""))
|
||||
.toBe(SemverResult.LEFT_IS_GREATER);
|
||||
expect(semverCompare("1.0.0", "x.y.z"))
|
||||
.toBe(SemverResult.LEFT_IS_GREATER);
|
||||
expect(semverCompare("x.y.z", "1.0.0"))
|
||||
.toBe(SemverResult.RIGHT_IS_GREATER);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("trim()", () => {
|
||||
it("formats whitespace", () => {
|
||||
const string = `foo
|
||||
bar`;
|
||||
const formattedString = trim(string);
|
||||
expect(formattedString).toEqual("foo bar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bitArray", () => {
|
||||
it("converts flags to numbers", () => {
|
||||
expect(bitArray(true)).toBe(0b1);
|
||||
expect(bitArray(true, false)).toBe(0b10);
|
||||
expect(bitArray(false, true)).toBe(0b01);
|
||||
expect(bitArray(true, true)).toBe(0b11);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withTimeout()", () => {
|
||||
it("rejects promises that do not meet a particular deadline", (done) => {
|
||||
const p = new Promise(res => setTimeout(() => res("Done"), 10));
|
||||
withTimeout(1, p).then(fail, (y) => {
|
||||
expect(y).toContain("Timed out");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves promises that meet a particular deadline", (done) => {
|
||||
withTimeout(10, new Promise(res => setTimeout(() => res("Done"), 1)))
|
||||
.then(y => {
|
||||
expect(y).toContain("Done");
|
||||
done();
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe("minFwVersionCheck()", () => {
|
||||
it("firmware version meets or exceeds minimum", () => {
|
||||
expect(minFwVersionCheck("1.0.1R", "1.0.1")).toBeTruthy();
|
||||
expect(minFwVersionCheck("1.0.2F", "1.0.1")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("firmware version doesn't meet minimum", () => {
|
||||
expect(minFwVersionCheck("1.0.0R", "1.0.1")).toBeFalsy();
|
||||
expect(minFwVersionCheck(undefined, "1.0.1")).toBeFalsy();
|
||||
expect(minFwVersionCheck("1.0.0", "1.0.1")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("move()", () => {
|
||||
it("shuffles array elems", () => {
|
||||
const fixture = [0, 1, 2];
|
||||
const case1 = move(fixture, 0, 2);
|
||||
expect(case1[0]).toEqual(1);
|
||||
expect(case1[1]).toEqual(2);
|
||||
expect(case1[2]).toEqual(0);
|
||||
|
||||
const case2 = move(fixture, 1, 0);
|
||||
expect(case2[0]).toEqual(1);
|
||||
expect(case2[1]).toEqual(0);
|
||||
expect(case2[2]).toEqual(2);
|
||||
|
||||
const case3 = move(fixture, 0, 0);
|
||||
expect(case3).toEqual(fixture);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortRevision()", () => {
|
||||
it("none", () => {
|
||||
globalConfig.SHORT_REVISION = "";
|
||||
const short = shortRevision();
|
||||
expect(short).toEqual("NONE");
|
||||
});
|
||||
|
||||
it("slices", () => {
|
||||
globalConfig.SHORT_REVISION = "0123456789";
|
||||
const short = shortRevision();
|
||||
expect(short).toEqual("01234567");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clampUnsignedInteger()", () => {
|
||||
function clampTest(
|
||||
input: string,
|
||||
output: number | undefined,
|
||||
message: string,
|
||||
size: IntegerSize) {
|
||||
it(`${size}: ${message}`, () => {
|
||||
const result = clampUnsignedInteger(input, size);
|
||||
expect(result.outcome).toEqual(message);
|
||||
expect(result.result).toEqual(output);
|
||||
});
|
||||
}
|
||||
clampTest("nope", undefined, "malformed", "short");
|
||||
clampTest("100000", 32000, "high", "short");
|
||||
clampTest("-100000", 0, "low", "short");
|
||||
clampTest("1000", 1000, "ok", "short");
|
||||
clampTest("1000000", 1000000, "ok", "long");
|
||||
clampTest("-1000000", 0, "low", "long");
|
||||
});
|
||||
|
||||
describe("isUndefined()", () => {
|
||||
it("undefined", () => {
|
||||
const result = isUndefined(undefined);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("defined", () => {
|
||||
const result = isUndefined({});
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Progress", () => {
|
||||
it("increments", () => {
|
||||
const cb = jest.fn();
|
||||
const counter = new Progress(3, cb);
|
||||
counter.inc();
|
||||
expect(cb).toHaveBeenCalledWith(counter);
|
||||
counter.inc();
|
||||
counter.inc(); // Now we're done.
|
||||
cb.mockClear();
|
||||
counter.inc();
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("force finishes", () => {
|
||||
const cb = jest.fn();
|
||||
const counter = new Progress(3, cb);
|
||||
counter.finish();
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(counter.isDone).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("randomColor()", () => {
|
||||
it("only picks valid colors", () => {
|
||||
times(colors.length * 1.5, () => expect(colors).toContain(randomColor()));
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@ import {
|
|||
WidgetHeader,
|
||||
WidgetBody,
|
||||
SaveBtn
|
||||
} from "../../ui";
|
||||
} from "../../ui/index";
|
||||
import { SpecialStatus } from "../../resources/tagged_resources";
|
||||
import Axios from "axios";
|
||||
import { API } from "../../api/index";
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
WidgetBody,
|
||||
Col,
|
||||
Row
|
||||
} from "../../ui";
|
||||
} from "../../ui/index";
|
||||
import { DeleteAccountProps, DeleteAccountState } from "../interfaces";
|
||||
import { Content } from "../../constants";
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn } from "../../ui";
|
||||
import {
|
||||
BlurableInput, Widget, WidgetHeader, WidgetBody, SaveBtn
|
||||
} from "../../ui/index";
|
||||
import { SettingsPropTypes } from "../interfaces";
|
||||
|
||||
export class Settings extends React.Component<SettingsPropTypes, {}> {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { t } from "i18next";
|
|||
import { connect } from "react-redux";
|
||||
import { Settings, DeleteAccount, ChangePassword } from "./components";
|
||||
import { Props } from "./interfaces";
|
||||
import { Page, Row, Col } from "../ui";
|
||||
import { Page, Row, Col } from "../ui/index";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { User } from "../auth/interfaces";
|
||||
import { edit, save } from "../api/crud";
|
||||
|
|
|
@ -8,7 +8,9 @@ import {
|
|||
import { GetState, ReduxAction } from "../redux/interfaces";
|
||||
import { API } from "./index";
|
||||
import axios from "axios";
|
||||
import { updateOK, updateNO, destroyOK, destroyNO, GeneralizedError } from "../resources/actions";
|
||||
import {
|
||||
updateOK, updateNO, destroyOK, destroyNO, GeneralizedError
|
||||
} from "../resources/actions";
|
||||
import { UnsafeError } from "../interfaces";
|
||||
import { findByUuid } from "../resources/reducer";
|
||||
import { generateUuid } from "../resources/util";
|
||||
|
|
|
@ -5,7 +5,8 @@ const BLACKLIST: ResourceName[] = [
|
|||
"Log",
|
||||
"Image",
|
||||
"WebcamFeed",
|
||||
"User"
|
||||
"User",
|
||||
"WebAppConfig",
|
||||
];
|
||||
|
||||
export function maybeStartTracking(uuid: string) {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Content } from "./constants";
|
|||
import { catchErrors } from "./util";
|
||||
import { Session } from "./session";
|
||||
import { BooleanSetting } from "./session_keys";
|
||||
import { getPathArray } from "./history";
|
||||
|
||||
/** Remove 300ms delay on touch devices - https://github.com/ftlabs/fastclick */
|
||||
const fastClick = require("fastclick");
|
||||
|
@ -99,7 +100,7 @@ export class App extends React.Component<AppProps, {}> {
|
|||
|
||||
render() {
|
||||
const syncLoaded = this.isLoaded;
|
||||
const currentPath = window.location.pathname;
|
||||
const currentPage = getPathArray()[2];
|
||||
return <div className="app">
|
||||
<HotKeys dispatch={this.props.dispatch} />
|
||||
<NavBar
|
||||
|
@ -113,9 +114,7 @@ export class App extends React.Component<AppProps, {}> {
|
|||
/>
|
||||
{!syncLoaded && <LoadingPlant />}
|
||||
{syncLoaded && this.props.children}
|
||||
{!currentPath.startsWith("/app/controls") &&
|
||||
!currentPath.startsWith("/app/account") &&
|
||||
!currentPath.startsWith("/app/regimens") &&
|
||||
{!(["controls", "account", "regimens"].includes(currentPage)) &&
|
||||
<ControlsPopup
|
||||
dispatch={this.props.dispatch}
|
||||
axisInversion={this.props.axisInversion}
|
||||
|
|
|
@ -19,7 +19,9 @@ jest.mock("../../redux/store", () => {
|
|||
import { getDevice } from "../../device";
|
||||
import { store } from "../../redux/store";
|
||||
import { Actions } from "../../constants";
|
||||
import { startTracking, outstandingRequests, stopTracking, cleanUUID } from "../data_consistency";
|
||||
import {
|
||||
startTracking, outstandingRequests, stopTracking, cleanUUID
|
||||
} from "../data_consistency";
|
||||
|
||||
const unprocessedUuid = "~UU.ID~";
|
||||
const niceUuid = cleanUUID(unprocessedUuid);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { GetState } from "../redux/interfaces";
|
||||
import { maybeDetermineUuid } from "../resources/selectors";
|
||||
import { ResourceName, TaggedResource, SpecialStatus } from "../resources/tagged_resources";
|
||||
import {
|
||||
ResourceName, TaggedResource, SpecialStatus
|
||||
} from "../resources/tagged_resources";
|
||||
import { overwrite, init } from "../api/crud";
|
||||
import { handleInbound } from "./auto_sync_handle_inbound";
|
||||
import { SyncPayload, MqttDataResult, Reason, UpdateMqttData } from "./interfaces";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { Row, Col } from "../ui";
|
||||
import { Row, Col } from "../ui/index";
|
||||
import { AxisDisplayGroupProps } from "./interfaces";
|
||||
import { isNumber } from "lodash";
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { AxisInputBox } from "./axis_input_box";
|
||||
import { t } from "i18next";
|
||||
import { Row, Col } from "../ui";
|
||||
import { Row, Col } from "../ui/index";
|
||||
import {
|
||||
AxisInputBoxGroupProps,
|
||||
AxisInputBoxGroupState,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Peripherals } from "./peripherals";
|
||||
import { Row, Page, Col } from "../ui";
|
||||
import { Row, Page, Col } from "../ui/index";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { WebcamPanel } from "./webcam";
|
||||
import { Props, MoveProps } from "./interfaces";
|
||||
|
|
|
@ -4,7 +4,7 @@ import { changeStepSize, moveAbs } from "../devices/actions";
|
|||
import { EStopButton } from "../devices/components/e_stop_btn";
|
||||
import { JogButtons } from "./jog_buttons";
|
||||
import { AxisInputBoxGroup } from "./axis_input_box_group";
|
||||
import { Row, Col, Widget, WidgetBody, WidgetHeader } from "../ui";
|
||||
import { Row, Col, Widget, WidgetBody, WidgetHeader } from "../ui/index";
|
||||
import { StepSizeSelector } from "./step_size_selector";
|
||||
import { MustBeOnline } from "../devices/must_be_online";
|
||||
import { ToolTips } from "../constants";
|
||||
|
@ -52,113 +52,111 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
? "Scaled Encoder (mm)"
|
||||
: "Scaled Encoder (steps)";
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetHeader
|
||||
title="Move"
|
||||
helpText={ToolTips.MOVE}>
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
<div>
|
||||
<label>
|
||||
{t("Invert Jog Buttons")}
|
||||
</label>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("X Axis")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + xBtnColor}
|
||||
onClick={this.toggle("x")} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Y Axis")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + yBtnColor}
|
||||
onClick={this.toggle("y")} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Z Axis")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + zBtnColor}
|
||||
onClick={this.toggle("z")} />
|
||||
</fieldset>
|
||||
<label>
|
||||
{t("Display Encoder Data")}
|
||||
</label>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Scaled encoder position")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + scaledBtnColor}
|
||||
onClick={this.toggle_encoder_data("scaled_encoders")} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Raw encoder position")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + rawBtnColor}
|
||||
onClick={this.toggle_encoder_data("raw_encoders")} />
|
||||
</fieldset>
|
||||
</div>
|
||||
</Popover>
|
||||
<EStopButton
|
||||
bot={this.props.bot}
|
||||
user={this.props.user} />
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<MustBeOnline
|
||||
lockOpen={process.env.NODE_ENV !== "production"}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
syncStatus={this.props.bot.hardware.informational_settings.sync_status}>
|
||||
<label className="text-center">
|
||||
{t("MOVE AMOUNT (mm)")}
|
||||
return <Widget>
|
||||
<WidgetHeader
|
||||
title="Move"
|
||||
helpText={ToolTips.MOVE}>
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
<div>
|
||||
<label>
|
||||
{t("Invert Jog Buttons")}
|
||||
</label>
|
||||
<StepSizeSelector
|
||||
choices={[1, 10, 100, 1000, 10000]}
|
||||
selector={num => this.props.dispatch(changeStepSize(num))}
|
||||
selected={this.props.bot.stepSize} />
|
||||
<JogButtons
|
||||
bot={this.props.bot}
|
||||
x_axis_inverted={x_axis_inverted}
|
||||
y_axis_inverted={y_axis_inverted}
|
||||
z_axis_inverted={z_axis_inverted}
|
||||
disabled={this.props.disabled} />
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<label>{t("X AXIS")}</label>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<label>{t("Y AXIS")}</label>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<label>{t("Z AXIS")}</label>
|
||||
</Col>
|
||||
</Row>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("X Axis")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + xBtnColor}
|
||||
onClick={this.toggle("x")} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Y Axis")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + yBtnColor}
|
||||
onClick={this.toggle("y")} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Z Axis")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + zBtnColor}
|
||||
onClick={this.toggle("z")} />
|
||||
</fieldset>
|
||||
<label>
|
||||
{t("Display Encoder Data")}
|
||||
</label>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Scaled encoder position")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + scaledBtnColor}
|
||||
onClick={this.toggle_encoder_data("scaled_encoders")} />
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
{t("Raw encoder position")}
|
||||
</label>
|
||||
<button
|
||||
className={"fb-button fb-toggle-button " + rawBtnColor}
|
||||
onClick={this.toggle_encoder_data("raw_encoders")} />
|
||||
</fieldset>
|
||||
</div>
|
||||
</Popover>
|
||||
<EStopButton
|
||||
bot={this.props.bot}
|
||||
user={this.props.user} />
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<MustBeOnline
|
||||
lockOpen={process.env.NODE_ENV !== "production"}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
syncStatus={this.props.bot.hardware.informational_settings.sync_status}>
|
||||
<label className="text-center">
|
||||
{t("MOVE AMOUNT (mm)")}
|
||||
</label>
|
||||
<StepSizeSelector
|
||||
choices={[1, 10, 100, 1000, 10000]}
|
||||
selector={num => this.props.dispatch(changeStepSize(num))}
|
||||
selected={this.props.bot.stepSize} />
|
||||
<JogButtons
|
||||
bot={this.props.bot}
|
||||
x_axis_inverted={x_axis_inverted}
|
||||
y_axis_inverted={y_axis_inverted}
|
||||
z_axis_inverted={z_axis_inverted}
|
||||
disabled={this.props.disabled} />
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<label>{t("X AXIS")}</label>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<label>{t("Y AXIS")}</label>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
<label>{t("Z AXIS")}</label>
|
||||
</Col>
|
||||
</Row>
|
||||
<AxisDisplayGroup
|
||||
position={motor_coordinates}
|
||||
label={"Motor Coordinates (mm)"} />
|
||||
{scaled_encoders &&
|
||||
<AxisDisplayGroup
|
||||
position={motor_coordinates}
|
||||
label={"Motor Coordinates (mm)"} />
|
||||
{scaled_encoders &&
|
||||
<AxisDisplayGroup
|
||||
position={scaled_encoders_data}
|
||||
label={scaled_encoder_label} />}
|
||||
{raw_encoders &&
|
||||
<AxisDisplayGroup
|
||||
position={raw_encoders_data}
|
||||
label={"Raw Encoder data"} />}
|
||||
<AxisInputBoxGroup
|
||||
position={motor_coordinates}
|
||||
onCommit={input => moveAbs(input)}
|
||||
disabled={this.props.disabled} />
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
position={scaled_encoders_data}
|
||||
label={scaled_encoder_label} />}
|
||||
{raw_encoders &&
|
||||
<AxisDisplayGroup
|
||||
position={raw_encoders_data}
|
||||
label={"Raw Encoder data"} />}
|
||||
<AxisInputBoxGroup
|
||||
position={motor_coordinates}
|
||||
onCommit={input => moveAbs(input)}
|
||||
disabled={this.props.disabled} />
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,12 @@ import { t } from "i18next";
|
|||
import { error } from "farmbot-toastr";
|
||||
import { PeripheralList } from "./peripheral_list";
|
||||
import { PeripheralForm } from "./peripheral_form";
|
||||
import { Widget, WidgetBody, WidgetHeader, SaveBtn } from "../../ui";
|
||||
import { Widget, WidgetBody, WidgetHeader, SaveBtn } from "../../ui/index";
|
||||
import { PeripheralsProps } from "../../devices/interfaces";
|
||||
import { PeripheralState } from "./interfaces";
|
||||
import { TaggedPeripheral, getArrayStatus, SpecialStatus } from "../../resources/tagged_resources";
|
||||
import {
|
||||
TaggedPeripheral, getArrayStatus, SpecialStatus
|
||||
} from "../../resources/tagged_resources";
|
||||
import { saveAll, init } from "../../api/crud";
|
||||
import { ToolTips } from "../../constants";
|
||||
import * as _ from "lodash";
|
||||
|
|
|
@ -42,13 +42,11 @@ export class ToggleButton extends React.Component<ToggleButtonProps, {}> {
|
|||
|
||||
render() {
|
||||
const cb = () => !this.props.disabled && this.props.toggleAction();
|
||||
return (
|
||||
<button
|
||||
disabled={!!this.props.disabled}
|
||||
className={this.css()}
|
||||
onClick={cb}>
|
||||
{this.caption()}
|
||||
</button>
|
||||
);
|
||||
return <button
|
||||
disabled={!!this.props.disabled}
|
||||
className={this.css()}
|
||||
onClick={cb}>
|
||||
{this.caption()}
|
||||
</button>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,28 +27,26 @@ export function Edit(props: WebcamPanelProps) {
|
|||
const unsaved = props
|
||||
.feeds
|
||||
.filter(x => x.specialStatus === SpecialStatus.DIRTY);
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetHeader title="Edit" helpText={ToolTips.WEBCAM}>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={props.init}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={() => { unsaved.map(x => props.save(x)); }}>
|
||||
{t("Save")}{unsaved.length > 0 ? "*" : ""}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={props.onToggle}>
|
||||
{t("View")}
|
||||
</button>
|
||||
</WidgetHeader>
|
||||
<div className="widget-body">
|
||||
{rows}
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
return <Widget>
|
||||
<WidgetHeader title="Edit" helpText={ToolTips.WEBCAM}>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={props.init}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={() => { unsaved.map(x => props.save(x)); }}>
|
||||
{t("Save")}{unsaved.length > 0 ? "*" : ""}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={props.onToggle}>
|
||||
{t("View")}
|
||||
</button>
|
||||
</WidgetHeader>
|
||||
<div className="widget-body">
|
||||
{rows}
|
||||
</div>
|
||||
</Widget>;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import * as React from "react";
|
||||
import { Widget, WidgetHeader } from "../../ui/index";
|
||||
import { Widget, WidgetHeader, FallbackImg } from "../../ui/index";
|
||||
import { t } from "i18next";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { WebcamPanelProps } from "./interfaces";
|
||||
import { PLACEHOLDER_FARMBOT } from "../../farmware/images/image_flipper";
|
||||
import { Flipper } from "./flipper";
|
||||
import { FallbackImg } from "../../ui/fallback_img";
|
||||
import { sortedFeeds } from "./edit";
|
||||
|
||||
type State = {
|
||||
|
@ -54,41 +53,39 @@ export class Show extends React.Component<WebcamPanelProps, State> {
|
|||
const title = flipper.current.name || "Webcam Feeds";
|
||||
const msg = this.getMessage(flipper.current.url);
|
||||
const imageClass = msg.length > 0 ? "no-flipper-image-container" : "";
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetHeader title={title} helpText={ToolTips.WEBCAM}>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={props.onToggle}>
|
||||
{t("Edit")}
|
||||
</button>
|
||||
<IndexIndicator i={this.state.current} total={feeds.length} />
|
||||
</WidgetHeader>
|
||||
<div className="widget-body">
|
||||
<div className="image-flipper">
|
||||
<div className={imageClass}>
|
||||
<p>{msg}</p>
|
||||
<FallbackImg className="image-flipper-image"
|
||||
src={flipper.current.url}
|
||||
fallback={PLACEHOLDER_FARMBOT} />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => flipper.down((_, current) => this.setState({ current }))}
|
||||
hidden={feeds.length < 2}
|
||||
disabled={false}
|
||||
className="image-flipper-left fb-button">
|
||||
{t("Prev")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => flipper.up((_, current) => this.setState({ current }))}
|
||||
hidden={feeds.length < 2}
|
||||
disabled={false}
|
||||
className="image-flipper-right fb-button">
|
||||
{t("Next")}
|
||||
</button>
|
||||
return <Widget>
|
||||
<WidgetHeader title={title} helpText={ToolTips.WEBCAM}>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={props.onToggle}>
|
||||
{t("Edit")}
|
||||
</button>
|
||||
<IndexIndicator i={this.state.current} total={feeds.length} />
|
||||
</WidgetHeader>
|
||||
<div className="widget-body">
|
||||
<div className="image-flipper">
|
||||
<div className={imageClass}>
|
||||
<p>{msg}</p>
|
||||
<FallbackImg className="image-flipper-image"
|
||||
src={flipper.current.url}
|
||||
fallback={PLACEHOLDER_FARMBOT} />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => flipper.down((_, current) => this.setState({ current }))}
|
||||
hidden={feeds.length < 2}
|
||||
disabled={false}
|
||||
className="image-flipper-left fb-button">
|
||||
{t("Prev")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => flipper.up((_, current) => this.setState({ current }))}
|
||||
hidden={feeds.length < 2}
|
||||
disabled={false}
|
||||
className="image-flipper-right fb-button">
|
||||
{t("Next")}
|
||||
</button>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
</div>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,16 +29,10 @@ jest.mock("farmbot-toastr", () => ({
|
|||
error: mockError
|
||||
}));
|
||||
|
||||
let mockGetRelease: Promise<{}> = Promise.resolve({});
|
||||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
get: jest.fn(() => { return Promise.reject("error"); })
|
||||
.mockImplementationOnce(() => { return Promise.resolve(); })
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({ data: { tag_name: "v1.0.0" } });
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({ data: { tag_name: "v1.0.0-beta" } });
|
||||
})
|
||||
get: jest.fn(() => { return mockGetRelease; })
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -260,6 +254,7 @@ describe("fetchReleases()", () => {
|
|||
});
|
||||
|
||||
it("fetches latest OS release version", async () => {
|
||||
mockGetRelease = Promise.resolve({ data: { tag_name: "v1.0.0" } });
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchReleases("url")(dispatch, jest.fn());
|
||||
expect(axios.get).toHaveBeenCalledWith("url");
|
||||
|
@ -271,6 +266,7 @@ describe("fetchReleases()", () => {
|
|||
});
|
||||
|
||||
it("fetches latest beta OS release version", async () => {
|
||||
mockGetRelease = Promise.resolve({ data: { tag_name: "v1.0.0-beta" } });
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchReleases("url", { beta: true })(dispatch, jest.fn());
|
||||
expect(axios.get).toHaveBeenCalledWith("url");
|
||||
|
@ -282,6 +278,7 @@ describe("fetchReleases()", () => {
|
|||
});
|
||||
|
||||
it("fails to fetches latest OS release version", async () => {
|
||||
mockGetRelease = Promise.reject("error");
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchReleases("url")(dispatch, jest.fn());
|
||||
await expect(axios.get).toHaveBeenCalledWith("url");
|
||||
|
@ -294,6 +291,7 @@ describe("fetchReleases()", () => {
|
|||
});
|
||||
|
||||
it("fails to fetches latest beta OS release version", async () => {
|
||||
mockGetRelease = Promise.reject("error");
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchReleases("url", { beta: true })(dispatch, jest.fn());
|
||||
await expect(axios.get).toHaveBeenCalledWith("url");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { MustBeOnline } from "../must_be_online";
|
||||
import { MustBeOnline, isBotUp } from "../must_be_online";
|
||||
|
||||
describe("<MustBeOnline/>", function () {
|
||||
it("Covers content when status is 'unknown'", function () {
|
||||
|
@ -29,3 +29,15 @@ describe("<MustBeOnline/>", function () {
|
|||
expect(overlay.hasClass("banner")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isBotUp()", () => {
|
||||
it("is up", () => {
|
||||
expect(isBotUp("synced")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is not up", () => {
|
||||
expect(isBotUp("unknown")).toBeFalsy();
|
||||
expect(isBotUp("maintenance")).toBeFalsy();
|
||||
expect(isBotUp(undefined)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { EStopButton } from "../e_stop_btn";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { taggedUser } from "../../../__test_support__/user";
|
||||
|
||||
describe("<EStopButton />", () => {
|
||||
it("renders", () => {
|
||||
bot.hardware.informational_settings.sync_status = "synced";
|
||||
const wrapper = mount(<EStopButton bot={bot} user={taggedUser} />);
|
||||
expect(wrapper.text()).toEqual("E-STOP");
|
||||
expect(wrapper.find("button").hasClass("red")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("grayed out", () => {
|
||||
bot.hardware.informational_settings.sync_status = undefined;
|
||||
const wrapper = mount(<EStopButton bot={bot} user={taggedUser} />);
|
||||
expect(wrapper.text()).toEqual("E-STOP");
|
||||
expect(wrapper.find("button").hasClass("gray")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("locked", () => {
|
||||
bot.hardware.informational_settings.sync_status = "synced";
|
||||
bot.hardware.informational_settings.locked = true;
|
||||
const wrapper = mount(<EStopButton bot={bot} user={taggedUser} />);
|
||||
expect(wrapper.text()).toEqual("UNLOCK");
|
||||
expect(wrapper.find("button").hasClass("yellow")).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,10 +1,7 @@
|
|||
let mockReleaseNoteData = {};
|
||||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
get: jest.fn(() => { return Promise.resolve({ data: "notes" }); })
|
||||
.mockImplementationOnce(() => { return Promise.resolve(); })
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({ data: "intro\n\n# v6\n\n* note" });
|
||||
})
|
||||
get: jest.fn(() => { return Promise.resolve(mockReleaseNoteData); })
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -12,9 +9,7 @@ import * as React from "react";
|
|||
import { FarmbotOsSettings } from "../farmbot_os_settings";
|
||||
import { mount } from "enzyme";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { fakeResource } from "../../../__test_support__/fake_resource";
|
||||
import { AuthState } from "../../../auth/interfaces";
|
||||
import { FbosDetails } from "../fbos_settings/farmbot_os_row";
|
||||
import { FarmbotOsProps } from "../../interfaces";
|
||||
import axios from "axios";
|
||||
|
@ -25,7 +20,6 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
account: fakeResource("Device", { id: 0, name: "", tz_offset_hrs: 0 }),
|
||||
dispatch: jest.fn(),
|
||||
bot: bot,
|
||||
auth: fakeState().auth as AuthState,
|
||||
botToMqttStatus: "up"
|
||||
};
|
||||
}
|
||||
|
@ -39,6 +33,7 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
});
|
||||
|
||||
it("fetches OS release notes", async () => {
|
||||
mockReleaseNoteData = { data: "intro\n\n# v6\n\n* note" };
|
||||
const osSettings = await mount(<FarmbotOsSettings {...fakeProps() } />);
|
||||
await expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("RELEASE_NOTES.md"));
|
||||
|
@ -47,6 +42,7 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
});
|
||||
|
||||
it("doesn't fetch OS release notes", async () => {
|
||||
mockReleaseNoteData = { data: "empty notes" };
|
||||
const osSettings = await mount(<FarmbotOsSettings {...fakeProps() } />);
|
||||
await expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("RELEASE_NOTES.md"));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { RpiGpioDiagram, RpiGpioDiagramProps } from "../rpi_gpio_diagram";
|
||||
import { Color } from "../../../ui/colors";
|
||||
import { Color } from "../../../ui/index";
|
||||
|
||||
describe("<RpiGpioDiagram />", () => {
|
||||
function fakeProps(): RpiGpioDiagramProps {
|
||||
|
|
|
@ -23,34 +23,32 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
|
|||
|
||||
const { mcu_params } = bot.hardware;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<label>
|
||||
{name}
|
||||
{caution &&
|
||||
<i className="fa fa-exclamation-triangle caution-icon" />}
|
||||
</label>
|
||||
<SpacePanelToolTip tooltip={tooltip} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<ToggleButton
|
||||
disabled={disableX}
|
||||
toggleValue={mcu_params[x]}
|
||||
toggleAction={() => settingToggle(x, bot, displayAlert)} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<ToggleButton
|
||||
disabled={disableY}
|
||||
toggleValue={mcu_params[y]}
|
||||
toggleAction={() => settingToggle(y, bot, displayAlert)} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<ToggleButton
|
||||
disabled={disableZ}
|
||||
toggleValue={mcu_params[z]}
|
||||
toggleAction={() => settingToggle(z, bot, displayAlert)} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
return <Row>
|
||||
<Col xs={6}>
|
||||
<label>
|
||||
{name}
|
||||
{caution &&
|
||||
<i className="fa fa-exclamation-triangle caution-icon" />}
|
||||
</label>
|
||||
<SpacePanelToolTip tooltip={tooltip} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<ToggleButton
|
||||
disabled={disableX}
|
||||
toggleValue={mcu_params[x]}
|
||||
toggleAction={() => settingToggle(x, bot, displayAlert)} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<ToggleButton
|
||||
disabled={disableY}
|
||||
toggleValue={mcu_params[y]}
|
||||
toggleAction={() => settingToggle(y, bot, displayAlert)} />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<ToggleButton
|
||||
disabled={disableZ}
|
||||
toggleValue={mcu_params[z]}
|
||||
toggleAction={() => settingToggle(z, bot, displayAlert)} />
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
||||
|
|
|
@ -2,13 +2,15 @@ import * as React from "react";
|
|||
import { t } from "i18next";
|
||||
import { emergencyLock, emergencyUnlock } from "../actions";
|
||||
import { EStopButtonProps } from "../interfaces";
|
||||
import { isBotUp } from "../must_be_online";
|
||||
|
||||
export class EStopButton extends React.Component<EStopButtonProps, {}> {
|
||||
render() {
|
||||
const i = this.props.bot.hardware.informational_settings;
|
||||
const isLocked = !!i.locked;
|
||||
const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock;
|
||||
const emergencyLockStatusColor = isLocked ? "yellow" : "red";
|
||||
const color = isLocked ? "yellow" : "red";
|
||||
const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : "gray";
|
||||
const emergencyLockStatusText = isLocked ? "UNLOCK" : "E-STOP";
|
||||
|
||||
if (this.props.user) {
|
||||
|
|
|
@ -27,6 +27,15 @@ describe("<OsUpdateButton/>", () => {
|
|||
const osUpdateButton = buttons.find("button").last();
|
||||
expect(osUpdateButton.text()).toBe("Can't Connect to release server");
|
||||
});
|
||||
it("renders buttons: no beta releases", () => {
|
||||
bot.hardware.configuration.beta_opt_in = true;
|
||||
const buttons = mount(<OsUpdateButton bot={bot} />);
|
||||
expect(buttons.find("button").length).toBe(1);
|
||||
const autoUpdate = buttons.find("button").first();
|
||||
expect(autoUpdate.hasClass("yellow")).toBeTruthy();
|
||||
const osUpdateButton = buttons.find("button").last();
|
||||
expect(osUpdateButton.text()).toBe("No beta releases available");
|
||||
});
|
||||
it("up to date", () => {
|
||||
bot.hardware.informational_settings.controller_version = "3.1.6";
|
||||
const buttons = mount(<OsUpdateButton bot={bot} />);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { Row, Col, DropDownItem } from "../../../ui/index";
|
||||
import { Row, Col, DropDownItem, FBSelect } from "../../../ui/index";
|
||||
import { t } from "i18next";
|
||||
import { FBSelect } from "../../../ui/new_fb_select";
|
||||
import { getDevice } from "../../../device";
|
||||
import { info, error } from "farmbot-toastr";
|
||||
import { FirmwareHardware } from "farmbot";
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import * as React from "react";
|
||||
import { DropDownItem, Row, Col } from "../../../ui/index";
|
||||
import { DropDownItem, Row, Col, FBSelect } from "../../../ui/index";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
CameraSelectionProps, CameraSelectionState
|
||||
} from "../../interfaces";
|
||||
import { info, success, error } from "farmbot-toastr/dist";
|
||||
import { getDevice } from "../../../device";
|
||||
import { FBSelect } from "../../../ui/new_fb_select";
|
||||
import { ColWidth } from "../farmbot_os_settings";
|
||||
|
||||
const CAMERA_CHOICES = [
|
||||
|
|
|
@ -28,7 +28,9 @@ export let OsUpdateButton = ({ bot }: BotProp) => {
|
|||
buttonColor = "green";
|
||||
}
|
||||
} else {
|
||||
buttonStr = "Can't Connect to release server";
|
||||
buttonStr = beta_opt_in
|
||||
? "No beta releases available"
|
||||
: "Can't Connect to release server";
|
||||
}
|
||||
|
||||
const osUpdateJob = (bot.hardware.jobs || {})["FBOS_OTA"];
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import * as React from "react";
|
||||
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
|
||||
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
|
||||
import { Widget, WidgetHeader, WidgetBody, SaveBtn } from "../../ui/index";
|
||||
import { HardwareSettingsProps } from "../interfaces";
|
||||
import { MustBeOnline } from "../must_be_online";
|
||||
import { SaveBtn } from "../../ui/save_button";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { DangerZone } from "./hardware_settings/danger_zone";
|
||||
import { PinGuard } from "./hardware_settings/pin_guard";
|
||||
|
@ -21,59 +20,57 @@ export class HardwareSettings extends
|
|||
render() {
|
||||
const { bot, dispatch } = this.props;
|
||||
const { sync_status } = this.props.bot.hardware.informational_settings;
|
||||
return (
|
||||
<Widget className="hardware-widget">
|
||||
<WidgetHeader title="Hardware" helpText={ToolTips.HW_SETTINGS}>
|
||||
<MustBeOnline
|
||||
hideBanner={true}
|
||||
syncStatus={sync_status}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<SaveBtn
|
||||
status={bot.isUpdating ? SpecialStatus.SAVING : SpecialStatus.SAVED}
|
||||
dirtyText={" "}
|
||||
savingText={"Updating..."}
|
||||
savedText={"saved"}
|
||||
hidden={false} />
|
||||
</MustBeOnline>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
onClick={() => dispatch(bulkToggleControlPanel(true))}>
|
||||
Expand All
|
||||
return <Widget className="hardware-widget">
|
||||
<WidgetHeader title="Hardware" helpText={ToolTips.HW_SETTINGS}>
|
||||
<MustBeOnline
|
||||
hideBanner={true}
|
||||
syncStatus={sync_status}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<SaveBtn
|
||||
status={bot.isUpdating ? SpecialStatus.SAVING : SpecialStatus.SAVED}
|
||||
dirtyText={" "}
|
||||
savingText={"Updating..."}
|
||||
savedText={"saved"}
|
||||
hidden={false} />
|
||||
</MustBeOnline>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
onClick={() => dispatch(bulkToggleControlPanel(true))}>
|
||||
Expand All
|
||||
</button>
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
onClick={() => dispatch(bulkToggleControlPanel(false))}>
|
||||
Collapse All
|
||||
<button
|
||||
className={"fb-button gray no-float"}
|
||||
onClick={() => dispatch(bulkToggleControlPanel(false))}>
|
||||
Collapse All
|
||||
</button>
|
||||
<MustBeOnline
|
||||
networkState={this.props.botToMqttStatus}
|
||||
syncStatus={sync_status}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<div className="label-headings">
|
||||
<SpacePanelHeader />
|
||||
</div>
|
||||
<HomingAndCalibration
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<Motors
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<EncodersAndEndStops
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<PinGuard
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<DangerZone
|
||||
dispatch={dispatch}
|
||||
bot={bot}
|
||||
onReset={MCUFactoryReset} />
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
<MustBeOnline
|
||||
networkState={this.props.botToMqttStatus}
|
||||
syncStatus={sync_status}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<div className="label-headings">
|
||||
<SpacePanelHeader />
|
||||
</div>
|
||||
<HomingAndCalibration
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<Motors
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<EncodersAndEndStops
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<PinGuard
|
||||
dispatch={dispatch}
|
||||
bot={bot} />
|
||||
<DangerZone
|
||||
dispatch={dispatch}
|
||||
bot={bot}
|
||||
onReset={MCUFactoryReset} />
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { EncoderType, EncoderTypeProps, LOOKUP, findByType, isEncoderValue } from "../encoder_type";
|
||||
import {
|
||||
EncoderType, EncoderTypeProps, LOOKUP, findByType, isEncoderValue
|
||||
} from "../encoder_type";
|
||||
import { shallow } from "enzyme";
|
||||
import { FBSelect } from "../../../../ui/new_fb_select";
|
||||
import { FBSelect } from "../../../../ui/index";
|
||||
import { Encoder } from "farmbot";
|
||||
|
||||
describe("<EncoderType/>", () => {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { McuParams, Encoder, McuParamName } from "farmbot/dist";
|
||||
import { t } from "i18next";
|
||||
import { FBSelect } from "../../../ui/new_fb_select";
|
||||
import { DropDownItem } from "../../../ui/fb_select";
|
||||
import { FBSelect, DropDownItem } from "../../../ui/index";
|
||||
|
||||
export interface EncoderTypeProps {
|
||||
hardware: McuParams;
|
||||
|
|
|
@ -2,8 +2,7 @@ import * as React from "react";
|
|||
import { McuInputBox } from "./mcu_input_box";
|
||||
import { SpacePanelToolTip } from "./space_panel_tool_tip";
|
||||
import { NumericMCUInputGroupProps } from "./interfaces";
|
||||
import { Row } from "../../ui/row";
|
||||
import { Col } from "../../ui/index";
|
||||
import { Row, Col } from "../../ui/index";
|
||||
|
||||
export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ import * as React from "react";
|
|||
import { t } from "i18next";
|
||||
import * as _ from "lodash";
|
||||
import {
|
||||
Widget, WidgetBody, WidgetHeader, Row, Col, BlurableInput, DropDownItem
|
||||
Widget, WidgetBody, WidgetHeader,
|
||||
Row, Col,
|
||||
BlurableInput,
|
||||
FBSelect, DropDownItem
|
||||
} from "../../ui/index";
|
||||
import { FBSelect } from "../../ui/new_fb_select";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { BotState } from "../interfaces";
|
||||
import { registerGpioPin, unregisterGpioPin } from "../actions";
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { McuInputBox } from "./mcu_input_box";
|
||||
import { PinGuardMCUInputGroupProps } from "./interfaces";
|
||||
import { Row } from "../../ui/row";
|
||||
import { Col } from "../../ui/index";
|
||||
import { Row, Col } from "../../ui/index";
|
||||
import { settingToggle } from "../actions";
|
||||
import { ToggleButton } from "../../controls/toggle_button";
|
||||
import { isUndefined } from "util";
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
DiagramNodes,
|
||||
getConnectionColor
|
||||
} from "../diagram";
|
||||
import { Color } from "../../../ui/colors";
|
||||
import { Color } from "../../../ui/index";
|
||||
|
||||
describe("<ConnectivityDiagram/>", () => {
|
||||
function fakeProps(): ConnectivityDiagramProps {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { StatusRowProps } from "./connectivity_row";
|
||||
import { CowardlyDictionary } from "../../util";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
export interface ConnectivityDiagramProps {
|
||||
rowData: StatusRowProps[];
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { connect } from "react-redux";
|
||||
import { HardwareSettings } from "./components/hardware_settings";
|
||||
import { FarmbotOsSettings } from "./components/farmbot_os_settings";
|
||||
import { Page, Col, Row } from "../ui";
|
||||
import { Page, Col, Row } from "../ui/index";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { Props } from "./interfaces";
|
||||
import { ConnectivityPanel } from "./connectivity/index";
|
||||
|
@ -60,7 +60,6 @@ export class Devices extends React.Component<Props, {}> {
|
|||
account={this.props.deviceAccount}
|
||||
dispatch={this.props.dispatch}
|
||||
bot={this.props.bot}
|
||||
auth={this.props.auth}
|
||||
botToMqttStatus={botToMqttStatus} />
|
||||
<ConnectivityPanel
|
||||
status={this.props.deviceAccount.specialStatus}
|
||||
|
|
|
@ -101,7 +101,6 @@ export interface CalibrationButtonProps {
|
|||
export interface FarmbotOsProps {
|
||||
bot: BotState;
|
||||
account: TaggedDevice;
|
||||
auth: AuthState;
|
||||
botToMqttStatus: NetworkState;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
|
|
@ -12,10 +12,14 @@ export interface MBOProps {
|
|||
children?: JSXChildren;
|
||||
}
|
||||
|
||||
export function isBotUp(status: SyncStatus | undefined) {
|
||||
return status && !(["maintenance", "unknown"].includes(status));
|
||||
}
|
||||
|
||||
export function MustBeOnline(props: MBOProps) {
|
||||
const { children, hideBanner, lockOpen, networkState, syncStatus } = props;
|
||||
const banner = hideBanner ? "" : "banner";
|
||||
const botUp = syncStatus && (syncStatus !== "maintenance");
|
||||
const botUp = isBotUp(syncStatus);
|
||||
const netUp = networkState === "up";
|
||||
if ((botUp && netUp) || lockOpen) {
|
||||
return <div> {children} </div>;
|
||||
|
|
|
@ -4,7 +4,9 @@ import { Actions } from "../constants";
|
|||
import { EncoderDisplay } from "../controls/interfaces";
|
||||
import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import { maybeNegateStatus, maybeNegateConsistency } from "../connectivity/maybe_negate_status";
|
||||
import {
|
||||
maybeNegateStatus, maybeNegateConsistency
|
||||
} from "../connectivity/maybe_negate_status";
|
||||
import { EdgeStatus } from "../connectivity/interfaces";
|
||||
import { ReduxAction } from "../redux/interfaces";
|
||||
import { connectivityReducer } from "../connectivity/reducer";
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { DropDownItem } from "../../ui/fb_select";
|
||||
import { FBSelect, DropDownItem } from "../../ui/index";
|
||||
import { list } from "./tz_list";
|
||||
import { inferTimezone } from "./guess_timezone";
|
||||
import { FBSelect } from "../../ui/new_fb_select";
|
||||
import * as _ from "lodash";
|
||||
|
||||
const CHOICES: DropDownItem[] = list.map(x => ({ label: x, value: x }));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { Widget, WidgetHeader } from "../ui";
|
||||
import { Widget, WidgetHeader } from "../ui/index";
|
||||
|
||||
/*
|
||||
* Widget to display if the desired widget fails to load.
|
||||
|
@ -25,15 +25,13 @@ export class FallbackWidget extends
|
|||
React.Component<FallbackWidgetProps, {}> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetHeader
|
||||
title={t(this.props.title)}
|
||||
helpText={this.props.helpText} />
|
||||
<div className="widget-body">
|
||||
{t("Widget load failed.")}
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
return <Widget>
|
||||
<WidgetHeader
|
||||
title={t(this.props.title)}
|
||||
helpText={this.props.helpText} />
|
||||
<div className="widget-body">
|
||||
{t("Widget load failed.")}
|
||||
</div>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
const mockHistory = jest.fn();
|
||||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../history", () => ({
|
||||
history: {
|
||||
push: mockHistory
|
||||
},
|
||||
getPathArray: jest.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return "/app/designer/plants".split("/");
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return "/app/designer/plants/1/edit".split("/");
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return "/app/designer/plants/1".split("/");
|
||||
})
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
jest.mock("../../api/crud", () => ({
|
||||
|
@ -60,16 +52,27 @@ describe("movePlant", () => {
|
|||
movePlantTest("too low", { x: -10000, y: -10000 }, { x: 0, y: 0 });
|
||||
});
|
||||
|
||||
describe("close plant", () => {
|
||||
it("closes plant info", () => {
|
||||
describe("closePlantInfo()", () => {
|
||||
it("no plant info open", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)(); // no plant info open
|
||||
closePlantInfo(dispatch)();
|
||||
expect(mockHistory).not.toHaveBeenCalled();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
closePlantInfo(dispatch)(); // plant edit open
|
||||
});
|
||||
|
||||
it("plant edit open", () => {
|
||||
mockPath = "/app/designer/plants/1/edit";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(mockHistory).not.toHaveBeenCalled();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
closePlantInfo(dispatch)(); // plant info open
|
||||
});
|
||||
|
||||
it("plant info open", () => {
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const dispatch = jest.fn();
|
||||
closePlantInfo(dispatch)();
|
||||
expect(mockHistory).toHaveBeenCalledWith("/app/designer/plants");
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: undefined, type: "SELECT_PLANT"
|
||||
|
|
|
@ -87,18 +87,16 @@ export class AddFarmEvent
|
|||
}
|
||||
|
||||
placeholderTemplate(children: JSXChildren) {
|
||||
return (
|
||||
<div className="panel-container magenta-panel add-farm-event-panel">
|
||||
<div className="panel-header magenta-panel">
|
||||
<p className="panel-title"> <BackArrow /> {t("No Executables")} </p>
|
||||
</div>
|
||||
<div className="panel-content">
|
||||
<label>
|
||||
{children}
|
||||
</label>
|
||||
</div>
|
||||
return <div className="panel-container magenta-panel add-farm-event-panel">
|
||||
<div className="panel-header magenta-panel">
|
||||
<p className="panel-title"> <BackArrow /> {t("No Executables")} </p>
|
||||
</div>
|
||||
);
|
||||
<div className="panel-content">
|
||||
<label>
|
||||
{children}
|
||||
</label>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -108,17 +106,15 @@ export class AddFarmEvent
|
|||
// to mapStateToProps instead of juggling arrays.
|
||||
const fe = uuid && this.props.farmEvents.filter(x => x.uuid === uuid)[0];
|
||||
if (fe) {
|
||||
return (
|
||||
<EditFEForm
|
||||
farmEvent={fe}
|
||||
deviceTimezone={this.props.deviceTimezone}
|
||||
repeatOptions={this.props.repeatOptions}
|
||||
executableOptions={this.props.executableOptions}
|
||||
dispatch={this.props.dispatch}
|
||||
findExecutable={this.props.findExecutable}
|
||||
title={t("Add Farm Event")}
|
||||
timeOffset={this.props.timeOffset} />
|
||||
);
|
||||
return <EditFEForm
|
||||
farmEvent={fe}
|
||||
deviceTimezone={this.props.deviceTimezone}
|
||||
repeatOptions={this.props.repeatOptions}
|
||||
executableOptions={this.props.executableOptions}
|
||||
dispatch={this.props.dispatch}
|
||||
findExecutable={this.props.findExecutable}
|
||||
title={t("Add Farm Event")}
|
||||
timeOffset={this.props.timeOffset} />;
|
||||
} else {
|
||||
return this
|
||||
.placeholderTemplate(((this.executable) ? this.loading : this.none)());
|
||||
|
|
|
@ -4,29 +4,17 @@ import * as _ from "lodash";
|
|||
import { t } from "i18next";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { TaggedFarmEvent, SpecialStatus } from "../../resources/tagged_resources";
|
||||
import {
|
||||
TimeUnit,
|
||||
ExecutableQuery,
|
||||
ExecutableType
|
||||
} from "../interfaces";
|
||||
import {
|
||||
formatTime,
|
||||
formatDate
|
||||
} from "./map_state_to_props_add_edit";
|
||||
import { TimeUnit, ExecutableQuery, ExecutableType } from "../interfaces";
|
||||
import { formatTime, formatDate } from "./map_state_to_props_add_edit";
|
||||
import {
|
||||
BackArrow,
|
||||
BlurableInput,
|
||||
Col,
|
||||
Row,
|
||||
SaveBtn
|
||||
Col, Row,
|
||||
SaveBtn,
|
||||
FBSelect,
|
||||
DropDownItem
|
||||
} from "../../ui/index";
|
||||
import { FBSelect } from "../../ui/new_fb_select";
|
||||
import {
|
||||
destroy,
|
||||
save,
|
||||
edit
|
||||
} from "../../api/crud";
|
||||
import { DropDownItem } from "../../ui/fb_select";
|
||||
import { destroy, save, edit } from "../../api/crud";
|
||||
import { history } from "../../history";
|
||||
// TIL: https://stackoverflow.com/a/24900248/1064917
|
||||
import { betterMerge } from "../../util";
|
||||
|
@ -227,81 +215,79 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
|
|||
const fe = this.props.farmEvent;
|
||||
const repeats = this.fieldGet("timeUnit") !== NEVER;
|
||||
const allowRepeat = (!this.isReg && repeats);
|
||||
return (
|
||||
<div className="panel-container magenta-panel add-farm-event-panel">
|
||||
<div className="panel-header magenta-panel">
|
||||
<p className="panel-title">
|
||||
<BackArrow onClick={() => {
|
||||
if (!this.props.farmEvent.body.id) {
|
||||
// Throw out unsaved farmevents.
|
||||
this.props.dispatch(destroyOK(this.props.farmEvent));
|
||||
return;
|
||||
}
|
||||
}} />
|
||||
{this.props.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="panel-content">
|
||||
<label>
|
||||
{t("Sequence or Regimen")}
|
||||
</label>
|
||||
<FBSelect
|
||||
list={this.props.executableOptions}
|
||||
onChange={this.executableSet}
|
||||
selectedItem={this.executableGet()} />
|
||||
<label>
|
||||
{t("Starts")}
|
||||
</label>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<BlurableInput
|
||||
type="date"
|
||||
className="add-event-start-date"
|
||||
name="start_date"
|
||||
value={this.fieldGet("startDate")}
|
||||
onCommit={this.fieldSet("startDate")} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<EventTimePicker
|
||||
className="add-event-start-time"
|
||||
name="start_time"
|
||||
tzOffset={this.props.timeOffset}
|
||||
value={this.fieldGet("startTime")}
|
||||
onCommit={this.fieldSet("startTime")} />
|
||||
</Col>
|
||||
</Row>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={this.toggleRepeat}
|
||||
disabled={this.isReg}
|
||||
checked={repeats && !this.isReg} />
|
||||
{t("Repeats?")}
|
||||
</label>
|
||||
<FarmEventRepeatForm
|
||||
tzOffset={this.props.timeOffset}
|
||||
disabled={!allowRepeat}
|
||||
hidden={!allowRepeat}
|
||||
onChange={this.mergeState}
|
||||
timeUnit={this.fieldGet("timeUnit") as TimeUnit}
|
||||
repeat={this.fieldGet("repeat")}
|
||||
endDate={this.fieldGet("endDate")}
|
||||
endTime={this.fieldGet("endTime")} />
|
||||
<SaveBtn
|
||||
status={fe.specialStatus || this.state.specialStatusLocal}
|
||||
color="magenta"
|
||||
onClick={this.commitViewModel} />
|
||||
<button className="fb-button red" hidden={!this.props.deleteBtn}
|
||||
onClick={() => {
|
||||
this.dispatch(destroy(fe.uuid)).then(() => {
|
||||
history.push("/app/designer/farm_events");
|
||||
success(t("Deleted farm event."), t("Deleted"));
|
||||
});
|
||||
}}>
|
||||
{t("Delete")}
|
||||
</button>
|
||||
<TzWarning deviceTimezone={this.props.deviceTimezone} />
|
||||
</div>
|
||||
return <div className="panel-container magenta-panel add-farm-event-panel">
|
||||
<div className="panel-header magenta-panel">
|
||||
<p className="panel-title">
|
||||
<BackArrow onClick={() => {
|
||||
if (!this.props.farmEvent.body.id) {
|
||||
// Throw out unsaved farmevents.
|
||||
this.props.dispatch(destroyOK(this.props.farmEvent));
|
||||
return;
|
||||
}
|
||||
}} />
|
||||
{this.props.title}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
<div className="panel-content">
|
||||
<label>
|
||||
{t("Sequence or Regimen")}
|
||||
</label>
|
||||
<FBSelect
|
||||
list={this.props.executableOptions}
|
||||
onChange={this.executableSet}
|
||||
selectedItem={this.executableGet()} />
|
||||
<label>
|
||||
{t("Starts")}
|
||||
</label>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<BlurableInput
|
||||
type="date"
|
||||
className="add-event-start-date"
|
||||
name="start_date"
|
||||
value={this.fieldGet("startDate")}
|
||||
onCommit={this.fieldSet("startDate")} />
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<EventTimePicker
|
||||
className="add-event-start-time"
|
||||
name="start_time"
|
||||
tzOffset={this.props.timeOffset}
|
||||
value={this.fieldGet("startTime")}
|
||||
onCommit={this.fieldSet("startTime")} />
|
||||
</Col>
|
||||
</Row>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={this.toggleRepeat}
|
||||
disabled={this.isReg}
|
||||
checked={repeats && !this.isReg} />
|
||||
{t("Repeats?")}
|
||||
</label>
|
||||
<FarmEventRepeatForm
|
||||
tzOffset={this.props.timeOffset}
|
||||
disabled={!allowRepeat}
|
||||
hidden={!allowRepeat}
|
||||
onChange={this.mergeState}
|
||||
timeUnit={this.fieldGet("timeUnit") as TimeUnit}
|
||||
repeat={this.fieldGet("repeat")}
|
||||
endDate={this.fieldGet("endDate")}
|
||||
endTime={this.fieldGet("endTime")} />
|
||||
<SaveBtn
|
||||
status={fe.specialStatus || this.state.specialStatusLocal}
|
||||
color="magenta"
|
||||
onClick={this.commitViewModel} />
|
||||
<button className="fb-button red" hidden={!this.props.deleteBtn}
|
||||
onClick={() => {
|
||||
this.dispatch(destroy(fe.uuid)).then(() => {
|
||||
history.push("/app/designer/farm_events");
|
||||
success(t("Deleted farm event."), t("Deleted"));
|
||||
});
|
||||
}}>
|
||||
{t("Delete")}
|
||||
</button>
|
||||
<TzWarning deviceTimezone={this.props.deviceTimezone} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { BlurableInput } from "../../ui/blurable_input";
|
||||
import { BlurableInput } from "../../ui/index";
|
||||
import * as moment from "moment";
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { Row, Col, BlurableInput, DropDownItem } from "../../ui/index";
|
||||
import { FBSelect } from "../../ui/new_fb_select";
|
||||
import {
|
||||
Row, Col, BlurableInput, FBSelect, DropDownItem
|
||||
} from "../../ui/index";
|
||||
import { repeatOptions } from "./map_state_to_props_add_edit";
|
||||
import { keyBy } from "lodash";
|
||||
import { TimeUnit } from "../interfaces";
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { Link } from "react-router";
|
||||
import { connect } from "react-redux";
|
||||
import { t } from "i18next";
|
||||
import { Row } from "../../ui";
|
||||
import { Row } from "../../ui/index";
|
||||
import { mapStateToProps } from "./map_state_to_props";
|
||||
import { FarmEventProps, CalendarOccurrence } from "../interfaces";
|
||||
import * as _ from "lodash";
|
||||
|
@ -25,22 +25,20 @@ export class PureFarmEvents extends React.Component<FarmEventProps, {}> {
|
|||
? <p style={{ color: "gray" }}> {occur.heading} </p>
|
||||
: <p />;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="farm-event-data-block"
|
||||
key={`${occur.sortKey}.${index}`}>
|
||||
<div className="farm-event-data-time">
|
||||
{occur.timeStr}
|
||||
</div>
|
||||
<div className="farm-event-data-executable">
|
||||
{heading}
|
||||
{subHeading}
|
||||
</div>
|
||||
<Link to={url}>
|
||||
<i className="fa fa-pencil-square-o edit-icon" />
|
||||
</Link>
|
||||
return <div
|
||||
className="farm-event-data-block"
|
||||
key={`${occur.sortKey}.${index}`}>
|
||||
<div className="farm-event-data-time">
|
||||
{occur.timeStr}
|
||||
</div>
|
||||
);
|
||||
<div className="farm-event-data-executable">
|
||||
{heading}
|
||||
{subHeading}
|
||||
</div>
|
||||
<Link to={url}>
|
||||
<i className="fa fa-pencil-square-o edit-icon" />
|
||||
</Link>
|
||||
</div>;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -48,21 +46,19 @@ export class PureFarmEvents extends React.Component<FarmEventProps, {}> {
|
|||
return this.props.calendarRows.filter((day) => {
|
||||
return day.year == year;
|
||||
}).map(item => {
|
||||
return (
|
||||
<div className="farm-event" key={item.sortKey}>
|
||||
<div className="farm-event-date">
|
||||
<div className="farm-event-date-month">
|
||||
{item.month}
|
||||
</div>
|
||||
<div className="farm-event-date-day">
|
||||
<b>{item.day}</b>
|
||||
</div>
|
||||
return <div className="farm-event" key={item.sortKey}>
|
||||
<div className="farm-event-date">
|
||||
<div className="farm-event-date-month">
|
||||
{item.month}
|
||||
</div>
|
||||
<div className="farm-event-data">
|
||||
{this.innerRows(item.items)}
|
||||
<div className="farm-event-date-day">
|
||||
<b>{item.day}</b>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="farm-event-data">
|
||||
{this.innerRows(item.items)}
|
||||
</div>
|
||||
</div>;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -117,24 +113,22 @@ export class PureFarmEvents extends React.Component<FarmEventProps, {}> {
|
|||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div className="panel-container magenta-panel farm-event-panel">
|
||||
<div className="panel-header magenta-panel">
|
||||
<div className="panel-tabs">
|
||||
<Link to="/app/designer" className="visible-xs">
|
||||
{t("Designer")}
|
||||
</Link>
|
||||
<Link to="/app/designer/plants">
|
||||
{t("Plants")}
|
||||
</Link>
|
||||
<Link to="/app/designer/farm_events" className="active">
|
||||
{t("Farm Events")}
|
||||
</Link>
|
||||
</div>
|
||||
return <div className="panel-container magenta-panel farm-event-panel">
|
||||
<div className="panel-header magenta-panel">
|
||||
<div className="panel-tabs">
|
||||
<Link to="/app/designer" className="visible-xs">
|
||||
{t("Designer")}
|
||||
</Link>
|
||||
<Link to="/app/designer/plants">
|
||||
{t("Plants")}
|
||||
</Link>
|
||||
<Link to="/app/designer/farm_events" className="active">
|
||||
{t("Farm Events")}
|
||||
</Link>
|
||||
</div>
|
||||
{this.props.timezoneIsSet ? this.normalContent() : this.tzwarning()}
|
||||
</div>
|
||||
);
|
||||
{this.props.timezoneIsSet ? this.normalContent() : this.tzwarning()}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
TaggedSequence,
|
||||
TaggedRegimen
|
||||
} from "../../resources/tagged_resources";
|
||||
import { DropDownItem } from "../../ui/fb_select";
|
||||
import { DropDownItem } from "../../ui/index";
|
||||
|
||||
export let formatTime = (input: string, timeOffset: number) => {
|
||||
const iso = new Date(input).toISOString();
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DragHelpers } from "../drag_helpers";
|
|||
import { shallow } from "enzyme";
|
||||
import { DragHelpersProps } from "../interfaces";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { Color } from "../../../ui/colors";
|
||||
import { Color } from "../../../ui/index";
|
||||
|
||||
describe("<DragHelpers/>", () => {
|
||||
function fakeProps(): DragHelpersProps {
|
||||
|
|
|
@ -13,6 +13,11 @@ jest.mock("../../../api/crud", () => ({
|
|||
save: () => "save resource",
|
||||
}));
|
||||
|
||||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { GardenMap } from "../garden_map";
|
||||
import { shallow } from "enzyme";
|
||||
|
@ -91,10 +96,7 @@ describe("<GardenPlant/>", () => {
|
|||
const dispatch = jest.fn();
|
||||
p.dispatch = dispatch;
|
||||
const wrapper = shallow(<GardenMap {...p} />);
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/crop_search/strawberry/add",
|
||||
configurable: true
|
||||
});
|
||||
mockPath = "/app/designer/plants/crop_search/strawberry/add";
|
||||
wrapper.find("#drop-area-svg").simulate("click", {
|
||||
preventDefault: jest.fn()
|
||||
});
|
||||
|
@ -105,10 +107,7 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("doesn't drop plant: error", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps() } />);
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/crop_search/aplant/add",
|
||||
configurable: true
|
||||
});
|
||||
mockPath = "/app/designer/plants/crop_search/aplant/add";
|
||||
Object.defineProperty(document, "querySelector", {
|
||||
value: () => { }, configurable: true
|
||||
});
|
||||
|
@ -121,10 +120,7 @@ describe("<GardenPlant/>", () => {
|
|||
|
||||
it("doesn't drop plant: outside planting area", () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps() } />);
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/crop_search/aplant/add",
|
||||
configurable: true
|
||||
});
|
||||
mockPath = "/app/designer/plants/crop_search/aplant/add";
|
||||
wrapper.find("#drop-area-svg").simulate("click", {
|
||||
preventDefault: jest.fn(), pageX: -100, pageY: -100
|
||||
});
|
||||
|
@ -135,9 +131,7 @@ describe("<GardenPlant/>", () => {
|
|||
it("starts drag and sets activeSpread", async () => {
|
||||
const wrapper = shallow(<GardenMap {...fakeProps() } />);
|
||||
expect(wrapper.state()).toEqual({});
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/1/edit/"
|
||||
});
|
||||
mockPath = "/app/designer/plants/1/edit/";
|
||||
await wrapper.find("#drop-area-svg").simulate("mouseDown");
|
||||
expect(wrapper.state()).toEqual({
|
||||
activeDragSpread: 1000,
|
||||
|
@ -165,9 +159,7 @@ describe("<GardenPlant/>", () => {
|
|||
});
|
||||
|
||||
it("drags: editing", () => {
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/1/edit", configurable: true
|
||||
});
|
||||
mockPath = "/app/designer/plants/1/edit";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenMap {...p } />);
|
||||
expect(wrapper.state()).toEqual({});
|
||||
|
@ -188,9 +180,7 @@ describe("<GardenPlant/>", () => {
|
|||
});
|
||||
|
||||
it("drags: selecting", () => {
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/select", configurable: true
|
||||
});
|
||||
mockPath = "/app/designer/plants/select";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<GardenMap {...p } />);
|
||||
expect(wrapper.state()).toEqual({});
|
||||
|
|
|
@ -37,12 +37,24 @@ describe("zoom utilities", () => {
|
|||
expect(ZoomUtils.atMinZoom()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("beyond max zoom", () => {
|
||||
mockZoomValue = 999;
|
||||
const result = ZoomUtils.getZoomLevelIndex();
|
||||
expect(result).toEqual(ZoomUtils.maxZoomIndex);
|
||||
});
|
||||
|
||||
it("at min zoom", () => {
|
||||
mockZoomValue = ZoomUtils.minZoomLevel;
|
||||
expect(ZoomUtils.atMaxZoom()).toBeFalsy();
|
||||
expect(ZoomUtils.atMinZoom()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("beyond min zoom", () => {
|
||||
mockZoomValue = -999;
|
||||
const result = ZoomUtils.getZoomLevelIndex();
|
||||
expect(result).toEqual(0);
|
||||
});
|
||||
|
||||
it("at unknown zoom", () => {
|
||||
mockZoomValue = undefined;
|
||||
const defaultZoom = ZoomUtils.calcZoomLevel(ZoomUtils.getZoomLevelIndex());
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DragHelpersProps } from "./interfaces";
|
|||
import { round, getXYFromQuadrant, getMapSize } from "./util";
|
||||
import { isUndefined } from "util";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
enum Alignment {
|
||||
NONE = "not aligned",
|
||||
|
@ -87,10 +87,9 @@ export function DragHelpers(props: DragHelpersProps) {
|
|||
</g>
|
||||
</defs>
|
||||
{[0, 90, 180, 270].map(rotation => {
|
||||
return (
|
||||
<use key={rotation.toString()}
|
||||
xlinkHref={"#crosshair-segment-" + plant.body.id}
|
||||
transform={`rotate(${rotation}, ${qx}, ${qy})`} />);
|
||||
return <use key={rotation.toString()}
|
||||
xlinkHref={"#crosshair-segment-" + plant.body.id}
|
||||
transform={`rotate(${rotation}, ${qx}, ${qy})`} />;
|
||||
})}
|
||||
</g>}
|
||||
{!dragging && // Non-active plants
|
||||
|
@ -105,10 +104,9 @@ export function DragHelpers(props: DragHelpersProps) {
|
|||
</g>
|
||||
</defs>
|
||||
{rotationArray(getAlignment(activeDragXY, gardenCoord)).map(rotation => {
|
||||
return (
|
||||
<use key={rotation.toString()}
|
||||
xlinkHref={"#alignment-indicator-segment-" + plant.body.id}
|
||||
transform={`rotate(${rotation}, ${qx}, ${qy})`} />);
|
||||
return <use key={rotation.toString()}
|
||||
xlinkHref={"#alignment-indicator-segment-" + plant.body.id}
|
||||
transform={`rotate(${rotation}, ${qx}, ${qy})`} />;
|
||||
})}
|
||||
</g>}
|
||||
</g>;
|
||||
|
|
|
@ -18,14 +18,16 @@ import {
|
|||
import { findBySlug } from "../search_selectors";
|
||||
import { Grid } from "./grid";
|
||||
import { MapBackground } from "./map_background";
|
||||
import { PlantLayer } from "./layers/plant_layer";
|
||||
import { PointLayer } from "./layers/point_layer";
|
||||
import { SpreadLayer } from "./layers/spread_layer";
|
||||
import { ToolSlotLayer } from "./layers/tool_slot_layer";
|
||||
import { HoveredPlantLayer } from "./layers/hovered_plant_layer";
|
||||
import { FarmBotLayer } from "./layers/farmbot_layer";
|
||||
import {
|
||||
PlantLayer,
|
||||
SpreadLayer,
|
||||
PointLayer,
|
||||
ToolSlotLayer,
|
||||
FarmBotLayer,
|
||||
HoveredPlantLayer,
|
||||
DragHelperLayer
|
||||
} from "./layers";
|
||||
import { cachedCrop } from "../../open_farm/icons";
|
||||
import { DragHelperLayer } from "./layers/drag_helper_layer";
|
||||
import { AxisNumberProperty } from "./interfaces";
|
||||
import { SelectionBox, SelectionBoxData } from "./selection_box";
|
||||
import { Actions } from "../../constants";
|
||||
|
|
|
@ -23,76 +23,74 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
|
|||
const minusBtnClass = atMinZoom() ? "disabled" : "";
|
||||
const menuClass = legendMenuOpen ? "active" : "";
|
||||
|
||||
return (
|
||||
return <div
|
||||
className={"garden-map-legend " + menuClass}
|
||||
style={{ zoom: 1 }}>
|
||||
<div
|
||||
className={"garden-map-legend " + menuClass}
|
||||
style={{ zoom: 1 }}>
|
||||
<div
|
||||
className={"menu-pullout " + menuClass}
|
||||
onClick={toggle("legend_menu_open")}>
|
||||
<span>
|
||||
{t("Menu")}
|
||||
</span>
|
||||
<i className="fa fa-2x fa-arrow-left" />
|
||||
className={"menu-pullout " + menuClass}
|
||||
onClick={toggle("legend_menu_open")}>
|
||||
<span>
|
||||
{t("Menu")}
|
||||
</span>
|
||||
<i className="fa fa-2x fa-arrow-left" />
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="zoom-buttons">
|
||||
<button
|
||||
className={"fb-button gray zoom " + plusBtnClass}
|
||||
onClick={zoom(1)}>
|
||||
<i className="fa fa-2x fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
className={"fb-button gray zoom zoom-out " + minusBtnClass}
|
||||
onClick={zoom(-1)}>
|
||||
<i className="fa fa-2x fa-minus" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="zoom-buttons">
|
||||
<button
|
||||
className={"fb-button gray zoom " + plusBtnClass}
|
||||
onClick={zoom(1)}>
|
||||
<i className="fa fa-2x fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
className={"fb-button gray zoom zoom-out " + minusBtnClass}
|
||||
onClick={zoom(-1)}>
|
||||
<i className="fa fa-2x fa-minus" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="toggle-buttons">
|
||||
<LayerToggle
|
||||
value={showPlants}
|
||||
label={t("Plants?")}
|
||||
onClick={toggle("show_plants")} />
|
||||
<LayerToggle
|
||||
value={showPoints}
|
||||
label={t("Points?")}
|
||||
onClick={toggle("show_points")} />
|
||||
<LayerToggle
|
||||
value={showSpread}
|
||||
label={t("Spread?")}
|
||||
onClick={toggle("show_spread")} />
|
||||
<LayerToggle
|
||||
value={showFarmbot}
|
||||
label={t("FarmBot?")}
|
||||
onClick={toggle("show_farmbot")} />
|
||||
</div>
|
||||
<div className="farmbot-origin">
|
||||
<label>
|
||||
{t("Origin")}
|
||||
</label>
|
||||
<div className="quadrants">
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 2 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(2)} />
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 1 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(1)} />
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 3 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(3)} />
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 4 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(4)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="move-to-mode">
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={() => history.push("/app/designer/plants/move_to")}>
|
||||
{t("move mode")}
|
||||
</button>
|
||||
<div className="toggle-buttons">
|
||||
<LayerToggle
|
||||
value={showPlants}
|
||||
label={t("Plants?")}
|
||||
onClick={toggle("show_plants")} />
|
||||
<LayerToggle
|
||||
value={showPoints}
|
||||
label={t("Points?")}
|
||||
onClick={toggle("show_points")} />
|
||||
<LayerToggle
|
||||
value={showSpread}
|
||||
label={t("Spread?")}
|
||||
onClick={toggle("show_spread")} />
|
||||
<LayerToggle
|
||||
value={showFarmbot}
|
||||
label={t("FarmBot?")}
|
||||
onClick={toggle("show_farmbot")} />
|
||||
</div>
|
||||
<div className="farmbot-origin">
|
||||
<label>
|
||||
{t("Origin")}
|
||||
</label>
|
||||
<div className="quadrants">
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 2 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(2)} />
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 1 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(1)} />
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 3 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(3)} />
|
||||
<div
|
||||
className={"quadrant " + (botOriginQuadrant === 4 && "selected")}
|
||||
onClick={updateBotOriginQuadrant(4)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="move-to-mode">
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={() => history.push("/app/designer/plants/move_to")}>
|
||||
{t("move mode")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { round, getXYFromQuadrant } from "./util";
|
|||
import { DragHelpers } from "./drag_helpers";
|
||||
import { Session } from "../../session";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
export class GardenPlant extends
|
||||
React.Component<GardenPlantProps, Partial<GardenPlantState>> {
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { GridProps } from "./interfaces";
|
||||
import { getXYFromQuadrant } from "./util";
|
||||
import * as _ from "lodash";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
export function Grid(props: GridProps) {
|
||||
const { quadrant, gridSize } = props.mapTransformProps;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../../../history", () => ({
|
||||
getPathArray: jest
|
||||
.fn(() => { return "/app/designer/plants/select".split("/"); })
|
||||
.mockImplementationOnce(() => { return "/app/designer/plants".split("/"); })
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
jest.mock("../../../../session", () => {
|
||||
|
@ -59,7 +58,15 @@ describe("<PlantLayer/>", () => {
|
|||
expect(wrapper.html()).toEqual("<g id=\"plant-layer\"></g>");
|
||||
});
|
||||
|
||||
it("is in clickable mode", () => {
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<PlantLayer {...p } />);
|
||||
expect(wrapper.find("Link").props().style).toEqual({});
|
||||
});
|
||||
|
||||
it("is in non-clickable mode", () => {
|
||||
mockPath = "/app/designer/plants/select";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<PlantLayer {...p } />);
|
||||
expect(wrapper.find("Link").props().style)
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
const mockHistory = jest.fn();
|
||||
let mockPath = "/app/designer/plants";
|
||||
jest.mock("../../../../history", () => ({
|
||||
history: {
|
||||
push: mockHistory
|
||||
},
|
||||
getPathArray: jest
|
||||
.fn(() => { return "/app/designer/plants/select".split("/"); })
|
||||
.mockImplementationOnce(() => { return "/app/designer/plants".split("/"); })
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
|
@ -53,9 +52,7 @@ describe("<ToolSlotLayer/>", () => {
|
|||
});
|
||||
|
||||
it("navigates to tools page", async () => {
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants", configurable: true
|
||||
});
|
||||
mockPath = "/app/designer/plants";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotLayer {...p } />);
|
||||
const tools = wrapper.find("g").first();
|
||||
|
@ -64,9 +61,7 @@ describe("<ToolSlotLayer/>", () => {
|
|||
});
|
||||
|
||||
it("doesn't navigate to tools page", async () => {
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/1", configurable: true
|
||||
});
|
||||
mockPath = "/app/designer/plants/1";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotLayer {...p } />);
|
||||
const tools = wrapper.find("g").first();
|
||||
|
@ -76,6 +71,7 @@ describe("<ToolSlotLayer/>", () => {
|
|||
});
|
||||
|
||||
it("is in non-clickable mode", () => {
|
||||
mockPath = "/app/designer/plants/select";
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<ToolSlotLayer {...p } />);
|
||||
expect(wrapper.find("g").props().style)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export * from "./drag_helper_layer";
|
||||
export * from "./farmbot_layer";
|
||||
export * from "./hovered_plant_layer";
|
||||
export * from "./plant_layer";
|
||||
export * from "./point_layer";
|
||||
export * from "./spread_layer";
|
||||
export * from "./tool_slot_layer";
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { SlotWithTool } from "../../../resources/interfaces";
|
||||
import { ToolSlotPoint } from "../tool_slot_point";
|
||||
import { MapTransformProps } from "../interfaces";
|
||||
import { history } from "../../../history";
|
||||
import { history, getPathArray } from "../../../history";
|
||||
import { getMode, Mode } from "../garden_map";
|
||||
|
||||
export interface ToolSlotLayerProps {
|
||||
|
@ -13,7 +13,7 @@ export interface ToolSlotLayerProps {
|
|||
}
|
||||
|
||||
export function ToolSlotLayer(props: ToolSlotLayerProps) {
|
||||
const pathArray = location.pathname.split("/");
|
||||
const pathArray = getPathArray();
|
||||
const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4);
|
||||
function goToToolsPage() {
|
||||
if (canClickTool) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { MapBackgroundProps } from "./interfaces";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
export function MapBackground(props: MapBackgroundProps) {
|
||||
const { mapTransformProps, plantAreaOffset } = props;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { MapTransformProps } from "./interfaces";
|
|||
import { getXYFromQuadrant, round } from "./util";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { isNumber } from "lodash";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
export interface TargetCoordinateProps {
|
||||
chosenLocation: BotPosition;
|
||||
|
@ -27,10 +27,9 @@ export function TargetCoordinate(props: TargetCoordinateProps) {
|
|||
</g>
|
||||
</defs>
|
||||
{[45, 135, 225, 315].map(rotation => {
|
||||
return (
|
||||
<use key={rotation.toString()}
|
||||
xlinkHref={"#target-coordinate-crosshair-segment"}
|
||||
transform={`rotate(${rotation}, ${qx}, ${qy})`} />);
|
||||
return <use key={rotation.toString()}
|
||||
xlinkHref={"#target-coordinate-crosshair-segment"}
|
||||
transform={`rotate(${rotation}, ${qx}, ${qy})`} />;
|
||||
})}
|
||||
</g>;
|
||||
} else {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { SlotWithTool } from "../../resources/interfaces";
|
|||
import { getXYFromQuadrant } from "./util";
|
||||
import { MapTransformProps } from "./interfaces";
|
||||
import * as _ from "lodash";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
export interface TSPProps {
|
||||
slot: SlotWithTool;
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import {
|
||||
BotOriginQuadrant,
|
||||
isBotOriginQuadrant
|
||||
} from "../interfaces";
|
||||
import { BotOriginQuadrant, isBotOriginQuadrant } from "../interfaces";
|
||||
import { McuParams } from "farmbot";
|
||||
import { StepsPerMmXY } from "../../devices/interfaces";
|
||||
import { CheckedAxisLength, AxisNumberProperty, BotSize } from "./interfaces";
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { shallow } from "enzyme";
|
||||
import { BotOriginQuadrant } from "../../../interfaces";
|
||||
import { BotFigure, BotFigureProps } from "../bot_figure";
|
||||
import { Color } from "../../../../ui/colors";
|
||||
import { Color } from "../../../../ui/index";
|
||||
|
||||
describe("<BotFigure/>", () => {
|
||||
function fakeProps(): BotFigureProps {
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { AxisNumberProperty, MapTransformProps } from "../interfaces";
|
||||
import { getMapSize, getXYFromQuadrant } from "../util";
|
||||
import { BotPosition } from "../../../devices/interfaces";
|
||||
import { Color } from "../../../ui/colors";
|
||||
import { Color } from "../../../ui/index";
|
||||
|
||||
export interface BotFigureProps {
|
||||
name: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Session } from "../../session";
|
||||
import { NumericSetting } from "../../session_keys";
|
||||
import { findIndex, isNumber } from "lodash";
|
||||
import { findIndex, isNumber, clamp } from "lodash";
|
||||
|
||||
/**
|
||||
* Map Zoom Level utilities
|
||||
|
@ -16,11 +16,13 @@ const zoomLevels =
|
|||
const foundIndex = findIndex(zoomLevels, (x) => x === 1);
|
||||
const zoomLevel1Index = foundIndex === -1 ? 9 : foundIndex;
|
||||
const zoomLevelsCount = zoomLevels.length;
|
||||
export const maxZoomIndex = zoomLevelsCount - 1;
|
||||
const clampZoom = (index: number): number => clamp(index, 0, maxZoomIndex);
|
||||
export const maxZoomLevel = zoomLevelsCount - zoomLevel1Index;
|
||||
export const minZoomLevel = 1 - zoomLevel1Index;
|
||||
|
||||
export function atMaxZoom(): boolean {
|
||||
return getZoomLevelIndex() >= (zoomLevelsCount - 1);
|
||||
return getZoomLevelIndex() >= maxZoomIndex;
|
||||
}
|
||||
|
||||
export function atMinZoom(): boolean {
|
||||
|
@ -30,9 +32,9 @@ export function atMinZoom(): boolean {
|
|||
/* Load the index of a saved zoom level. */
|
||||
export function getZoomLevelIndex(): number {
|
||||
const savedValue = Session.deprecatedGetNum(NumericSetting.zoom_level);
|
||||
return isNumber(savedValue)
|
||||
? savedValue + zoomLevel1Index - 1
|
||||
: zoomLevel1Index;
|
||||
if (!isNumber(savedValue)) { return zoomLevel1Index; }
|
||||
const zoomLevelIndex = savedValue + zoomLevel1Index - 1;
|
||||
return clampZoom(zoomLevelIndex);
|
||||
}
|
||||
|
||||
/* Save a zoom level index. */
|
||||
|
@ -43,5 +45,5 @@ export function saveZoomLevelIndex(index: number) {
|
|||
|
||||
/* Calculate map zoom level from a zoom level index. */
|
||||
export function calcZoomLevel(index: number): number {
|
||||
return zoomLevels[index];
|
||||
return zoomLevels[clampZoom(index)];
|
||||
}
|
||||
|
|
|
@ -2,6 +2,11 @@ jest.mock("react-redux", () => ({
|
|||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { AddPlant, AddPlantProps } from "../add_plant";
|
||||
|
@ -24,9 +29,7 @@ describe("<AddPlant />", () => {
|
|||
}
|
||||
}]
|
||||
};
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/crop_search/mint/add"
|
||||
});
|
||||
mockPath = "/app/designer/plants/crop_search/mint/add";
|
||||
const wrapper = mount(<AddPlant {...props} />);
|
||||
expect(wrapper.text()).toContain("Mint");
|
||||
expect(wrapper.text()).toContain("Done");
|
||||
|
|
|
@ -2,6 +2,11 @@ jest.mock("react-redux", () => ({
|
|||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
jest.mock("../../search_selectors", () => ({
|
||||
findBySlug: () => {
|
||||
return {
|
||||
|
@ -26,9 +31,7 @@ import { shallow } from "enzyme";
|
|||
|
||||
describe("<CropInfo />", () => {
|
||||
it("renders", () => {
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/crop_search/mint"
|
||||
});
|
||||
mockPath = "/app/designer/plants/crop_search/mint";
|
||||
const wrapper = shallow(
|
||||
<CropInfo
|
||||
OFSearch={jest.fn()}
|
||||
|
|
|
@ -10,6 +10,11 @@ jest.mock("../../../device", () => ({
|
|||
getDevice: () => (mockDevice)
|
||||
}));
|
||||
|
||||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { MoveTo, MoveToProps } from "../move_to";
|
||||
|
@ -17,9 +22,7 @@ import { MoveTo, MoveToProps } from "../move_to";
|
|||
describe("<MoveTo />", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/move_to"
|
||||
});
|
||||
mockPath = "/app/designer/plants/move_to";
|
||||
});
|
||||
|
||||
function fakeProps(): MoveToProps {
|
||||
|
|
|
@ -2,6 +2,11 @@ jest.mock("react-redux", () => ({
|
|||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
let mockPath = "";
|
||||
jest.mock("../../../history", () => ({
|
||||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { SelectPlants, SelectPlantsProps } from "../select_plants";
|
||||
|
@ -11,9 +16,7 @@ import { Actions } from "../../../constants";
|
|||
describe("<SelectPlants />", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(location, "pathname", {
|
||||
value: "/app/designer/plants/select"
|
||||
});
|
||||
mockPath = "/app/designer/plants/select";
|
||||
});
|
||||
|
||||
function fakeProps(): SelectPlantsProps {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { BackArrow } from "../../ui";
|
||||
import { BackArrow } from "../../ui/index";
|
||||
import { Everything } from "../../interfaces";
|
||||
import { connect } from "react-redux";
|
||||
import { t } from "i18next";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { t } from "i18next";
|
||||
import { BackArrow } from "../../ui";
|
||||
import { BackArrow } from "../../ui/index";
|
||||
import { TaggedPlantPointer } from "../../resources/tagged_resources";
|
||||
import { mapStateToProps, formatPlantInfo } from "./map_state_to_props";
|
||||
import { PlantInfoBase } from "./plant_info_base";
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { CropLiveSearchResult } from "./interfaces";
|
||||
import { generateReducer } from "../redux/generate_reducer";
|
||||
import {
|
||||
DesignerState,
|
||||
HoveredPlantPayl
|
||||
} from "./interfaces";
|
||||
import { DesignerState, HoveredPlantPayl } from "./interfaces";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { TaggedResource } from "../resources/tagged_resources";
|
||||
import { Actions } from "../constants";
|
||||
|
|
|
@ -16,57 +16,55 @@ export class CameraCalibration extends
|
|||
React.Component<CameraCalibrationProps, CameraCalibrationState> {
|
||||
render() {
|
||||
const classname = "weed-detector-widget";
|
||||
return (
|
||||
<Widget className={classname}>
|
||||
<TitleBar
|
||||
title={"Camera Calibration"}
|
||||
help={t(ToolTips.CAMERA_CALIBRATION)}
|
||||
docs={"farmware#section-weed-detector"}
|
||||
onCalibrate={this.props.dispatch(calibrate)}
|
||||
env={this.props.env} />
|
||||
<WidgetBody>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.syncStatus}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<ImageWorkspace
|
||||
onProcessPhoto={(id) => { this.props.dispatch(scanImage(id)); }}
|
||||
onFlip={(uuid) => this.props.dispatch(selectImage(uuid))}
|
||||
images={this.props.images}
|
||||
currentImage={this.props.currentImage}
|
||||
onChange={(key, value) => {
|
||||
const MAPPING: Record<typeof key, WDENVKey> = {
|
||||
"iteration": "CAMERA_CALIBRATION_iteration",
|
||||
"morph": "CAMERA_CALIBRATION_morph",
|
||||
"blur": "CAMERA_CALIBRATION_blur",
|
||||
"H_HI": "CAMERA_CALIBRATION_H_HI",
|
||||
"H_LO": "CAMERA_CALIBRATION_H_LO",
|
||||
"S_HI": "CAMERA_CALIBRATION_S_HI",
|
||||
"S_LO": "CAMERA_CALIBRATION_S_LO",
|
||||
"V_HI": "CAMERA_CALIBRATION_V_HI",
|
||||
"V_LO": "CAMERA_CALIBRATION_V_LO"
|
||||
};
|
||||
envSave(MAPPING[key], value);
|
||||
}}
|
||||
iteration={this.props.iteration}
|
||||
morph={this.props.morph}
|
||||
blur={this.props.blur}
|
||||
H_LO={this.props.H_LO}
|
||||
S_LO={this.props.S_LO}
|
||||
V_LO={this.props.V_LO}
|
||||
H_HI={this.props.H_HI}
|
||||
S_HI={this.props.S_HI}
|
||||
V_HI={this.props.V_HI}
|
||||
invertHue={!!envGet(
|
||||
"CAMERA_CALIBRATION_invert_hue_selection",
|
||||
this.props.env)} />
|
||||
</MustBeOnline>
|
||||
</Col>
|
||||
</Row>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
return <Widget className={classname}>
|
||||
<TitleBar
|
||||
title={"Camera Calibration"}
|
||||
help={t(ToolTips.CAMERA_CALIBRATION)}
|
||||
docs={"farmware#section-weed-detector"}
|
||||
onCalibrate={this.props.dispatch(calibrate)}
|
||||
env={this.props.env} />
|
||||
<WidgetBody>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.syncStatus}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<ImageWorkspace
|
||||
onProcessPhoto={(id) => { this.props.dispatch(scanImage(id)); }}
|
||||
onFlip={(uuid) => this.props.dispatch(selectImage(uuid))}
|
||||
images={this.props.images}
|
||||
currentImage={this.props.currentImage}
|
||||
onChange={(key, value) => {
|
||||
const MAPPING: Record<typeof key, WDENVKey> = {
|
||||
"iteration": "CAMERA_CALIBRATION_iteration",
|
||||
"morph": "CAMERA_CALIBRATION_morph",
|
||||
"blur": "CAMERA_CALIBRATION_blur",
|
||||
"H_HI": "CAMERA_CALIBRATION_H_HI",
|
||||
"H_LO": "CAMERA_CALIBRATION_H_LO",
|
||||
"S_HI": "CAMERA_CALIBRATION_S_HI",
|
||||
"S_LO": "CAMERA_CALIBRATION_S_LO",
|
||||
"V_HI": "CAMERA_CALIBRATION_V_HI",
|
||||
"V_LO": "CAMERA_CALIBRATION_V_LO"
|
||||
};
|
||||
envSave(MAPPING[key], value);
|
||||
}}
|
||||
iteration={this.props.iteration}
|
||||
morph={this.props.morph}
|
||||
blur={this.props.blur}
|
||||
H_LO={this.props.H_LO}
|
||||
S_LO={this.props.S_LO}
|
||||
V_LO={this.props.V_LO}
|
||||
H_HI={this.props.H_HI}
|
||||
S_HI={this.props.S_HI}
|
||||
V_HI={this.props.V_HI}
|
||||
invertHue={!!envGet(
|
||||
"CAMERA_CALIBRATION_invert_hue_selection",
|
||||
this.props.env)} />
|
||||
</MustBeOnline>
|
||||
</Col>
|
||||
</Row>
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,15 +8,11 @@ import {
|
|||
import { MustBeOnline } from "../devices/must_be_online";
|
||||
import { ToolTips, Content } from "../constants";
|
||||
import {
|
||||
Widget,
|
||||
WidgetHeader,
|
||||
WidgetBody,
|
||||
Row,
|
||||
Col,
|
||||
DropDownItem
|
||||
} from "../ui";
|
||||
Widget, WidgetHeader, WidgetBody,
|
||||
Row, Col,
|
||||
FBSelect, DropDownItem
|
||||
} from "../ui/index";
|
||||
import { betterCompact } from "../util";
|
||||
import { FBSelect } from "../ui/new_fb_select";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
import { getFirstPartyFarmwareList } from "./actions";
|
||||
|
||||
|
@ -155,88 +151,86 @@ export class FarmwarePanel extends React.Component<FWProps, Partial<FWState>> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Widget className="farmware-widget">
|
||||
<WidgetHeader
|
||||
title="Farmware"
|
||||
helpText={ToolTips.FARMWARE}>
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
<FarmwareConfigMenu
|
||||
show={this.state.showFirstParty}
|
||||
toggle={this.toggleFirstPartyDisplay}
|
||||
firstPartyFwsInstalled={
|
||||
this.firstPartyFarmwaresPresent(this.state.firstPartyList)} />
|
||||
</Popover>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.syncStatus}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<Row>
|
||||
<fieldset>
|
||||
<Col xs={12}>
|
||||
<input type="url"
|
||||
placeholder={"https://...."}
|
||||
value={this.state.packageUrl || ""}
|
||||
onChange={(e) => {
|
||||
this.setState({ packageUrl: e.currentTarget.value });
|
||||
}} />
|
||||
</Col>
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={this.install}>
|
||||
{t("Install")}
|
||||
</button>
|
||||
</Col>
|
||||
</fieldset>
|
||||
</Row>
|
||||
<Row>
|
||||
<fieldset>
|
||||
<Col xs={12}>
|
||||
<FBSelect
|
||||
key={"farmware_" + this.selectedItem()}
|
||||
list={this.fwList()}
|
||||
selectedItem={this.selectedItem()}
|
||||
onChange={(x) => {
|
||||
const selectedFarmware = x.value;
|
||||
if (_.isString(selectedFarmware)) {
|
||||
this.setState({ selectedFarmware });
|
||||
} else {
|
||||
throw new Error(`Bad farmware name: ${x.value}`);
|
||||
}
|
||||
}}
|
||||
placeholder="Installed Farmware Packages" />
|
||||
</Col>
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={this.remove}>
|
||||
{t("Remove")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button yellow"
|
||||
onClick={this.update}>
|
||||
{t("Update")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={this.run}>
|
||||
{t("Run")}
|
||||
</button>
|
||||
</Col>
|
||||
</fieldset>
|
||||
</Row>
|
||||
<Row>
|
||||
return <Widget className="farmware-widget">
|
||||
<WidgetHeader
|
||||
title="Farmware"
|
||||
helpText={ToolTips.FARMWARE}>
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
<FarmwareConfigMenu
|
||||
show={this.state.showFirstParty}
|
||||
toggle={this.toggleFirstPartyDisplay}
|
||||
firstPartyFwsInstalled={
|
||||
this.firstPartyFarmwaresPresent(this.state.firstPartyList)} />
|
||||
</Popover>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<MustBeOnline
|
||||
syncStatus={this.props.syncStatus}
|
||||
networkState={this.props.botToMqttStatus}
|
||||
lockOpen={process.env.NODE_ENV !== "production"}>
|
||||
<Row>
|
||||
<fieldset>
|
||||
<Col xs={12}>
|
||||
{this.fwDescription(this.state.selectedFarmware)}
|
||||
<input type="url"
|
||||
placeholder={"https://...."}
|
||||
value={this.state.packageUrl || ""}
|
||||
onChange={(e) => {
|
||||
this.setState({ packageUrl: e.currentTarget.value });
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={this.install}>
|
||||
{t("Install")}
|
||||
</button>
|
||||
</Col>
|
||||
</fieldset>
|
||||
</Row>
|
||||
<Row>
|
||||
<fieldset>
|
||||
<Col xs={12}>
|
||||
<FBSelect
|
||||
key={"farmware_" + this.selectedItem()}
|
||||
list={this.fwList()}
|
||||
selectedItem={this.selectedItem()}
|
||||
onChange={(x) => {
|
||||
const selectedFarmware = x.value;
|
||||
if (_.isString(selectedFarmware)) {
|
||||
this.setState({ selectedFarmware });
|
||||
} else {
|
||||
throw new Error(`Bad farmware name: ${x.value}`);
|
||||
}
|
||||
}}
|
||||
placeholder="Installed Farmware Packages" />
|
||||
</Col>
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={this.remove}>
|
||||
{t("Remove")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button yellow"
|
||||
onClick={this.update}>
|
||||
{t("Update")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={this.run}>
|
||||
{t("Run")}
|
||||
</button>
|
||||
</Col>
|
||||
</fieldset>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
{this.fwDescription(this.state.selectedFarmware)}
|
||||
</Col>
|
||||
</Row>
|
||||
</MustBeOnline>
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,22 +63,20 @@ export class ImageFlipper extends
|
|||
render() {
|
||||
const image = this.imageJSX();
|
||||
const multipleImages = this.props.images.length > 1;
|
||||
return (
|
||||
<div className="image-flipper">
|
||||
{image}
|
||||
<button
|
||||
onClick={this.go(1)}
|
||||
disabled={!multipleImages || this.state.disablePrev}
|
||||
className="image-flipper-left fb-button">
|
||||
{t("Prev")}
|
||||
</button>
|
||||
<button
|
||||
onClick={this.go(-1)}
|
||||
disabled={!multipleImages || this.state.disableNext}
|
||||
className="image-flipper-right fb-button">
|
||||
{t("Next")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
return <div className="image-flipper">
|
||||
{image}
|
||||
<button
|
||||
onClick={this.go(1)}
|
||||
disabled={!multipleImages || this.state.disablePrev}
|
||||
className="image-flipper-left fb-button">
|
||||
{t("Prev")}
|
||||
</button>
|
||||
<button
|
||||
onClick={this.go(-1)}
|
||||
disabled={!multipleImages || this.state.disableNext}
|
||||
className="image-flipper-right fb-button">
|
||||
{t("Next")}
|
||||
</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,12 @@ import * as _ from "lodash";
|
|||
import * as moment from "moment";
|
||||
import { t } from "i18next";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
|
||||
import { Widget, WidgetHeader, WidgetBody, WidgetFooter } from "../../ui/index";
|
||||
import { ImageFlipper } from "./image_flipper";
|
||||
import { PhotosProps } from "./interfaces";
|
||||
import { getDevice } from "../../device";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { selectImage } from "./actions";
|
||||
import { WidgetFooter } from "../../ui/widget_footer";
|
||||
import { safeStringFetch } from "../../util";
|
||||
import { destroy } from "../../api/crud";
|
||||
|
||||
|
@ -26,12 +25,10 @@ interface MetaInfoProps {
|
|||
function MetaInfo({ obj, attr, label }: MetaInfoProps) {
|
||||
const top = label || _.startCase(attr.split("_").join());
|
||||
const bottom = safeStringFetch(obj, attr);
|
||||
return (
|
||||
<div>
|
||||
<label>{top}:</label>
|
||||
<span>{bottom || "unknown"}</span>
|
||||
</div>
|
||||
);
|
||||
return <div>
|
||||
<label>{top}:</label>
|
||||
<span>{bottom || "unknown"}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export class Photos extends React.Component<PhotosProps, {}> {
|
||||
|
@ -66,43 +63,43 @@ export class Photos extends React.Component<PhotosProps, {}> {
|
|||
render() {
|
||||
const image = this.props.currentImage;
|
||||
const created_at = image
|
||||
? moment(image.body.created_at).utcOffset(this.props.timeOffset).format("MMMM Do, YYYY h:mma")
|
||||
? moment(image.body.created_at)
|
||||
.utcOffset(this.props.timeOffset)
|
||||
.format("MMMM Do, YYYY h:mma")
|
||||
: "";
|
||||
return (
|
||||
<Widget className="photos-widget">
|
||||
<WidgetHeader helpText={ToolTips.PHOTOS} title={"Photos"}>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={this.takePhoto}>
|
||||
{t("Take Photo")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={() => this.destroy()}>
|
||||
{t("Delete Photo")}
|
||||
</button>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<ImageFlipper
|
||||
onFlip={id => { this.props.dispatch(selectImage(id)); }}
|
||||
currentImage={this.props.currentImage}
|
||||
images={this.props.images} />
|
||||
</WidgetBody>
|
||||
<WidgetFooter>
|
||||
{/** Separated from <MetaInfo /> for stylistic purposes. */}
|
||||
{image ?
|
||||
<div className="image-created-at">
|
||||
<label>{t("Created At:")}</label>
|
||||
<span>
|
||||
{created_at}
|
||||
</span>
|
||||
</div>
|
||||
: ""}
|
||||
<div className="image-metadatas">
|
||||
{this.metaDatas()}
|
||||
return <Widget className="photos-widget">
|
||||
<WidgetHeader helpText={ToolTips.PHOTOS} title={"Photos"}>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={this.takePhoto}>
|
||||
{t("Take Photo")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={() => this.destroy()}>
|
||||
{t("Delete Photo")}
|
||||
</button>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<ImageFlipper
|
||||
onFlip={id => { this.props.dispatch(selectImage(id)); }}
|
||||
currentImage={this.props.currentImage}
|
||||
images={this.props.images} />
|
||||
</WidgetBody>
|
||||
<WidgetFooter>
|
||||
{/** Separated from <MetaInfo /> for stylistic purposes. */}
|
||||
{image ?
|
||||
<div className="image-created-at">
|
||||
<label>{t("Created At:")}</label>
|
||||
<span>
|
||||
{created_at}
|
||||
</span>
|
||||
</div>
|
||||
</WidgetFooter>
|
||||
</Widget>
|
||||
);
|
||||
: ""}
|
||||
<div className="image-metadatas">
|
||||
{this.metaDatas()}
|
||||
</div>
|
||||
</WidgetFooter>
|
||||
</Widget>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { DropDownItem } from "../../ui/fb_select";
|
||||
import { Row, Col, NULL_CHOICE } from "../../ui/index";
|
||||
import { FBSelect } from "../../ui/new_fb_select";
|
||||
import {
|
||||
BlurableInput,
|
||||
Row, Col,
|
||||
FBSelect, NULL_CHOICE, DropDownItem
|
||||
} from "../../ui/index";
|
||||
import { SettingsMenuProps } from "./interfaces";
|
||||
import * as _ from "lodash";
|
||||
import { BlurableInput } from "../../ui/blurable_input";
|
||||
import { SPECIAL_VALUE_DDI, CALIBRATION_DROPDOWNS, ORIGIN_DROPDOWNS } from "./constants";
|
||||
import {
|
||||
SPECIAL_VALUE_DDI, CALIBRATION_DROPDOWNS, ORIGIN_DROPDOWNS
|
||||
} from "./constants";
|
||||
import { WD_ENV } from "./remote_env/interfaces";
|
||||
import { envGet } from "./remote_env/selectors";
|
||||
import { SPECIAL_VALUES } from "./remote_env/constants";
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import { Hue, Saturation } from "react-color/lib/components/common";
|
||||
import { FarmbotPickerProps } from "./interfaces";
|
||||
import * as _ from "lodash";
|
||||
import { Color } from "../../ui/colors";
|
||||
import { Color } from "../../ui/index";
|
||||
|
||||
/** Wrapper class around `react-color`'s `<Saturation />` and `<Hue />`.
|
||||
* Add an extra white box feature for showing user weed detection settings.
|
||||
|
|
|
@ -76,92 +76,90 @@ export class ImageWorkspace extends React.Component<Props, {}> {
|
|||
render() {
|
||||
const { H_LO, H_HI, S_LO, S_HI, V_LO, V_HI } = this.props;
|
||||
|
||||
return (
|
||||
<div className="widget-content">
|
||||
<Row>
|
||||
<Col xs={12} md={6}>
|
||||
<h4>
|
||||
<i>{t("Color Range")}</i>
|
||||
</h4>
|
||||
<label htmlFor="hue">{t("HUE")}</label>
|
||||
<WeedDetectorSlider
|
||||
onRelease={this.onHslChange("H")}
|
||||
lowest={RANGES.H.LOWEST}
|
||||
highest={RANGES.H.HIGHEST}
|
||||
lowValue={Math.min(H_LO, H_HI)}
|
||||
highValue={Math.max(H_LO, H_HI)} />
|
||||
<label htmlFor="saturation">{t("SATURATION")}</label>
|
||||
<WeedDetectorSlider
|
||||
onRelease={this.onHslChange("S")}
|
||||
lowest={RANGES.S.LOWEST}
|
||||
highest={RANGES.S.HIGHEST}
|
||||
lowValue={S_LO}
|
||||
highValue={S_HI} />
|
||||
<label htmlFor="value">{t("VALUE")}</label>
|
||||
<WeedDetectorSlider
|
||||
onRelease={this.onHslChange("V")}
|
||||
lowest={RANGES.V.LOWEST}
|
||||
highest={RANGES.V.HIGHEST}
|
||||
lowValue={V_LO}
|
||||
highValue={V_HI} />
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<FarmbotColorPicker
|
||||
h={[H_LO, H_HI]}
|
||||
s={[S_LO, S_HI]}
|
||||
v={[V_LO, V_HI]}
|
||||
invertHue={this.props.invertHue} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<h4>
|
||||
<i>{t("Processing Parameters")}</i>
|
||||
</h4>
|
||||
</Col>
|
||||
return <div className="widget-content">
|
||||
<Row>
|
||||
<Col xs={12} md={6}>
|
||||
<h4>
|
||||
<i>{t("Color Range")}</i>
|
||||
</h4>
|
||||
<label htmlFor="hue">{t("HUE")}</label>
|
||||
<WeedDetectorSlider
|
||||
onRelease={this.onHslChange("H")}
|
||||
lowest={RANGES.H.LOWEST}
|
||||
highest={RANGES.H.HIGHEST}
|
||||
lowValue={Math.min(H_LO, H_HI)}
|
||||
highValue={Math.max(H_LO, H_HI)} />
|
||||
<label htmlFor="saturation">{t("SATURATION")}</label>
|
||||
<WeedDetectorSlider
|
||||
onRelease={this.onHslChange("S")}
|
||||
lowest={RANGES.S.LOWEST}
|
||||
highest={RANGES.S.HIGHEST}
|
||||
lowValue={S_LO}
|
||||
highValue={S_HI} />
|
||||
<label htmlFor="value">{t("VALUE")}</label>
|
||||
<WeedDetectorSlider
|
||||
onRelease={this.onHslChange("V")}
|
||||
lowest={RANGES.V.LOWEST}
|
||||
highest={RANGES.V.HIGHEST}
|
||||
lowValue={V_LO}
|
||||
highValue={V_HI} />
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<FarmbotColorPicker
|
||||
h={[H_LO, H_HI]}
|
||||
s={[S_LO, S_HI]}
|
||||
v={[V_LO, V_HI]}
|
||||
invertHue={this.props.invertHue} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<h4>
|
||||
<i>{t("Processing Parameters")}</i>
|
||||
</h4>
|
||||
</Col>
|
||||
|
||||
<Col xs={4}>
|
||||
<label>{t("BLUR")}</label>
|
||||
<BlurableInput type="number"
|
||||
min={RANGES.BLUR.LOWEST}
|
||||
max={RANGES.BLUR.HIGHEST}
|
||||
onCommit={this.numericChange("blur")}
|
||||
value={"" + this.props.blur} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<label>{t("BLUR")}</label>
|
||||
<BlurableInput type="number"
|
||||
min={RANGES.BLUR.LOWEST}
|
||||
max={RANGES.BLUR.HIGHEST}
|
||||
onCommit={this.numericChange("blur")}
|
||||
value={"" + this.props.blur} />
|
||||
</Col>
|
||||
|
||||
<Col xs={4}>
|
||||
<label>{t("MORPH")}</label>
|
||||
<BlurableInput type="number"
|
||||
min={RANGES.MORPH.LOWEST}
|
||||
max={RANGES.MORPH.HIGHEST}
|
||||
onCommit={this.numericChange("morph")}
|
||||
value={"" + this.props.morph} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<label>{t("ITERATION")}</label>
|
||||
<BlurableInput type="number"
|
||||
min={RANGES.ITERATION.LOWEST}
|
||||
max={RANGES.ITERATION.HIGHEST}
|
||||
onCommit={this.numericChange("iteration")}
|
||||
value={"" + this.props.iteration} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="green fb-button"
|
||||
title="Scan this image"
|
||||
onClick={this.maybeProcessPhoto}
|
||||
hidden={!this.props.images.length} >
|
||||
{t("Scan image")}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
<ImageFlipper
|
||||
onFlip={this.props.onFlip}
|
||||
images={this.props.images}
|
||||
currentImage={this.props.currentImage} />
|
||||
</div>
|
||||
);
|
||||
<Col xs={4}>
|
||||
<label>{t("MORPH")}</label>
|
||||
<BlurableInput type="number"
|
||||
min={RANGES.MORPH.LOWEST}
|
||||
max={RANGES.MORPH.HIGHEST}
|
||||
onCommit={this.numericChange("morph")}
|
||||
value={"" + this.props.morph} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<label>{t("ITERATION")}</label>
|
||||
<BlurableInput type="number"
|
||||
min={RANGES.ITERATION.LOWEST}
|
||||
max={RANGES.ITERATION.HIGHEST}
|
||||
onCommit={this.numericChange("iteration")}
|
||||
value={"" + this.props.iteration} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="green fb-button"
|
||||
title="Scan this image"
|
||||
onClick={this.maybeProcessPhoto}
|
||||
hidden={!this.props.images.length} >
|
||||
{t("Scan image")}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
<ImageFlipper
|
||||
onFlip={this.props.onFlip}
|
||||
images={this.props.images}
|
||||
currentImage={this.props.currentImage} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DropDownItem, NULL_CHOICE } from "../../ui/fb_select";
|
||||
import { DropDownItem, NULL_CHOICE } from "../../ui/index";
|
||||
import { SPECIAL_VALUE_DDI } from "./constants";
|
||||
import { WD_ENV } from "./remote_env/interfaces";
|
||||
import { envGet } from "./remote_env/selectors";
|
||||
|
|
|
@ -5,7 +5,7 @@ import { WidgetHeader } from "../../ui/index";
|
|||
import { WD_ENV } from "./remote_env/interfaces";
|
||||
import { envSave } from "./remote_env/actions";
|
||||
import { Popover, PopoverInteractionKind } from "@blueprintjs/core";
|
||||
import { DocSlug } from "../../ui/doc_link";
|
||||
import { DocSlug } from "../../ui/index";
|
||||
|
||||
type ClickHandler = React.EventHandler<React.MouseEvent<HTMLButtonElement>>;
|
||||
|
||||
|
@ -32,41 +32,39 @@ export function TitleBar({
|
|||
help,
|
||||
docs
|
||||
}: Props) {
|
||||
return (
|
||||
<WidgetHeader helpText={help} title={title} docPage={docs}>
|
||||
<button
|
||||
hidden={!onSave}
|
||||
onClick={onSave}
|
||||
className="fb-button green" >
|
||||
{t("SAVE")}
|
||||
</button>
|
||||
<button
|
||||
hidden={!onTest}
|
||||
onClick={onTest}
|
||||
className="fb-button yellow" >
|
||||
{t("TEST")}
|
||||
</button>
|
||||
<button
|
||||
hidden={!onDeletionClick}
|
||||
onClick={onDeletionClick}
|
||||
className="fb-button red" >
|
||||
{deletionProgress || t("CLEAR WEEDS")}
|
||||
</button>
|
||||
<button
|
||||
hidden={!onCalibrate}
|
||||
onClick={onCalibrate}
|
||||
className="fb-button green" >
|
||||
{t("Calibrate")}
|
||||
</button>
|
||||
<div hidden={!env}>
|
||||
<Popover
|
||||
interactionKind={PopoverInteractionKind.CLICK_TARGET_ONLY}>
|
||||
<i className="fa fa-cog" />
|
||||
{(env && <WeedDetectorConfig
|
||||
values={env}
|
||||
onChange={envSave} />)}
|
||||
</Popover>
|
||||
</div>
|
||||
</WidgetHeader>
|
||||
);
|
||||
return <WidgetHeader helpText={help} title={title} docPage={docs}>
|
||||
<button
|
||||
hidden={!onSave}
|
||||
onClick={onSave}
|
||||
className="fb-button green" >
|
||||
{t("SAVE")}
|
||||
</button>
|
||||
<button
|
||||
hidden={!onTest}
|
||||
onClick={onTest}
|
||||
className="fb-button yellow" >
|
||||
{t("TEST")}
|
||||
</button>
|
||||
<button
|
||||
hidden={!onDeletionClick}
|
||||
onClick={onDeletionClick}
|
||||
className="fb-button red" >
|
||||
{deletionProgress || t("CLEAR WEEDS")}
|
||||
</button>
|
||||
<button
|
||||
hidden={!onCalibrate}
|
||||
onClick={onCalibrate}
|
||||
className="fb-button green" >
|
||||
{t("Calibrate")}
|
||||
</button>
|
||||
<div hidden={!env}>
|
||||
<Popover
|
||||
interactionKind={PopoverInteractionKind.CLICK_TARGET_ONLY}>
|
||||
<i className="fa fa-cog" />
|
||||
{(env && <WeedDetectorConfig
|
||||
values={env}
|
||||
onChange={envSave} />)}
|
||||
</Popover>
|
||||
</div>
|
||||
</WidgetHeader>;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@ jest.mock("../resend_verification", () => {
|
|||
});
|
||||
|
||||
import * as React from "react";
|
||||
import { FormField, sendEmail, DidRegister, MustRegister, CreateAccount } from "../create_account";
|
||||
import {
|
||||
FormField, sendEmail, DidRegister, MustRegister, CreateAccount
|
||||
} from "../create_account";
|
||||
import { shallow } from "enzyme";
|
||||
import { BlurableInput } from "../../ui/index";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
|
|
|
@ -144,32 +144,30 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
|
|||
const TOS_URL = globalConfig.TOS_URL;
|
||||
if (TOS_URL) {
|
||||
const PRV_URL = globalConfig.PRIV_URL;
|
||||
return (
|
||||
<div>
|
||||
<div className={"tos"}>
|
||||
<label>{t("I agree to the terms of use")}</label>
|
||||
<input type="checkbox"
|
||||
onChange={this.set("agreeToTerms")}
|
||||
value={this.state.agreeToTerms ? "false" : "true"} />
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href={PRV_URL}
|
||||
target="_blank">
|
||||
{t("Privacy Policy")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={TOS_URL}
|
||||
target="_blank">
|
||||
{t("Terms of Use")}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
return <div>
|
||||
<div className={"tos"}>
|
||||
<label>{t("I agree to the terms of use")}</label>
|
||||
<input type="checkbox"
|
||||
onChange={this.set("agreeToTerms")}
|
||||
value={this.state.agreeToTerms ? "false" : "true"} />
|
||||
</div>
|
||||
);
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href={PRV_URL}
|
||||
target="_blank">
|
||||
{t("Privacy Policy")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={TOS_URL}
|
||||
target="_blank">
|
||||
{t("Terms of Use")}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,35 +34,31 @@ export class HotKeys extends React.Component<Props, Partial<State>> {
|
|||
state: State = { guideOpen: false };
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Overlay
|
||||
isOpen={this.state.guideOpen}
|
||||
onClose={this.toggle("guideOpen")}>
|
||||
<div className={hotkeyGuideClasses}>
|
||||
<h3>{t("Hotkeys")}</h3>
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={this.toggle("guideOpen")} />
|
||||
{
|
||||
this.hotkeys(this.props.dispatch, "")
|
||||
.map(hotkey => {
|
||||
return (
|
||||
<Row key={hotkey.combo}>
|
||||
<Col xs={5}>
|
||||
<label>{hotkey.label}</label>
|
||||
</Col>
|
||||
<Col xs={7}>
|
||||
<code>{hotkey.combo}</code>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
return <div>
|
||||
<Overlay
|
||||
isOpen={this.state.guideOpen}
|
||||
onClose={this.toggle("guideOpen")}>
|
||||
<div className={hotkeyGuideClasses}>
|
||||
<h3>{t("Hotkeys")}</h3>
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={this.toggle("guideOpen")} />
|
||||
{
|
||||
this.hotkeys(this.props.dispatch, "")
|
||||
.map(hotkey => {
|
||||
return <Row key={hotkey.combo}>
|
||||
<Col xs={5}>
|
||||
<label>{hotkey.label}</label>
|
||||
</Col>
|
||||
<Col xs={7}>
|
||||
<code>{hotkey.combo}</code>
|
||||
</Col>
|
||||
</Row>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>;
|
||||
}
|
||||
|
||||
toggle = (property: keyof State) => () =>
|
||||
|
|
|
@ -68,7 +68,7 @@ const filterByVerbosity = (state: LogsState, logs: TaggedLog[]) => {
|
|||
})
|
||||
.filter((log: TaggedLog) => {
|
||||
const type = (log.body.meta || {}).type;
|
||||
const verbosity = log.body.meta.verbosity;
|
||||
const { verbosity } = log.body.meta;
|
||||
const filterLevel = state[type as keyof LogsState];
|
||||
const displayLog = verbosity
|
||||
? verbosity <= filterLevel
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import * as moment from "moment";
|
||||
import { connect } from "react-redux";
|
||||
import { Col, Row, Page, ToolTip } from "../ui";
|
||||
import { Col, Row, Page, ToolTip } from "../ui/index";
|
||||
import { mapStateToProps } from "./state_to_props";
|
||||
import { t } from "i18next";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
|
|
|
@ -3,7 +3,7 @@ import { t } from "i18next";
|
|||
import { NavBarProps, NavBarState } from "./interfaces";
|
||||
import { EStopButton } from "../devices/components/e_stop_btn";
|
||||
import { Session } from "../session";
|
||||
import { Row, Col } from "../ui";
|
||||
import { Row, Col } from "../ui/index";
|
||||
import { getPathArray } from "../history";
|
||||
import { updatePageInfo } from "../util";
|
||||
import { SyncButton } from "./sync_button";
|
||||
|
@ -61,49 +61,47 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
const { mobileMenuOpen, tickerListOpen, accountMenuOpen } = this.state;
|
||||
const { logs, timeOffset } = this.props;
|
||||
|
||||
return (
|
||||
<div className="nav-wrapper">
|
||||
<nav role="navigation">
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<div>
|
||||
<TickerList { ...{ logs, tickerListOpen, toggle, timeOffset } } />
|
||||
<div className="nav-group">
|
||||
<div className="nav-left">
|
||||
<i
|
||||
className={menuIconClassNames.join(" ")}
|
||||
onClick={this.toggle("mobileMenuOpen")} />
|
||||
<span className="mobile-menu-container">
|
||||
{MobileMenu({ close, mobileMenuOpen })}
|
||||
</span>
|
||||
<span className="top-menu-container">
|
||||
{NavLinks({ close })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-right">
|
||||
<Popover
|
||||
inline
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
target={
|
||||
<div className="nav-name"
|
||||
onClick={this.toggle("accountMenuOpen")}>
|
||||
{firstName}
|
||||
</div>}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
content={AdditionalMenu({ logout: this.logout, close })}
|
||||
isOpen={accountMenuOpen}
|
||||
onClose={this.close("accountMenuOpen")} />
|
||||
<EStopButton
|
||||
bot={this.props.bot}
|
||||
user={this.props.user} />
|
||||
{this.syncButton()}
|
||||
</div>
|
||||
return <div className="nav-wrapper">
|
||||
<nav role="navigation">
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<div>
|
||||
<TickerList { ...{ logs, tickerListOpen, toggle, timeOffset } } />
|
||||
<div className="nav-group">
|
||||
<div className="nav-left">
|
||||
<i
|
||||
className={menuIconClassNames.join(" ")}
|
||||
onClick={this.toggle("mobileMenuOpen")} />
|
||||
<span className="mobile-menu-container">
|
||||
{MobileMenu({ close, mobileMenuOpen })}
|
||||
</span>
|
||||
<span className="top-menu-container">
|
||||
{NavLinks({ close })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-right">
|
||||
<Popover
|
||||
inline
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
target={
|
||||
<div className="nav-name"
|
||||
onClick={this.toggle("accountMenuOpen")}>
|
||||
{firstName}
|
||||
</div>}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
content={AdditionalMenu({ logout: this.logout, close })}
|
||||
isOpen={accountMenuOpen}
|
||||
onClose={this.close("accountMenuOpen")} />
|
||||
<EStopButton
|
||||
bot={this.props.bot}
|
||||
user={this.props.user} />
|
||||
{this.syncButton()}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</nav>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,15 +8,13 @@ const classes = [Classes.CARD, Classes.ELEVATION_4, "mobile-menu"];
|
|||
|
||||
export let MobileMenu = (props: MobileMenuProps) => {
|
||||
const isActive = props.mobileMenuOpen ? "active" : "inactive";
|
||||
return (
|
||||
<div>
|
||||
<Overlay
|
||||
isOpen={props.mobileMenuOpen}
|
||||
onClose={props.close("mobileMenuOpen")}>
|
||||
<div className={`${classes.join(" ")} ${isActive}`}>
|
||||
{NavLinks({ close: props.close })}
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
return <div>
|
||||
<Overlay
|
||||
isOpen={props.mobileMenuOpen}
|
||||
onClose={props.close("mobileMenuOpen")}>
|
||||
<div className={`${classes.join(" ")} ${isActive}`}>
|
||||
{NavLinks({ close: props.close })}
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -16,23 +16,19 @@ export const links = [
|
|||
|
||||
export const NavLinks = (props: NavLinksProps) => {
|
||||
const currPageSlug = getPathArray()[2];
|
||||
return (
|
||||
<div className="links">
|
||||
<div className="nav-links">
|
||||
{links.map(link => {
|
||||
const isActive = (currPageSlug === link.slug) ? "active" : "";
|
||||
return (
|
||||
<Link
|
||||
to={"/app/" + link.slug}
|
||||
className={`${isActive}`}
|
||||
key={link.slug}
|
||||
onClick={props.close("mobileMenuOpen")}>
|
||||
<i className={`fa fa-${link.icon}`} />
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return <div className="links">
|
||||
<div className="nav-links">
|
||||
{links.map(link => {
|
||||
const isActive = (currPageSlug === link.slug) ? "active" : "";
|
||||
return <Link
|
||||
to={"/app/" + link.slug}
|
||||
className={`${isActive}`}
|
||||
key={link.slug}
|
||||
onClick={props.close("mobileMenuOpen")}>
|
||||
<i className={`fa fa-${link.icon}`} />
|
||||
{link.name}
|
||||
</Link>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -31,11 +31,9 @@ export function SyncButton({ user, bot, dispatch, consistent }: NavButtonProps)
|
|||
sync_status = sync_status || "unknown";
|
||||
const color = consistent ? (COLOR_MAPPING[sync_status] || "red") : "gray";
|
||||
const text = TEXT_MAPPING[sync_status] || "DISCONNECTED";
|
||||
return (
|
||||
<button
|
||||
className={`nav-sync ${color} fb-button`}
|
||||
onClick={() => dispatch(sync())}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
return <button
|
||||
className={`nav-sync ${color} fb-button`}
|
||||
onClick={() => dispatch(sync())}>
|
||||
{text}
|
||||
</button>;
|
||||
}
|
||||
|
|
|
@ -48,46 +48,42 @@ const getfirstTickerLog = (logs: Log[]): Log => {
|
|||
const Ticker = (log: Log, index: number, timeOffset: number) => {
|
||||
const time = formatLogTime(log.created_at, timeOffset);
|
||||
const type = (log.meta || {}).type;
|
||||
return (
|
||||
// TODO: Should utilize log's `uuid` instead of index.
|
||||
<div key={index} className="status-ticker-wrapper">
|
||||
<div className={`saucer ${type}`} />
|
||||
<label className="status-ticker-message">
|
||||
<Markdown>
|
||||
{log.message.replace(/\s+/g, " ") || "Loading"}
|
||||
</Markdown>
|
||||
</label>
|
||||
<label className="status-ticker-created-at">
|
||||
{time}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
// TODO: Should utilize log's `uuid` instead of index.
|
||||
return <div key={index} className="status-ticker-wrapper">
|
||||
<div className={`saucer ${type}`} />
|
||||
<label className="status-ticker-message">
|
||||
<Markdown>
|
||||
{log.message.replace(/\s+/g, " ") || "Loading"}
|
||||
</Markdown>
|
||||
</label>
|
||||
<label className="status-ticker-created-at">
|
||||
{time}
|
||||
</label>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export let TickerList = (props: TickerListProps) => {
|
||||
return (
|
||||
<div
|
||||
className="ticker-list"
|
||||
onClick={props.toggle("tickerListOpen")} >
|
||||
<div className="first-ticker">
|
||||
{Ticker(getfirstTickerLog(props.logs), -1, props.timeOffset)}
|
||||
</div>
|
||||
<Collapse isOpen={props.tickerListOpen}>
|
||||
{props
|
||||
.logs
|
||||
.filter((log, index) => index !== 0)
|
||||
.filter((log) => logFilter(log))
|
||||
.map((log: Log, index: number) => Ticker(log, index, props.timeOffset))}
|
||||
</Collapse>
|
||||
<Collapse isOpen={props.tickerListOpen}>
|
||||
<Link to={"/app/logs"}>
|
||||
<div className="logs-page-link">
|
||||
<label>
|
||||
{t("Filter logs")}
|
||||
</label>
|
||||
</div>
|
||||
</Link>
|
||||
</Collapse>
|
||||
return <div
|
||||
className="ticker-list"
|
||||
onClick={props.toggle("tickerListOpen")} >
|
||||
<div className="first-ticker">
|
||||
{Ticker(getfirstTickerLog(props.logs), -1, props.timeOffset)}
|
||||
</div>
|
||||
);
|
||||
<Collapse isOpen={props.tickerListOpen}>
|
||||
{props
|
||||
.logs
|
||||
.filter((log, index) => index !== 0)
|
||||
.filter((log) => logFilter(log))
|
||||
.map((log: Log, index: number) => Ticker(log, index, props.timeOffset))}
|
||||
</Collapse>
|
||||
<Collapse isOpen={props.tickerListOpen}>
|
||||
<Link to={"/app/logs"}>
|
||||
<div className="logs-page-link">
|
||||
<label>
|
||||
{t("Filter logs")}
|
||||
</label>
|
||||
</div>
|
||||
</Link>
|
||||
</Collapse>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -66,47 +66,45 @@ export class PasswordReset extends React.Component<Props, State> {
|
|||
borderBottom: "none"
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="static-page">
|
||||
<div className="all-content-wrapper">
|
||||
<h1 className="text-center">
|
||||
{t("Reset your password")}
|
||||
</h1>
|
||||
<br />
|
||||
<Row>
|
||||
<Col xs={12} sm={6} className="col-sm-push-3">
|
||||
<Widget>
|
||||
<WidgetHeader title={"Reset Password"} />
|
||||
<WidgetBody>
|
||||
<form onSubmit={this.submit.bind(this)}>
|
||||
<label>
|
||||
{t("New Password")}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this.set("password").bind(this)} />
|
||||
<label>
|
||||
{t("Confirm New Password")}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this.set("passwordConfirmation").bind(this)} />
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="fb-button green pull-right"
|
||||
style={buttonStylesUniqueToOnlyThisPage}>
|
||||
{t("Reset")}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</form>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
return <div className="static-page">
|
||||
<div className="all-content-wrapper">
|
||||
<h1 className="text-center">
|
||||
{t("Reset your password")}
|
||||
</h1>
|
||||
<br />
|
||||
<Row>
|
||||
<Col xs={12} sm={6} className="col-sm-push-3">
|
||||
<Widget>
|
||||
<WidgetHeader title={"Reset Password"} />
|
||||
<WidgetBody>
|
||||
<form onSubmit={this.submit.bind(this)}>
|
||||
<label>
|
||||
{t("New Password")}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this.set("password").bind(this)} />
|
||||
<label>
|
||||
{t("Confirm New Password")}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this.set("passwordConfirmation").bind(this)} />
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<button
|
||||
className="fb-button green pull-right"
|
||||
style={buttonStylesUniqueToOnlyThisPage}>
|
||||
{t("Reset")}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</form>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue