add messages
parent
beee68850a
commit
45908ff660
|
@ -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
|
||||
|
|
|
@ -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)) &&
|
||||
|
|
|
@ -346,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.`);
|
||||
|
@ -384,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
|
||||
|
@ -391,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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)",
|
||||
|
|
Loading…
Reference in New Issue