commit
14f19e76e4
2
Gemfile
2
Gemfile
|
@ -16,7 +16,7 @@ gem "mutations"
|
|||
gem "paperclip"
|
||||
gem "pg"
|
||||
gem "polymorphic_constraints"
|
||||
gem "rack-attack"
|
||||
# gem "rack-attack" # RC 17 APR 19
|
||||
gem "rack-cors"
|
||||
gem "rails_12factor"
|
||||
gem "rails"
|
||||
|
|
|
@ -1,37 +1,38 @@
|
|||
require_relative "../app/models/transport.rb"
|
||||
require File.expand_path('../boot', __FILE__)
|
||||
require File.expand_path("../boot", __FILE__)
|
||||
require_relative "../app/lib/celery_script/csheap"
|
||||
require "rails/all"
|
||||
|
||||
# Require the gems listed in Gemfile, including any gems
|
||||
# you've limited to :test, :development, or :production.
|
||||
Bundler.require(:default, Rails.env)
|
||||
|
||||
module FarmBot
|
||||
class Application < Rails::Application
|
||||
Delayed::Worker.max_attempts = 4
|
||||
REDIS_ENV_KEY = ENV.fetch("WHERE_IS_REDIS_URL", "REDIS_URL")
|
||||
REDIS_URL = ENV.fetch(REDIS_ENV_KEY, "redis://redis:6379/0")
|
||||
config.cache_store = :redis_cache_store, { url: REDIS_URL }
|
||||
config.middleware.use Rack::Attack
|
||||
REDIS_URL = ENV.fetch(REDIS_ENV_KEY, "redis://redis:6379/0")
|
||||
config.cache_store = :redis_cache_store, { url: REDIS_URL }
|
||||
# config.middleware.use Rack::Attack
|
||||
config.active_record.schema_format = :sql
|
||||
config.active_job.queue_adapter = :delayed_job
|
||||
config.active_job.queue_adapter = :delayed_job
|
||||
config.action_dispatch.perform_deep_munge = false
|
||||
I18n.enforce_available_locales = false
|
||||
LOCAL_API_HOST = ENV.fetch("API_HOST", "parcel")
|
||||
PARCELJS_URL = "http://#{LOCAL_API_HOST}:3808"
|
||||
PARCELJS_URL = "http://#{LOCAL_API_HOST}:3808"
|
||||
config.generators do |g|
|
||||
g.template_engine :erb
|
||||
g.test_framework :rspec, :fixture_replacement => :factory_bot, :views => false, :helper => false
|
||||
g.view_specs false
|
||||
g.helper_specs false
|
||||
g.fixture_replacement :factory_bot, :dir => 'spec/factories'
|
||||
g.fixture_replacement :factory_bot, :dir => "spec/factories"
|
||||
end
|
||||
config.autoload_paths << Rails.root.join('lib')
|
||||
config.autoload_paths << Rails.root.join('lib/sequence_migrations')
|
||||
config.autoload_paths << Rails.root.join("lib")
|
||||
config.autoload_paths << Rails.root.join("lib/sequence_migrations")
|
||||
config.middleware.insert_before ActionDispatch::Static, Rack::Cors do
|
||||
allow do
|
||||
origins '*'
|
||||
resource '/api/*',
|
||||
origins "*"
|
||||
resource "/api/*",
|
||||
headers: :any,
|
||||
methods: [:get, :post, :delete, :put, :patch, :options, :head],
|
||||
expose: "X-Farmbot-Rpc-Id",
|
||||
|
@ -42,20 +43,18 @@ module FarmBot
|
|||
Rails.application.routes.default_url_options[:host] = LOCAL_API_HOST
|
||||
Rails.application.routes.default_url_options[:port] = ENV["API_PORT"] || 3000
|
||||
# ¯\_(ツ)_/¯
|
||||
$API_URL = "//#{ Rails.application.routes.default_url_options[:host] }:#{ Rails.application.routes.default_url_options[:port] }"
|
||||
ALL_LOCAL_URIS = (
|
||||
[ENV["API_HOST"]] + (ENV["EXTRA_DOMAINS"] || "").split(",")
|
||||
)
|
||||
$API_URL = "//#{Rails.application.routes.default_url_options[:host]}:#{Rails.application.routes.default_url_options[:port]}"
|
||||
ALL_LOCAL_URIS = ([ENV["API_HOST"]] + (ENV["EXTRA_DOMAINS"] || "").split(","))
|
||||
.map { |x| x.present? ? "#{x}:#{ENV["API_PORT"]}" : nil }.compact
|
||||
SecureHeaders::Configuration.default do |config|
|
||||
config.hsts = "max-age=#{1.week.to_i}"
|
||||
config.hsts = "max-age=#{1.week.to_i}"
|
||||
# We need this off in dev mode otherwise email previews won't show up.
|
||||
config.x_frame_options = "DENY" if Rails.env.production?
|
||||
config.x_content_type_options = "nosniff"
|
||||
config.x_xss_protection = "1; mode=block"
|
||||
config.x_download_options = "noopen"
|
||||
config.x_frame_options = "DENY" if Rails.env.production?
|
||||
config.x_content_type_options = "nosniff"
|
||||
config.x_xss_protection = "1; mode=block"
|
||||
config.x_download_options = "noopen"
|
||||
config.x_permitted_cross_domain_policies = "none"
|
||||
config.referrer_policy = %w(
|
||||
config.referrer_policy = %w(
|
||||
origin-when-cross-origin
|
||||
strict-origin-when-cross-origin
|
||||
)
|
||||
|
@ -84,12 +83,12 @@ module FarmBot
|
|||
fonts.googleapis.com
|
||||
fonts.gstatic.com
|
||||
),
|
||||
form_action: %w('self'),
|
||||
frame_src: %w(*), # We need "*" to support webcam users.
|
||||
img_src: %w(* data:), # We need "*" to support webcam users.
|
||||
form_action: %w('self'),
|
||||
frame_src: %w(*), # We need "*" to support webcam users.
|
||||
img_src: %w(* data:), # We need "*" to support webcam users.
|
||||
manifest_src: %w('self'),
|
||||
media_src: %w(),
|
||||
object_src: %w(),
|
||||
media_src: %w(),
|
||||
object_src: %w(),
|
||||
sandbox: %w(
|
||||
allow-scripts
|
||||
allow-forms
|
||||
|
@ -120,7 +119,7 @@ module FarmBot
|
|||
# wouldn't, but I think it's too much
|
||||
# of an inconvenience to block that
|
||||
# feature. Comments welcome -RC.
|
||||
report_uri: %w(/csp_reports)
|
||||
report_uri: %w(/csp_reports),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
class Rack::Attack
|
||||
### Throttle Spammy Clients ###
|
||||
throttle('req/ip', limit: 1000, period: 1.minutes) do |req|
|
||||
req.ip
|
||||
end
|
||||
# RC 17 APR 19
|
||||
# class Rack::Attack
|
||||
# ### Throttle Spammy Clients ###
|
||||
# throttle('req/ip', limit: 1000, period: 1.minutes) do |req|
|
||||
# req.ip
|
||||
# end
|
||||
|
||||
### Stop people from overusing the sync object. ###
|
||||
throttle('sync_req/ip', limit: 5, period: 1.minutes) do |req|
|
||||
req.ip if req.url.include?("/sync")
|
||||
end
|
||||
end
|
||||
# ### Stop people from overusing the sync object. ###
|
||||
# throttle('sync_req/ip', limit: 5, period: 1.minutes) do |req|
|
||||
# req.ip if req.url.include?("/sync")
|
||||
# end
|
||||
# end
|
||||
|
||||
# Always allow requests from localhost
|
||||
# (blacklist & throttles are skipped)
|
||||
Rack::Attack.safelist('allow from localhost') do |req|
|
||||
# Requests are allowed if the return value is truthy
|
||||
'127.0.0.1' == req.ip || '::1' == req.ip
|
||||
end
|
||||
# # Always allow requests from localhost
|
||||
# # (blacklist & throttles are skipped)
|
||||
# Rack::Attack.safelist('allow from localhost') do |req|
|
||||
# # Requests are allowed if the return value is truthy
|
||||
# '127.0.0.1' == req.ip || '::1' == req.ip
|
||||
# end
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class AddDisableEmergencyUnlockConfirmationToWebAppConfig < ActiveRecord::Migration[5.2]
|
||||
safety_assured
|
||||
def change
|
||||
add_column :web_app_configs,
|
||||
:disable_emergency_unlock_confirmation,
|
||||
:boolean,
|
||||
default: false
|
||||
end
|
||||
end
|
|
@ -310,6 +310,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
|
|||
show_historic_points: false,
|
||||
time_format_24_hour: false,
|
||||
show_pins: false,
|
||||
disable_emergency_unlock_confirmation: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
let mockDev = false;
|
||||
jest.mock("../account/dev/dev_support", () => ({
|
||||
DevSettings: { futureFeaturesEnabled: () => mockDev }
|
||||
}));
|
||||
|
||||
jest.mock("react-redux", () => ({ connect: jest.fn() }));
|
||||
|
||||
let mockPath = "";
|
||||
|
@ -16,7 +11,7 @@ import { App, AppProps, mapStateToProps } from "../app";
|
|||
import { mount } from "enzyme";
|
||||
import { bot } from "../__test_support__/fake_state/bot";
|
||||
import {
|
||||
fakeUser, fakeWebAppConfig, fakeEnigma
|
||||
fakeUser, fakeWebAppConfig
|
||||
} from "../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../__test_support__/fake_state";
|
||||
import {
|
||||
|
@ -147,21 +142,4 @@ 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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ describe("fetchLabFeatures", () => {
|
|||
Object.defineProperty(window.location, "reload", { value: jest.fn() });
|
||||
it("basically just initializes stuff", () => {
|
||||
const val = fetchLabFeatures(jest.fn());
|
||||
expect(val.length).toBe(9);
|
||||
expect(val.length).toBe(10);
|
||||
expect(val[0].value).toBeFalsy();
|
||||
const { callback } = val[0];
|
||||
if (callback) {
|
||||
|
|
|
@ -82,7 +82,15 @@ export const fetchLabFeatures =
|
|||
description: t(Content.TIME_FORMAT_24_HOUR),
|
||||
storageKey: BooleanSetting.time_format_24_hour,
|
||||
value: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t("Confirm emergency unlock"),
|
||||
description: t(Content.EMERGENCY_UNLOCK_CONFIRM_CONFIG),
|
||||
confirmationMessage: t(Content.CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE),
|
||||
storageKey: BooleanSetting.disable_emergency_unlock_confirmation,
|
||||
value: false,
|
||||
displayInvert: true,
|
||||
},
|
||||
].map(fetchSettingValue(getConfigValue)));
|
||||
|
||||
/** Always allow toggling from true => false (deactivate).
|
||||
|
|
|
@ -10,12 +10,11 @@ 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, betterCompact } from "./util";
|
||||
import { validBotLocationData, validFwConfig, validFbosConfig } from "./util";
|
||||
import { BooleanSetting } from "./session_keys";
|
||||
import { getPathArray } from "./history";
|
||||
import {
|
||||
|
@ -29,7 +28,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";
|
||||
import { getAlerts } from "./messages/state_to_props";
|
||||
|
||||
/** For the logger module */
|
||||
init();
|
||||
|
@ -56,10 +55,6 @@ export interface AppProps {
|
|||
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,
|
||||
|
@ -80,7 +75,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,
|
||||
alertCount: getAlerts(props.resources.index, props.bot).length,
|
||||
};
|
||||
}
|
||||
/** Time at which the app gives up and asks the user to refresh */
|
||||
|
|
|
@ -422,8 +422,7 @@ export namespace Content {
|
|||
|
||||
// App Settings
|
||||
export const CONFIRM_STEP_DELETION =
|
||||
trim(`Show a confirmation dialog when the sequence delete step
|
||||
icon is pressed.`);
|
||||
trim(`Show a confirmation dialog when deleting a sequence step.`);
|
||||
|
||||
export const HIDE_WEBCAM_WIDGET =
|
||||
trim(`If not using a webcam, use this setting to remove the
|
||||
|
@ -465,6 +464,15 @@ export namespace Content {
|
|||
export const SHOW_PINS =
|
||||
trim(`Show raw pin lists in Read Sensor and Control Peripheral steps.`);
|
||||
|
||||
export const EMERGENCY_UNLOCK_CONFIRM_CONFIG =
|
||||
trim(`Confirm when unlocking FarmBot after an emergency stop.`);
|
||||
|
||||
export const CONFIRM_EMERGENCY_UNLOCK_CONFIRM_DISABLE =
|
||||
trim(`Warning! When disabled, clicking the UNLOCK button will immediately
|
||||
unlock FarmBot instead of confirming that it is safe to do so.
|
||||
As a result, double-clicking the E-STOP button may not stop FarmBot.
|
||||
Are you sure you want to disable this feature?`);
|
||||
|
||||
// Device
|
||||
export const NOT_HTTPS =
|
||||
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);
|
||||
|
|
|
@ -13,7 +13,7 @@ describe("calcMicrostepsPerMm()", () => {
|
|||
});
|
||||
|
||||
it("calculates value with microstepping", () => {
|
||||
expect(calcMicrostepsPerMm(5, 4)).toEqual(20);
|
||||
expect(calcMicrostepsPerMm(5, 4)).toEqual(5);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -30,7 +30,7 @@ describe("calculateAxialLengths()", () => {
|
|||
firmwareSettings.movement_step_per_mm_z = 25;
|
||||
firmwareSettings.movement_microsteps_z = 4;
|
||||
expect(calculateAxialLengths({ firmwareSettings })).toEqual({
|
||||
x: 0, y: 20, z: 1
|
||||
x: 0, y: 20, z: 4
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,8 @@ import { McuParams } from "farmbot";
|
|||
export const calcMicrostepsPerMm = (
|
||||
steps_per_mm: number | undefined,
|
||||
microsteps_per_step: number | undefined) =>
|
||||
(steps_per_mm || 1) * (microsteps_per_step || 1);
|
||||
// The firmware currently interprets steps_per_mm as microsteps_per_mm.
|
||||
(steps_per_mm || 1) * (1 || microsteps_per_step || 1);
|
||||
|
||||
const calcAxisLength = (
|
||||
nr_steps: number | undefined,
|
||||
|
|
|
@ -37,7 +37,10 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
toggle={this.toggle}
|
||||
getValue={this.getValue} />
|
||||
</Popover>
|
||||
<EStopButton bot={this.props.bot} />
|
||||
<EStopButton
|
||||
bot={this.props.bot}
|
||||
forceUnlock={this.getValue(
|
||||
BooleanSetting.disable_emergency_unlock_confirmation)} />
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<MustBeOnline
|
||||
|
|
|
@ -26,9 +26,6 @@
|
|||
0% {
|
||||
transform: translateX(-11rem)
|
||||
}
|
||||
90% {
|
||||
transform: translateX(1rem)
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0)
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@ nav {
|
|||
}
|
||||
div {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,9 +116,9 @@ export function emergencyLock() {
|
|||
.then(commandOK(noun), commandErr(noun));
|
||||
}
|
||||
|
||||
export function emergencyUnlock() {
|
||||
export function emergencyUnlock(force = false) {
|
||||
const noun = "Emergency unlock";
|
||||
if (confirm(t(`Are you sure you want to unlock the device?`))) {
|
||||
if (force || confirm(t(`Are you sure you want to unlock the device?`))) {
|
||||
getDevice()
|
||||
.emergencyUnlock()
|
||||
.then(commandOK(noun), commandErr(noun));
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
const mockDevice = { emergencyUnlock: jest.fn(() => Promise.resolve()) };
|
||||
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { EStopButton } from "../e_stop_btn";
|
||||
|
@ -5,7 +8,7 @@ import { bot } from "../../../__test_support__/fake_state/bot";
|
|||
import { EStopButtonProps } from "../../interfaces";
|
||||
|
||||
describe("<EStopButton />", () => {
|
||||
const fakeProps = (): EStopButtonProps => ({ bot });
|
||||
const fakeProps = (): EStopButtonProps => ({ bot, forceUnlock: false });
|
||||
it("renders", () => {
|
||||
bot.hardware.informational_settings.sync_status = "synced";
|
||||
const wrapper = mount(<EStopButton {...fakeProps()} />);
|
||||
|
@ -13,18 +16,45 @@ describe("<EStopButton />", () => {
|
|||
expect(wrapper.find("button").hasClass("red")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("grayed out", () => {
|
||||
it("is grayed out when offline", () => {
|
||||
bot.hardware.informational_settings.sync_status = undefined;
|
||||
const wrapper = mount(<EStopButton {...fakeProps()} />);
|
||||
expect(wrapper.text()).toEqual("E-STOP");
|
||||
expect(wrapper.find("button").hasClass("pseudo-disabled")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("locked", () => {
|
||||
it("shows locked state", () => {
|
||||
bot.hardware.informational_settings.sync_status = "synced";
|
||||
bot.hardware.informational_settings.locked = true;
|
||||
const wrapper = mount(<EStopButton {...fakeProps()} />);
|
||||
expect(wrapper.text()).toEqual("UNLOCK");
|
||||
expect(wrapper.find("button").hasClass("yellow")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("confirms unlock", () => {
|
||||
bot.hardware.informational_settings.sync_status = "synced";
|
||||
bot.hardware.informational_settings.locked = true;
|
||||
const p = fakeProps();
|
||||
p.forceUnlock = false;
|
||||
window.confirm = jest.fn(() => false);
|
||||
const wrapper = mount(<EStopButton {...p} />);
|
||||
expect(wrapper.text()).toEqual("UNLOCK");
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(window.confirm).toHaveBeenCalledWith(
|
||||
"Are you sure you want to unlock the device?");
|
||||
expect(mockDevice.emergencyUnlock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("doesn't confirm unlock", () => {
|
||||
bot.hardware.informational_settings.sync_status = "synced";
|
||||
bot.hardware.informational_settings.locked = true;
|
||||
const p = fakeProps();
|
||||
p.forceUnlock = true;
|
||||
window.confirm = jest.fn(() => false);
|
||||
const wrapper = mount(<EStopButton {...p} />);
|
||||
expect(wrapper.text()).toEqual("UNLOCK");
|
||||
wrapper.find("button").simulate("click");
|
||||
expect(window.confirm).not.toHaveBeenCalled();
|
||||
expect(mockDevice.emergencyUnlock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,9 @@ export class EStopButton extends React.Component<EStopButtonProps, {}> {
|
|||
render() {
|
||||
const i = this.props.bot.hardware.informational_settings;
|
||||
const isLocked = !!i.locked;
|
||||
const toggleEmergencyLock = isLocked ? emergencyUnlock : emergencyLock;
|
||||
const toggleEmergencyLock = isLocked
|
||||
? () => emergencyUnlock(this.props.forceUnlock)
|
||||
: emergencyLock;
|
||||
const color = isLocked ? "yellow" : "red";
|
||||
const emergencyLockStatusColor = isBotUp(i.sync_status) ? color : GRAY;
|
||||
const emergencyLockStatusText = isLocked ? t("UNLOCK") : "E-STOP";
|
||||
|
|
|
@ -76,6 +76,9 @@ export function EncodersAndEndStops(props: EncodersProps) {
|
|||
x={"encoder_scaling_x"}
|
||||
y={"encoder_scaling_y"}
|
||||
z={"encoder_scaling_z"}
|
||||
xScale={sourceFwConfig("movement_microsteps_x").value}
|
||||
yScale={sourceFwConfig("movement_microsteps_y").value}
|
||||
zScale={sourceFwConfig("movement_microsteps_z").value}
|
||||
intSize={shouldDisplay(Feature.long_scaling_factor) ? "long" : "short"}
|
||||
gray={encodersDisabled}
|
||||
sourceFwConfig={sourceFwConfig}
|
||||
|
|
|
@ -198,6 +198,7 @@ export interface McuInputBoxProps {
|
|||
|
||||
export interface EStopButtonProps {
|
||||
bot: BotState;
|
||||
forceUnlock: boolean;
|
||||
}
|
||||
|
||||
export interface PeripheralsProps {
|
||||
|
|
|
@ -37,7 +37,7 @@ describe("<Tour />", () => {
|
|||
const wrapper = shallow<Tour>(<Tour steps={steps} />);
|
||||
wrapper.instance().callback(fakeCallbackData({ type: "tour:end" }));
|
||||
expect(wrapper.state()).toEqual({ run: false, index: 0 });
|
||||
expect(history.push).toHaveBeenCalledWith("/app/help");
|
||||
expect(history.push).toHaveBeenCalledWith("/app/messages");
|
||||
});
|
||||
|
||||
it("navigates through tour: next", () => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
|
||||
import Joyride, { Step as TourStep, CallBackProps } from "react-joyride";
|
||||
import { Color } from "../ui";
|
||||
import { history } from "../history";
|
||||
|
@ -44,7 +43,7 @@ export class Tour extends React.Component<TourProps, TourState> {
|
|||
}
|
||||
if (type === "tour:end") {
|
||||
this.setState({ run: false });
|
||||
history.push("/app/help");
|
||||
history.push("/app/messages");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ describe("<Alerts />", () => {
|
|||
apiFirmwareValue: undefined,
|
||||
timeSettings: fakeTimeSettings(),
|
||||
dispatch: jest.fn(),
|
||||
findApiAlertById: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders no alerts", () => {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
jest.mock("../../api/crud", () => ({ destroy: jest.fn() }));
|
||||
|
||||
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";
|
||||
import { destroy } from "../../api/crud";
|
||||
|
||||
describe("<AlertCard />", () => {
|
||||
const fakeProps = (): AlertCardProps => ({
|
||||
|
@ -19,8 +22,13 @@ describe("<AlertCard />", () => {
|
|||
});
|
||||
|
||||
it("renders unknown card", () => {
|
||||
const wrapper = mount(<AlertCard {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.alert.id = 1;
|
||||
p.findApiAlertById = () => "uuid";
|
||||
const wrapper = mount(<AlertCard {...p} />);
|
||||
expect(wrapper.text()).toContain("noun: verb (author)");
|
||||
wrapper.find(".fa-times").simulate("click");
|
||||
expect(destroy).toHaveBeenCalledWith("uuid");
|
||||
});
|
||||
|
||||
it("renders firmware card", () => {
|
||||
|
@ -28,6 +36,8 @@ describe("<AlertCard />", () => {
|
|||
p.alert.problem_tag = "farmbot_os.firmware.missing";
|
||||
const wrapper = mount(<AlertCard {...p} />);
|
||||
expect(wrapper.text()).toContain("Firmware missing");
|
||||
wrapper.find(".fa-times").simulate("click");
|
||||
expect(destroy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders seed data card", () => {
|
||||
|
|
|
@ -12,6 +12,7 @@ describe("<Messages />", () => {
|
|||
apiFirmwareValue: undefined,
|
||||
timeSettings: fakeTimeSettings(),
|
||||
dispatch: Function,
|
||||
findApiAlertById: jest.fn(),
|
||||
});
|
||||
|
||||
it("renders page", () => {
|
||||
|
|
|
@ -6,7 +6,9 @@ jest.mock("../../account/dev/dev_support", () => ({
|
|||
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";
|
||||
import {
|
||||
fakeEnigma, fakeFbosConfig
|
||||
} from "../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("handles undefined", () => {
|
||||
|
@ -18,7 +20,9 @@ describe("mapStateToProps()", () => {
|
|||
|
||||
it("doesn't show API alerts", () => {
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([fakeEnigma()]);
|
||||
const enigma = fakeEnigma();
|
||||
enigma.body.problem_tag = "api.seed_data.missing";
|
||||
state.resources = buildResourceIndex([enigma]);
|
||||
mockDev = false;
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.alerts).toEqual([]);
|
||||
|
@ -27,6 +31,7 @@ describe("mapStateToProps()", () => {
|
|||
it("shows API alerts", () => {
|
||||
const state = fakeState();
|
||||
const enigma = fakeEnigma();
|
||||
enigma.body.problem_tag = "api.seed_data.missing";
|
||||
state.resources = buildResourceIndex([enigma]);
|
||||
mockDev = true;
|
||||
const props = mapStateToProps(state);
|
||||
|
@ -42,4 +47,13 @@ describe("mapStateToProps()", () => {
|
|||
const props = mapStateToProps(state);
|
||||
expect(props.apiFirmwareValue).toEqual("arduino");
|
||||
});
|
||||
|
||||
it("finds alert", () => {
|
||||
const state = fakeState();
|
||||
const alert = fakeEnigma();
|
||||
alert.body.id = 1;
|
||||
state.resources = buildResourceIndex([alert]);
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.findApiAlertById(1)).toEqual(alert.uuid);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ export const Alerts = (props: AlertsProps) =>
|
|||
alert={x}
|
||||
dispatch={props.dispatch}
|
||||
apiFirmwareValue={props.apiFirmwareValue}
|
||||
timeSettings={props.timeSettings} />)}
|
||||
timeSettings={props.timeSettings}
|
||||
findApiAlertById={props.findApiAlertById} />)}
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -3,7 +3,8 @@ import { t } from "../i18next_wrapper";
|
|||
import {
|
||||
AlertCardProps, AlertCardTemplateProps, FirmwareMissingProps,
|
||||
SeedDataMissingProps, SeedDataMissingState, TourNotTakenProps,
|
||||
CommonAlertCardProps
|
||||
CommonAlertCardProps,
|
||||
DismissAlertProps
|
||||
} from "./interfaces";
|
||||
import { formatLogTime } from "../logs";
|
||||
import {
|
||||
|
@ -13,20 +14,21 @@ import { DropDownItem, Row, Col, FBSelect, docLink } from "../ui";
|
|||
import { Content } from "../constants";
|
||||
import { TourList } from "../help/tour_list";
|
||||
import { splitProblemTag } from "./alerts";
|
||||
import { destroy } from "../api/crud";
|
||||
|
||||
export const AlertCard = (props: AlertCardProps) => {
|
||||
const { alert, timeSettings } = props;
|
||||
const commonProps = { alert, timeSettings };
|
||||
const { alert, timeSettings, findApiAlertById, dispatch } = props;
|
||||
const commonProps = { alert, timeSettings, findApiAlertById, dispatch };
|
||||
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} />;
|
||||
dispatch={dispatch} />;
|
||||
case "api.tour.not_taken":
|
||||
return <TourNotTaken {...commonProps}
|
||||
dispatch={props.dispatch} />;
|
||||
dispatch={dispatch} />;
|
||||
case "api.user.not_welcomed":
|
||||
return <UserNotWelcomed {...commonProps} />;
|
||||
case "api.documentation.unread":
|
||||
|
@ -35,20 +37,27 @@ export const AlertCard = (props: AlertCardProps) => {
|
|||
return UnknownAlert(commonProps);
|
||||
}
|
||||
};
|
||||
const dismissAlert = (props: DismissAlertProps) => () =>
|
||||
(props.id && props.findApiAlertById && props.dispatch)
|
||||
? props.dispatch(destroy(props.findApiAlertById(props.id)))
|
||||
: () => { };
|
||||
|
||||
const AlertCardTemplate = (props: AlertCardTemplateProps) =>
|
||||
<div className={`problem-alert ${props.className}`}>
|
||||
const AlertCardTemplate = (props: AlertCardTemplateProps) => {
|
||||
const { alert, findApiAlertById, dispatch } = props;
|
||||
return <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" />
|
||||
<p>{formatLogTime(alert.created_at, props.timeSettings)}</p>
|
||||
<i className="fa fa-times"
|
||||
onClick={dismissAlert({ id: alert.id, findApiAlertById, dispatch })} />
|
||||
</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;
|
||||
|
@ -60,7 +69,9 @@ const UnknownAlert = (props: CommonAlertCardProps) => {
|
|||
title={`${t(noun)}: ${t(verb)} (${t(author)})`}
|
||||
message={t("Unknown problem of priority {{priority}} created at {{createdAt}}",
|
||||
{ priority, createdAt })}
|
||||
timeSettings={props.timeSettings} />;
|
||||
timeSettings={props.timeSettings}
|
||||
dispatch={props.dispatch}
|
||||
findApiAlertById={props.findApiAlertById} />;
|
||||
};
|
||||
|
||||
const FirmwareMissing = (props: FirmwareMissingProps) =>
|
||||
|
@ -69,7 +80,9 @@ const FirmwareMissing = (props: FirmwareMissingProps) =>
|
|||
className={"firmware-missing-alert"}
|
||||
title={t("Firmware missing")}
|
||||
message={t("Your device has no firmware installed.")}
|
||||
timeSettings={props.timeSettings}>
|
||||
timeSettings={props.timeSettings}
|
||||
dispatch={props.dispatch}
|
||||
findApiAlertById={props.findApiAlertById}>
|
||||
<FirmwareActions
|
||||
apiFirmwareValue={props.apiFirmwareValue}
|
||||
botOnline={true} />
|
||||
|
@ -92,7 +105,9 @@ class SeedDataMissing
|
|||
className={"seed-data-missing-alert"}
|
||||
title={t("Choose your FarmBot")}
|
||||
message={t(Content.SEED_DATA_SELECTION)}
|
||||
timeSettings={this.props.timeSettings}>
|
||||
timeSettings={this.props.timeSettings}
|
||||
dispatch={this.props.dispatch}
|
||||
findApiAlertById={this.props.findApiAlertById}>
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<label>{t("Choose your FarmBot")}</label>
|
||||
|
@ -115,7 +130,9 @@ const TourNotTaken = (props: TourNotTakenProps) =>
|
|||
className={"tour-not-taken-alert"}
|
||||
title={t("Take a guided tour")}
|
||||
message={t(Content.TAKE_A_TOUR)}
|
||||
timeSettings={props.timeSettings}>
|
||||
timeSettings={props.timeSettings}
|
||||
dispatch={props.dispatch}
|
||||
findApiAlertById={props.findApiAlertById}>
|
||||
<p>{t("Choose a tour to begin")}:</p>
|
||||
<TourList dispatch={props.dispatch} />
|
||||
</AlertCardTemplate>;
|
||||
|
@ -126,7 +143,9 @@ const UserNotWelcomed = (props: CommonAlertCardProps) =>
|
|||
className={"user-not-welcomed-alert"}
|
||||
title={t("Welcome to the FarmBot Web App")}
|
||||
message={t(Content.WELCOME)}
|
||||
timeSettings={props.timeSettings}>
|
||||
timeSettings={props.timeSettings}
|
||||
dispatch={props.dispatch}
|
||||
findApiAlertById={props.findApiAlertById}>
|
||||
<p>
|
||||
{t("You're currently viewing the")} <b>{t("Message Center")}</b>.
|
||||
{t(Content.MESSAGE_CENTER_WELCOME)}
|
||||
|
@ -142,7 +161,9 @@ const DocumentationUnread = (props: CommonAlertCardProps) =>
|
|||
className={"documentation-unread-alert"}
|
||||
title={t("Learn more about the app")}
|
||||
message={t(Content.READ_THE_DOCS)}
|
||||
timeSettings={props.timeSettings}>
|
||||
timeSettings={props.timeSettings}
|
||||
dispatch={props.dispatch}
|
||||
findApiAlertById={props.findApiAlertById}>
|
||||
<p>
|
||||
{t("Head over to")}
|
||||
<a href={docLink()} target="_blank"
|
||||
|
|
|
@ -24,7 +24,8 @@ export class Messages extends React.Component<MessagesProps, {}> {
|
|||
<Alerts alerts={this.props.alerts}
|
||||
dispatch={this.props.dispatch}
|
||||
apiFirmwareValue={this.props.apiFirmwareValue}
|
||||
timeSettings={this.props.timeSettings} />
|
||||
timeSettings={this.props.timeSettings}
|
||||
findApiAlertById={this.props.findApiAlertById} />
|
||||
</Row>
|
||||
<Row>
|
||||
<div className="link-to-logs">
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { FirmwareHardware, Enigma } from "farmbot";
|
||||
import { TimeSettings } from "../interfaces";
|
||||
import { BotState } from "../devices/interfaces";
|
||||
import { UUID } from "../resources/interfaces";
|
||||
|
||||
export interface MessagesProps {
|
||||
alerts: Alert[];
|
||||
apiFirmwareValue: FirmwareHardware | undefined;
|
||||
timeSettings: TimeSettings;
|
||||
dispatch: Function;
|
||||
findApiAlertById(id: number): UUID;
|
||||
}
|
||||
|
||||
export interface AlertsProps {
|
||||
|
@ -14,6 +16,7 @@ export interface AlertsProps {
|
|||
apiFirmwareValue: string | undefined;
|
||||
timeSettings: TimeSettings;
|
||||
dispatch: Function;
|
||||
findApiAlertById(id: number): UUID;
|
||||
}
|
||||
|
||||
export interface ProblemTag {
|
||||
|
@ -36,11 +39,14 @@ export interface AlertCardProps {
|
|||
apiFirmwareValue: string | undefined;
|
||||
timeSettings: TimeSettings;
|
||||
dispatch: Function;
|
||||
findApiAlertById?(id: number): UUID;
|
||||
}
|
||||
|
||||
export interface CommonAlertCardProps {
|
||||
alert: Alert;
|
||||
timeSettings: TimeSettings;
|
||||
findApiAlertById?(id: number): UUID;
|
||||
dispatch?: Function;
|
||||
}
|
||||
|
||||
export interface AlertCardTemplateProps {
|
||||
|
@ -50,6 +56,14 @@ export interface AlertCardTemplateProps {
|
|||
message: string;
|
||||
timeSettings: TimeSettings;
|
||||
children?: React.ReactNode;
|
||||
findApiAlertById?(id: number): UUID;
|
||||
dispatch?: Function;
|
||||
}
|
||||
|
||||
export interface DismissAlertProps {
|
||||
id?: number;
|
||||
findApiAlertById?(id: number): UUID;
|
||||
dispatch?: Function;
|
||||
}
|
||||
|
||||
export interface FirmwareMissingProps extends CommonAlertCardProps {
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { Everything } from "../interfaces";
|
||||
import { MessagesProps } from "./interfaces";
|
||||
import { MessagesProps, Alert } 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 {
|
||||
selectAllEnigmas, maybeGetTimeSettings, findResourceById
|
||||
} from "../resources/selectors";
|
||||
import { isFwHardwareValue } from "../devices/components/fbos_settings/board_type";
|
||||
import { ResourceIndex, UUID } from "../resources/interfaces";
|
||||
import { BotState } from "../devices/interfaces";
|
||||
|
||||
export const mapStateToProps = (props: Everything): MessagesProps => {
|
||||
const { hardware } = props.bot;
|
||||
|
@ -13,15 +17,23 @@ export const mapStateToProps = (props: Everything): MessagesProps => {
|
|||
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 : []);
|
||||
const findApiAlertById = (id: number): UUID =>
|
||||
findResourceById(props.resources.index, "Enigma", id);
|
||||
return {
|
||||
alerts,
|
||||
alerts: getAlerts(props.resources.index, props.bot),
|
||||
apiFirmwareValue: isFwHardwareValue(apiFirmwareValue)
|
||||
? apiFirmwareValue : undefined,
|
||||
timeSettings: maybeGetTimeSettings(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
findApiAlertById,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAlerts =
|
||||
(resourceIndex: ResourceIndex, bot: BotState): Alert[] => {
|
||||
const botAlerts = betterCompact(Object.values(bot.hardware.enigmas || {}));
|
||||
const apiAlerts = selectAllEnigmas(resourceIndex).map(x => x.body)
|
||||
.filter(x => DevSettings.futureFeaturesEnabled() ||
|
||||
x.problem_tag !== "api.seed_data.missing");
|
||||
return botAlerts.concat(apiAlerts);
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import { Connectivity } from "../devices/connectivity/connectivity";
|
|||
import { connectivityData } from "../devices/connectivity/generate_data";
|
||||
import { DiagnosisSaucer } from "../devices/connectivity/diagnosis";
|
||||
import { maybeSetTimezone } from "../devices/timezones/guess_timezone";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
|
||||
export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
||||
|
||||
|
@ -111,7 +112,10 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
{AdditionalMenu({ logout: this.logout, close })}
|
||||
</Popover>
|
||||
</div>
|
||||
<EStopButton bot={this.props.bot} />
|
||||
<EStopButton
|
||||
bot={this.props.bot}
|
||||
forceUnlock={!!this.props.getConfigValue(
|
||||
BooleanSetting.disable_emergency_unlock_confirmation)} />
|
||||
{this.syncButton()}
|
||||
<div className="connection-status-popover">
|
||||
<Popover position={Position.BOTTOM_RIGHT}
|
||||
|
|
|
@ -22,6 +22,7 @@ export const BooleanSetting: Record<BooleanConfigKey, BooleanConfigKey> = {
|
|||
show_historic_points: "show_historic_points",
|
||||
time_format_24_hour: "time_format_24_hour",
|
||||
show_pins: "show_pins",
|
||||
disable_emergency_unlock_confirmation: "disable_emergency_unlock_confirmation",
|
||||
|
||||
/** "Labs" feature names. (App preferences) */
|
||||
stub_config: "stub_config",
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { stopIE } from "../stop_ie";
|
||||
|
||||
describe("stopIE()", () => {
|
||||
it("not IE", () => {
|
||||
expect(stopIE).not.toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { updatePageInfo, attachToRoot } from "../page";
|
||||
import React from "react";
|
||||
|
||||
describe("updatePageInfo()", () => {
|
||||
it("sets page title", () => {
|
||||
updatePageInfo("page name");
|
||||
expect(document.title).toEqual("Page name - FarmBot");
|
||||
});
|
||||
|
||||
it("sets page title: Farm Designer", () => {
|
||||
updatePageInfo("designer");
|
||||
expect(document.title).toEqual("Farm designer - FarmBot");
|
||||
});
|
||||
});
|
||||
|
||||
describe("attachToRoot()", () => {
|
||||
class Foo extends React.Component<{ text: string }> {
|
||||
render() { return <p>{this.props.text}</p>; }
|
||||
}
|
||||
it("attaches page", () => {
|
||||
attachToRoot(Foo, { text: "Bar" });
|
||||
expect(document.body.innerHTML).toEqual(`<div id="root"><p>Bar</p></div>`);
|
||||
expect(document.body.textContent).toEqual("Bar");
|
||||
});
|
||||
});
|
|
@ -4,14 +4,13 @@ import {
|
|||
Attributes
|
||||
} from "react";
|
||||
import { render } from "react-dom";
|
||||
|
||||
import { capitalize } from "lodash";
|
||||
import { t } from "../i18next_wrapper";
|
||||
|
||||
/** Dynamically change the meta title of the page. */
|
||||
export function updatePageInfo(pageName: string) {
|
||||
if (pageName === "designer") { pageName = "Farm Designer"; }
|
||||
document.title = t(capitalize(pageName));
|
||||
document.title = `${t(capitalize(pageName))} - FarmBot`;
|
||||
// Possibly add meta "content" here dynamically as well
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"coveralls": "3.0.3",
|
||||
"enzyme": "3.9.0",
|
||||
"enzyme-adapter-react-16": "1.12.1",
|
||||
"farmbot": "7.0.4-rc3",
|
||||
"farmbot": "7.0.4",
|
||||
"farmbot-toastr": "1.0.3",
|
||||
"i18next": "15.0.9",
|
||||
"jest": "24.7.1",
|
||||
|
|
Loading…
Reference in New Issue