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.
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. [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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ describe("<GardenPlant/>", () => {
}
it("renders plant", () => {
mockStorj[BooleanSetting.disableAnimations] = true;
const wrapper = shallow(<GardenPlant {...fakeProps() } />);
expect(wrapper.find("image").length).toEqual(1);
expect(wrapper.find("image").props().opacity).toEqual(1);
@ -46,7 +47,7 @@ describe("<GardenPlant/>", () => {
});
it("renders plant animations", () => {
mockStorj[BooleanSetting.plantAnimations] = true;
mockStorj[BooleanSetting.disableAnimations] = false;
const wrapper = shallow(<GardenPlant {...fakeProps() } />);
expect(wrapper.find(".soil-cloud").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 alpha = dragging ? 0.4 : 1.0;
const animate = Session.getBool(BooleanSetting.plantAnimations);
const animate = !Session.getBool(BooleanSetting.disableAnimations);
return <g id={"plant-" + id}>

View File

@ -1,7 +1,7 @@
jest.mock("../../../../session", () => {
return {
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 scaledRadius = currentPlant ? radius : radius * 1.2;
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">
{this.props.visible && hovered &&

View File

@ -83,7 +83,7 @@ export class SpreadCircle extends
const { selected, mapTransformProps } = this.props;
const { quadrant, gridSize } = mapTransformProps;
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}>
{!selected &&

View File

@ -21,7 +21,7 @@ function fakeFarmwares(): Dictionary<FarmwareManifest | undefined> {
args: ["my_farmware.fth"],
url: "https://",
path: "my_farmware",
config: [],
config: [{ name: "config_1", label: "Config 1", value: "4" }],
meta: {
min_os_version_major: "3",
description: "Does things.",
@ -35,19 +35,40 @@ function fakeFarmwares(): Dictionary<FarmwareManifest | undefined> {
}
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", () => {
const wrapper = mount(<FarmwareForms farmwares={fakeFarmwares()} />);
const wrapper = mount(<FarmwareForms
farmwares={fakeFarmwares()}
user_env={{}} />);
expect(wrapper.text()).toContain("My Farmware");
expect(wrapper.text()).toContain("version: 0.0.0");
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", () => {
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();
run.simulate("click");
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: {},
syncStatus: "unknown",
env: {},
user_env: {},
dispatch: jest.fn(),
currentImage: undefined,
images: []

View File

@ -1,44 +1,63 @@
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 { FarmwareManifest, Dictionary } from "farmbot";
import { FarmwareManifest, Dictionary, Pair, FarmwareConfig } from "farmbot";
import { betterCompact } from "../util";
import { getDevice } from "../device";
import * as _ from "lodash";
interface FarmwareFormsProps {
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 {
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
.keys(farmwares)
.map(x => farmwares[x]))
.map((fw) => {
// TODO: Add optional "config" field to farmbot-js and check for it here.
const needsWidget = !firstParty.includes(fw.name);
const needsWidget = fw.config && fw.config.length > 0;
return needsWidget ? fw : undefined;
});
return <div id="farmware-forms">
{farmwareData.map((farmware, i) => {
return farmware ?
<Col key={i} xs={12} sm={6}>
<Widget>
<Widget className={_.kebabCase(farmware.name)}>
<WidgetHeader
title={farmware.name}
helpText={farmware.meta.version ? " version: "
+ farmware.meta.version : ""}>
<button
className="fb-button gray"
onClick={() => getDevice().execScript(farmware.name)}>
className="fb-button green"
onClick={() => run(farmware.name, farmware.config)}>
{t("Run")}
</button>
</WidgetHeader>
@ -47,8 +66,17 @@ export function FarmwareForms(props: FarmwareFormsProps): JSX.Element {
<div>
<label>Description</label>
<p>{farmware.meta.description}</p>
<hr />
</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>
</Widget>
</Col> : <div key={i} />;

View File

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

View File

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

View File

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

View File

@ -84,8 +84,8 @@ export class ImageWorkspace extends React.Component<Props, {}> {
onRelease={this.onHslChange("H")}
lowest={RANGES.H.LOWEST}
highest={RANGES.H.HIGHEST}
lowValue={H_LO}
highValue={H_HI} />
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")}

View File

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

View File

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