Merge branch 'staging' of https://github.com/DDDIM/Farmbot-Web-App into staging

pull/948/head
DDDIM 2018-08-03 19:24:18 +03:00
commit 483923f50a
65 changed files with 460 additions and 433 deletions

View File

@ -1,4 +1,3 @@
background_jobs: bundle exec rake jobs:work
log_worker: bin/rails r lib/log_service_runner.rb
resource_worker: bin/rails r lib/resource_service_runner.rb
rabbit_workers: bin/rails r lib/rabbit_workers.rb
web: bundle exec passenger start -p $PORT -e $RAILS_ENV --max-pool-size 3

View File

@ -1,5 +1,4 @@
# Run Rails & Webpack concurrently
rails: rails s -e development -p ${API_PORT:-3000} -b 0.0.0.0
log_service: rails r lib/log_service_runner.rb
resource_service: rails r lib/resource_service_runner.rb
worker: rake jobs:work
rails: rails s -e development -p ${API_PORT:-3000} -b 0.0.0.0
rabbit_workers: bin/rails r lib/rabbit_workers.rb
worker: rake jobs:work

View File

@ -1,9 +1,8 @@
# Run Rails & Webpack concurrently
rails: rails s -e development -p ${API_PORT:-3000} -b 0.0.0.0
log_service: rails r lib/log_service_runner.rb
resource_service: rails r lib/resource_service_runner.rb
webpack: ./node_modules/.bin/webpack-dev-server --config config/webpack.config.js
worker: rake jobs:work
rails: rails s -e development -p ${API_PORT:-3000} -b 0.0.0.0
rabbit_workers: bin/rails r lib/rabbit_workers.rb
webpack: ./node_modules/.bin/webpack-dev-server --config config/webpack.config.js
worker: rake jobs:work
# UNCOMMENT THIS LINE IF YOU ARE DOING MOBILE TESTING:
# Get started with `npm install weinre -g`

View File

@ -37,6 +37,8 @@ module Api
end
def destroy
# TODO: We don't need to do batch requests like this any more.
# This should be removed when possible. -RC 1 AUG 2018
ids = params[:id].to_s.split(",").map(&:to_i)
mutate Points::Destroy.run({point_ids: ids}, device_params)
end

View File

@ -1,9 +1,6 @@
module Resources
class Job < Mutations::Command
NOT_FOUND = "Resource not found"
NO_CREATE_YET = "You did not put a numeric `id` in the `body`. " +
"This would be handled as the creation of a new " +
"resource, but we don't support it yet."
required do
duck :body, methods: [:[], :[]=]
duck :resource, duck: [:where, :find_by]
@ -24,6 +21,8 @@ module Resources
when SAVE then do_save
else; never
end
rescue ActiveRecord::RecordNotFound
add_error :not_found, :not_found, NOT_FOUND
end
private
@ -42,7 +41,7 @@ module Resources
# device_params is ALWAYS last because security.
klass::Update.run!(body, model_params, device_params) # Security!
else
add_error :body, :body, NO_CREATE_YET
klass::Create.run!(body, device_params)
end
end

View File

@ -1,10 +0,0 @@
require_relative "../app/lib/service_runner_base.rb"
begin
ServiceRunner.go!(Transport.current.log_channel, LogService)
# :nocov:
rescue
sleep 3
retry
end
# :nocov:

View File

@ -0,0 +1,50 @@
# :nocov:
require "thread"
require "thwait"
require_relative "../app/lib/resources.rb"
require_relative "../app/lib/resources/job.rb"
require_relative "../app/lib/resources/preprocessor.rb"
require_relative "../app/lib/resources/service.rb"
require_relative "../app/lib/service_runner_base.rb"
require_relative "../app/lib/service_runner_base.rb"
class RabbitWorker
WAIT = 3
SERVICES = {
log_channel: LogService,
resource_channel: Resources::Service
}
def run_it!(chan, service)
puts " Attempting to connect #{service} to #{chan}"
ServiceRunner.go!(Transport.current.send(chan), service)
rescue
puts "Connecting to broker in #{WAIT} seconds."
sleep WAIT
retry
end
def thread(channel, service)
Thread.new { run_it!(channel, service) }
end
def threads
@threads ||= SERVICES.map { |(c,s)| thread(c, s) }
end
def self.go!
loop do # TODO: What if only one service
ThreadsWait.all_waits(self.new.threads)
end
end
end
sleep(RabbitWorker::WAIT * 2)
begin
RabbitWorker.go!
rescue
sleep RabbitWorker::WAIT
retry
end

View File

@ -51,7 +51,7 @@
"css-loader": "1.0.0",
"enzyme": "^3.1.0",
"enzyme-adapter-react-16": "^1.1.0",
"farmbot": "6.4.2",
"farmbot": "6.4.3",
"farmbot-toastr": "^1.0.3",
"fastclick": "^1.0.6",
"file-loader": "1.1.11",
@ -87,7 +87,7 @@
"ts-lint": "^4.5.1",
"ts-loader": "4.4.2",
"tslint": "5.11.0",
"typescript": "2.9.2",
"typescript": "3.0.1",
"url-loader": "1.0.1",
"webpack": "4.16.3",
"webpack-uglify-js-plugin": "1.1.9",

View File

