Merge pull request #1157 from gabrielburnworth/staging

Misc additions
pull/1160/head
Rick Carlino 2019-04-16 10:49:55 -07:00 committed by GitHub
commit 83d9cb8287
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 936 additions and 390 deletions

View File

@ -1,6 +1,9 @@
class Enigma < ApplicationRecord
belongs_to :device
PROBLEM_TAGS = [
SEED_DATA = "api.seed_data.missing"
SEED_DATA = "api.seed_data.missing",
TOUR = "api.tour.not_taken",
USER = "api.user.not_welcomed",
DOCUMENTATION = "api.documentation.unread"
]
end

View File

@ -374,6 +374,9 @@ export function fakeFirmwareConfig(): TaggedFirmwareConfig {
movement_step_per_mm_x: 5,
movement_step_per_mm_y: 5,
movement_step_per_mm_z: 25,
movement_microsteps_x: 1,
movement_microsteps_y: 1,
movement_microsteps_z: 1,
movement_steps_acc_dec_x: 300,
movement_steps_acc_dec_y: 300,
movement_steps_acc_dec_z: 300,

View File

@ -1,3 +1,8 @@
let mockDev = false;
jest.mock("../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
jest.mock("react-redux", () => ({ connect: jest.fn() }));
let mockPath = "";
@ -11,7 +16,7 @@ import { App, AppProps, mapStateToProps } from "../app";
import { mount } from "enzyme";
import { bot } from "../__test_support__/fake_state/bot";
import {
fakeUser, fakeWebAppConfig
fakeUser, fakeWebAppConfig, fakeEnigma
} from "../__test_support__/fake_state/resources";
import { fakeState } from "../__test_support__/fake_state";
import {
@ -41,6 +46,7 @@ const fakeProps = (): AppProps => {
tour: undefined,
resources: buildResourceIndex().index,
autoSync: false,
alertCount: 0,
};
};
@ -141,4 +147,21 @@ describe("mapStateToProps()", () => {
const result = mapStateToProps(state);
expect(result.axisInversion.x).toEqual(true);
});
it("doesn't show API alerts", () => {
const state = fakeState();
state.resources = buildResourceIndex([fakeEnigma()]);
mockDev = false;
const props = mapStateToProps(state);
expect(props.alertCount).toEqual(0);
});
it("shows API alerts", () => {
const state = fakeState();
const enigma = fakeEnigma();
state.resources = buildResourceIndex([enigma]);
mockDev = true;
const props = mapStateToProps(state);
expect(props.alertCount).toEqual(1);
});
});

View File

