commit
83d9cb8287
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)) &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -293,6 +293,10 @@
|
|||
&.grayscale {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
background: $medium_light_gray !important;
|
||||
}
|
||||
}
|
||||
|
||||
.front-page-button {
|
||||
|
|
|
@ -572,6 +572,12 @@
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.map-rotate-button {
|
||||
text-align: center;
|
||||
.fb-button {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-points-submenu {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
box-shadow: none;
|
||||
color: $dark_gray;
|
||||
font-weight: bold;
|
||||
min-width: 30%;
|
||||
min-width: 130px;
|
||||
}
|
||||
p {
|
||||
font-size: 1rem;
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()} />);
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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", () => {
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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>;
|
|
@ -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>.
|
||||
{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")}
|
||||
<a href={docLink()} target="_blank"
|
||||
title={t("Open documentation in a new tab")}>
|
||||
{t("software.farm.bot")}
|
||||
</a>
|
||||
{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>;
|
|
@ -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.")}
|
||||
<Link to="/app/logs">{t("View Logs")}</Link>
|
||||
</div>
|
||||
</Row>
|
||||
</Page>;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ describe("NavBar", () => {
|
|||
tour: undefined,
|
||||
device: fakeDevice(),
|
||||
autoSync: false,
|
||||
alertCount: 0,
|
||||
});
|
||||
|
||||
it("has correct parent classname", () => {
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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 }),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue