Merge branch 'master' into master

pull/496/head
Rick Carlino 2017-10-11 13:24:37 -05:00 committed by GitHub
commit f22b209872
21 changed files with 138 additions and 42 deletions

View File

@ -28,7 +28,7 @@ You will need the following:
1. A Linux or Mac based machine. We do not support windows at this time. 1. A Linux or Mac based machine. We do not support windows at this time.
0. [Docker 17.06.0-ce or greater](https://docs.docker.com/engine/installation/) 0. [Docker 17.06.0-ce or greater](https://docs.docker.com/engine/installation/)
0. [Ruby 2.4.1](http://rvm.io/rvm/install) 0. [Ruby 2.4.2](http://rvm.io/rvm/install)
0. [ImageMagick](https://www.imagemagick.org/script/index.php) (`brew install imagemagick` (Mac) or `sudo apt-get install imagemagick` (Ubuntu)) 0. [ImageMagick](https://www.imagemagick.org/script/index.php) (`brew install imagemagick` (Mac) or `sudo apt-get install imagemagick` (Ubuntu))
0. [Node JS > v6](https://nodejs.org/en/download/) 0. [Node JS > v6](https://nodejs.org/en/download/)
0. [`libpq-dev` and `postgresql`](http://stackoverflow.com/questions/6040583/cant-find-the-libpq-fe-h-header-when-trying-to-install-pg-gem/6040822#6040822) and `postgresql-contrib` 0. [`libpq-dev` and `postgresql`](http://stackoverflow.com/questions/6040583/cant-find-the-libpq-fe-h-header-when-trying-to-install-pg-gem/6040822#6040822) and `postgresql-contrib`

View File

@ -19,6 +19,7 @@ import { BooleanSetting } from "../session_keys";
describe("<LoadingPlant/>", () => { describe("<LoadingPlant/>", () => {
it("renders loading text", () => { it("renders loading text", () => {
mockStorj[BooleanSetting.disableAnimations] = true;
const wrapper = shallow(<LoadingPlant />); const wrapper = shallow(<LoadingPlant />);
expect(wrapper.find(".loading-plant").length).toEqual(0); expect(wrapper.find(".loading-plant").length).toEqual(0);
expect(wrapper.find(".loading-plant-text").props().y).toEqual(150); expect(wrapper.find(".loading-plant-text").props().y).toEqual(150);
@ -27,7 +28,7 @@ describe("<LoadingPlant/>", () => {
}); });
it("renders loading animation", () => { it("renders loading animation", () => {
mockStorj[BooleanSetting.plantAnimations] = true; mockStorj[BooleanSetting.disableAnimations] = false;
const wrapper = shallow(<LoadingPlant />); const wrapper = shallow(<LoadingPlant />);
expect(wrapper.find(".loading-plant")).toBeTruthy(); expect(wrapper.find(".loading-plant")).toBeTruthy();
const circleProps = wrapper.find(".loading-plant-circle").props(); const circleProps = wrapper.find(".loading-plant-circle").props();

View File

@ -11,14 +11,16 @@ export interface LabsFeature {
storageKey: BooleanSetting; storageKey: BooleanSetting;
value: boolean; value: boolean;
experimental?: boolean; experimental?: boolean;
displayInvert?: boolean;
} }
export const fetchLabFeatures = (): LabsFeature[] => ([ export const fetchLabFeatures = (): LabsFeature[] => ([
{ {
name: t("Disable Web App internationalization"), name: t("Internationalize Web App"),
description: t("Set Web App to English."), description: t("Turn off to set Web App to English."),
storageKey: BooleanSetting.disableI18n, storageKey: BooleanSetting.disableI18n,
value: false value: false,
displayInvert: true
}, },
{ {
name: t("Confirm Sequence step deletion"), name: t("Confirm Sequence step deletion"),
@ -51,9 +53,10 @@ export const fetchLabFeatures = (): LabsFeature[] => ([
}, },
{ {
name: t("Display plant animations"), name: t("Display plant animations"),
description: trim(t(`Turn on plant animations in the Farm Designer.`)), description: trim(t(`Enable plant animations in the Farm Designer.`)),
storageKey: BooleanSetting.plantAnimations, storageKey: BooleanSetting.disableAnimations,
value: true value: false,
displayInvert: true
} }
].map(fetchRealValue)); ].map(fetchRealValue));

View File

@ -9,11 +9,12 @@ interface LabsFeaturesListProps {
export function LabsFeaturesList(props: LabsFeaturesListProps) { export function LabsFeaturesList(props: LabsFeaturesListProps) {
return <div> return <div>
{fetchLabFeatures().map((p, i) => { {fetchLabFeatures().map((p, i) => {
const displayValue = p.displayInvert ? !p.value : p.value;
return <KeyValShowRow key={i} return <KeyValShowRow key={i}
label={p.name} label={p.name}
labelPlaceholder="" labelPlaceholder=""
value={p.description} value={p.description}
toggleValue={p.value ? 1 : 0} toggleValue={displayValue ? 1 : 0}
valuePlaceholder="" valuePlaceholder=""
onClick={() => props.onToggle(p)} onClick={() => props.onToggle(p)}
disabled={false} />; disabled={false} />;

View File

@ -10,6 +10,10 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
} }
@media screen and (max-width: 974px) {
margin-left: 15px;
margin-right: 15px;
}
} }
// Regimen Editor // Regimen Editor

View File

@ -13,20 +13,34 @@
margin-top: 0.4rem; margin-top: 0.4rem;
} }
@media screen and (max-width: 974px) { @media screen and (max-width: 974px) {
h3, p {
margin-left: 15px;
margin-right: 15px;
}
h3 { h3 {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.button-group {
margin-right: 15px;
}
} }
} }
.sequence-editor-content, .sequence-editor-content,
.regimen-editor-content { .regimen-editor-content {
margin-right: -15px; margin-right: -15px;
@media screen and (max-width: 974px) {
margin-left: 15px;
margin-right: 0;
}
} }
.sequence-editor-tools, .sequence-editor-tools,
.regimen-editor-tools { .regimen-editor-tools {
margin-right: 15px; margin-right: 15px;
@media screen and (max-width: 974px) {
margin-right: 10px;
}
} }
.sequence, .sequence,
@ -47,7 +61,7 @@
.regimen-list { .regimen-list {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
height: calc(100vh - 21rem); max-height: calc(100vh - 21rem);
} }
.step-button-cluster, .step-button-cluster,
@ -55,6 +69,20 @@
margin-right: -15px; margin-right: -15px;
} }
.step-button-cluster-panel {
@media screen and (max-width: 974px) {
margin-left: 15px;
margin-right: 15px;
}
}
.step-button-cluster {
@media screen and (max-width: 974px) {
margin-left: 0;
margin-right: 0;
}
}
.sequence-list-items { .sequence-list-items {
margin-right: 15px; margin-right: 15px;
} }
@ -64,6 +92,10 @@
padding-top: 0.4rem; padding-top: 0.4rem;
margin-bottom: 3rem; margin-bottom: 3rem;
margin-right: 5px; margin-right: 5px;
@media screen and (max-width: 974px) {
margin-left: 15px;
margin-right: 15px;
}
} }
.sequence-list-panel input, .sequence-list-panel input,

View File

@ -136,6 +136,7 @@ export interface PeripheralsProps {
export interface FarmwareProps { export interface FarmwareProps {
dispatch: Function; dispatch: Function;
env: Partial<WD_ENV>; env: Partial<WD_ENV>;
user_env: Record<string, string | undefined>;
images: TaggedImage[]; images: TaggedImage[];
currentImage: TaggedImage | undefined; currentImage: TaggedImage | undefined;
syncStatus: SyncStatus; syncStatus: SyncStatus;

View File

@ -36,6 +36,7 @@ describe("<GardenPlant/>", () => {
} }
it("renders plant", () => { it("renders plant", () => {
mockStorj[BooleanSetting.disableAnimations] = true;
const wrapper = shallow(<GardenPlant {...fakeProps() } />); const wrapper = shallow(<GardenPlant {...fakeProps() } />);
expect(wrapper.find("image").length).toEqual(1); expect(wrapper.find("image").length).toEqual(1);
expect(wrapper.find("image").props().opacity).toEqual(1); expect(wrapper.find("image").props().opacity).toEqual(1);
@ -46,7 +47,7 @@ describe("<GardenPlant/>", () => {
}); });
it("renders plant animations", () => { it("renders plant animations", () => {
mockStorj[BooleanSetting.plantAnimations] = true; mockStorj[BooleanSetting.disableAnimations] = false;
const wrapper = shallow(<GardenPlant {...fakeProps() } />); const wrapper = shallow(<GardenPlant {...fakeProps() } />);
expect(wrapper.find(".soil-cloud").length).toEqual(1); expect(wrapper.find(".soil-cloud").length).toEqual(1);
expect(wrapper.find(".animate").length).toEqual(1); expect(wrapper.find(".animate").length).toEqual(1);

View File

@ -37,7 +37,7 @@ export class GardenPlant extends
const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize); const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize);
const alpha = dragging ? 0.4 : 1.0; const alpha = dragging ? 0.4 : 1.0;
const animate = Session.getBool(BooleanSetting.plantAnimations); const animate = !Session.getBool(BooleanSetting.disableAnimations);
return <g id={"plant-" + id}> return <g id={"plant-" + id}>

View File

@ -1,7 +1,7 @@
jest.mock("../../../../session", () => { jest.mock("../../../../session", () => {
return { return {
Session: { Session: {
getBool: () => { return true; } getBool: () => { return false; }
} }
}; };
}); });

View File

@ -47,7 +47,7 @@ export class HoveredPlantLayer extends
const hovered = !!this.props.designer.hoveredPlant.icon; const hovered = !!this.props.designer.hoveredPlant.icon;
const scaledRadius = currentPlant ? radius : radius * 1.2; const scaledRadius = currentPlant ? radius : radius * 1.2;
const alpha = dragging ? 0.4 : 1.0; const alpha = dragging ? 0.4 : 1.0;
const animate = Session.getBool(BooleanSetting.plantAnimations); const animate = !Session.getBool(BooleanSetting.disableAnimations);
return <g id="hovered-plant-layer"> return <g id="hovered-plant-layer">
{this.props.visible && hovered && {this.props.visible && hovered &&

View File

@ -83,7 +83,7 @@ export class SpreadCircle extends
const { selected, mapTransformProps } = this.props; const { selected, mapTransformProps } = this.props;
const { quadrant, gridSize } = mapTransformProps; const { quadrant, gridSize } = mapTransformProps;
const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize); const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize);
const animate = Session.getBool(BooleanSetting.plantAnimations); const animate = !Session.getBool(BooleanSetting.disableAnimations);
return <g id={"spread-" + id}> return <g id={"spread-" + id}>
{!selected && {!selected &&

View File

@ -21,7 +21,7 @@ function fakeFarmwares(): Dictionary<FarmwareManifest | undefined> {
args: ["my_farmware.fth"], args: ["my_farmware.fth"],
url: "https://", url: "https://",
path: "my_farmware", path: "my_farmware",
config: [], config: [{ name: "config_1", label: "Config 1", value: "4" }],
meta: { meta: {
min_os_version_major: "3", min_os_version_major: "3",
description: "Does things.", description: "Does things.",
@ -35,19 +35,40 @@ function fakeFarmwares(): Dictionary<FarmwareManifest | undefined> {
} }
describe("<FarmwareForms/>", () => { describe("<FarmwareForms/>", () => {
it("doesn't render", () => {
const farmwares = fakeFarmwares();
const farmware = farmwares.farmware_0;
if (farmware) { farmware.config = []; }
const wrapper = mount(<FarmwareForms
farmwares={farmwares}
user_env={{}} />);
expect(wrapper.text()).toEqual("");
});
it("renders", () => { it("renders", () => {
const wrapper = mount(<FarmwareForms farmwares={fakeFarmwares()} />); const wrapper = mount(<FarmwareForms
farmwares={fakeFarmwares()}
user_env={{}} />);
expect(wrapper.text()).toContain("My Farmware"); expect(wrapper.text()).toContain("My Farmware");
expect(wrapper.text()).toContain("version: 0.0.0"); expect(wrapper.text()).toContain("version: 0.0.0");
expect(wrapper.text()).toContain("Does things."); expect(wrapper.text()).toContain("Does things.");
expect(wrapper.find("label").last().text()).toContain("Config 1");
expect(wrapper.find("input").props().value).toEqual("4");
}); });
it("runs", () => { it("runs", () => {
const runFarmware = getDevice().execScript as jest.Mock<{}>; const runFarmware = getDevice().execScript as jest.Mock<{}>;
const wrapper = mount(<FarmwareForms farmwares={fakeFarmwares()} />); const wrapper = mount(<FarmwareForms
farmwares={fakeFarmwares()}
user_env={{}} />);
const run = wrapper.find("button").first(); const run = wrapper.find("button").first();
run.simulate("click"); run.simulate("click");
expect(run.text()).toEqual("Run"); expect(run.text()).toEqual("Run");
expect(runFarmware).toHaveBeenCalledWith("My Farmware"); const argsList = runFarmware.mock.calls[0];
expect(argsList[0]).toEqual("My Farmware");
const pairs = argsList[1][0];
expect(pairs.kind).toEqual("pair");
expect(pairs.args)
.toEqual({ "label": "my_farmware_config_1", "value": "4" });
}); });
}); });

View File

@ -19,6 +19,7 @@ describe("<FarmwarePage />", () => {
farmwares: {}, farmwares: {},
syncStatus: "unknown", syncStatus: "unknown",
env: {}, env: {},
user_env: {},
dispatch: jest.fn(), dispatch: jest.fn(),
currentImage: undefined, currentImage: undefined,
images: [] images: []

View File

@ -1,44 +1,63 @@
import * as React from "react"; import * as React from "react";
import { Widget, WidgetHeader, WidgetBody, Col } from "../ui/index"; import {
Widget, WidgetHeader, WidgetBody, Col, BlurableInput
} from "../ui/index";
import { t } from "i18next"; import { t } from "i18next";
import { FarmwareManifest, Dictionary } from "farmbot"; import { FarmwareManifest, Dictionary, Pair, FarmwareConfig } from "farmbot";
import { betterCompact } from "../util"; import { betterCompact } from "../util";
import { getDevice } from "../device"; import { getDevice } from "../device";
import * as _ from "lodash";
interface FarmwareFormsProps { interface FarmwareFormsProps {
farmwares: Dictionary<FarmwareManifest | undefined>; farmwares: Dictionary<FarmwareManifest | undefined>;
user_env: Record<string, string | undefined>;
} }
// TODO: download and parse the "manifest.json" file instead.
const firstParty = [
"camera-calibration",
"historical-camera-calibration",
"take-photo",
"plant-detection",
"historical-plant-detection"];
export function FarmwareForms(props: FarmwareFormsProps): JSX.Element { export function FarmwareForms(props: FarmwareFormsProps): JSX.Element {
const { farmwares } = props;
function inputChange(key: string, e: React.SyntheticEvent<HTMLInputElement>) {
const value = e.currentTarget.value;
getDevice().setUserEnv({ [key]: value });
}
function getEnvName(farmwareName: string, configName: string) {
return `${_.snakeCase(farmwareName)}_${configName}`;
}
function getValue(farmwareName: string, currentConfig: FarmwareConfig) {
return (user_env[getEnvName(farmwareName, currentConfig.name)]
|| _.toString(currentConfig.value));
}
function run(farmwareName: string, config: FarmwareConfig[]) {
const pairs = config.map<Pair>((x) => {
const label = getEnvName(farmwareName, x.name);
const value = getValue(farmwareName, x);
return { kind: "pair", args: { value, label } };
});
getDevice().execScript(farmwareName, pairs);
}
const { farmwares, user_env } = props;
const farmwareData = betterCompact(Object const farmwareData = betterCompact(Object
.keys(farmwares) .keys(farmwares)
.map(x => farmwares[x])) .map(x => farmwares[x]))
.map((fw) => { .map((fw) => {
// TODO: Add optional "config" field to farmbot-js and check for it here. const needsWidget = fw.config && fw.config.length > 0;
const needsWidget = !firstParty.includes(fw.name);
return needsWidget ? fw : undefined; return needsWidget ? fw : undefined;
}); });
return <div id="farmware-forms"> return <div id="farmware-forms">
{farmwareData.map((farmware, i) => { {farmwareData.map((farmware, i) => {
return farmware ? return farmware ?
<Col key={i} xs={12} sm={6}> <Col key={i} xs={12} sm={6}>
<Widget> <Widget className={_.kebabCase(farmware.name)}>
<WidgetHeader <WidgetHeader
title={farmware.name} title={farmware.name}
helpText={farmware.meta.version ? " version: " helpText={farmware.meta.version ? " version: "
+ farmware.meta.version : ""}> + farmware.meta.version : ""}>
<button <button
className="fb-button gray" className="fb-button green"
onClick={() => getDevice().execScript(farmware.name)}> onClick={() => run(farmware.name, farmware.config)}>
{t("Run")} {t("Run")}
</button> </button>
</WidgetHeader> </WidgetHeader>
@ -47,8 +66,17 @@ export function FarmwareForms(props: FarmwareFormsProps): JSX.Element {
<div> <div>
<label>Description</label> <label>Description</label>
<p>{farmware.meta.description}</p> <p>{farmware.meta.description}</p>
<hr />
</div>} </div>}
{/* TODO: Render inputs described in "farmware.config". */} {farmware.config.map((config) => {
return <div key={config.name} id={config.name}>
<label>{config.label}</label>
<BlurableInput type="text"
onCommit={(e) =>
inputChange(getEnvName(farmware.name, config.name), e)}
value={getValue(farmware.name, config)} />
</div>;
})}
</WidgetBody> </WidgetBody>
</Widget> </Widget>
</Col> : <div key={i} />; </Col> : <div key={i} />;

View File

@ -8,7 +8,7 @@ import { CameraCalibration } from "./camera_calibration/camera_calibration";
import { FarmwareProps } from "../devices/interfaces"; import { FarmwareProps } from "../devices/interfaces";
import { WeedDetector } from "./weed_detector/index"; import { WeedDetector } from "./weed_detector/index";
import { envGet } from "./weed_detector/remote_env/selectors"; import { envGet } from "./weed_detector/remote_env/selectors";
// import { FarmwareForms } from "./farmware_forms"; import { FarmwareForms } from "./farmware_forms";
@connect(mapStateToProps) @connect(mapStateToProps)
export class FarmwarePage extends React.Component<FarmwareProps, {}> { export class FarmwarePage extends React.Component<FarmwareProps, {}> {
@ -48,7 +48,8 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
<WeedDetector {...this.props} /> <WeedDetector {...this.props} />
</Col> </Col>
</Row> </Row>
{/* <FarmwareForms farmwares={this.props.farmwares} /> */} <FarmwareForms farmwares={this.props.farmwares}
user_env={this.props.user_env} />
</Page>; </Page>;
} }
} }

View File

@ -24,6 +24,7 @@ export function mapStateToProps(props: Everything): FarmwareProps {
farmwares, farmwares,
syncStatus, syncStatus,
env: prepopulateEnv(props.bot.hardware.user_env), env: prepopulateEnv(props.bot.hardware.user_env),
user_env: props.bot.hardware.user_env,
dispatch: props.dispatch, dispatch: props.dispatch,
currentImage, currentImage,
images images

View File

@ -13,6 +13,7 @@ describe("<WeedDetector />", () => {
farmwares: {}, farmwares: {},
syncStatus: "unknown", syncStatus: "unknown",
env: {}, env: {},
user_env: {},
dispatch: jest.fn(), dispatch: jest.fn(),
currentImage: undefined, currentImage: undefined,
images: [] images: []

View File

@ -84,8 +84,8 @@ export class ImageWorkspace extends React.Component<Props, {}> {
onRelease={this.onHslChange("H")} onRelease={this.onHslChange("H")}
lowest={RANGES.H.LOWEST} lowest={RANGES.H.LOWEST}
highest={RANGES.H.HIGHEST} highest={RANGES.H.HIGHEST}
lowValue={H_LO} lowValue={Math.min(H_LO, H_HI)}
highValue={H_HI} /> highValue={Math.max(H_LO, H_HI)} />
<label htmlFor="saturation">{t("SATURATION")}</label> <label htmlFor="saturation">{t("SATURATION")}</label>
<WeedDetectorSlider <WeedDetectorSlider
onRelease={this.onHslChange("S")} onRelease={this.onHslChange("S")}

View File

@ -3,7 +3,7 @@ import { Session } from "./session";
import { BooleanSetting } from "./session_keys"; import { BooleanSetting } from "./session_keys";
export function LoadingPlant() { export function LoadingPlant() {
const animations = Session.getBool(BooleanSetting.plantAnimations); const animations = !Session.getBool(BooleanSetting.disableAnimations);
return <div className="loading-plant-div-container"> return <div className="loading-plant-div-container">
<svg width="300px" height="500px"> <svg width="300px" height="500px">
{animations && {animations &&

View File

@ -17,7 +17,7 @@ export enum BooleanSetting {
hideWebcamWidget = "hideWebcamWidget", hideWebcamWidget = "hideWebcamWidget",
dynamicMap = "dynamicMap", dynamicMap = "dynamicMap",
mapXL = "mapXL", mapXL = "mapXL",
plantAnimations = "plantAnimations", disableAnimations = "disableAnimations",
} }
export enum NumericSetting { export enum NumericSetting {