@ -10,11 +10,12 @@ import {
maybeFetchUser,
maybeGetTimeSettings,
getDeviceAccountSettings,
selectAllEnigmas,
} from "./resources/selectors";
import { HotKeys } from "./hotkeys";
import { ControlsPopup } from "./controls_popup";
import { Content } from "./constants";
import { validBotLocationData, validFwConfig, validFbosConfig } from "./util";
import { validBotLocationData, validFwConfig, validFbosConfig, betterCompact } from "./util";
import { BooleanSetting } from "./session_keys";
import { getPathArray } from "./history";
import {
@ -28,6 +29,7 @@ import { t } from "./i18next_wrapper";
import { ResourceIndex } from "./resources/interfaces";
import { isBotOnline } from "./devices/must_be_online";
import { getStatus } from "./connectivity/reducer_support";
import { DevSettings } from "./account/dev/dev_support";
/** For the logger module */
init();
@ -48,11 +50,16 @@ export interface AppProps {
tour: string | undefined;
resources: ResourceIndex;
autoSync: boolean;
alertCount: number;
}
export function mapStateToProps(props: Everything): AppProps {
const webAppConfigValue = getWebAppConfigValue(() => props);
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const botAlerts = betterCompact(Object.values(props.bot.hardware.enigmas || {}));
const apiAlerts = selectAllEnigmas(props.resources.index).map(x => x.body);
const alerts =
botAlerts.concat(DevSettings.futureFeaturesEnabled() ? apiAlerts : []);
return {
timeSettings: maybeGetTimeSettings(props.resources.index),
dispatch: props.dispatch,
@ -73,6 +80,7 @@ export function mapStateToProps(props: Everything): AppProps {
tour: props.resources.consumers.help.currentTour,
resources: props.resources.index,
autoSync: !!(fbosConfig && fbosConfig.auto_sync),
alertCount: alerts.length,
};
}
/** Time at which the app gives up and asks the user to refresh */
@ -129,6 +137,7 @@ export class App extends React.Component<AppProps, {}> {
getConfigValue={this.props.getConfigValue}
tour={this.props.tour}
autoSync={this.props.autoSync}
alertCount={this.props.alertCount}
device={getDeviceAccountSettings(this.props.resources)} />}
{syncLoaded && this.props.children}
{!(["controls", "account", "regimens"].includes(currentPage)) &&

View File

@ -113,6 +113,10 @@ export namespace ToolTips {
trim(`The number of motor steps required to move the axis one millimeter.
(default: x: 5, y: 5, z: 25)`);
export const MICROSTEPS_PER_STEP =
trim(`The number of microsteps required to move the motor one step.
(default: x: 1, y: 1, z: 1)`);
export const ALWAYS_POWER_MOTORS =
trim(`Keep power applied to motors. Prevents slipping from gravity in
certain situations. (default: enabled)`);
@ -342,6 +346,9 @@ export namespace ToolTips {
export const FIRMWARE_DEBUG_MESSAGES =
trim(`Log all debug received from firmware (clears after refresh).`);
export const MESSAGES =
trim(`View messages.`);
// App
export const LABS =
trim(`Customize your web app experience.`);
@ -380,6 +387,7 @@ export namespace Content {
trim(`Export request received. Please allow up to 10 minutes for
delivery.`);
// Messages
export const SEED_DATA_SELECTION =
trim(`To finish setting up your account and FarmBot, please select which
FarmBot you have. Once you make a selection, we'll automatically add some
@ -387,6 +395,31 @@ export namespace Content {
faster. If you want to start completely from scratch, feel free to select
"Custom bot" and we won't change a thing.`);
export const TAKE_A_TOUR =
trim(`Since you're new around here, we recommend taking our guided tours
of the app. This is the fastest way to learn about the most important pages
and features at your fingertips.`);
export const READ_THE_DOCS =
trim(`The FarmBot web app is a powerful tool that allows you to control
and configure your FarmBot in any way you want. To give you so much power,
we've packed the app with a ton of settings, features, and pages, which
can be a lot to understand. That's why we've created comprehensive written
documentation and videos to teach you how to use everything.`);
export const WELCOME =
trim(`Let's get you familiar with the app and finish setting everything
up.`);
export const MESSAGE_CENTER_WELCOME =
trim(`Here you'll find important information about your account, your
FarmBot, and news such as new feature announcements. Look for the blue
badge in the main menu to see when new messages are available.`);
export const MESSAGE_DISMISS =
trim(`When you're finished with a message, press the x button in the
top right of the card to dismiss it.`);
// App Settings
export const CONFIRM_STEP_DELETION =
trim(`Show a confirmation dialog when the sequence delete step

View File

@ -1,52 +1,60 @@
import * as React from "react";
import { mount } from "enzyme";
import { ToggleButton } from "../toggle_button";
import { ToggleButtonProps } from "../interfaces";
describe("<ToggleButton/>", function () {
const fakeProps = (): ToggleButtonProps => ({
toggleValue: 0,
toggleAction: jest.fn(),
});
it("calls toggle action", () => {
const toggle = jest.fn();
const toggleButton = mount(<ToggleButton
toggleValue={0}
toggleAction={() => toggle()} />);
const p = fakeProps();
const toggleButton = mount(<ToggleButton {...p} />);
toggleButton.simulate("click");
expect(toggle).toHaveBeenCalledTimes(1);
expect(p.toggleAction).toHaveBeenCalledTimes(1);
});
it("displays no", () => {
const toggleButton = mount(<ToggleButton
toggleValue={0}
toggleAction={jest.fn()} />);
const toggleButton = mount(<ToggleButton {...fakeProps()} />);
expect(toggleButton.text()).toBe("no");
});
it("displays yes", () => {
const toggleButton = mount(<ToggleButton
toggleValue={1}
toggleAction={jest.fn()} />);
const p = fakeProps();
p.toggleValue = 1;
const toggleButton = mount(<ToggleButton {...p} />);
expect(toggleButton.text()).toBe("yes");
});
it("displays off", () => {
const toggleButton = mount(<ToggleButton
toggleValue={0}
toggleAction={jest.fn()}
customText={{ textFalse: "off", textTrue: "on" }} />);
const p = fakeProps();
p.customText = { textFalse: "off", textTrue: "on" };
const toggleButton = mount(<ToggleButton {...p} />);
expect(toggleButton.text()).toEqual("off");
});
it("displays on", () => {
const toggleButton = mount(<ToggleButton
toggleValue={1}
toggleAction={jest.fn()}
customText={{ textFalse: "off", textTrue: "on" }} />);
const p = fakeProps();
p.toggleValue = 1;
p.customText = { textFalse: "off", textTrue: "on" };
const toggleButton = mount(<ToggleButton {...p} />);
expect(toggleButton.text()).toEqual("on");
});
it("displays 🚫", () => {
const toggleButton = mount(<ToggleButton
toggleValue={undefined}
toggleAction={jest.fn()}
customText={{ textFalse: "off", textTrue: "on" }} />);
const p = fakeProps();
p.toggleValue = undefined;
p.customText = { textFalse: "off", textTrue: "on" };
const toggleButton = mount(<ToggleButton {...p} />);
expect(toggleButton.text()).toEqual("🚫");
});
it("displays dim", () => {
const p = fakeProps();
p.dim = true;
const toggleButton = mount(<ToggleButton {...p} />);
expect(toggleButton.html()).toContain("dim");
});
});

View File

@ -0,0 +1,36 @@
import {
calcMicrostepsPerMm, calculateAxialLengths
} from "../direction_axes_props";
import { fakeFirmwareConfig } from "../../../__test_support__/fake_state/resources";
describe("calcMicrostepsPerMm()", () => {
it("uses fallback values", () => {
expect(calcMicrostepsPerMm(undefined, undefined)).toEqual(1);
});
it("calculates value with no microstepping", () => {
expect(calcMicrostepsPerMm(5, 1)).toEqual(5);
});
it("calculates value with microstepping", () => {
expect(calcMicrostepsPerMm(5, 4)).toEqual(20);
});
});
describe("calculateAxialLengths()", () => {
it("calculates lengths", () => {
const firmwareSettings = fakeFirmwareConfig().body;
firmwareSettings.movement_axis_nr_steps_x = 0;
firmwareSettings.movement_step_per_mm_x = 0;
firmwareSettings.movement_microsteps_x = 0;
firmwareSettings.movement_axis_nr_steps_y = 100;
firmwareSettings.movement_step_per_mm_y = 5;
firmwareSettings.movement_microsteps_y = 1;
firmwareSettings.movement_axis_nr_steps_z = 100;
firmwareSettings.movement_step_per_mm_z = 25;
firmwareSettings.movement_microsteps_z = 4;
expect(calculateAxialLengths({ firmwareSettings })).toEqual({
x: 0, y: 20, z: 1
});
});
});

View File

@ -1,17 +1,34 @@
import { DirectionAxesProps } from "./interfaces";
import { McuParams } from "farmbot";
const _ = (nr_steps: number | undefined, steps_mm: number | undefined) => {
return (nr_steps || 0) / (steps_mm || 1);
export const calcMicrostepsPerMm = (
steps_per_mm: number | undefined,
microsteps_per_step: number | undefined) =>
(steps_per_mm || 1) * (microsteps_per_step || 1);
const calcAxisLength = (
nr_steps: number | undefined,
steps_per_mm: number | undefined,
microsteps_per_step: number | undefined) => {
return (nr_steps || 0)
/ calcMicrostepsPerMm(steps_per_mm, microsteps_per_step);
};
function calculateAxialLengths(props: { firmwareSettings: McuParams }) {
export function calculateAxialLengths(props: { firmwareSettings: McuParams }) {
const fwParams = props.firmwareSettings;
return {
x: _(fwParams.movement_axis_nr_steps_x, fwParams.movement_step_per_mm_x),
y: _(fwParams.movement_axis_nr_steps_y, fwParams.movement_step_per_mm_y),
z: _(fwParams.movement_axis_nr_steps_z, fwParams.movement_step_per_mm_z),
x: calcAxisLength(
fwParams.movement_axis_nr_steps_x,
fwParams.movement_step_per_mm_x,
fwParams.movement_microsteps_x),
y: calcAxisLength(
fwParams.movement_axis_nr_steps_y,
fwParams.movement_step_per_mm_y,
fwParams.movement_microsteps_y),
z: calcAxisLength(
fwParams.movement_axis_nr_steps_z,
fwParams.movement_step_per_mm_z,
fwParams.movement_microsteps_z),
};
}

View File

@ -23,7 +23,6 @@ export class ToggleButton extends React.Component<ToggleButtonProps, {}> {
css() {
const css = "fb-toggle-button fb-button ";
if (this.props.disabled) { return css + "gray"; }
const greenCSS = css + "green";
const redCSS = css + "red";
const yellowCSS = css + "yellow";

View File

@ -293,6 +293,10 @@
&.grayscale {
filter: grayscale(100%);
}
&.disabled,
&:disabled {
background: $medium_light_gray !important;
}
}
.front-page-button {

View File

@ -572,6 +572,12 @@
opacity: 0;
}
}
.map-rotate-button {
text-align: center;
.fb-button {
float: none;
}
}
}
.map-points-submenu {

View File

@ -938,6 +938,18 @@ ul {
}
}
.messages-page {
max-width: 600px;
padding-left: 2rem !important;
padding-right: 2rem !important;
.link-to-logs {
margin-top: 2rem;
margin-bottom: 4rem;
text-align: center;
font-style: italic;
}
}
.account-page {
label {
margin-top: 5px;
@ -1151,42 +1163,12 @@ ul {
}
}
.problem-alerts {
position: fixed;
top: 85px;
left: 50%;
z-index: 3;
padding: 1rem;
background: $off_white;
border-radius: 5px;
box-shadow: 0px 1px 4px #555;
transform: translate(-50%);
.problem-alerts-header {
text-align: center;
i {
font-size: 2rem;
color: $dark_gray;
margin-left: 1rem;
}
.saucer {
display: inline-flex;
top: -0.25rem;
margin-left: 0.5rem;
}
h3 {
margin: 0;
}
}
button {
float: none !important;
}
}
.problem-alert {
margin: 1rem;
padding: 1rem;
border-radius: 3px;
box-shadow: 0px 2px 5px $gray;
margin-bottom: 3rem;
padding: 2rem;
border-radius: 4px;
box-shadow: 0px 2px 5px $medium_gray;
background: $off_white;
.problem-alert-title {
.fa-exclamation-triangle {
@ -1204,10 +1186,34 @@ ul {
color: $medium_gray;
font-size: 1.2rem;
}
.fa-times {
color: $medium_light_gray;
float: right;
&:hover {
color: $dark_gray;
}
}
}
.problem-alert-content {
p {
margin-bottom: 0.75rem !important;
font-size: 1.4rem;
line-height: 2rem;
}
label {
margin-top: 0.5rem;
}
.row {
margin-top: 2rem;
}
.tour-list {
margin: 0;
}
.link-button {
color: $off_white;
font-weight: bold;
float: none;
margin-bottom: 2rem;
}
}
}

View File

@ -48,47 +48,58 @@ nav {
.external-links {
display: none;
}
.nav-links a {
display: inline-block;
position: relative;
font-size: 1.2rem;
white-space: nowrap;
padding: 2rem 1rem;
letter-spacing: 1.2px;
transition: font-weight 0.2s ease;
&:focus {
font-weight: bold;
}
&:hover {
font-weight: bold;
color: $white;
}
&.active {
pointer-events: none;
font-weight: bold;
color: $white;
.nav-links {
display: inline-flex;
a {
display: inline-block;
position: relative;
height: 5.5rem;
font-size: 1.2rem;
white-space: nowrap;
padding: 2rem 1rem;
letter-spacing: 1.2px;
transition: font-weight 0.2s ease;
&:focus {
font-weight: bold;
}
&:hover {
font-weight: bold;
color: $white;
}
&.active {
pointer-events: none;
font-weight: bold;
color: $white;
&:after {
transition: all 0.4s ease;
transform: translateY(-3px);
}
}
&:before {
content: "";
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 3px;
background: $white;
}
&:after {
transition: all 0.4s ease;
transform: translateY(-3px);
content: "";
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 3px;
background: $dark_gray;
}
div .saucer {
display: inline-flex;
margin-left: 1rem;
}
}
&:before {
content: "";
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 3px;
background: $white;
}
&:after {
content: "";
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 3px;
background: $dark_gray;
div {
display: inline;
}
}
}
@ -156,8 +167,13 @@ nav {
.mobile-menu {
.nav-links {
display: block;
a {
display: block;
div .saucer {
display: inline-flex;
margin-left: 1rem;
}
}
i {
display: inline-block;
@ -168,7 +184,7 @@ nav {
@media screen and (max-width: 1075px) {
.top-menu-container .nav-links {
display: none;
display: none !important;
}
}

View File

@ -58,7 +58,7 @@
box-shadow: none;
color: $dark_gray;
font-weight: bold;
min-width: 30%;
min-width: 130px;
}
p {
font-size: 1rem;

View File

@ -24,6 +24,7 @@ describe("<FirmwareHardwareStatusDetails />", () => {
mcuFirmwareValue: undefined,
shouldDisplay: () => true,
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
});
it("renders details: unknown", () => {
@ -79,6 +80,7 @@ describe("<FirmwareHardwareStatus />", () => {
apiFirmwareValue: undefined,
shouldDisplay: () => true,
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
});
it("renders: inconsistent", () => {

View File

@ -126,6 +126,7 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
botOnline={this.props.botOnline}
apiFirmwareValue={this.apiValue}
bot={this.props.bot}
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings}
shouldDisplay={this.props.shouldDisplay} />
</Col>

View File

@ -4,7 +4,7 @@ import { FIRMWARE_CHOICES_DDI, isFwHardwareValue, boardType } from "./board_type
import { flashFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper";
import { BotState, Feature, ShouldDisplay } from "../../interfaces";
import { FirmwareAlerts } from "../../../logs/alerts";
import { FirmwareAlerts } from "../../../messages/alerts";
import { TimeSettings } from "../../../interfaces";
import { trim } from "../../../util";
@ -36,6 +36,7 @@ export interface FirmwareHardwareStatusDetailsProps {
mcuFirmwareValue: string | undefined;
shouldDisplay: ShouldDisplay;
timeSettings: TimeSettings;
dispatch: Function;
}
export interface FirmwareActionsProps {
@ -77,6 +78,7 @@ export const FirmwareHardwareStatusDetails =
</div>}
<FirmwareAlerts
bot={props.bot}
dispatch={props.dispatch}
apiFirmwareValue={props.apiFirmwareValue}
timeSettings={props.timeSettings} />
</div>;
@ -88,6 +90,7 @@ export interface FirmwareHardwareStatusProps {
botOnline: boolean;
timeSettings: TimeSettings;
shouldDisplay: ShouldDisplay;
dispatch: Function;
}
export const FirmwareHardwareStatus = (props: FirmwareHardwareStatusProps) => {
@ -106,6 +109,7 @@ export const FirmwareHardwareStatus = (props: FirmwareHardwareStatusProps) => {
botFirmwareValue={firmware_hardware}
mcuFirmwareValue={boardType(firmware_version)}
timeSettings={props.timeSettings}
dispatch={props.dispatch}
shouldDisplay={props.shouldDisplay} />
</Popover>;
};

View File

@ -11,6 +11,7 @@ import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { minFwVersionCheck } from "../../../util";
import { t } from "../../../i18next_wrapper";
import { calculateScale } from "./motors";
export function HomingAndCalibration(props: HomingAndCalibrationProps) {
@ -31,6 +32,8 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
*/
const disabled = disabledAxisMap(hardware);
const scale = calculateScale(sourceFwConfig);
return <section>
<Header
title={t("Homing and Calibration")}
@ -79,11 +82,11 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
name={t("Axis Length (mm)")}
tooltip={ToolTips.LENGTH}
x={"movement_axis_nr_steps_x"}
xScale={sourceFwConfig("movement_step_per_mm_x").value}
y={"movement_axis_nr_steps_y"}
yScale={sourceFwConfig("movement_step_per_mm_y").value}
z={"movement_axis_nr_steps_z"}
zScale={sourceFwConfig("movement_step_per_mm_z").value}
xScale={scale.x}
yScale={scale.y}
zScale={scale.z}
gray={{
x: !sourceFwConfig("movement_stop_at_max_x").value,
y: !sourceFwConfig("movement_stop_at_max_y").value,

View File

@ -12,6 +12,9 @@ import { Collapse } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { minFwVersionCheck } from "../../../util";
import { t } from "../../../i18next_wrapper";
import { Xyz, McuParamName } from "farmbot";
import { SourceFwConfig } from "../../interfaces";
import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props";
const SingleSettingRow =
({ label, tooltip, settingType, children }: {
@ -30,6 +33,19 @@ const SingleSettingRow =
: <Col xs={6}>{children}</Col>}
</Row>;
export const calculateScale =
(sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => {
const getV = (name: McuParamName) => sourceFwConfig(name).value;
return {
x: calcMicrostepsPerMm(getV("movement_step_per_mm_x"),
getV("movement_microsteps_x")),
y: calcMicrostepsPerMm(getV("movement_step_per_mm_y"),
getV("movement_microsteps_y")),
z: calcMicrostepsPerMm(getV("movement_step_per_mm_z"),
getV("movement_microsteps_z")),
};
};
export function Motors(props: MotorsProps) {
const {
dispatch, firmwareVersion, controlPanelState,
@ -38,6 +54,7 @@ export function Motors(props: MotorsProps) {
const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x");
const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x");
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
const scale = calculateScale(sourceFwConfig);
return <section>
<Header
@ -69,9 +86,9 @@ export function Motors(props: MotorsProps) {
x={"movement_max_spd_x"}
y={"movement_max_spd_y"}
z={"movement_max_spd_z"}
xScale={sourceFwConfig("movement_step_per_mm_x").value}
yScale={sourceFwConfig("movement_step_per_mm_y").value}
zScale={sourceFwConfig("movement_step_per_mm_z").value}
xScale={scale.x}
yScale={scale.y}
zScale={scale.z}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
{(minFwVersionCheck(firmwareVersion, "5.0.5") || isValidFwConfig) &&
@ -81,9 +98,9 @@ export function Motors(props: MotorsProps) {
x={"movement_home_spd_x"}
y={"movement_home_spd_y"}
z={"movement_home_spd_z"}
xScale={sourceFwConfig("movement_step_per_mm_x").value}
yScale={sourceFwConfig("movement_step_per_mm_y").value}
zScale={sourceFwConfig("movement_step_per_mm_z").value}
xScale={scale.x}
yScale={scale.y}
zScale={scale.z}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />}
<NumericMCUInputGroup
@ -92,9 +109,9 @@ export function Motors(props: MotorsProps) {
x={"movement_min_spd_x"}
y={"movement_min_spd_y"}
z={"movement_min_spd_z"}
xScale={sourceFwConfig("movement_step_per_mm_x").value}
yScale={sourceFwConfig("movement_step_per_mm_y").value}
zScale={sourceFwConfig("movement_step_per_mm_z").value}
xScale={scale.x}
yScale={scale.y}
zScale={scale.z}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
@ -103,9 +120,9 @@ export function Motors(props: MotorsProps) {
x={"movement_steps_acc_dec_x"}
y={"movement_steps_acc_dec_y"}
z={"movement_steps_acc_dec_z"}
xScale={sourceFwConfig("movement_step_per_mm_x").value}
yScale={sourceFwConfig("movement_step_per_mm_y").value}
zScale={sourceFwConfig("movement_step_per_mm_z").value}
xScale={scale.x}
yScale={scale.y}
zScale={scale.z}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
@ -114,9 +131,20 @@ export function Motors(props: MotorsProps) {
x={"movement_step_per_mm_x"}
y={"movement_step_per_mm_y"}
z={"movement_step_per_mm_z"}
xScale={sourceFwConfig("movement_microsteps_x").value}
yScale={sourceFwConfig("movement_microsteps_y").value}
zScale={sourceFwConfig("movement_microsteps_z").value}
float={false}
sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} />
<NumericMCUInputGroup
name={t("Microsteps per step")}
tooltip={ToolTips.MICROSTEPS_PER_STEP}
x={"movement_microsteps_x"}
y={"movement_microsteps_y"}
z={"movement_microsteps_z"}
sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} />
<BooleanMCUInputGroup
name={t("Always Power Motors")}
tooltip={ToolTips.ALWAYS_POWER_MOTORS}

View File

@ -382,8 +382,25 @@ describe("getGardenCoordinates()", () => {
});
describe("mapPanelClassName()", () => {
it("returns correct panel status", () => {
it("returns correct panel status: short panel", () => {
Object.defineProperty(window, "innerWidth", {
value: 400,
configurable: true
});
mockPath = "/app/designer/move_to";
expect(mapPanelClassName()).toEqual("short-panel");
mockPath = "/app/designer/plants/crop_search/mint/add";
expect(mapPanelClassName()).toEqual("short-panel");
});
it("returns correct panel status: panel open", () => {
Object.defineProperty(window, "innerWidth", {
value: 500,
configurable: true
});
mockPath = "/app/designer/move_to";
expect(mapPanelClassName()).toEqual("panel-open");
mockPath = "/app/designer/plants/crop_search/mint/add";
expect(mapPanelClassName()).toEqual("panel-open");
});
});

View File

@ -22,7 +22,7 @@ jest.mock("../../../../account/dev/dev_support", () => ({
import * as React from "react";
import { shallow, mount } from "enzyme";
import {
GardenMapLegend, ZoomControls, PointsSubMenu
GardenMapLegend, ZoomControls, PointsSubMenu, RotationSelector
} from "../garden_map_legend";
import { GardenMapLegendProps } from "../../interfaces";
import { clickButton } from "../../../../__test_support__/helpers";
@ -114,3 +114,19 @@ describe("<PointsSubMenu />", () => {
expect(toggle).toHaveBeenCalledWith(BooleanSetting.show_historic_points);
});
});
describe("<RotationSelector />", () => {
it("swaps map x&y", () => {
const dispatch = jest.fn();
const wrapper = mount(<RotationSelector
dispatch={dispatch} value={false} />);
wrapper.find("button").simulate("click");
expect(dispatch).toHaveBeenCalled();
});
it("shows correct status", () => {
const wrapper = mount(<RotationSelector
dispatch={jest.fn()} value={true} />);
expect(wrapper.find("button").hasClass("green")).toBeTruthy();
});
});

View File

@ -8,7 +8,9 @@ import { BugsControls } from "../easter_eggs/bugs";
import { BotOriginQuadrant, State } from "../../interfaces";
import { MoveModeLink } from "../../move_to";
import { SavedGardensLink } from "../../saved_gardens/saved_gardens";
import { GetWebAppConfigValue } from "../../../config_storage/actions";
import {
GetWebAppConfigValue, setWebAppConfigValue
} from "../../../config_storage/actions";
import { BooleanSetting } from "../../../session_keys";
import { DevSettings } from "../../../account/dev/dev_support";
import { t } from "../../../i18next_wrapper";
@ -30,6 +32,16 @@ const OriginSelector = ({ quadrant, update }: {
</div>
</div>;
export const RotationSelector = ({ dispatch, value }:
{ dispatch: Function, value: boolean }) => {
const classNames = `fb-button fb-toggle-button ${value ? "green" : "red"}`;
return <div className={"map-rotate-button"}>
<label>{t("rotate")}</label>
<button className={classNames} onClick={() =>
dispatch(setWebAppConfigValue(BooleanSetting.xy_swap, !value))} />
</div>;
};
export const ZoomControls = ({ zoom, getConfigValue }: {
zoom: (value: number) => () => void,
getConfigValue: GetWebAppConfigValue
@ -127,6 +139,8 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
<OriginSelector
quadrant={props.botOriginQuadrant}
update={props.updateBotOriginQuadrant} />
<RotationSelector dispatch={props.dispatch}
value={!!props.getConfigValue(BooleanSetting.xy_swap)} />
<MoveModeLink />
<SavedGardensLink />
<BugsControls />

View File

@ -63,7 +63,8 @@ export const getPanelStatus = (): MapPanelStatus => {
return MapPanelStatus.closed;
}
const mode = getMode();
if (mode === Mode.moveTo || mode === Mode.clickToAdd) {
if (window.innerWidth <= 450 &&
(mode === Mode.moveTo || mode === Mode.clickToAdd)) {
return MapPanelStatus.short;
}
return MapPanelStatus.open;

View File

@ -27,6 +27,7 @@ import { Feature } from "../devices/interfaces";
import { reduceFarmwareEnv } from "../farmware/state_to_props";
import { getFirmwareConfig } from "../resources/getters";
import { DevSettings } from "../account/dev/dev_support";
import { calcMicrostepsPerMm } from "../controls/move/direction_axes_props";
const plantFinder = (plants: TaggedPlant[]) =>
(uuid: string | undefined): TaggedPlant =>
@ -64,7 +65,11 @@ export function mapStateToProps(props: Everything): Props {
const { mcu_params } = props.bot.hardware;
const firmwareSettings = fwConfig || mcu_params;
const { movement_step_per_mm_x, movement_step_per_mm_y } = firmwareSettings;
const fw = firmwareSettings;
const stepsPerMmXY = {
x: calcMicrostepsPerMm(fw.movement_step_per_mm_x, fw.movement_microsteps_x),
y: calcMicrostepsPerMm(fw.movement_step_per_mm_y, fw.movement_microsteps_y),
};
const peripherals = uniq(selectAllPeripherals(props.resources.index))
.map(x => {
@ -119,7 +124,7 @@ export function mapStateToProps(props: Everything): Props {
plants,
botLocationData: validBotLocationData(props.bot.hardware.location_data),
botMcuParams: firmwareSettings,
stepsPerMmXY: { x: movement_step_per_mm_x, y: movement_step_per_mm_y },
stepsPerMmXY,
peripherals,
eStopStatus: props.bot.hardware.informational_settings.locked,
latestImages,

View File

@ -5,7 +5,7 @@ import { ToolTips, Actions } from "../constants";
import { tourNames } from "./tours";
import { t } from "../i18next_wrapper";
const TourList = ({ dispatch }: { dispatch: Function }) =>
export const TourList = ({ dispatch }: { dispatch: Function }) =>
<div className="tour-list">
{tourNames().map(tour => <div key={tour.name}>
<label>{tour.description}</label>

View File

@ -1,6 +1,4 @@
jest.mock("react-redux", () => ({
connect: jest.fn()
}));
jest.mock("react-redux", () => ({ connect: jest.fn() }));
const mockStorj: Dictionary<number | boolean> = {};
@ -25,17 +23,13 @@ describe("<Logs />", () => {
return [log1, log2];
}
const fakeProps = (): LogsProps => {
return {
logs: fakeLogs(),
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
sourceFbosConfig: jest.fn(),
getConfigValue: x => mockStorj[x],
alerts: [],
apiFirmwareValue: undefined,
};
};
const fakeProps = (): LogsProps => ({
logs: fakeLogs(),
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
sourceFbosConfig: jest.fn(),
getConfigValue: x => mockStorj[x],
});
it("renders", () => {
const wrapper = mount(<Logs {...fakeProps()} />);

View File

@ -1,17 +1,10 @@
let mockDev = false;
jest.mock("../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import { mapStateToProps } from "../state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { TaggedLog } from "farmbot";
import { times } from "lodash";
import {
fakeFbosConfig, fakeLog, fakeEnigma
fakeFbosConfig, fakeLog
} from "../../__test_support__/fake_state/resources";
describe("mapStateToProps()", () => {
@ -51,28 +44,4 @@ describe("mapStateToProps()", () => {
value: false, consistent: true
});
});
it("handles undefined", () => {
const state = fakeState();
state.bot.hardware.enigmas = undefined;
const props = mapStateToProps(state);
expect(props.alerts).toEqual([]);
});
it("doesn't show API alerts", () => {
const state = fakeState();
state.resources = buildResourceIndex([fakeEnigma()]);
mockDev = false;
const props = mapStateToProps(state);
expect(props.alerts).toEqual([]);
});
it("shows API alerts", () => {
const state = fakeState();
const enigma = fakeEnigma();
state.resources = buildResourceIndex([enigma]);
mockDev = true;
const props = mapStateToProps(state);
expect(props.alerts).toEqual([enigma.body]);
});
});

View File

@ -1,161 +0,0 @@
import * as React from "react";
import { t } from "../i18next_wrapper";
import { betterCompact } from "../util";
import { BotState } from "../devices/interfaces";
import {
FirmwareActions
} from "../devices/components/fbos_settings/firmware_hardware_status";
import { formatLogTime } from "./index";
import { TimeSettings } from "../interfaces";
import { Enigma } from "farmbot";
import { sortBy } from "lodash";
import { Content } from "../constants";
export interface AlertsProps {
alerts: Alert[];
apiFirmwareValue: string | undefined;
timeSettings: TimeSettings;
}
interface AlertsState {
open: boolean;
}
interface ProblemTag {
author: string;
noun: string;
verb: string;
}
const splitTag = (problemTag: string): ProblemTag => {
const parts = problemTag.split(".");
return { author: parts[0], noun: parts[1], verb: parts[2] };
};
export const sortAlerts = (alerts: Alert[]): Alert[] =>
sortBy(alerts, "priority", "created_at");
export class Alerts extends React.Component<AlertsProps, AlertsState> {
state: AlertsState = { open: true };
render() {
const alertCount = Object.entries(this.props.alerts).length;
return alertCount > 0
? <div className="problem-alerts">
<div className="problem-alerts-header"
onClick={() => this.setState({ open: !this.state.open })}>
<h3>{t("Alerts")}</h3>
<div className={"saucer warn"}>
<p>{alertCount}</p>
</div>
<i className={`fa fa-caret-${this.state.open ? "up" : "down"}`} />
</div>
{this.state.open &&
<div className="problem-alerts-content">
{sortAlerts(this.props.alerts).map((x, i) =>
<AlertCard key={i}
alert={x}
apiFirmwareValue={this.props.apiFirmwareValue}
timeSettings={this.props.timeSettings} />)}
</div>}
</div> : <div />;
}
}
export interface FirmwareAlertsProps {
bot: BotState;
apiFirmwareValue: string | undefined;
timeSettings: TimeSettings;
}
export const FirmwareAlerts = (props: FirmwareAlertsProps) => {
const alerts = betterCompact(Object.values(props.bot.hardware.enigmas || {}));
const firmwareAlerts = sortAlerts(alerts)
.filter(x => splitTag(x.problem_tag).noun === "firmware");
return <div className="firmware-alerts">
{firmwareAlerts.filter(x => x.problem_tag && x.priority && x.created_at)
.map((x, i) =>
<AlertCard key={i}
alert={x}
apiFirmwareValue={props.apiFirmwareValue}
timeSettings={props.timeSettings} />)}
</div>;
};
export interface Alert extends Enigma { }
interface AlertCardProps {
alert: Alert;
apiFirmwareValue: string | undefined;
timeSettings: TimeSettings;
}
const AlertCard = (props: AlertCardProps) => {
switch (props.alert.problem_tag) {
case "farmbot_os.firmware.missing":
return <FirmwareMissing
createdAt={props.alert.created_at}
apiFirmwareValue={props.apiFirmwareValue}
timeSettings={props.timeSettings} />;
case "api.seed_data.missing":
return <SeedDataMissing
createdAt={props.alert.created_at}
timeSettings={props.timeSettings} />;
default:
return UnknownAlert(props.alert, props.timeSettings);
}
};
const UnknownAlert = (alert: Alert, timeSettings: TimeSettings) => {
const { problem_tag, priority, created_at } = alert;
const { author, noun, verb } = splitTag(problem_tag);
const createdAt = formatLogTime(created_at, timeSettings);
return <div className="problem-alert unknown-alert">
<div className="problem-alert-title">
<i className="fa fa-exclamation-triangle" />
<h3>{`${t(noun)}: ${t(verb)} (${t(author)})`}</h3>
<p>{createdAt}</p>
</div>
<div className="problem-alert-content">
<p>
{t("Unknown problem of priority {{priority}} created at {{createdAt}}",
{ priority, createdAt })}
</p>
</div>
</div>;
};
interface CommonAlertCardProps {
createdAt: number;
timeSettings: TimeSettings;
}
interface FirmwareMissingProps extends CommonAlertCardProps {
apiFirmwareValue: string | undefined;
}
const FirmwareMissing = (props: FirmwareMissingProps) =>
<div className="problem-alert firmware-missing-alert">
<div className="problem-alert-title">
<i className="fa fa-exclamation-triangle" />
<h3>{t("Firmware missing")}</h3>
<p>{formatLogTime(props.createdAt, props.timeSettings)}</p>
</div>
<div className="problem-alert-content">
<p>{t("Your device has no firmware installed.")}</p>
<FirmwareActions
apiFirmwareValue={props.apiFirmwareValue}
botOnline={true} />
</div>
</div>;
const SeedDataMissing = (props: CommonAlertCardProps) =>
<div className="problem-alert seed-data-missing-alert">
<div className="problem-alert-title">
<i className="fa fa-exclamation-triangle" />
<h3>{t("Choose your FarmBot")}</h3>
<p>{formatLogTime(props.createdAt, props.timeSettings)}</p>
</div>
<div className="problem-alert-content">
<p>{Content.SEED_DATA_SELECTION}</p>
</div>
</div>;

View File

@ -15,7 +15,6 @@ import { NumericSetting } from "../session_keys";
import { setWebAppConfigValue } from "../config_storage/actions";
import { NumberConfigKey } from "farmbot/dist/resources/configs/web_app";
import { t } from "../i18next_wrapper";
import { Alerts } from "./alerts";
import { TimeSettings } from "../interfaces";
import { timeFormatString } from "../util";
@ -85,9 +84,6 @@ export class Logs extends React.Component<LogsProps, Partial<LogsState>> {
render() {
const filterBtnColor = this.filterActive ? "green" : "gray";
return <Page className="logs-page">
<Alerts alerts={this.props.alerts}
apiFirmwareValue={this.props.apiFirmwareValue}
timeSettings={this.props.timeSettings} />
<Row>
<Col xs={7}>
<h3>

View File

@ -1,7 +1,6 @@
import { TaggedLog, ConfigurationName, ALLOWED_MESSAGE_TYPES } from "farmbot";
import { SourceFbosConfig } from "../devices/interfaces";
import { GetWebAppConfigValue } from "../config_storage/actions";
import { Alert } from "./alerts";
import { TimeSettings } from "../interfaces";
export interface LogsProps {
@ -10,8 +9,6 @@ export interface LogsProps {
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
getConfigValue: GetWebAppConfigValue;
alerts: Alert[];
apiFirmwareValue: string | undefined;
}
export type Filters = Record<ALLOWED_MESSAGE_TYPES, number>;

View File

@ -1,19 +1,17 @@
import { Everything } from "../interfaces";
import {
selectAllLogs, maybeGetTimeSettings, selectAllEnigmas
selectAllLogs, maybeGetTimeSettings
} from "../resources/selectors";
import { LogsProps } from "./interfaces";
import {
sourceFbosConfigValue
} from "../devices/components/source_config_value";
import { validFbosConfig, betterCompact } from "../util";
import { validFbosConfig } from "../util";
import { ResourceIndex } from "../resources/interfaces";
import { TaggedLog } from "farmbot";
import { getWebAppConfigValue } from "../config_storage/actions";
import { getFbosConfig } from "../resources/getters";
import { chain } from "lodash";
import { isFwHardwareValue } from "../devices/components/fbos_settings/board_type";
import { DevSettings } from "../account/dev/dev_support";
/** Take the specified number of logs after sorting by time created. */
export function takeSortedLogs(
@ -30,19 +28,11 @@ export function mapStateToProps(props: Everything): LogsProps {
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const sourceFbosConfig =
sourceFbosConfigValue(fbosConfig, hardware.configuration);
const apiFirmwareValue = sourceFbosConfig("firmware_hardware").value;
const botAlerts = betterCompact(Object.values(props.bot.hardware.enigmas || {}));
const apiAlerts = selectAllEnigmas(props.resources.index).map(x => x.body);
const alerts =
botAlerts.concat(DevSettings.futureFeaturesEnabled() ? apiAlerts : []);
return {
dispatch: props.dispatch,
sourceFbosConfig,
logs: takeSortedLogs(250, props.resources.index),
timeSettings: maybeGetTimeSettings(props.resources.index),
getConfigValue: getWebAppConfigValue(() => props),
alerts,
apiFirmwareValue: isFwHardwareValue(apiFirmwareValue)
? apiFirmwareValue : undefined,
};
}

View File

@ -1,10 +1,9 @@
import * as React from "react";
import { mount } from "enzyme";
import {
Alerts, AlertsProps, Alert, FirmwareAlerts, FirmwareAlertsProps, sortAlerts
} from "../alerts";
import { FirmwareAlerts, sortAlerts, Alerts } from "../alerts";
import { bot } from "../../__test_support__/fake_state/bot";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
import { Alert, AlertsProps, FirmwareAlertsProps } from "../interfaces";
const FIRMWARE_MISSING_ALERT: Alert = {
created_at: 123,
@ -39,11 +38,13 @@ describe("<Alerts />", () => {
alerts: [],
apiFirmwareValue: undefined,
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
});
it("renders no alerts", () => {
const wrapper = mount(<Alerts {...fakeProps()} />);
expect(wrapper.html()).toEqual("<div></div>");
expect(wrapper.html())
.toContain(`<div class="problem-alerts-content"></div>`);
});
it("renders alerts", () => {
@ -62,15 +63,6 @@ describe("<Alerts />", () => {
expect(wrapper.text()).toContain("1");
expect(wrapper.text()).toContain("firmware: alert");
});
it("collapses alerts", () => {
const p = fakeProps();
p.alerts = [FIRMWARE_MISSING_ALERT];
const wrapper = mount<Alerts>(<Alerts {...p} />);
expect(wrapper.state().open).toEqual(true);
wrapper.find(".problem-alerts-header").simulate("click");
expect(wrapper.state().open).toEqual(false);
});
});
describe("<FirmwareAlerts />", () => {
@ -78,6 +70,7 @@ describe("<FirmwareAlerts />", () => {
bot,
apiFirmwareValue: undefined,
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
});
it("renders no alerts", () => {

View File

@ -0,0 +1,61 @@
import * as React from "react";
import { mount } from "enzyme";
import { AlertCard } from "../cards";
import { AlertCardProps } from "../interfaces";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
import { FBSelect } from "../../ui";
describe("<AlertCard />", () => {
const fakeProps = (): AlertCardProps => ({
alert: {
created_at: 123,
problem_tag: "author.noun.verb",
priority: 100,
uuid: "uuid",
},
apiFirmwareValue: undefined,
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
});
it("renders unknown card", () => {
const wrapper = mount(<AlertCard {...fakeProps()} />);
expect(wrapper.text()).toContain("noun: verb (author)");
});
it("renders firmware card", () => {
const p = fakeProps();
p.alert.problem_tag = "farmbot_os.firmware.missing";
const wrapper = mount(<AlertCard {...p} />);
expect(wrapper.text()).toContain("Firmware missing");
});
it("renders seed data card", () => {
const p = fakeProps();
p.alert.problem_tag = "api.seed_data.missing";
const wrapper = mount(<AlertCard {...p} />);
expect(wrapper.text()).toContain("FarmBot");
wrapper.find(FBSelect).simulate("change");
});
it("renders tour card", () => {
const p = fakeProps();
p.alert.problem_tag = "api.tour.not_taken";
const wrapper = mount(<AlertCard {...p} />);
expect(wrapper.text()).toContain("tour");
});
it("renders welcome card", () => {
const p = fakeProps();
p.alert.problem_tag = "api.user.not_welcomed";
const wrapper = mount(<AlertCard {...p} />);
expect(wrapper.text()).toContain("Welcome");
});
it("renders documentation card", () => {
const p = fakeProps();
p.alert.problem_tag = "api.documentation.unread";
const wrapper = mount(<AlertCard {...p} />);
expect(wrapper.text()).toContain("Learn");
});
});

View File

@ -0,0 +1,35 @@
jest.mock("react-redux", () => ({ connect: jest.fn() }));
import * as React from "react";
import { mount } from "enzyme";
import { Messages } from "../index";
import { MessagesProps } from "../interfaces";
import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
describe("<Messages />", () => {
const fakeProps = (): MessagesProps => ({
alerts: [],
apiFirmwareValue: undefined,
timeSettings: fakeTimeSettings(),
dispatch: Function,
});
it("renders page", () => {
const wrapper = mount(<Messages {...fakeProps()} />);
expect(wrapper.text()).toContain("Message Center");
expect(wrapper.text()).toContain("No messages");
});
it("renders content", () => {
const p = fakeProps();
p.alerts = [{
created_at: 123,
problem_tag: "author.noun.verb",
priority: 100,
uuid: "uuid",
}];
const wrapper = mount(<Messages {...p} />);
expect(wrapper.text()).toContain("Message Center");
expect(wrapper.text()).toContain("No more messages");
});
});

View File

@ -0,0 +1,45 @@
let mockDev = false;
jest.mock("../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
import { fakeState } from "../../__test_support__/fake_state";
import { mapStateToProps } from "../state_to_props";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { fakeEnigma, fakeFbosConfig } from "../../__test_support__/fake_state/resources";
describe("mapStateToProps()", () => {
it("handles undefined", () => {
const state = fakeState();
state.bot.hardware.enigmas = undefined;
const props = mapStateToProps(state);
expect(props.alerts).toEqual([]);
});
it("doesn't show API alerts", () => {
const state = fakeState();
state.resources = buildResourceIndex([fakeEnigma()]);
mockDev = false;
const props = mapStateToProps(state);
expect(props.alerts).toEqual([]);
});
it("shows API alerts", () => {
const state = fakeState();
const enigma = fakeEnigma();
state.resources = buildResourceIndex([enigma]);
mockDev = true;
const props = mapStateToProps(state);
expect(props.alerts).toEqual([enigma.body]);
});
it("returns firmware value", () => {
const state = fakeState();
const fbosConfig = fakeFbosConfig();
fbosConfig.body.api_migrated = true;
fbosConfig.body.firmware_hardware = "arduino";
state.resources = buildResourceIndex([fbosConfig]);
const props = mapStateToProps(state);
expect(props.apiFirmwareValue).toEqual("arduino");
});
});

View File

@ -0,0 +1,42 @@
import * as React from "react";
import { betterCompact } from "../util";
import { sortBy } from "lodash";
import {
ProblemTag, Alert, FirmwareAlertsProps, AlertsProps,
} from "./interfaces";
import { AlertCard } from "./cards";
export const splitProblemTag = (problemTag: string): ProblemTag => {
const parts = problemTag.split(".");
return { author: parts[0], noun: parts[1], verb: parts[2] };
};
export const sortAlerts = (alerts: Alert[]): Alert[] =>
sortBy(alerts, "priority", "created_at");
export const FirmwareAlerts = (props: FirmwareAlertsProps) => {
const alerts = betterCompact(Object.values(props.bot.hardware.enigmas || {}));
const firmwareAlerts = sortAlerts(alerts)
.filter(x => splitProblemTag(x.problem_tag).noun === "firmware");
return <div className="firmware-alerts">
{firmwareAlerts.filter(x => x.problem_tag && x.priority && x.created_at)
.map((x, i) =>
<AlertCard key={i}
alert={x}
dispatch={props.dispatch}
apiFirmwareValue={props.apiFirmwareValue}
timeSettings={props.timeSettings} />)}
</div>;
};
export const Alerts = (props: AlertsProps) =>
<div className="problem-alerts">
<div className="problem-alerts-content">
{sortAlerts(props.alerts).map((x, i) =>
<AlertCard key={i}
alert={x}
dispatch={props.dispatch}
apiFirmwareValue={props.apiFirmwareValue}
timeSettings={props.timeSettings} />)}
</div>
</div>;

View File

@ -0,0 +1,159 @@
import React from "react";
import { t } from "../i18next_wrapper";
import {
AlertCardProps, AlertCardTemplateProps, FirmwareMissingProps,
SeedDataMissingProps, SeedDataMissingState, TourNotTakenProps,
CommonAlertCardProps
} from "./interfaces";
import { formatLogTime } from "../logs";
import {
FirmwareActions
} from "../devices/components/fbos_settings/firmware_hardware_status";
import { DropDownItem, Row, Col, FBSelect, docLink } from "../ui";
import { Content } from "../constants";
import { TourList } from "../help/tour_list";
import { splitProblemTag } from "./alerts";
export const AlertCard = (props: AlertCardProps) => {
const { alert, timeSettings } = props;
const commonProps = { alert, timeSettings };
switch (alert.problem_tag) {
case "farmbot_os.firmware.missing":
return <FirmwareMissing {...commonProps}
apiFirmwareValue={props.apiFirmwareValue} />;
case "api.seed_data.missing":
return <SeedDataMissing {...commonProps}
dispatch={props.dispatch} />;
case "api.tour.not_taken":
return <TourNotTaken {...commonProps}
dispatch={props.dispatch} />;
case "api.user.not_welcomed":
return <UserNotWelcomed {...commonProps} />;
case "api.documentation.unread":
return <DocumentationUnread {...commonProps} />;
default:
return UnknownAlert(commonProps);
}
};
const AlertCardTemplate = (props: AlertCardTemplateProps) =>
<div className={`problem-alert ${props.className}`}>
<div className="problem-alert-title">
<i className="fa fa-exclamation-triangle" />
<h3>{t(props.title)}</h3>
<p>{formatLogTime(props.alert.created_at, props.timeSettings)}</p>
<i className="fa fa-times" />
</div>
<div className="problem-alert-content">
<p>{t(props.message)}</p>
{props.children}
</div>
</div>;
const UnknownAlert = (props: CommonAlertCardProps) => {
const { problem_tag, created_at, priority } = props.alert;
const { author, noun, verb } = splitProblemTag(problem_tag);
const createdAt = formatLogTime(created_at, props.timeSettings);
return <AlertCardTemplate
alert={props.alert}
className={"unknown-alert"}
title={`${t(noun)}: ${t(verb)} (${t(author)})`}
message={t("Unknown problem of priority {{priority}} created at {{createdAt}}",
{ priority, createdAt })}
timeSettings={props.timeSettings} />;
};
const FirmwareMissing = (props: FirmwareMissingProps) =>
<AlertCardTemplate
alert={props.alert}
className={"firmware-missing-alert"}
title={t("Firmware missing")}
message={t("Your device has no firmware installed.")}
timeSettings={props.timeSettings}>
<FirmwareActions
apiFirmwareValue={props.apiFirmwareValue}
botOnline={true} />
</AlertCardTemplate>;
const SEED_DATA_OPTIONS: DropDownItem[] = [
{ label: "Genesis v1.2", value: "12" },
{ label: "Genesis v1.3", value: "13" },
{ label: "Genesis v1.4", value: "14" },
{ label: "Genesis v1.4 XL", value: "14XL" },
{ label: "Custom Bot", value: "custom" },
];
class SeedDataMissing
extends React.Component<SeedDataMissingProps, SeedDataMissingState> {
state: SeedDataMissingState = { selection: "" };
render() {
return <AlertCardTemplate
alert={this.props.alert}
className={"seed-data-missing-alert"}
title={t("Choose your FarmBot")}
message={t(Content.SEED_DATA_SELECTION)}
timeSettings={this.props.timeSettings}>
<Row>
<Col xs={4}>
<label>{t("Choose your FarmBot")}</label>
</Col>
<Col xs={5}>
<FBSelect
key={this.state.selection}
list={SEED_DATA_OPTIONS}
selectedItem={SEED_DATA_OPTIONS[0]}
onChange={() => { }} />
</Col>
</Row>
</AlertCardTemplate>;
}
}
const TourNotTaken = (props: TourNotTakenProps) =>
<AlertCardTemplate
alert={props.alert}
className={"tour-not-taken-alert"}
title={t("Take a guided tour")}
message={t(Content.TAKE_A_TOUR)}
timeSettings={props.timeSettings}>
<p>{t("Choose a tour to begin")}:</p>
<TourList dispatch={props.dispatch} />
</AlertCardTemplate>;
const UserNotWelcomed = (props: CommonAlertCardProps) =>
<AlertCardTemplate
alert={props.alert}
className={"user-not-welcomed-alert"}
title={t("Welcome to the FarmBot Web App")}
message={t(Content.WELCOME)}
timeSettings={props.timeSettings}>
<p>
{t("You're currently viewing the")} <b>{t("Message Center")}</b>.
&nbsp;{t(Content.MESSAGE_CENTER_WELCOME)}
</p>
<p>
{t(Content.MESSAGE_DISMISS)}
</p>
</AlertCardTemplate>;
const DocumentationUnread = (props: CommonAlertCardProps) =>
<AlertCardTemplate
alert={props.alert}
className={"documentation-unread-alert"}
title={t("Learn more about the app")}
message={t(Content.READ_THE_DOCS)}
timeSettings={props.timeSettings}>
<p>
{t("Head over to")}
&nbsp;<a href={docLink()} target="_blank"
title={t("Open documentation in a new tab")}>
{t("software.farm.bot")}
</a>
&nbsp;{t("to get started")}.
</p>
<a className="link-button fb-button green"
href={docLink()} target="_blank"
title={t("Open documentation in a new tab")}>
{t("Read the docs")}
</a>
</AlertCardTemplate>;

View File

@ -0,0 +1,38 @@
import * as React from "react";
import { connect } from "react-redux";
import { Col, Row, Page, ToolTip } from "../ui/index";
import { ToolTips } from "../constants";
import { t } from "../i18next_wrapper";
import { Alerts } from "./alerts";
import { mapStateToProps } from "./state_to_props";
import { MessagesProps } from "./interfaces";
import { Link } from "../link";
@connect(mapStateToProps)
export class Messages extends React.Component<MessagesProps, {}> {
render() {
return <Page className="messages-page">
<Row>
<Col xs={12}>
<h3>
<i>{t("Message Center")}</i>
</h3>
<ToolTip helpText={ToolTips.MESSAGES} />
</Col>
</Row>
<Row>
<Alerts alerts={this.props.alerts}
dispatch={this.props.dispatch}
apiFirmwareValue={this.props.apiFirmwareValue}
timeSettings={this.props.timeSettings} />
</Row>
<Row>
<div className="link-to-logs">
{this.props.alerts.length > 0
? t("No more messages.") : t("No messages.")}
&nbsp;<Link to="/app/logs">{t("View Logs")}</Link>
</div>
</Row>
</Page>;
}
}

View File

@ -0,0 +1,69 @@
import { FirmwareHardware, Enigma } from "farmbot";
import { TimeSettings } from "../interfaces";
import { BotState } from "../devices/interfaces";
export interface MessagesProps {
alerts: Alert[];
apiFirmwareValue: FirmwareHardware | undefined;
timeSettings: TimeSettings;
dispatch: Function;
}
export interface AlertsProps {
alerts: Alert[];
apiFirmwareValue: string | undefined;
timeSettings: TimeSettings;
dispatch: Function;
}
export interface ProblemTag {
author: string;
noun: string;
verb: string;
}
export interface FirmwareAlertsProps {
bot: BotState;
apiFirmwareValue: string | undefined;
timeSettings: TimeSettings;
dispatch: Function;
}
export interface Alert extends Enigma { }
export interface AlertCardProps {
alert: Alert;
apiFirmwareValue: string | undefined;
timeSettings: TimeSettings;
dispatch: Function;
}
export interface CommonAlertCardProps {
alert: Alert;
timeSettings: TimeSettings;
}
export interface AlertCardTemplateProps {
className: string;
title: string;
alert: Alert;
message: string;
timeSettings: TimeSettings;
children?: React.ReactNode;
}
export interface FirmwareMissingProps extends CommonAlertCardProps {
apiFirmwareValue: string | undefined;
}
export interface SeedDataMissingProps extends CommonAlertCardProps {
dispatch: Function;
}
export interface SeedDataMissingState {
selection: string;
}
export interface TourNotTakenProps extends CommonAlertCardProps {
dispatch: Function;
}

View File

@ -0,0 +1,27 @@
import { Everything } from "../interfaces";
import { MessagesProps } from "./interfaces";
import { validFbosConfig, betterCompact } from "../util";
import { getFbosConfig } from "../resources/getters";
import { sourceFbosConfigValue } from "../devices/components/source_config_value";
import { DevSettings } from "../account/dev/dev_support";
import { selectAllEnigmas, maybeGetTimeSettings } from "../resources/selectors";
import { isFwHardwareValue } from "../devices/components/fbos_settings/board_type";
export const mapStateToProps = (props: Everything): MessagesProps => {
const { hardware } = props.bot;
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const sourceFbosConfig =
sourceFbosConfigValue(fbosConfig, hardware.configuration);
const apiFirmwareValue = sourceFbosConfig("firmware_hardware").value;
const botAlerts = betterCompact(Object.values(props.bot.hardware.enigmas || {}));
const apiAlerts = selectAllEnigmas(props.resources.index).map(x => x.body);
const alerts =
botAlerts.concat(DevSettings.futureFeaturesEnabled() ? apiAlerts : []);
return {
alerts,
apiFirmwareValue: isFwHardwareValue(apiFirmwareValue)
? apiFirmwareValue : undefined,
timeSettings: maybeGetTimeSettings(props.resources.index),
dispatch: props.dispatch,
};
};

View File

@ -1,3 +1,8 @@
let mockDev = false;
jest.mock("../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { AdditionalMenu } from "../additional_menu";
@ -31,4 +36,12 @@ describe("AdditionalMenu", () => {
wrapper.find("a").at(1).simulate("click");
expect(logout).toHaveBeenCalled();
});
it("navigates to help page", () => {
mockDev = true;
const wrapper = shallow(<AdditionalMenu
logout={jest.fn()}
close={jest.fn()} />);
wrapper.find("Link").at(2).simulate("click");
});
});

View File

@ -24,6 +24,7 @@ describe("NavBar", () => {
tour: undefined,
device: fakeDevice(),
autoSync: false,
alertCount: 0,
});
it("has correct parent classname", () => {

View File

@ -1,12 +1,19 @@
import * as React from "react";
import { shallow } from "enzyme";
import { shallow, mount } from "enzyme";
import { NavLinks } from "../nav_links";
describe("NavLinks", () => {
it("toggles the mobile nav menu", () => {
const close = jest.fn();
const wrapper = shallow(<NavLinks close={(x) => () => close(x)} />);
const wrapper = shallow(<NavLinks close={(x) => () => close(x)}
alertCount={0} />);
wrapper.find("Link").first().simulate("click");
expect(close).toHaveBeenCalledWith("mobileMenuOpen");
expect(wrapper.text()).not.toContain("0");
});
it("shows indicator", () => {
const wrapper = mount(<NavLinks close={jest.fn()} alertCount={1} />);
expect(wrapper.text()).toContain("1");
});
});

View File

@ -15,6 +15,12 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
{t("Account Settings")}
</Link>
</div>
<div>
<Link to="/app/logs" onClick={props.close("accountMenuOpen")}>
<i className="fa fa-list"></i>
{t("Logs")}
</Link>
</div>
{DevSettings.futureFeaturesEnabled() &&
<Link to="/app/help" onClick={props.close("accountMenuOpen")}>
<i className="fa fa-question-circle"></i>

View File

@ -73,7 +73,7 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
const { toggle, close } = this;
const { mobileMenuOpen, tickerListOpen, accountMenuOpen } = this.state;
const { logs, timeSettings, getConfigValue } = this.props;
const { logs, timeSettings, getConfigValue, alertCount } = this.props;
const tickerListProps = {
logs, tickerListOpen, toggle, timeSettings, getConfigValue
};
@ -90,10 +90,10 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
className={menuIconClassNames.join(" ")}
onClick={this.toggle("mobileMenuOpen")} />
<span className="mobile-menu-container">
{MobileMenu({ close, mobileMenuOpen })}
{MobileMenu({ close, mobileMenuOpen, alertCount })}
</span>
<span className="top-menu-container">
{NavLinks({ close })}
{NavLinks({ close, alertCount })}
</span>
</div>
<div className="nav-right">

View File

@ -22,6 +22,7 @@ export interface NavBarProps {
tour: string | undefined;
device: TaggedDevice;
autoSync: boolean;
alertCount: number;
}
export interface NavBarState {
@ -35,6 +36,7 @@ type ToggleEventHandler = (e: React.MouseEvent<HTMLElement>) => void;
export interface MobileMenuProps {
close: (property: keyof NavBarState) => ToggleEventHandler;
mobileMenuOpen: boolean;
alertCount: number;
}
export interface TickerListProps {
@ -47,6 +49,7 @@ export interface TickerListProps {
export interface NavLinksProps {
close: (property: keyof NavBarState) => ToggleEventHandler;
alertCount: number;
}
export interface AccountMenuProps {

View File

@ -8,12 +8,13 @@ const classes = [Classes.CARD, Classes.ELEVATION_4, "mobile-menu"];
export let MobileMenu = (props: MobileMenuProps) => {
const isActive = props.mobileMenuOpen ? "active" : "inactive";
const { alertCount } = props;
return <div>
<Overlay
isOpen={props.mobileMenuOpen}
onClose={props.close("mobileMenuOpen")}>
<div className={`${classes.join(" ")} ${isActive}`}>
{NavLinks({ close: props.close })}
{NavLinks({ close: props.close, alertCount })}
</div>
</Overlay>
</div>;

View File

@ -41,7 +41,7 @@ export const links: NavLinkParams[] = [
name: "Farmware", icon: "crosshairs", slug: "farmware",
computeHref: computeFarmwareUrlFromState
},
{ name: "Logs", icon: "list", slug: "logs" },
{ name: "Messages", icon: "list", slug: "messages" },
];
export const NavLinks = (props: NavLinksProps) => {
@ -59,7 +59,13 @@ export const NavLinks = (props: NavLinksProps) => {
draggable={false}
onClick={props.close("mobileMenuOpen")}>
<i className={`fa fa-${link.icon}`} />
{t(link.name)}
<div>
{t(link.name)}
{link.slug === "messages" && props.alertCount > 0 &&
<div className={"saucer fun"}>
<p>{props.alertCount}</p>
</div>}
</div>
</Link>;
})}
</div>

View File

@ -57,7 +57,7 @@ const getfirstTickerLog = (getConfigValue: GetWebAppConfigValue) =>
/** Format a single log for display in the ticker. */
const Ticker = (log: TaggedLog, timeSettings: TimeSettings) => {
const { message, type, created_at } = log.body;
const time = formatLogTime(created_at || NaN, timeSettings);
const time = created_at ? formatLogTime(created_at, timeSettings) : "";
return <div key={log.uuid} className="status-ticker-wrapper">
<div className={`saucer ${type}`} />
<label className="status-ticker-message">

View File

@ -1,6 +1,5 @@
import * as React from "react";
import axios from "axios";
import { error as log, init as logInit } from "farmbot-toastr";
import { prettyPrintApiErrors } from "../util";
import { API } from "../api";
@ -76,7 +75,9 @@ export class PasswordReset extends React.Component<Props, State> {
<Row>
<Col xs={12} sm={6} className="col-sm-push-3">
<Widget>
<WidgetHeader title={"Reset Password"} />
<WidgetHeader
title={"Reset Password"}
helpText={t("Password must be 8 or more characters.")} />
<WidgetBody>
<form onSubmit={this.submit.bind(this)}>
<label>

View File

@ -130,6 +130,12 @@ export const UNBOUND_ROUTES = [
getModule: () => import("./logs"),
key: "Logs",
}),
route({
children: false,
$: "/messages",
getModule: () => import("./messages"),
key: "Messages",
}),
route({
children: false,
$: "/regimens(/:regimen)",

View File

@ -15,6 +15,7 @@ import { getFirmwareConfig } from "../resources/getters";
import { Farmwares } from "../farmware/interfaces";
import { manifestInfo } from "../farmware/generate_manifest_info";
import { DevSettings } from "../account/dev/dev_support";
import { calculateAxialLengths } from "../controls/move/direction_axes_props";
export function mapStateToProps(props: Everything): Props {
const uuid = props.resources.consumers.sequences.current;
@ -42,14 +43,7 @@ export function mapStateToProps(props: Everything): Props {
y: !!firmwareSettings.movement_home_up_y,
z: !!firmwareSettings.movement_home_up_z
},
axisLength: {
x: (firmwareSettings.movement_axis_nr_steps_x || 0)
/ (firmwareSettings.movement_step_per_mm_x || 1),
y: (firmwareSettings.movement_axis_nr_steps_y || 0)
/ (firmwareSettings.movement_step_per_mm_y || 1),
z: (firmwareSettings.movement_axis_nr_steps_z || 0)
/ (firmwareSettings.movement_step_per_mm_z || 1)
},
axisLength: calculateAxialLengths({ firmwareSettings }),
};
};

View File

@ -43,7 +43,7 @@
"coveralls": "3.0.3",
"enzyme": "3.9.0",
"enzyme-adapter-react-16": "1.12.1",
"farmbot": "7.0.4-rc1",
"farmbot": "7.0.4-rc3",
"farmbot-toastr": "1.0.3",
"i18next": "15.0.9",
"jest": "24.7.1",