@ -0,0 +1,54 @@
=> #<FakeTransport:0x000055affcb84618
@amqp_topic=
#<FakeTransport:0x000055afff2652a0
@calls=
{:create_channel=>[[]],
:topic=>[["amq.topic", {:auto_delete=>true}]],
:publish=>
[["{\"args\":{\"label\":\"4649201b-c882-4cdd-9eef-3bb61f4459b4\"},\"body\":{\"id\":92,\"created_at\":\"2018-08-02T20:56:42.266Z\",\"updated_at\":\"2018-08-02T20:56:42.266Z\",\"name\":\"Elden Goodwin\",\"email\":\"faviola@kozey.co\"}}",
{:routing_key=>"bot.device_142.sync.User.92"}],
["{\"args\":{\"label\":\"4649201b-c882-4cdd-9eef-3bb61f4459b4\"},\"body\":{\"id\":12,\"name\":\"OmastarLapras\",\"color\":null,\"device_id\":143,\"in_use\":false,\"regimen_items\":[]}}",
{:routing_key=>"bot.device_142.sync.Regimen.12"}],
["{\"args\":{\"label\":\"4649201b-c882-4cdd-9eef-3bb61f4459b4\"},\"body\":{\"id\":11,\"start_time\":\"2018-08-03T20:56:42.275Z\",\"end_time\":\"2018-08-05T00:01:00.000Z\",\"repeat\":3,\"time_unit\":\"hourly\",\"executable_id\":12,\"executable_type\":\"Regimen\",\"calendar\":[]}}",
{:routing_key=>"bot.device_142.sync.FarmEvent.11"}],
["{\"args\":{\"label\":\"4559a4a8-1878-49df-98c7-b7ba9daccd00\"},\"body\":{\"id\":144,\"name\":\"Red Leader\",\"timezone\":\"Africa/Tunis\",\"last_saw_api\":null,\"last_saw_mq\":null,\"tz_offset_hrs\":1,\"fbos_version\":null,\"throttled_until\":null,\"throttled_at\":null}}",
{:routing_key=>"bot.device_143.sync.Device.144"}],
["{\"args\":{\"label\":\"4559a4a8-1878-49df-98c7-b7ba9daccd00\"},\"body\":{\"id\":93,\"created_at\":\"2018-08-02T20:56:42.300Z\",\"updated_at\":\"2018-08-02T20:56:42.300Z\",\"name\":\"Perla Schowalter\",\"email\":\"murrayortiz@trantow.com\"}}",
{:routing_key=>"bot.device_143.sync.User.93"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":1,\"url\":\"url1\",\"name\":\"name1\",\"updated_at\":\"2018-08-02T20:56:42.313Z\",\"created_at\":\"2018-08-02T20:56:42.313Z\"}}",
{:routing_key=>"bot.device_144.sync.WebcamFeed.1"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":145,\"name\":\"Gray Leader\",\"timezone\":\"America/Kralendijk\",\"last_saw_api\":null,\"last_saw_mq\":null,\"tz_offset_hrs\":-4,\"fbos_version\":null,\"throttled_until\":null,\"throttled_at\":null}}",
{:routing_key=>"bot.device_144.sync.Device.145"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":94,\"created_at\":\"2018-08-02T20:56:42.335Z\",\"updated_at\":\"2018-08-02T20:56:42.335Z\",\"name\":\"Dr. Suzi Miller\",\"email\":\"kathaleengoldner@gerlachdenesik.biz\"}}",
{:routing_key=>"bot.device_144.sync.User.94"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":2,\"url\":\"Url!\",\"name\":\"Name!\",\"updated_at\":\"2018-08-02T20:56:42.341Z\",\"created_at\":\"2018-08-02T20:56:42.341Z\"}}",
{:routing_key=>"bot.device_144.sync.WebcamFeed.2"}],
["{\"args\":{\"label\":\"6374acef-2b38-45e3-a03d-7ee95b80eca1\"},\"body\":{\"id\":146,\"name\":\"Green 2\",\"timezone\":\"Australia/Broken_Hill\",\"last_saw_api\":null,\"last_saw_mq\":null,\"tz_offset_hrs\":9,\"fbos_version\":null,\"throttled_until\":null,\"throttled_at\":null}}",
{:routing_key=>"bot.device_145.sync.Device.146"}]]}>,
@calls={},
@connection=
#<FakeTransport:0x000055afff2652a0
@calls=
{:create_channel=>[[]],
:topic=>[["amq.topic", {:auto_delete=>true}]],
:publish=>
[["{\"args\":{\"label\":\"4649201b-c882-4cdd-9eef-3bb61f4459b4\"},\"body\":{\"id\":92,\"created_at\":\"2018-08-02T20:56:42.266Z\",\"updated_at\":\"2018-08-02T20:56:42.266Z\",\"name\":\"Elden Goodwin\",\"email\":\"faviola@kozey.co\"}}",
{:routing_key=>"bot.device_142.sync.User.92"}],
["{\"args\":{\"label\":\"4649201b-c882-4cdd-9eef-3bb61f4459b4\"},\"body\":{\"id\":12,\"name\":\"OmastarLapras\",\"color\":null,\"device_id\":143,\"in_use\":false,\"regimen_items\":[]}}",
{:routing_key=>"bot.device_142.sync.Regimen.12"}],
["{\"args\":{\"label\":\"4649201b-c882-4cdd-9eef-3bb61f4459b4\"},\"body\":{\"id\":11,\"start_time\":\"2018-08-03T20:56:42.275Z\",\"end_time\":\"2018-08-05T00:01:00.000Z\",\"repeat\":3,\"time_unit\":\"hourly\",\"executable_id\":12,\"executable_type\":\"Regimen\",\"calendar\":[]}}",
{:routing_key=>"bot.device_142.sync.FarmEvent.11"}],
["{\"args\":{\"label\":\"4559a4a8-1878-49df-98c7-b7ba9daccd00\"},\"body\":{\"id\":144,\"name\":\"Red Leader\",\"timezone\":\"Africa/Tunis\",\"last_saw_api\":null,\"last_saw_mq\":null,\"tz_offset_hrs\":1,\"fbos_version\":null,\"throttled_until\":null,\"throttled_at\":null}}",
{:routing_key=>"bot.device_143.sync.Device.144"}],
["{\"args\":{\"label\":\"4559a4a8-1878-49df-98c7-b7ba9daccd00\"},\"body\":{\"id\":93,\"created_at\":\"2018-08-02T20:56:42.300Z\",\"updated_at\":\"2018-08-02T20:56:42.300Z\",\"name\":\"Perla Schowalter\",\"email\":\"murrayortiz@trantow.com\"}}",
{:routing_key=>"bot.device_143.sync.User.93"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":1,\"url\":\"url1\",\"name\":\"name1\",\"updated_at\":\"2018-08-02T20:56:42.313Z\",\"created_at\":\"2018-08-02T20:56:42.313Z\"}}",
{:routing_key=>"bot.device_144.sync.WebcamFeed.1"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":145,\"name\":\"Gray Leader\",\"timezone\":\"America/Kralendijk\",\"last_saw_api\":null,\"last_saw_mq\":null,\"tz_offset_hrs\":-4,\"fbos_version\":null,\"throttled_until\":null,\"throttled_at\":null}}",
{:routing_key=>"bot.device_144.sync.Device.145"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":94,\"created_at\":\"2018-08-02T20:56:42.335Z\",\"updated_at\":\"2018-08-02T20:56:42.335Z\",\"name\":\"Dr. Suzi Miller\",\"email\":\"kathaleengoldner@gerlachdenesik.biz\"}}",
{:routing_key=>"bot.device_144.sync.User.94"}],
["{\"args\":{\"label\":\"d133ad1f-80d6-4059-9e2b-3f0c366d4454\"},\"body\":{\"id\":2,\"url\":\"Url!\",\"name\":\"Name!\",\"updated_at\":\"2018-08-02T20:56:42.341Z\",\"created_at\":\"2018-08-02T20:56:42.341Z\"}}",
{:routing_key=>"bot.device_144.sync.WebcamFeed.2"}],
["{\"args\":{\"label\":\"6374acef-2b38-45e3-a03d-7ee95b80eca1\"},\"body\":{\"id\":146,\"name\":\"Green 2\",\"timezone\":\"Australia/Broken_Hill\",\"last_saw_api\":null,\"last_saw_mq\":null,\"tz_offset_hrs\":9,\"fbos_version\":null,\"throttled_until\":null,\"throttled_at\":null}}",
{:routing_key=>"bot.device_145.sync.Device.146"}]]}>>

View File

@ -15,34 +15,15 @@ describe LogService do
FakeDeliveryInfo.new("bot.device_#{device_id}.logs")
end
class FakeLogChan
attr_reader :subcribe_calls
def initialize
@subcribe_calls = 0
end
def subscribe(*)
@subcribe_calls += 1
end
it "has a log_channel" do
calls = Transport.current.log_channel.calls[:bind]
expect(calls).to include(["amq.topic", {routing_key: "bot.*.logs"}])
end
it "calls .subscribe() on Transport." do
Transport.current.clear!
load "./lib/log_service_runner.rb"
arg1 = Transport.current.connection.calls[:subscribe].last[0]
routing_key = Transport.current.connection.calls[:bind].last[1][:routing_key]
expect(arg1).to eq({block: true})
expect(routing_key).to eq("bot.*.logs")
end
it "calls .subscribe() on Transport." do
Transport.current.clear!
load "./lib/resource_service_runner.rb"
arg1 = Transport.current.connection.calls[:subscribe].last[0]
routing_key = Transport.current.connection.calls[:bind].last[1][:routing_key]
expect(arg1).to eq({block: true})
expect(routing_key).to eq("bot.*.resources_v0.#")
it "has a resource_channel" do
calls = Transport.current.resource_channel.calls[:bind]
expect(calls)
.to include(["amq.topic", {routing_key: "bot.*.resources_v0.#"}])
end
it "creates new messages in the DB when called" do

View File

@ -67,16 +67,54 @@ describe Resources::Job do
expect(result.name).to eq("Heyo!")
end
it "does not support `create` yet" do
device = FactoryBot.create(:device)
it "updates a tool" do
tool = FactoryBot.create(:tool)
result = Resources::Job.run!(body: {name: "Heyo!"},
resource: Tool,
resource_id: tool.id,
device: tool.device,
action: "save",
uuid: "whatever")
expect(result).to be_kind_of(Tool)
expect(result.name).to eq("Heyo!")
end
it "can't update someone elses tool" do
theirs = FactoryBot.create(:tool)
them = theirs.device
me = FactoryBot.create(:device)
result = Resources::Job.run(body: {name: "Heyo!"},
resource: Point,
resource_id: 0,
device: device,
resource: Tool,
resource_id: theirs.id,
device: me,
action: "save",
uuid: "whatever")
expect(result.errors.fetch("body").message)
.to eq(Resources::Job::NO_CREATE_YET)
expect(result.errors.message_list).to include(Resources::Job::NOT_FOUND)
expect(theirs.reload.name).not_to eq("Heyo!")
end
it "updates a saved_garden" do
saved_garden = FactoryBot.create(:saved_garden)
result = Resources::Job.run!(body: {name: "Heyo!"},
resource: SavedGarden,
resource_id: saved_garden.id,
device: saved_garden.device,
action: "save",
uuid: "whatever")
expect(result).to be_kind_of(SavedGarden)
expect(result.name).to eq("Heyo!")
end
it "updates a plant_template" do
plant_template = FactoryBot.create(:plant_template)
result = Resources::Job.run!(body: {name: "Heyo!"},
resource: PlantTemplate,
resource_id: plant_template.id,
device: plant_template.device,
action: "save",
uuid: "whatever")
expect(result).to be_kind_of(PlantTemplate)
expect(result.name).to eq("Heyo!")
end
it "deals with points" do
@ -96,4 +134,27 @@ describe Resources::Job do
expect(res.where(discarded_at: nil).count).to eq(count - 1)
end
end
it "creates a point" do
device = FactoryBot.create(:device)
body = { name: SecureRandom.uuid,
x: 1,
y: 1,
z: 1,
radius: 1,
meta: {} }
result = Resources::Job.run!(body: body,
resource: Point,
resource_id: 0,
device: device,
action: "save",
uuid: "whatever")
expect(result).to be_kind_of(GenericPointer)
expect(result[:x]).to eq(1)
expect(result[:y]).to eq(1)
expect(result[:z]).to eq(1)
expect(result[:radius]).to eq(1)
expect(result[:meta]).to eq({})
expect(Point.where(name: body[:name]).count).to eq(1)
end
end

View File

@ -22,11 +22,10 @@ import {
TaggedSavedGarden,
TaggedPlantTemplate,
} from "farmbot";
import { ExecutableType } from "../../farm_designer/interfaces";
import { fakeResource } from "../fake_resource";
import { emptyToolSlot } from "../../tools/components/empty_tool_slot";
import { FirmwareConfig } from "../../config_storage/firmware_configs";
import { PinBindingType } from "../../devices/pin_bindings/interfaces";
import { ExecutableType, PinBindingType } from "farmbot/dist/resources/api_resources";
export let resources: Everything["resources"] = buildResourceIndex();
let idCounter = 1;

View File

@ -2,25 +2,25 @@
// /questions/32911630/how-do-i-deal-with-localstorage-in-jest-tests
// https://github.com/facebook/jest/issues/2098
function whatever() {
function Whatever() {
var store = {};
return {
store,
clear() {
store = {};
},
getItem(key) {
return store[key];
},
setItem(key, value) {
store[key] = value.toString();
},
removeItem(key) {
delete store[key];
}
}
store.isFakeStore = true;
store.getItem = (key) => {
return store[key];
};
store.setItem = (key, value) => {
store[key] = value;
};
store.removeItem = (key) => {
store[key] = undefined;
};
return store;
}
global.localStorage = whatever();
global.sessionStorage = whatever();
global.localStorage = Whatever();
global.sessionStorage = Whatever();

View File

@ -13,7 +13,7 @@ describe("fetchStoredToken", () => {
});
it("can fetch token", () => {
localStorage["session"] = JSON.stringify(auth);
localStorage.setItem("session", JSON.stringify(auth));
expect(Session.fetchStoredToken()).toEqual(auth);
});
});

View File

@ -79,7 +79,7 @@ export const fetchLabFeatures = (): LabsFeature[] => ([
description: t(Content.VIRTUAL_TRAIL),
storageKey: BooleanSetting.display_trail,
value: false,
callback: () => sessionStorage.virtualTrailRecords = "[]"
callback: () => sessionStorage.setItem("virtualTrailRecords", "[]")
},
].map(fetchRealValue));

View File

@ -43,7 +43,7 @@ import {
} from "../../connect_device";
import { onLogs } from "../../log_handlers";
import { Actions, Content } from "../../../constants";
import { Log } from "../../../interfaces";
import { Log } from "farmbot/dist/resources/api_resources";
import { ALLOWED_CHANNEL_NAMES, ALLOWED_MESSAGE_TYPES, Farmbot } from "farmbot";
import { success, error, info, warning } from "farmbot-toastr";
import { dispatchNetworkUp, dispatchNetworkDown } from "../../index";

View File

@ -1,6 +1,6 @@
import { fetchNewDevice, getDevice } from "../device";
import { dispatchNetworkUp, dispatchNetworkDown } from "./index";
import { Log } from "../interfaces";
import { Log } from "farmbot/dist/resources/api_resources";
import { Farmbot, BotStateTree, TaggedResource, SpecialStatus } from "farmbot";
import { noop, throttle } from "lodash";
import { success, error, info, warning } from "farmbot-toastr";

View File

@ -7,7 +7,7 @@ import {
} from "./connect_device";
import { GetState } from "../redux/interfaces";
import { dispatchNetworkDown } from ".";
import { Log } from "../interfaces";
import { Log } from "farmbot/dist/resources/api_resources";
import * as _ from "lodash";
import { globalQueue } from "./batch_queue";

View File

@ -5,12 +5,6 @@ export interface PeripheralState {
isEditing: boolean;
}
export interface Peripheral {
id?: number;
pin: number | undefined;
label: string;
}
export interface PeripheralFormProps {
dispatch: Function;
peripherals: TaggedPeripheral[];

View File

@ -3,7 +3,7 @@ import axios from "axios";
import * as _ from "lodash";
import { success, warning, info, error } from "farmbot-toastr";
import { getDevice } from "../device";
import { Log, Everything } from "../interfaces";
import { Everything } from "../interfaces";
import {
GithubRelease, MoveRelProps, MinOsFeatureLookup, SourceFwConfig, Axis
} from "./interfaces";
@ -28,6 +28,7 @@ import { getFbosConfig } from "../resources/selectors_by_kind";
import { FbosConfig } from "../config_storage/fbos_configs";
import { FirmwareConfig } from "../config_storage/firmware_configs";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
import { Log } from "farmbot/dist/resources/api_resources";
const ON = 1, OFF = 0;
export type ConfigKey = keyof McuParams;

View File

@ -33,7 +33,7 @@ export function HomingRow(props: HomingRowProps) {
<LockableButton
disabled={disabled || botDisconnected}
onClick={() => findHome(axis)}>
{t("HOME {{axis}}", { axis })}
{t("FIND HOME {{axis}}", { axis })}
</LockableButton>
</Col>;
})}

View File

@ -20,9 +20,7 @@ import {
fakeSequence
} from "../../../__test_support__/fake_state/resources";
import { initSave } from "../../../api/crud";
import {
PinBindingInputGroupProps, PinBindingType, PinBindingSpecialAction
} from "../interfaces";
import { PinBindingInputGroupProps } from "../interfaces";
import {
PinBindingInputGroup, PinNumberInputGroup, BindingTypeDropDown,
ActionTargetDropDown, SequenceTargetDropDown
@ -31,6 +29,7 @@ import { error, warning } from "farmbot-toastr";
import {
fakeResourceIndex
} from "../../../sequences/step_tiles/tile_move_absolute/test_helpers";
import { PinBindingType, PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
describe("<PinBindingInputGroup/>", () => {
function fakeProps(): PinBindingInputGroupProps {

View File

@ -1,3 +1,4 @@
import { PinBindingType, PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
const mockDevice = {
registerGpio: jest.fn(() => { return Promise.resolve(); }),
unregisterGpio: jest.fn(() => { return Promise.resolve(); }),
@ -10,7 +11,6 @@ jest.mock("../../../api/crud", () => ({
destroy: jest.fn()
}));
import { PinBindingSpecialAction, PinBindingType, } from "../interfaces";
const mockData = [{
pin_number: 1, sequence_id: undefined,
special_action: PinBindingSpecialAction.sync,

View File

@ -8,9 +8,12 @@ import {
import {
fakeSequence, fakePinBinding
} from "../../../__test_support__/fake_state/resources";
import { PinBindingsProps } from "../interfaces";
import {
PinBindingsProps, PinBindingType, PinBindingSpecialAction, SpecialPinBinding
} from "../interfaces";
SpecialPinBinding,
PinBindingType,
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
describe("<PinBindings/>", () => {
function fakeProps(): PinBindingsProps {
@ -62,7 +65,7 @@ describe("<PinBindings/>", () => {
p.shouldDisplay = () => true;
const wrapper = mount(<PinBindings {...p} />);
["pin bindings", "pin number", "none", "bind", "stock bindings"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
["26", "action"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));
const buttons = wrapper.find("button");

View File

@ -1,36 +1,10 @@
import { BotState, ShouldDisplay } from "../interfaces";
import { NetworkState } from "../../connectivity/interfaces";
import { ResourceIndex } from "../../resources/interfaces";
export type PinBinding = StandardPinBinding | SpecialPinBinding;
interface PinBindingBase { id?: number; pin_num: number; }
export enum PinBindingType {
special = "special",
standard = "standard",
}
interface StandardPinBinding extends PinBindingBase {
binding_type: PinBindingType.standard;
sequence_id: number;
}
export interface SpecialPinBinding extends PinBindingBase {
binding_type: PinBindingType.special;
special_action: PinBindingSpecialAction;
}
export enum PinBindingSpecialAction {
emergency_lock = "emergency_lock",
emergency_unlock = "emergency_unlock",
sync = "sync",
reboot = "reboot",
power_off = "power_off",
dump_info = "dump_info",
read_status = "read_status",
take_photo = "take_photo",
}
import {
PinBindingType,
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
export interface PinBindingsProps {
bot: BotState;

View File

@ -1,5 +1,8 @@
import { t } from "i18next";
import { PinBindingType, PinBindingSpecialAction } from "./interfaces";
import {
PinBindingType,
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
import { DropDownItem } from "../../ui";
import { gpio } from "./rpi_gpio_diagram";
import { flattenDeep, isNumber } from "lodash";

View File

@ -5,8 +5,8 @@ import { PinBindingColWidth } from "./pin_bindings";
import { Popover, Position } from "@blueprintjs/core";
import { RpiGpioDiagram } from "./rpi_gpio_diagram";
import {
PinBindingType, PinBindingSpecialAction,
PinBindingInputGroupProps, PinBindingInputGroupState
PinBindingInputGroupProps,
PinBindingInputGroupState
} from "./interfaces";
import { isNumber, includes } from "lodash";
import { Feature, ShouldDisplay } from "../interfaces";
@ -22,6 +22,7 @@ import {
} from "./list_and_label_support";
import { SequenceSelectBox } from "../../sequences/sequence_select_box";
import { ResourceIndex } from "../../resources/interfaces";
import { PinBindingType, PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
export class PinBindingInputGroup
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {

View File

@ -6,7 +6,7 @@ import { Feature, BotState } from "../interfaces";
import { selectAllPinBindings } from "../../resources/selectors";
import { MustBeOnline } from "../must_be_online";
import {
PinBinding, PinBindingSpecialAction, PinBindingType, PinBindingsProps,
PinBindingsProps,
PinBindingListItems
} from "./interfaces";
import { PinBindingsList } from "./pin_bindings_list";
@ -16,6 +16,11 @@ import {
} from "./tagged_pin_binding_init";
import { ResourceIndex } from "../../resources/interfaces";
import { Popover, Position, PopoverInteractionKind } from "@blueprintjs/core";
import {
PinBindingSpecialAction,
PinBindingType,
PinBinding
} from "farmbot/dist/resources/api_resources";
/** Width of UI columns in Pin Bindings widget. */
export enum PinBindingColWidth {

View File

@ -1,7 +1,10 @@
import * as React from "react";
import {
PinBindingType, PinBindingSpecialAction, PinBinding, PinBindingListItems
} from "./interfaces";
PinBindingType,
PinBindingSpecialAction,
PinBinding
} from "farmbot/dist/resources/api_resources";
import { PinBindingListItems } from "./interfaces";
import { TaggedPinBinding, SpecialStatus } from "farmbot";
import { ShouldDisplay, Feature } from "../interfaces";
import { stockPinBindings } from "./list_and_label_support";

View File

@ -63,7 +63,7 @@ describe("<FarmDesigner/>", () => {
}
it("loads default map settings", () => {
localStorage["showPoints"] = "false";
localStorage.setItem("showPoints", "false");
const wrapper = mount(<FarmDesigner {...fakeProps()} />);
const legendProps = wrapper.find("GardenMapLegend").props() as GardenMapLegendProps;
expect(legendProps.legendMenuOpen).toBeFalsy();

View File

@ -10,7 +10,7 @@ import {
} from "../../../__test_support__/resource_index_builder";
import * as moment from "moment";
import { countBy } from "lodash";
import { TimeUnit } from "../../interfaces";
import { TimeUnit } from "farmbot/dist/resources/api_resources";
describe("mapStateToProps()", () => {
function testState() {

View File

@ -10,11 +10,11 @@ import { entries } from "../../resources/util";
import { Link } from "react-router";
import {
AddEditFarmEventProps,
TaggedExecutable,
ExecutableType
TaggedExecutable
} from "../interfaces";
import { BackArrow } from "../../ui/index";
import { SpecialStatus } from "farmbot";
import { ExecutableType } from "farmbot/dist/resources/api_resources";
interface State {
uuid: string;

View File

@ -7,9 +7,9 @@ import {
gracePeriodSeconds
} from "../scheduler";
import * as moment from "moment";
import { TimeUnit } from "../../../interfaces";
import { Moment } from "moment";
import { range, padStart } from "lodash";
import { TimeUnit } from "farmbot/dist/resources/api_resources";
describe("scheduler", () => {
it("runs every 4 hours, starting Tu, until Th w/ origin of Mo", () => {

View File

@ -1,6 +1,6 @@
import { FarmEvent, ExecutableType } from "../../interfaces";
import { Regimen } from "../../../regimens/interfaces";
import { Sequence } from "../../../sequences/interfaces";
import { ExecutableType, FarmEvent } from "farmbot/dist/resources/api_resources";
/** Would it be better to make a fully formed farm event? Join regimen, sequence, etc. */

View File

@ -1,8 +1,8 @@
import * as moment from "moment";
import { Moment, unitOfTime } from "moment";
import { range } from "lodash";
import { TimeUnit } from "../../interfaces";
import { NEVER } from "../edit_fe_form";
import { TimeUnit } from "farmbot/dist/resources/api_resources";
interface SchedulerProps {
startTime: Moment;

View File

@ -5,9 +5,7 @@ import { success, error } from "farmbot-toastr";
import {
TaggedFarmEvent, SpecialStatus, TaggedSequence, TaggedRegimen
} from "farmbot";
import {
TimeUnit, ExecutableQuery, ExecutableType, FarmEvent
} from "../interfaces";
import { ExecutableQuery } from "../interfaces";
import { formatTime, formatDate } from "./map_state_to_props_add_edit";
import {
BackArrow,
@ -31,6 +29,7 @@ import { EventTimePicker } from "./event_time_picker";
import { TzWarning } from "./tz_warning";
import { nextRegItemTimes } from "./map_state_to_props";
import { first } from "lodash";
import { TimeUnit, ExecutableType, FarmEvent } from "farmbot/dist/resources/api_resources";
type FormEvent = React.SyntheticEvent<HTMLInputElement>;
export const NEVER: TimeUnit = "never";

View File

@ -5,9 +5,9 @@ import {
} from "../../ui/index";
import { repeatOptions } from "./map_state_to_props_add_edit";
import { keyBy } from "lodash";
import { TimeUnit } from "../interfaces";
import { FarmEventViewModel } from "./edit_fe_form";
import { EventTimePicker } from "./event_time_picker";
import { TimeUnit } from "farmbot/dist/resources/api_resources";
type Ev = React.SyntheticEvent<HTMLInputElement>;
type Key = keyof FarmEventViewModel;

View File

@ -1,4 +1,4 @@
import { AddEditFarmEventProps, ExecutableType } from "../interfaces";
import { AddEditFarmEventProps } from "../interfaces";
import { Everything } from "../../interfaces";
import * as moment from "moment";
import { t } from "i18next";
@ -29,6 +29,7 @@ import {
import { sourceFbosConfigValue } from "../../devices/components/source_config_value";
import { Feature } from "../../devices/interfaces";
import { hasId } from "../../resources/util";
import { ExecutableType } from "farmbot/dist/resources/api_resources";
export let formatTime = (input: string, timeOffset: number) => {
const iso = new Date(input).toISOString();

View File

@ -9,7 +9,6 @@ import {
TaggedPlantPointer,
TaggedImage,
} from "farmbot";
import { PlantPointer } from "../interfaces";
import { SlotWithTool } from "../resources/interfaces";
import { BotPosition, StepsPerMmXY, BotLocationData } from "../devices/interfaces";
import { isNumber } from "lodash";
@ -18,6 +17,7 @@ import { AxisNumberProperty, BotSize } from "./map/interfaces";
import { SelectionBoxData } from "./map/selection_box";
import { BooleanConfigKey } from "../config_storage/web_app_configs";
import { GetWebAppConfigValue } from "../config_storage/actions";
import { ExecutableType, PlantPointer } from "farmbot/dist/resources/api_resources";
/* BotOriginQuadrant diagram
@ -67,27 +67,6 @@ export interface Props {
getConfigValue: GetWebAppConfigValue;
}
export type TimeUnit =
| "never"
| "minutely"
| "hourly"
| "daily"
| "weekly"
| "monthly"
| "yearly";
export type ExecutableType = "Sequence" | "Regimen";
export interface FarmEvent {
id?: number | undefined;
start_time: string;
end_time?: string | undefined;
repeat?: number | undefined;
time_unit: TimeUnit;
executable_id: number;
executable_type: ExecutableType;
}
export interface MovePlantProps {
deltaX: number;
deltaY: number;
@ -265,17 +244,6 @@ export interface CurrentPointPayl {
color?: string;
}
export interface PlantTemplate {
id?: number;
saved_garden_id: number;
radius: number;
x: number;
y: number;
z: number;
name: string;
openfarm_slug: string;
}
export interface SavedGarden {
id?: number;
name?: string;

View File

@ -8,9 +8,9 @@ import * as React from "react";
import { ToolSlotLayer, ToolSlotLayerProps } from "../tool_slot_layer";
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
import { fakeResource } from "../../../../__test_support__/fake_resource";
import { ToolSlotPointer } from "../../../../interfaces";
import { shallow } from "enzyme";
import { history } from "../../../../history";
import { ToolSlotPointer } from "farmbot/dist/resources/api_resources";
describe("<ToolSlotLayer/>", () => {
function fakeProps(): ToolSlotLayerProps {

View File

@ -1,8 +1,8 @@
import * as React from "react";
import { Color } from "../../ui/index";
import { trim } from "../../util";
import { ToolPulloutDirection } from "../../interfaces";
import { BotOriginQuadrant } from "../interfaces";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
export interface ToolGraphicProps {
x: number;

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { Color } from "../../ui/index";
import { ToolPulloutDirection } from "../../interfaces";
import { BotOriginQuadrant } from "../interfaces";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
enum Anchor {
start = 0,

View File

@ -5,12 +5,12 @@ import { fakeMapTransformProps } from "../../../../__test_support__/map_transfor
describe("<BotTrail/>", () => {
function fakeProps(): BotTrailProps {
sessionStorage[VirtualTrail.records] = JSON.stringify([
sessionStorage.setItem(VirtualTrail.records, JSON.stringify([
{ coord: { x: 0, y: 0 }, water: 0 },
{ coord: { x: 1, y: 1 }, water: 10 },
{ coord: { x: 2, y: 2 }, water: 0 },
{ coord: { x: 3, y: 3 }, water: 0 },
{ coord: { x: 4, y: 4 }, water: 20 }]);
{ coord: { x: 4, y: 4 }, water: 20 }]));
return {
position: { x: 0, y: 0, z: 0 },
mapTransformProps: fakeMapTransformProps(),
@ -19,7 +19,7 @@ describe("<BotTrail/>", () => {
}
it("shows custom length trail", () => {
sessionStorage[VirtualTrail.length] = JSON.stringify(5);
sessionStorage.setItem(VirtualTrail.length, JSON.stringify(5));
const p = fakeProps();
p.mapTransformProps.quadrant = 2;
const wrapper = shallow(<BotTrail {...p} />);
@ -42,14 +42,14 @@ describe("<BotTrail/>", () => {
});
it("shows default length trail", () => {
sessionStorage[VirtualTrail.length] = undefined;
sessionStorage.removeItem(VirtualTrail.length);
const wrapper = shallow(<BotTrail {...fakeProps()} />);
const lines = wrapper.find(".virtual-bot-trail").find("line");
expect(lines.length).toEqual(5);
});
it("doesn't store duplicate last trail point", () => {
sessionStorage[VirtualTrail.length] = undefined;
sessionStorage.removeItem(VirtualTrail.length);
const p = fakeProps();
p.position = { x: 4, y: 4, z: 0 };
const wrapper = shallow(<BotTrail {...p} />);

View File

@ -1,5 +1,5 @@
import { PlantOptions } from "./interfaces";
import { PlantPointer } from "../interfaces";
import { PlantPointer } from "farmbot/dist/resources/api_resources";
export const DEFAULT_PLANT_RADIUS = 25;

View File

@ -36,6 +36,7 @@ interface SelectPlantsState {
stashedUuid: string | undefined;
stashedIcon: string;
}
const YOU_SURE = "Are you sure you want to delete {{length}} plants?";
@connect(mapStateToProps)
export class SelectPlants
@ -66,7 +67,7 @@ export class SelectPlants
destroySelected = (plantUUIDs: string[]) => {
if (plantUUIDs &&
confirm(t("Are you sure you want to delete {{length}} plants?", { length: plantUUIDs.length }))) {
confirm(t(YOU_SURE, { length: plantUUIDs.length }))) {
plantUUIDs.map(uuid => {
this
.props

View File

@ -2,8 +2,8 @@ import axios, { AxiosPromise } from "axios";
import * as _ from "lodash";
import { OpenFarm, CropSearchResult } from "./openfarm";
import { DEFAULT_ICON } from "../open_farm/icons";
import { ExecutableType } from "./interfaces";
import { Actions } from "../constants";
import { ExecutableType } from "farmbot/dist/resources/api_resources";
const url = (q: string) => `${OpenFarm.cropUrl}?include=pictures&filter=${q}`;
const openFarmSearchQuery = (q: string): AxiosPromise<CropSearchResult> =>

View File

@ -1,20 +1,5 @@
import { TaggedImage } from "farmbot";
export interface Image {
id: number;
device_id: number;
attachment_processed_at: string | undefined;
updated_at: string;
created_at: string;
attachment_url: string;
meta: {
x: number | undefined;
y: number | undefined;
z: number | undefined;
name?: string;
};
}
export interface ImageFlipperProps {
onFlip(uuid: string | undefined): void;
images: TaggedImage[];

View File

@ -30,9 +30,4 @@ export interface FarmwareConfigMenuProps {
firstPartyFwsInstalled: boolean;
}
export interface FarmwareInstallation {
id?: number;
url: string;
}
export type Farmwares = Dictionary<FarmwareManifest | undefined>;

View File

@ -5,12 +5,12 @@ import { success, error } from "farmbot-toastr";
import { Thunk } from "../../redux/interfaces";
import { API } from "../../api";
import { Progress, ProgressCallback } from "../../util";
import { GenericPointer } from "../../interfaces";
import { getDevice } from "../../device";
import { WDENVKey } from "./remote_env/interfaces";
import { NumericValues } from "./image_workspace";
import { envSave } from "./remote_env/actions";
import { noop } from "lodash";
import { GenericPointer } from "farmbot/dist/resources/api_resources";
type Key = keyof NumericValues;
type Translation = Record<Key, WDENVKey>;

View File

@ -1,11 +1,11 @@
import { AuthState } from "./auth/interfaces";
import { ConfigState } from "./config/interfaces";
import { BotState } from "./devices/interfaces";
import { Color as FarmBotJsColor, ALLOWED_MESSAGE_TYPES, PlantStage } from "farmbot";
import { Color as FarmBotJsColor } from "farmbot";
import { Point } from "farmbot/dist/resources/api_resources";
import { DraggableState } from "./draggable/interfaces";
import { PeripheralState } from "./controls/peripherals/interfaces";
import { RestResources } from "./resources/interfaces";
import { ChannelName } from "./sequences/interfaces";
/** Regimens and sequences may have a "color" which determines how it looks
in the UI. Only certain colors are valid. */
@ -34,19 +34,6 @@ export interface DeviceConfig {
key: string;
value: string | number | boolean;
}
export interface Log {
id?: number | undefined;
message: string;
type: ALLOWED_MESSAGE_TYPES;
x?: number;
y?: number;
z?: number;
verbosity?: number;
major_version?: number;
minor_version?: number;
channels: ChannelName[];
created_at: number;
}
interface Location {
/** EX: /app/designer */
@ -85,48 +72,4 @@ export interface Everything {
// tslint:disable-next-line:no-any
export type UnsafeError = any;
interface BasePoint {
id?: number | undefined;
dirty?: boolean | undefined;
created_at?: string | undefined;
updated_at?: string | undefined;
radius: number;
x: number;
y: number;
z: number;
pointer_id?: number | undefined;
meta: { [key: string]: (string | undefined) };
name: string;
}
export interface PlantPointer extends BasePoint {
openfarm_slug: string;
pointer_type: "Plant";
planted_at?: string;
plant_stage: PlantStage;
}
export enum ToolPulloutDirection {
NONE = 0,
POSITIVE_X = 1,
NEGATIVE_X = 2,
POSITIVE_Y = 3,
NEGATIVE_Y = 4,
}
export interface ToolSlotPointer extends BasePoint {
tool_id: number | undefined;
pointer_type: "ToolSlot";
pullout_direction: ToolPulloutDirection;
}
export interface GenericPointer extends BasePoint {
pointer_type: "GenericPointer";
}
export type Point =
| GenericPointer
| ToolSlotPointer
| PlantPointer;
export type PointerTypeName = Point["pointer_type"];

View File

@ -32,13 +32,13 @@ type OFIcon = Readonly<OFCropAttrs>;
const STORAGE_KEY = "openfarm_icons_with_spread";
function initLocalStorage() {
localStorage[STORAGE_KEY] = "{}";
localStorage.setItem(STORAGE_KEY, "{}");
return {};
}
function getAllIconsFromCache(): Dictionary<OFIcon | undefined> {
try {
const dictionary = JSON.parse(localStorage[STORAGE_KEY]);
const dictionary = JSON.parse(localStorage.getItem(STORAGE_KEY) || "");
return isObject(dictionary) ? dictionary : initLocalStorage();
} catch (error) {
return initLocalStorage();
@ -53,7 +53,7 @@ function localStorageIconFetch(slug: string): Promise<OFIcon> | undefined {
function localStorageIconSet(icon: OFIcon): void {
const dictionary = getAllIconsFromCache();
dictionary[icon.slug] = icon;
localStorage[STORAGE_KEY] = JSON.stringify(dictionary);
localStorage.setItem(STORAGE_KEY, JSON.stringify(dictionary));
}
/** PROBLEM: HTTP requests get fired too fast. If you have 10 garlic plants,

View File

@ -25,7 +25,7 @@ describe("configureStore", () => {
});
it("does not crash on malformed states", () => {
sessionStorage.lastState = "Not JSON at all.";
sessionStorage.setItem("lastState", "Not JSON at all.");
const result1 = configureStore().getState();
// tslint:disable-next-line:no-null-keyword
expect(result1.auth).toBe(null); // Initialize to default value.

View File

@ -29,13 +29,13 @@ describe("unsavedCheck", () => {
}
it("stops users if they have unsaved work", () => {
localStorage.session = "YES";
localStorage.setItem("session", "YES");
unsavedCheck(setItUp(SpecialStatus.DIRTY, { discard_unsaved: false }));
expect(window.onbeforeunload).toBe(stopThem);
});
it("does nothing when logged out", () => {
localStorage.session = undefined;
localStorage.removeItem("session");
unsavedCheck(setItUp(SpecialStatus.DIRTY, { discard_unsaved: false }));
expect(window.onbeforeunload).toBe(dontStopThem);
});

View File

@ -1,9 +1,9 @@
import { Actions } from "../constants";
import { API } from "../api";
import { Log } from "../interfaces";
import { noop, throttle } from "lodash";
import axios from "axios";
import { ResourceName } from "farmbot";
import { Log } from "farmbot/dist/resources/api_resources";
const name: ResourceName = "Log";
/** re-Downloads all logs from the API and force replaces all entries for logs

View File

@ -31,7 +31,7 @@ export let store = configureStore();
* Returns {} if nothing is found. Used mostly for hot reloading. */
function maybeFetchOldState() {
try {
return JSON.parse(sessionStorage["lastState"] || "{}");
return JSON.parse(sessionStorage.getItem("lastState") || "{}");
} catch (e) {
return {};
}

View File

@ -16,7 +16,7 @@ export function unsavedCheck(state: Everything) {
const total = dirty.length;
const doStop = (total !== 0);
const conf = getWebAppConfig(index);
const loggedOut = !localStorage.session;
const loggedOut = !localStorage.getItem("session");
if ((conf && conf.body.discard_unsaved) || loggedOut) {
window.onbeforeunload = dontStopThem;

View File

@ -19,13 +19,13 @@ export namespace Session {
/** Replace the contents of session storage. */
export function replaceToken(nextState: AuthState) {
localStorage[KEY] = JSON.stringify(nextState);
localStorage.setItem(KEY, JSON.stringify(nextState));
}
/** Fetch the previous session. */
export function fetchStoredToken(): AuthState | undefined {
try {
const v: AuthState = JSON.parse(localStorage[KEY]);
const v: AuthState = JSON.parse(localStorage.getItem(KEY) || "");
if (box(v).kind === "object") {
return v;
} else {

View File

@ -1,12 +1,10 @@
import axios from "axios";
import { Log, Point, SensorReading, Sensor, DeviceConfig } from "../interfaces";
import { SensorReading, Sensor, DeviceConfig } from "../interfaces";
import { API } from "../api";
import { Sequence } from "../sequences/interfaces";
import { Tool } from "../tools/interfaces";
import { Regimen } from "../regimens/interfaces";
import { Peripheral } from "../controls/peripherals/interfaces";
import { FarmEvent, SavedGarden, PlantTemplate } from "../farm_designer/interfaces";
import { Image } from "../farmware/images/interfaces";
import { SavedGarden } from "../farm_designer/interfaces";
import { DeviceAccountSettings } from "../devices/interfaces";
import { ResourceName, DiagnosticDump } from "farmbot";
import { User } from "../auth/interfaces";
@ -14,9 +12,17 @@ import { WebcamFeed } from "../controls/interfaces";
import { WebAppConfig } from "../config_storage/web_app_configs";
import { Session } from "../session";
import { FbosConfig } from "../config_storage/fbos_configs";
import { FarmwareInstallation } from "../farmware/interfaces";
import { FirmwareConfig } from "../config_storage/firmware_configs";
import { PinBinding } from "../devices/pin_bindings/interfaces";
import {
FarmEvent,
Image,
Log,
Point,
Peripheral,
FarmwareInstallation,
PinBinding,
PlantTemplate
} from "farmbot/dist/resources/api_resources";
export interface ResourceReadyPayl {
name: ResourceName;

View File

@ -2,9 +2,9 @@ import * as React from "react";
import { t } from "i18next";
import { FBSelect, DropDownItem } from "../../ui/index";
import { TaggedToolSlotPointer } from "farmbot";
import { ToolPulloutDirection } from "../../interfaces";
import { edit } from "../../api/crud";
import { isNumber } from "lodash";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
const DIRECTION_CHOICES_DDI: { [index: number]: DropDownItem } = {
[ToolPulloutDirection.NONE]:

View File

@ -3,9 +3,9 @@ import { t } from "i18next";
import { isNumber } from "lodash";
import { BotPosition } from "../../devices/interfaces";
import { TaggedToolSlotPointer } from "farmbot";
import { ToolPulloutDirection } from "../../interfaces";
import { edit } from "../../api/crud";
import { SlotDirectionSelect } from "./toolbay_slot_direction_selection";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
const positionIsDefined = (position: BotPosition): boolean => {
return isNumber(position.x) && isNumber(position.y) && isNumber(position.z);

View File

@ -17,7 +17,7 @@ jest.mock("axios", () => {
});
import * as React from "react";
import { TosUpdate } from "../index";
import { TosUpdate } from "../component";
import { shallow, mount } from "enzyme";
import axios from "axios";
import { API } from "../../api/index";

View File

@ -0,0 +1,16 @@
jest.mock("i18next", () => ({ init: jest.fn((_, ok) => ok()) }));
jest.mock("react-dom", () => ({ render: jest.fn() }));
jest.mock("../../i18n",
() => ({ detectLanguage: jest.fn(() => Promise.resolve()) }));
import { detectLanguage } from "../../i18n";
import { render } from "react-dom";
describe("index.ts", () => {
it("attaches the TOS page to the DOM", async () => {
await import("../index");
expect(detectLanguage).toHaveBeenCalled();
expect(document.getElementById("root")).toBeTruthy();
expect(render).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,122 @@
import * as React from "react";
import axios from "axios";
import { t } from "i18next";
import { fun as log, error as logError, init as logInit } from "farmbot-toastr";
import { AuthState } from "../auth/interfaces";
import { Session } from "../session";
import { prettyPrintApiErrors } from "../util";
import { API } from "../api";
import "../css/_index.scss";
import { Row, Col, Widget, WidgetHeader, WidgetBody } from "../ui/index";
interface Props { }
interface State {
email: string;
password: string;
agree_to_terms: boolean;
}
export class TosUpdate extends React.Component<Props, Partial<State>> {
constructor(props: Props) {
super(props);
this.submit = this.submit.bind(this);
this.state = { agree_to_terms: true };
}
set = (name: keyof State) => (event: React.FormEvent<HTMLInputElement>) => {
const state: { [name: string]: State[keyof State] } = {};
state[name] = (event.currentTarget).value;
this.setState(state);
};
submit(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const { email, password, agree_to_terms } = this.state;
const payload = { user: { email, password, agree_to_terms } };
API.setBaseUrl(API.fetchBrowserLocation());
axios
.post<AuthState>(API.current.tokensPath, payload)
.then(resp => {
Session.replaceToken(resp.data);
window.location.href = "/app/controls";
})
.catch(error => {
logError(prettyPrintApiErrors(error));
});
}
get tosLoadOk() { return (globalConfig.TOS_URL && globalConfig.PRIV_URL); }
tosForm() {
if (this.tosLoadOk) {
return <form onSubmit={this.submit}>
<div className="input-group">
<label> {t("Email")} </label>
<input type="email"
onChange={this.set("email").bind(this)}>
</input>
<label>{t("Password")}</label>
<input type="password"
onChange={this.set("password").bind(this)}>
</input>
<ul>
<li>
<a href={globalConfig.TOS_URL}>
{t("Terms of Service")}
</a>
<span className="fa fa-external-link" />
</li>
<li>
<a href={globalConfig.PRIV_URL}>
{t("Privacy Policy")}
</a>
<span className="fa fa-external-link" />
</li>
</ul>
<Row>
<Col xs={12}>
<button className="green fb-button">
{t("I Agree to the Terms of Service")}
</button>
</Col>
</Row>
</div>
</form>;
} else {
return <div>
<p>
{t("Something went wrong while rendering this page.")}
</p>
<p>
{t("Please send us an email at contact@farmbot.io or see the ")}
<a href="http://forum.farmbot.org/">
{t("FarmBot forum.")}
</a>
</p>
</div>;
}
}
componentDidMount() {
logInit();
const body = t("Before logging in, you must agree to our latest Terms" +
" of Service and Privacy Policy");
log(body, "New Terms of Service");
}
render() {
return <div className="static-page">
<div className="all-content-wrapper">
<Widget>
<WidgetHeader title={
this.tosLoadOk
? "Agree to Terms of Service"
: "Problem Loading Terms of Service"} />
<WidgetBody>
{this.tosForm()}
</WidgetBody>
</Widget>
</div>
</div>;
}
}

View File

@ -1,141 +1,16 @@
import * as React from "react";
import { render } from "react-dom";
import axios from "axios";
import { t, init } from "i18next";
import { fun as log, error as logError, init as logInit } from "farmbot-toastr";
import { AuthState } from "../auth/interfaces";
import { Session } from "../session";
import { prettyPrintApiErrors } from "../util";
import { init } from "i18next";
import { detectLanguage } from "../i18n";
import { API } from "../api";
import "../css/_index.scss";
import { Row, Col, Widget, WidgetHeader, WidgetBody } from "../ui/index";
import * as React from "react";
import { TosUpdate } from "./component";
interface Props { }
interface State {
email: string;
password: string;
agree_to_terms: boolean;
}
const node = document.createElement("DIV");
node.id = "root";
document.body.appendChild(node);
const domElem = document.getElementById("root");
const reactElem = React.createElement(TosUpdate, {});
export class TosUpdate extends React.Component<Props, Partial<State>> {
constructor(props: Props) {
super(props);
this.submit = this.submit.bind(this);
this.state = { agree_to_terms: true };
}
set = (name: keyof State) => (event: React.FormEvent<HTMLInputElement>) => {
const state: { [name: string]: State[keyof State] } = {};
state[name] = (event.currentTarget).value;
this.setState(state);
};
const ok = () => domElem && render(reactElem, domElem);
submit(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const { email, password, agree_to_terms } = this.state;
const payload = { user: { email, password, agree_to_terms } };
API.setBaseUrl(API.fetchBrowserLocation());
axios
.post<AuthState>(API.current.tokensPath, payload)
.then(resp => {
Session.replaceToken(resp.data);
window.location.href = "/app/controls";
})
.catch(error => {
logError(prettyPrintApiErrors(error));
});
}
get tosLoadOk() { return (globalConfig.TOS_URL && globalConfig.PRIV_URL); }
tosForm() {
if (this.tosLoadOk) {
return <form onSubmit={this.submit}>
<div className="input-group">
<label> {t("Email")} </label>
<input type="email"
onChange={this.set("email").bind(this)}>
</input>
<label>{t("Password")}</label>
<input type="password"
onChange={this.set("password").bind(this)}>
</input>
<ul>
<li>
<a href={globalConfig.TOS_URL}>
{t("Terms of Service")}
</a>
<span className="fa fa-external-link" />
</li>
<li>
<a href={globalConfig.PRIV_URL}>
{t("Privacy Policy")}
</a>
<span className="fa fa-external-link" />
</li>
</ul>
<Row>
<Col xs={12}>
<button className="green fb-button">
{t("I Agree to the Terms of Service")}
</button>
</Col>
</Row>
</div>
</form>;
} else {
return <div>
<p>
{t("Something went wrong while rendering this page.")}
</p>
<p>
{t("Please send us an email at contact@farmbot.io or see the ")}
<a href="http://forum.farmbot.org/">
{t("FarmBot forum.")}
</a>
</p>
</div>;
}
}
componentDidMount() {
logInit();
const body = t("Before logging in, you must agree to our latest Terms" +
" of Service and Privacy Policy");
log(body, "New Terms of Service");
}
render() {
return <div className="static-page">
<div className="all-content-wrapper">
<Widget>
<WidgetHeader title={
this.tosLoadOk
? "Agree to Terms of Service"
: "Problem Loading Terms of Service"} />
<WidgetBody>
{this.tosForm()}
</WidgetBody>
</Widget>
</div>
</div>;
}
}
detectLanguage().then((config) => {
init(config, (_, t2) => {
const node = document.createElement("DIV");
node.id = "root";
document.body.appendChild(node);
const reactElem = React.createElement(TosUpdate, {});
const domElem = document.getElementById("root");
if (domElem) {
render(reactElem, domElem);
} else {
throw new Error(t2("Add a div with id `root` to the page first."));
}
});
});
detectLanguage().then(conf => init(conf, ok));

View File

@ -2417,9 +2417,9 @@ farmbot-toastr@^1.0.0, farmbot-toastr@^1.0.3:
farmbot-toastr "^1.0.0"
typescript "^2.3.4"
farmbot@6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-6.4.2.tgz#8a3a7727cf9329fb4bc39ad4dbec5d17c8146669"
farmbot@6.4.3:
version "6.4.3"
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-6.4.3.tgz#08f6c361e006410aac87dbba28d3c1a291a458b2"
dependencies:
mqtt "2.15.0"
@ -7164,9 +7164,9 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typescript@2.9.2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
typescript@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb"
typescript@^2.0.9, typescript@^2.3.4:
version "2.8.3"