commit
011e900aee
4
Gemfile
4
Gemfile
|
@ -1,6 +1,7 @@
|
||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
ruby "~> 2.6.5"
|
ruby "~> 2.6.5"
|
||||||
|
|
||||||
|
gem "rails"
|
||||||
gem "active_model_serializers"
|
gem "active_model_serializers"
|
||||||
gem "bunny"
|
gem "bunny"
|
||||||
gem "delayed_job_active_record" # TODO: Get off of SQL backed jobs. Use Redis
|
gem "delayed_job_active_record" # TODO: Get off of SQL backed jobs. Use Redis
|
||||||
|
@ -16,7 +17,6 @@ gem "rabbitmq_http_api_client"
|
||||||
gem "rack-attack"
|
gem "rack-attack"
|
||||||
gem "rack-cors"
|
gem "rack-cors"
|
||||||
gem "rails_12factor"
|
gem "rails_12factor"
|
||||||
gem "rails", "5.2.3" # TODO: Upgrade to Rails 6
|
|
||||||
gem "redis", "~> 4.0"
|
gem "redis", "~> 4.0"
|
||||||
gem "request_store"
|
gem "request_store"
|
||||||
gem "rollbar"
|
gem "rollbar"
|
||||||
|
@ -35,7 +35,7 @@ group :development, :test do
|
||||||
gem "hashdiff"
|
gem "hashdiff"
|
||||||
gem "pry-rails"
|
gem "pry-rails"
|
||||||
gem "pry"
|
gem "pry"
|
||||||
gem "rspec-rails"
|
gem "rspec-rails", "4.0.0.beta3"
|
||||||
gem "rspec"
|
gem "rspec"
|
||||||
gem "simplecov"
|
gem "simplecov"
|
||||||
gem "smarf_doc", git: "https://github.com/RickCarlino/smarf_doc.git"
|
gem "smarf_doc", git: "https://github.com/RickCarlino/smarf_doc.git"
|
||||||
|
|
124
Gemfile.lock
124
Gemfile.lock
|
@ -7,56 +7,69 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (5.2.3)
|
actioncable (6.0.0)
|
||||||
actionpack (= 5.2.3)
|
actionpack (= 6.0.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailer (5.2.3)
|
actionmailbox (6.0.0)
|
||||||
actionpack (= 5.2.3)
|
actionpack (= 6.0.0)
|
||||||
actionview (= 5.2.3)
|
activejob (= 6.0.0)
|
||||||
activejob (= 5.2.3)
|
activerecord (= 6.0.0)
|
||||||
|
activestorage (= 6.0.0)
|
||||||
|
activesupport (= 6.0.0)
|
||||||
|
mail (>= 2.7.1)
|
||||||
|
actionmailer (6.0.0)
|
||||||
|
actionpack (= 6.0.0)
|
||||||
|
actionview (= 6.0.0)
|
||||||
|
activejob (= 6.0.0)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (5.2.3)
|
actionpack (6.0.0)
|
||||||
actionview (= 5.2.3)
|
actionview (= 6.0.0)
|
||||||
activesupport (= 5.2.3)
|
activesupport (= 6.0.0)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actionview (5.2.3)
|
actiontext (6.0.0)
|
||||||
activesupport (= 5.2.3)
|
actionpack (= 6.0.0)
|
||||||
|
activerecord (= 6.0.0)
|
||||||
|
activestorage (= 6.0.0)
|
||||||
|
activesupport (= 6.0.0)
|
||||||
|
nokogiri (>= 1.8.5)
|
||||||
|
actionview (6.0.0)
|
||||||
|
activesupport (= 6.0.0)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_model_serializers (0.10.10)
|
active_model_serializers (0.10.10)
|
||||||
actionpack (>= 4.1, < 6.1)
|
actionpack (>= 4.1, < 6.1)
|
||||||
activemodel (>= 4.1, < 6.1)
|
activemodel (>= 4.1, < 6.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (5.2.3)
|
activejob (6.0.0)
|
||||||
activesupport (= 5.2.3)
|
activesupport (= 6.0.0)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (5.2.3)
|
activemodel (6.0.0)
|
||||||
activesupport (= 5.2.3)
|
activesupport (= 6.0.0)
|
||||||
activerecord (5.2.3)
|
activerecord (6.0.0)
|
||||||
activemodel (= 5.2.3)
|
activemodel (= 6.0.0)
|
||||||
activesupport (= 5.2.3)
|
activesupport (= 6.0.0)
|
||||||
arel (>= 9.0)
|
activestorage (6.0.0)
|
||||||
activestorage (5.2.3)
|
actionpack (= 6.0.0)
|
||||||
actionpack (= 5.2.3)
|
activejob (= 6.0.0)
|
||||||
activerecord (= 5.2.3)
|
activerecord (= 6.0.0)
|
||||||
marcel (~> 0.3.1)
|
marcel (~> 0.3.1)
|
||||||
activesupport (5.2.3)
|
activesupport (6.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
|
zeitwerk (~> 2.1, >= 2.1.8)
|
||||||
addressable (2.7.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
amq-protocol (2.3.0)
|
amq-protocol (2.3.0)
|
||||||
arel (9.0.0)
|
|
||||||
bcrypt (3.1.13)
|
bcrypt (3.1.13)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bunny (2.14.3)
|
bunny (2.14.3)
|
||||||
|
@ -106,7 +119,7 @@ GEM
|
||||||
railties (>= 3.2, < 6.1)
|
railties (>= 3.2, < 6.1)
|
||||||
globalid (0.4.2)
|
globalid (0.4.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
google-api-client (0.33.1)
|
google-api-client (0.33.2)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (~> 0.9)
|
googleauth (~> 0.9)
|
||||||
httpclient (>= 2.8.1, < 3.0)
|
httpclient (>= 2.8.1, < 3.0)
|
||||||
|
@ -114,9 +127,9 @@ GEM
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.0)
|
retriable (>= 2.0, < 4.0)
|
||||||
signet (~> 0.12)
|
signet (~> 0.12)
|
||||||
google-cloud-core (1.3.2)
|
google-cloud-core (1.4.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-env (1.2.1)
|
google-cloud-env (1.3.0)
|
||||||
faraday (~> 0.11)
|
faraday (~> 0.11)
|
||||||
google-cloud-storage (1.21.1)
|
google-cloud-storage (1.21.1)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
|
@ -183,18 +196,20 @@ GEM
|
||||||
rack-cors (1.0.3)
|
rack-cors (1.0.3)
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (5.2.3)
|
rails (6.0.0)
|
||||||
actioncable (= 5.2.3)
|
actioncable (= 6.0.0)
|
||||||
actionmailer (= 5.2.3)
|
actionmailbox (= 6.0.0)
|
||||||
actionpack (= 5.2.3)
|
actionmailer (= 6.0.0)
|
||||||
actionview (= 5.2.3)
|
actionpack (= 6.0.0)
|
||||||
activejob (= 5.2.3)
|
actiontext (= 6.0.0)
|
||||||
activemodel (= 5.2.3)
|
actionview (= 6.0.0)
|
||||||
activerecord (= 5.2.3)
|
activejob (= 6.0.0)
|
||||||
activestorage (= 5.2.3)
|
activemodel (= 6.0.0)
|
||||||
activesupport (= 5.2.3)
|
activerecord (= 6.0.0)
|
||||||
|
activestorage (= 6.0.0)
|
||||||
|
activesupport (= 6.0.0)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 5.2.3)
|
railties (= 6.0.0)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
@ -206,12 +221,12 @@ GEM
|
||||||
rails_stdout_logging
|
rails_stdout_logging
|
||||||
rails_serve_static_assets (0.0.5)
|
rails_serve_static_assets (0.0.5)
|
||||||
rails_stdout_logging (0.0.5)
|
rails_stdout_logging (0.0.5)
|
||||||
railties (5.2.3)
|
railties (6.0.0)
|
||||||
actionpack (= 5.2.3)
|
actionpack (= 6.0.0)
|
||||||
activesupport (= 5.2.3)
|
activesupport (= 6.0.0)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.20.3, < 2.0)
|
||||||
rake (13.0.0)
|
rake (13.0.0)
|
||||||
redis (4.1.3)
|
redis (4.1.3)
|
||||||
representable (3.0.4)
|
representable (3.0.4)
|
||||||
|
@ -237,14 +252,14 @@ GEM
|
||||||
rspec-mocks (3.9.0)
|
rspec-mocks (3.9.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.9.0)
|
rspec-support (~> 3.9.0)
|
||||||
rspec-rails (3.9.0)
|
rspec-rails (4.0.0.beta3)
|
||||||
actionpack (>= 3.0)
|
actionpack (>= 4.2)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 4.2)
|
||||||
railties (>= 3.0)
|
railties (>= 4.2)
|
||||||
rspec-core (~> 3.9.0)
|
rspec-core (~> 3.8)
|
||||||
rspec-expectations (~> 3.9.0)
|
rspec-expectations (~> 3.8)
|
||||||
rspec-mocks (~> 3.9.0)
|
rspec-mocks (~> 3.8)
|
||||||
rspec-support (~> 3.9.0)
|
rspec-support (~> 3.8)
|
||||||
rspec-support (3.9.0)
|
rspec-support (3.9.0)
|
||||||
scenic (1.5.1)
|
scenic (1.5.1)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
|
@ -281,6 +296,7 @@ GEM
|
||||||
websocket-driver (0.7.1)
|
websocket-driver (0.7.1)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.4)
|
websocket-extensions (0.1.4)
|
||||||
|
zeitwerk (2.2.0)
|
||||||
zero_downtime_migrations (0.0.7)
|
zero_downtime_migrations (0.0.7)
|
||||||
activerecord
|
activerecord
|
||||||
|
|
||||||
|
@ -311,13 +327,13 @@ DEPENDENCIES
|
||||||
rabbitmq_http_api_client
|
rabbitmq_http_api_client
|
||||||
rack-attack
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rails (= 5.2.3)
|
rails
|
||||||
rails_12factor
|
rails_12factor
|
||||||
redis (~> 4.0)
|
redis (~> 4.0)
|
||||||
request_store
|
request_store
|
||||||
rollbar
|
rollbar
|
||||||
rspec
|
rspec
|
||||||
rspec-rails
|
rspec-rails (= 4.0.0.beta3)
|
||||||
scenic
|
scenic
|
||||||
secure_headers
|
secure_headers
|
||||||
simplecov
|
simplecov
|
||||||
|
|
|
@ -136,7 +136,7 @@ module Api
|
||||||
strategy = Auth::DetermineAuthStrategy.run!(context)
|
strategy = Auth::DetermineAuthStrategy.run!(context)
|
||||||
case strategy
|
case strategy
|
||||||
when :jwt
|
when :jwt
|
||||||
sign_in(Auth::FromJWT.run!(context).require_consent!)
|
sign_in(Auth::FromJwt.run!(context).require_consent!)
|
||||||
when :already_connected
|
when :already_connected
|
||||||
# Probably provided a cookie.
|
# Probably provided a cookie.
|
||||||
# 9 times out of 10, it's a unit test.
|
# 9 times out of 10, it's a unit test.
|
||||||
|
|
|
@ -44,7 +44,7 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_fields
|
def update_fields
|
||||||
user.update_attributes!(confirmed_at: Time.now)
|
user.update!(confirmed_at: Time.now)
|
||||||
end
|
end
|
||||||
|
|
||||||
def seed_user
|
def seed_user
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
require_relative "../../lib/hstore_filter"
|
# require_relative "../../lib/mutations/hstore_filter"
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
class PointsController < Api::AbstractController
|
class PointsController < Api::AbstractController
|
||||||
|
|
|
@ -273,7 +273,7 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_device
|
def current_device
|
||||||
@current_device ||= Auth::FromJWT.run!(jwt: password_param).device
|
@current_device ||= Auth::FromJwt.run!(jwt: password_param).device
|
||||||
rescue Mutations::ValidationException => e
|
rescue Mutations::ValidationException => e
|
||||||
raise JWT::VerificationError, "RMQ Provided bad token"
|
raise JWT::VerificationError, "RMQ Provided bad token"
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,16 +11,18 @@ class AmqpLogParser < Mutations::Command
|
||||||
# I keep a Ruby copy of the JSON here for reference.
|
# I keep a Ruby copy of the JSON here for reference.
|
||||||
# This is what a log will look like after JSON.parse()
|
# This is what a log will look like after JSON.parse()
|
||||||
EXAMPLE_JSON = {
|
EXAMPLE_JSON = {
|
||||||
"meta" => {
|
|
||||||
"x" => 0,
|
|
||||||
"y" => 0,
|
|
||||||
"z" => 0,
|
|
||||||
"type" => "info",
|
|
||||||
},
|
|
||||||
"major_version" => 6, # <= up-to-date bots do this
|
|
||||||
"message" => "HQ FarmBot TEST 123 Pin 13 is 0",
|
|
||||||
"created_at" => 1512585641,
|
|
||||||
"channels" => [],
|
"channels" => [],
|
||||||
|
"created_at" => 1572015955,
|
||||||
|
"major_version" => 8,
|
||||||
|
"message" => "Syncing",
|
||||||
|
"meta" => {},
|
||||||
|
"minor_version" => 1,
|
||||||
|
"patch_version" => 1,
|
||||||
|
"type" => "info",
|
||||||
|
"verbosity" => 3,
|
||||||
|
"x" => 0.0,
|
||||||
|
"y" => 0.0,
|
||||||
|
"z" => 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
required do
|
required do
|
||||||
|
@ -102,8 +104,19 @@ class AmqpLogParser < Mutations::Command
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_problems!
|
def find_problems!
|
||||||
@output.problems.push(NOT_HASH) and return if not_hash?
|
if not_hash?
|
||||||
@output.problems.push(TOO_OLD) and return if major_version < 6
|
@output.problems.push(NOT_HASH)
|
||||||
@output.problems.push(DISCARD) and return if discard?
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if (major_version || 0) < 7
|
||||||
|
@output.problems.push(TOO_OLD)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if discard?
|
||||||
|
@output.problems.push(DISCARD)
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
# * You need to create "traces" of where you are in a sequence (using numbers)
|
# * You need to create "traces" of where you are in a sequence (using numbers)
|
||||||
# MORE INFO: https://github.com/FarmBot-Labs/Celery-Slicer
|
# MORE INFO: https://github.com/FarmBot-Labs/Celery-Slicer
|
||||||
module CeleryScript
|
module CeleryScript
|
||||||
# Supporting class for CSHeap (below this class)
|
# Supporting class for CsHeap (below this class)
|
||||||
# PROBLEM: CSHeap uses numbers to address sibling/parent nodes.
|
# PROBLEM: CsHeap uses numbers to address sibling/parent nodes.
|
||||||
# PROBLEM: Numbers are very easy to mix up. Is it an array index? A SQL
|
# PROBLEM: Numbers are very easy to mix up. Is it an array index? A SQL
|
||||||
# primary key? A primitive value? It's not always easy to say.
|
# primary key? A primitive value? It's not always easy to say.
|
||||||
# SOLUTION: Create a `HeapAddress` value type to remove ambiguity.
|
# SOLUTION: Create a `HeapAddress` value type to remove ambiguity.
|
||||||
|
@ -60,21 +60,22 @@ module CeleryScript
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class CSHeap
|
class CsHeap
|
||||||
class BadAddress < Exception; end;
|
class BadAddress < Exception; end
|
||||||
|
|
||||||
BAD_ADDR = "Bad node address: "
|
BAD_ADDR = "Bad node address: "
|
||||||
# Nodes that point to other nodes rather than primitive data types (eg:
|
# Nodes that point to other nodes rather than primitive data types (eg:
|
||||||
# `locals` and friends) will be prepended with a LINK.
|
# `locals` and friends) will be prepended with a LINK.
|
||||||
LINK = "__"
|
LINK = "__"
|
||||||
# Points to the originator (parent) of an `arg` or `body` node.
|
# Points to the originator (parent) of an `arg` or `body` node.
|
||||||
PARENT = (LINK + "parent").to_sym
|
PARENT = (LINK + "parent").to_sym
|
||||||
# Points to the first element in the `body``
|
# Points to the first element in the `body``
|
||||||
BODY = (LINK + "body").to_sym
|
BODY = (LINK + "body").to_sym
|
||||||
# Points to the next node in the body chain. Pointing to NOTHING indicates
|
# Points to the next node in the body chain. Pointing to NOTHING indicates
|
||||||
# the end of the body linked list.
|
# the end of the body linked list.
|
||||||
NEXT = (LINK + "next").to_sym
|
NEXT = (LINK + "next").to_sym
|
||||||
# Unique key name. See `celery_script_settings_bag.rb`
|
# Unique key name. See `celery_script_settings_bag.rb`
|
||||||
KIND = :__KIND__
|
KIND = :__KIND__
|
||||||
COMMENT = :__COMMENT__
|
COMMENT = :__COMMENT__
|
||||||
|
|
||||||
# Keys that primary nodes must have
|
# Keys that primary nodes must have
|
||||||
|
@ -82,17 +83,16 @@ module CeleryScript
|
||||||
|
|
||||||
# Index 0 of the heap represents a null pointer of sorts.
|
# Index 0 of the heap represents a null pointer of sorts.
|
||||||
# If a field points to this address, it is considered empty.
|
# If a field points to this address, it is considered empty.
|
||||||
NULL = HeapAddress[0]
|
NULL = HeapAddress[0]
|
||||||
|
|
||||||
# What you will find at index 0 of the heap:
|
# What you will find at index 0 of the heap:
|
||||||
NOTHING = {
|
NOTHING = {
|
||||||
KIND => "nothing",
|
KIND => "nothing",
|
||||||
PARENT => NULL,
|
PARENT => NULL,
|
||||||
BODY => NULL,
|
BODY => NULL,
|
||||||
NEXT => NULL
|
NEXT => NULL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# A dictionary of nodes in the CeleryScript tree, as stored in the heap.
|
# A dictionary of nodes in the CeleryScript tree, as stored in the heap.
|
||||||
# Nodes will have:
|
# Nodes will have:
|
||||||
# * A `KIND` field - What kind of node is it?
|
# * A `KIND` field - What kind of node is it?
|
||||||
|
@ -113,7 +113,7 @@ module CeleryScript
|
||||||
|
|
||||||
# Set "here" to "null". Prepopulates "here" with an empty entry.
|
# Set "here" to "null". Prepopulates "here" with an empty entry.
|
||||||
def initialize
|
def initialize
|
||||||
@here = NULL
|
@here = NULL
|
||||||
@entries = { @here => NOTHING }
|
@entries = { @here => NOTHING }
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
require_relative "./csheap"
|
require_relative "./cs_heap"
|
||||||
|
|
||||||
# Service object that:
|
# Service object that:
|
||||||
# 1. Pulls out all PrimaryNodes and EdgeNodes for a sequence node (AST Flat IR form)
|
# 1. Pulls out all PrimaryNodes and EdgeNodes for a sequence node (AST Flat IR form)
|
||||||
|
@ -8,7 +8,7 @@ require_relative "./csheap"
|
||||||
# DEFAULT.
|
# DEFAULT.
|
||||||
module CeleryScript
|
module CeleryScript
|
||||||
class FetchCelery < Mutations::Command
|
class FetchCelery < Mutations::Command
|
||||||
private # = = = = = = =
|
private # = = = = = = =
|
||||||
# This class is too CPU intensive to make multiple SQL requests.
|
# This class is too CPU intensive to make multiple SQL requests.
|
||||||
# To speed up querying, we create an in-memory index for frequently
|
# To speed up querying, we create an in-memory index for frequently
|
||||||
# looked up attributes such as :id, :kind, :parent_id, :primary_node_id
|
# looked up attributes such as :id, :kind, :parent_id, :primary_node_id
|
||||||
|
@ -60,7 +60,7 @@ module CeleryScript
|
||||||
# that node's children (or an empty array, since body is always optional).
|
# that node's children (or an empty array, since body is always optional).
|
||||||
def get_body_elements(origin)
|
def get_body_elements(origin)
|
||||||
next_node = find_by_id_in_memory(origin.body_id)
|
next_node = find_by_id_in_memory(origin.body_id)
|
||||||
results = []
|
results = []
|
||||||
until next_node.kind == "nothing"
|
until next_node.kind == "nothing"
|
||||||
results.push(next_node)
|
results.push(next_node)
|
||||||
next_node = find_by_id_in_memory(next_node[:next_id])
|
next_node = find_by_id_in_memory(next_node[:next_id])
|
||||||
|
@ -71,7 +71,7 @@ module CeleryScript
|
||||||
# Top level function call for converting a single EdgeNode into a JSON
|
# Top level function call for converting a single EdgeNode into a JSON
|
||||||
# document. Returns Ruby hash that conforms to CeleryScript semantics.
|
# document. Returns Ruby hash that conforms to CeleryScript semantics.
|
||||||
def recurse_into_node(node)
|
def recurse_into_node(node)
|
||||||
out = { kind: node.kind, args: recurse_into_args(node) }
|
out = { kind: node.kind, args: recurse_into_args(node) }
|
||||||
body = get_body_elements(node)
|
body = get_body_elements(node)
|
||||||
if body.empty?
|
if body.empty?
|
||||||
# Legacy sequences *must* have body on sequence. Others are fine.
|
# Legacy sequences *must* have body on sequence. Others are fine.
|
||||||
|
@ -87,16 +87,17 @@ module CeleryScript
|
||||||
# Eg: color, id, etc.
|
# Eg: color, id, etc.
|
||||||
def misc_fields
|
def misc_fields
|
||||||
return {
|
return {
|
||||||
id: sequence.id,
|
id: sequence.id,
|
||||||
created_at: sequence.created_at,
|
created_at: sequence.created_at,
|
||||||
updated_at: sequence.updated_at,
|
updated_at: sequence.updated_at,
|
||||||
args: Sequence::DEFAULT_ARGS,
|
args: Sequence::DEFAULT_ARGS,
|
||||||
color: sequence.color,
|
color: sequence.color,
|
||||||
name: sequence.name
|
name: sequence.name,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
public # = = = = = = =
|
public # = = = = = = =
|
||||||
|
|
||||||
NO_SEQUENCE = "You must have a root node `sequence` at a minimum."
|
NO_SEQUENCE = "You must have a root node `sequence` at a minimum."
|
||||||
|
|
||||||
required do
|
required do
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require_relative "./csheap"
|
require_relative "./cs_heap"
|
||||||
|
|
||||||
# ABOUT THIS CLASS:
|
# ABOUT THIS CLASS:
|
||||||
# CSHeap creates an in memory representation of a Flat IR tree using array
|
# CsHeap creates an in memory representation of a Flat IR tree using array
|
||||||
# indexes (HeapAddress instances, really). This class takes a flat IR tree
|
# indexes (HeapAddress instances, really). This class takes a flat IR tree
|
||||||
# from memory and converts `HeapAddress`es to SQL primary/foreign keys.
|
# from memory and converts `HeapAddress`es to SQL primary/foreign keys.
|
||||||
module CeleryScript
|
module CeleryScript
|
||||||
|
@ -10,14 +10,14 @@ module CeleryScript
|
||||||
# The following constants are abbreviations of the full name, since the
|
# The following constants are abbreviations of the full name, since the
|
||||||
# full name is quite long and they are referenced frequently in the code.
|
# full name is quite long and they are referenced frequently in the code.
|
||||||
# Just remember that "B" is "BODY", "K" is "KIND", etc...
|
# Just remember that "B" is "BODY", "K" is "KIND", etc...
|
||||||
B = CeleryScript::CSHeap::BODY
|
B = CeleryScript::CsHeap::BODY
|
||||||
C = CeleryScript::CSHeap::COMMENT
|
C = CeleryScript::CsHeap::COMMENT
|
||||||
K = CeleryScript::CSHeap::KIND
|
K = CeleryScript::CsHeap::KIND
|
||||||
L = CeleryScript::CSHeap::LINK
|
L = CeleryScript::CsHeap::LINK
|
||||||
N = CeleryScript::CSHeap::NEXT
|
N = CeleryScript::CsHeap::NEXT
|
||||||
P = CeleryScript::CSHeap::PARENT
|
P = CeleryScript::CsHeap::PARENT
|
||||||
NULL = CeleryScript::CSHeap::NULL
|
NULL = CeleryScript::CsHeap::NULL
|
||||||
I = :instance
|
I = :instance
|
||||||
|
|
||||||
required do
|
required do
|
||||||
model :sequence, class: Sequence
|
model :sequence, class: Sequence
|
||||||
|
@ -28,7 +28,7 @@ module CeleryScript
|
||||||
def validate
|
def validate
|
||||||
# IF YOU REMOVE THIS BAD STUFF WILL HAPPEN:
|
# IF YOU REMOVE THIS BAD STUFF WILL HAPPEN:
|
||||||
# version is never user definable!
|
# version is never user definable!
|
||||||
sequence_hash[:args] = \
|
sequence_hash[:args] =
|
||||||
Sequence::DEFAULT_ARGS.merge(sequence_hash[:args] || {})
|
Sequence::DEFAULT_ARGS.merge(sequence_hash[:args] || {})
|
||||||
# See comment above ^ TODO: Investigate removal now that EdgeNodes exist.
|
# See comment above ^ TODO: Investigate removal now that EdgeNodes exist.
|
||||||
end
|
end
|
||||||
|
@ -37,67 +37,67 @@ module CeleryScript
|
||||||
Sequence.transaction do
|
Sequence.transaction do
|
||||||
flat_ir
|
flat_ir
|
||||||
.each do |node|
|
.each do |node|
|
||||||
# Step 1- instantiate records.
|
# Step 1- instantiate records.
|
||||||
node[I] = PrimaryNode.create!(kind: node[K],
|
node[I] = PrimaryNode.create!(kind: node[K],
|
||||||
sequence: sequence,
|
sequence: sequence,
|
||||||
comment: node[C] || nil)
|
comment: node[C] || nil)
|
||||||
end
|
end
|
||||||
.each_with_index do |node, index|
|
.each_with_index do |node, index|
|
||||||
# Step 2- Assign SQL ids (not to be confused with array index IDs or
|
# Step 2- Assign SQL ids (not to be confused with array index IDs or
|
||||||
# instances of HeapAddress), also sets parent_arg_name
|
# instances of HeapAddress), also sets parent_arg_name
|
||||||
model = node[I]
|
model = node[I]
|
||||||
model.parent_arg_name = parent_arg_name_for(node, index)
|
model.parent_arg_name = parent_arg_name_for(node, index)
|
||||||
model.body_id = fetch_sql_id_for(B, node)
|
model.body_id = fetch_sql_id_for(B, node)
|
||||||
model.parent_id = fetch_sql_id_for(P, node)
|
model.parent_id = fetch_sql_id_for(P, node)
|
||||||
model.next_id = fetch_sql_id_for(N, node)
|
model.next_id = fetch_sql_id_for(N, node)
|
||||||
node
|
node
|
||||||
end
|
end
|
||||||
.map do |node|
|
.map do |node|
|
||||||
# Step 3- Set edge nodes
|
# Step 3- Set edge nodes
|
||||||
pairs = node
|
pairs = node
|
||||||
.to_a
|
.to_a
|
||||||
.select do |x|
|
.select do |x|
|
||||||
key = x.first.to_s
|
key = x.first.to_s
|
||||||
(x.first != I) && !key.starts_with?(L)
|
(x.first != I) && !key.starts_with?(L)
|
||||||
end
|
|
||||||
.map do |(key, value)|
|
|
||||||
EdgeNode.create!(kind: key,
|
|
||||||
value: value,
|
|
||||||
sequence_id: sequence.id,
|
|
||||||
primary_node_id: node[:instance].id)
|
|
||||||
end
|
|
||||||
node[:instance]
|
|
||||||
end
|
end
|
||||||
.tap { |x| sequence.update_attributes(migrated_nodes: true) unless sequence.migrated_nodes }
|
.map do |(key, value)|
|
||||||
|
EdgeNode.create!(kind: key,
|
||||||
|
value: value,
|
||||||
|
sequence_id: sequence.id,
|
||||||
|
primary_node_id: node[:instance].id)
|
||||||
|
end
|
||||||
|
node[:instance]
|
||||||
|
end
|
||||||
|
.tap { |x| sequence.update(migrated_nodes: true) unless sequence.migrated_nodes }
|
||||||
.map { |x|
|
.map { |x|
|
||||||
x.save! if x.changed?
|
x.save! if x.changed?
|
||||||
x
|
x
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Index every primary node in memory by its `HeapAddress`.
|
# Index every primary node in memory by its `HeapAddress`.
|
||||||
# We need this info in order to fill out the `parent_arg_name` of a node.
|
# We need this info in order to fill out the `parent_arg_name` of a node.
|
||||||
def every_primary_link
|
def every_primary_link
|
||||||
@every_primary_link ||= flat_ir
|
@every_primary_link ||= flat_ir
|
||||||
.map do |x|
|
.map do |x|
|
||||||
x
|
x
|
||||||
.except(B,C,I,K,L,N,P)
|
.except(B, C, I, K, L, N, P)
|
||||||
.invert
|
.invert
|
||||||
.to_a
|
.to_a
|
||||||
.select{|(k,v)| k.is_a?(HeapAddress)}
|
.select { |(k, v)| k.is_a?(HeapAddress) }
|
||||||
end
|
end
|
||||||
.map(&:to_h)
|
.map(&:to_h)
|
||||||
.reduce({}, :merge)
|
.reduce({}, :merge)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parent_arg_name_for(node, index)
|
def parent_arg_name_for(node, index)
|
||||||
resides_in_args = (node[N] == NULL) && (node[P] != NULL)
|
resides_in_args = (node[N] == NULL) && (node[P] != NULL)
|
||||||
link_symbol = every_primary_link[HeapAddress[index]]
|
link_symbol = every_primary_link[HeapAddress[index]]
|
||||||
needs_p_arg_name = (resides_in_args && link_symbol)
|
needs_p_arg_name = (resides_in_args && link_symbol)
|
||||||
parent_arg_name = (needs_p_arg_name ? link_symbol.to_s.gsub(L, "") : nil)
|
parent_arg_name = (needs_p_arg_name ? link_symbol.to_s.gsub(L, "") : nil)
|
||||||
return parent_arg_name
|
return parent_arg_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ private
|
||||||
end
|
end
|
||||||
|
|
||||||
def sequence_hash
|
def sequence_hash
|
||||||
@sequence_hash ||= \
|
@sequence_hash ||=
|
||||||
HashWithIndifferentAccess.new(kind: "sequence", args: args, body: body)
|
HashWithIndifferentAccess.new(kind: "sequence", args: args, body: body)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,18 @@ module CeleryScript
|
||||||
# THIS IS A MORE MINIMAL VERSION OF CeleryScript::TreeClimber.
|
# THIS IS A MORE MINIMAL VERSION OF CeleryScript::TreeClimber.
|
||||||
# It is a NON-VALIDATING tree climber.
|
# It is a NON-VALIDATING tree climber.
|
||||||
# Don't use this on unverified data structures.
|
# Don't use this on unverified data structures.
|
||||||
class JSONClimber
|
class JsonClimber
|
||||||
HASH_ONLY = "Expected a Hash."
|
HASH_ONLY = "Expected a Hash."
|
||||||
NOT_NODE = "Expected hash with at least a `kind` and `args` prop."
|
NOT_NODE = "Expected hash with at least a `kind` and `args` prop."
|
||||||
|
|
||||||
def self.climb(thing, &callable)
|
def self.climb(thing, &callable)
|
||||||
raise HASH_ONLY unless thing.is_a?(Hash)
|
raise HASH_ONLY unless thing.is_a?(Hash)
|
||||||
raise NOT_NODE unless is_node?(thing)
|
raise NOT_NODE unless is_node?(thing)
|
||||||
go(thing, callable)
|
go(thing, callable)
|
||||||
thing
|
thing
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def self.is_node?(maybe)
|
def self.is_node?(maybe)
|
||||||
maybe.is_a?(Hash) &&
|
maybe.is_a?(Hash) &&
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
require_relative "./csheap.rb"
|
require_relative "./cs_heap.rb"
|
||||||
# ORIGINAL IMPLEMENTATION HERE: https://github.com/FarmBot-Labs/Celery-Slicer
|
# ORIGINAL IMPLEMENTATION HERE: https://github.com/FarmBot-Labs/Celery-Slicer
|
||||||
# Take a nested ("canonical") representation of a CeleryScript sequence and
|
# Take a nested ("canonical") representation of a CeleryScript sequence and
|
||||||
# transforms it to a flat/homogenous intermediate representation which is better
|
# transforms it to a flat/homogenous intermediate representation which is better
|
||||||
|
@ -11,12 +11,12 @@ module CeleryScript
|
||||||
raise "Not a hash" unless node.is_a?(Hash)
|
raise "Not a hash" unless node.is_a?(Hash)
|
||||||
@nesting_level = 0
|
@nesting_level = 0
|
||||||
@root_node = node
|
@root_node = node
|
||||||
heap = CSHeap.new()
|
heap = CsHeap.new()
|
||||||
allocate(heap, node, CSHeap::NULL)
|
allocate(heap, node, CsHeap::NULL)
|
||||||
@heap_values = heap.values
|
@heap_values = heap.values
|
||||||
@heap_values.map do |x|
|
@heap_values.map do |x|
|
||||||
x[CSHeap::BODY] ||= CSHeap::NULL
|
x[CsHeap::BODY] ||= CsHeap::NULL
|
||||||
x[CSHeap::NEXT] ||= CSHeap::NULL
|
x[CsHeap::NEXT] ||= CsHeap::NULL
|
||||||
end
|
end
|
||||||
heap.dump()
|
heap.dump()
|
||||||
end
|
end
|
||||||
|
@ -31,8 +31,8 @@ module CeleryScript
|
||||||
|
|
||||||
def allocate(h, s, parentAddr)
|
def allocate(h, s, parentAddr)
|
||||||
addr = h.allot(s[:kind])
|
addr = h.allot(s[:kind])
|
||||||
h.put(addr, CSHeap::PARENT, parentAddr)
|
h.put(addr, CsHeap::PARENT, parentAddr)
|
||||||
h.put(addr, CSHeap::COMMENT, s[:comment]) if s[:comment]
|
h.put(addr, CsHeap::COMMENT, s[:comment]) if s[:comment]
|
||||||
iterate_over_body(h, s, addr)
|
iterate_over_body(h, s, addr)
|
||||||
iterate_over_args(h, s, addr)
|
iterate_over_args(h, s, addr)
|
||||||
addr
|
addr
|
||||||
|
@ -44,7 +44,7 @@ module CeleryScript
|
||||||
.map do |key|
|
.map do |key|
|
||||||
v = s[:args][key]
|
v = s[:args][key]
|
||||||
if (is_celery_script(v))
|
if (is_celery_script(v))
|
||||||
k = CSHeap::LINK + key.to_s
|
k = CsHeap::LINK + key.to_s
|
||||||
h.put(parentAddr, k, allocate(h, v, parentAddr))
|
h.put(parentAddr, k, allocate(h, v, parentAddr))
|
||||||
else
|
else
|
||||||
h.put(parentAddr, key, v)
|
h.put(parentAddr, key, v)
|
||||||
|
@ -64,12 +64,12 @@ module CeleryScript
|
||||||
is_head = index == 0
|
is_head = index == 0
|
||||||
# BE CAREFUL EDITING THIS LINE, YOU MIGHT BREAK `BODY` NODES:
|
# BE CAREFUL EDITING THIS LINE, YOU MIGHT BREAK `BODY` NODES:
|
||||||
heap # See note above!
|
heap # See note above!
|
||||||
.put(previous_address, CSHeap::BODY, previous_address + 1) if is_head
|
.put(previous_address, CsHeap::BODY, previous_address + 1) if is_head
|
||||||
|
|
||||||
my_heap_address = allocate(heap, canonical_list[index], previous_address)
|
my_heap_address = allocate(heap, canonical_list[index], previous_address)
|
||||||
|
|
||||||
prev_next_key = is_head ? CSHeap::NULL : my_heap_address
|
prev_next_key = is_head ? CsHeap::NULL : my_heap_address
|
||||||
heap.put(previous_address, CSHeap::NEXT, prev_next_key)
|
heap.put(previous_address, CsHeap::NEXT, prev_next_key)
|
||||||
|
|
||||||
recurse_into_body(heap, canonical_list, my_heap_address, index + 1)
|
recurse_into_body(heap, canonical_list, my_heap_address, index + 1)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
# Listens to *ALL* incoming logs and stores them to the DB.
|
# Listens to *ALL* incoming logs and stores them to the DB.
|
||||||
# Also handles throttling.
|
# Also handles throttling.
|
||||||
class LogService < AbstractServiceRunner
|
class LogService < AbstractServiceRunner
|
||||||
T = ThrottlePolicy::TimePeriod
|
THROTTLE_POLICY = ThrottlePolicy.new(name, {
|
||||||
THROTTLE_POLICY = ThrottlePolicy.new T.new(1.minute) => 0.5 * 1_000,
|
1.minute => 0.5 * 1_000,
|
||||||
T.new(1.hour) => 0.5 * 10_000,
|
1.hour => 0.5 * 10_000,
|
||||||
T.new(1.day) => 0.5 * 100_000
|
1.day => 0.5 * 100_000,
|
||||||
|
})
|
||||||
|
|
||||||
LOG_TPL = Rails.env.test? ?
|
LOG_TPL = Rails.env.test? ?
|
||||||
"\e[32m.\e[0m" : "FBOS LOG (device_%s): %s\n"
|
"\e[32m.\e[0m" : "FBOS LOG (device_%s): %s\n"
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
class TelemetryService < AbstractServiceRunner
|
class TelemetryService < AbstractServiceRunner
|
||||||
MESSAGE = "TELEMETRY MESSAGE FROM %s"
|
MESSAGE = "TELEMETRY MESSAGE FROM %s"
|
||||||
FAILURE = "FAILED TELEMETRY MESSAGE FROM %s"
|
FAILURE = "FAILED TELEMETRY MESSAGE FROM %s"
|
||||||
THROTTLE_POLICY = ThrottlePolicy.new({
|
THROTTLE_POLICY = ThrottlePolicy.new(name, {
|
||||||
ThrottlePolicy::TimePeriod.new(1.minute) => 25,
|
1.minute => 25,
|
||||||
ThrottlePolicy::TimePeriod.new(1.hour) => 250,
|
1.hour => 250,
|
||||||
ThrottlePolicy::TimePeriod.new(1.day) => 1500,
|
1.day => 1500,
|
||||||
})
|
})
|
||||||
|
|
||||||
def process(delivery_info, payload)
|
def process(delivery_info, payload)
|
||||||
|
|
|
@ -1,27 +1,30 @@
|
||||||
# Handles devices that spin out of control and send too many logs to the server.
|
# Handles devices that spin out of control and send too many logs to the server.
|
||||||
# Class Hierarchy:
|
# Class Hierarchy:
|
||||||
# ThrottlePolicy has => Rules creates => Violation
|
# ThrottlePolicy
|
||||||
# Violation has => Rule has => TimePeriod
|
# \
|
||||||
|
# +----> Rule --> TimePeriod
|
||||||
|
# |\
|
||||||
|
# | `--> Rule --> TimePeriod
|
||||||
|
# \_
|
||||||
|
# `-> Rule --> TimePeriod
|
||||||
class ThrottlePolicy
|
class ThrottlePolicy
|
||||||
attr_reader :rules
|
attr_reader :rules
|
||||||
|
|
||||||
# Dictionary<TimePeriod, Integer>
|
# Dictionary<TimePeriod, Integer>
|
||||||
def initialize(policy_rules)
|
def initialize(namespace, rule_map, now = Time.now)
|
||||||
@rules = policy_rules.map { |rule_set| Rule.new(*rule_set) }
|
@rules = rule_map
|
||||||
|
.map { |(period, limit)| Rule.new(namespace, period, limit, now) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def track(unique_id, now = Time.now)
|
def track(unique_id, now = Time.now)
|
||||||
rules.each { |r| r.time_period.record_event(unique_id, now) }
|
rules.each { |r| r.record_event(unique_id, now) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# If throttled, returns the timeperiod when device will be unthrottled
|
# If throttled, returns the timeperiod when device will be unthrottled
|
||||||
# returns nil if not throttled
|
# returns nil if not throttled
|
||||||
def is_throttled(unique_id)
|
def is_throttled(unique_id)
|
||||||
rules
|
rules
|
||||||
.map do |rule|
|
.map { |rule| rule.violation?(unique_id) }
|
||||||
is_violation = rule.time_period.usage_count_for(unique_id) > rule.limit
|
|
||||||
is_violation ? Violation.new(rule) : nil
|
|
||||||
end
|
|
||||||
.compact
|
.compact
|
||||||
.max
|
.max
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,21 @@ class ThrottlePolicy
|
||||||
class Rule
|
class Rule
|
||||||
attr_reader :time_period, :limit
|
attr_reader :time_period, :limit
|
||||||
|
|
||||||
def initialize(time_period, limit)
|
def initialize(namespace, time_period, limit, now = Time.now)
|
||||||
@time_period, @limit = time_period, limit
|
@time_period = ThrottlePolicy::TimePeriod.new(namespace, time_period, now)
|
||||||
|
@limit = limit
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns the timeperiod when device will be
|
||||||
|
# unthrottled. returns `nil` if not throttled
|
||||||
|
def violation?(unique_id)
|
||||||
|
if (time_period.usage_count_for(unique_id) > limit)
|
||||||
|
Violation.new(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_event(unique_id, now)
|
||||||
|
time_period.record_event(unique_id, now)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,48 +10,66 @@
|
||||||
# the initiator ID.
|
# the initiator ID.
|
||||||
class ThrottlePolicy
|
class ThrottlePolicy
|
||||||
class TimePeriod
|
class TimePeriod
|
||||||
attr_reader :time_unit,
|
attr_reader :time_unit, :current_period
|
||||||
:current_period, # Slice time into fixed size windows
|
|
||||||
:entries
|
|
||||||
|
|
||||||
def initialize(active_support_duration, now = Time.now)
|
def initialize(namespace, duration, now = Time.now)
|
||||||
@time_unit = active_support_duration
|
@time_unit = duration
|
||||||
reset_everything now
|
@namespace = namespace
|
||||||
|
reset_everything(now)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def record_event(unique_id, now = Time.now)
|
def record_event(unique_id, now = Time.now)
|
||||||
period = calculate_period(now)
|
period = calculate_period(now)
|
||||||
case period <=> current_period
|
case period <=> current_period
|
||||||
when -1 then return # Out of date- don't record.
|
when -1 then return # Out of date- don't record.
|
||||||
when 0 then increment_count_for(unique_id) # Right on schedule.
|
when 0 then increment_count_for(unique_id) # Right on schedule.
|
||||||
when 1 then reset_everything(now) # Clear out old data.
|
when 1 then reset_everything(now) # Clear out old data.
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def usage_count_for(unique_id)
|
def usage_count_for(unique_id)
|
||||||
@entries[unique_id] || 0
|
fetch(unique_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def when_does_next_period_start?
|
def when_does_next_period_start?
|
||||||
Time.at(current_period * time_unit.to_i) + time_unit
|
Time.at(current_period * time_unit.to_i) + time_unit
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def reset_everything(now)
|
def reset_everything(now)
|
||||||
@current_period = calculate_period(now)
|
@current_period = calculate_period(now)
|
||||||
@entries = {}
|
reset_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_count_for(unique_id)
|
def increment_count_for(unique_id)
|
||||||
@entries[unique_id] ||= 0
|
incr(unique_id)
|
||||||
@entries[unique_id] += 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns integer representation of current clock period
|
# Returns integer representation of current clock period
|
||||||
def calculate_period(time)
|
def calculate_period(time)
|
||||||
(time.to_i / @time_unit)
|
(time.to_i / @time_unit)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def redis
|
||||||
|
Rails.cache.redis
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_key(unique_id)
|
||||||
|
[@namespace, current_period.to_i, unique_id].join(":")
|
||||||
|
end
|
||||||
|
|
||||||
|
def incr(unique_id)
|
||||||
|
redis.incr(cache_key(unique_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch(unique_id)
|
||||||
|
(redis.get(cache_key(unique_id)) || "0").to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_cache
|
||||||
|
keys = redis.keys([@namespace, "*"].join(":"))
|
||||||
|
redis.del(*keys) unless keys.empty?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
class ApplicationRecord < ActiveRecord::Base
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
after_save :maybe_broadcast, on: [:create, :update]
|
after_create :maybe_broadcast
|
||||||
|
after_update :maybe_broadcast
|
||||||
after_destroy :maybe_broadcast
|
after_destroy :maybe_broadcast
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
@ -37,7 +38,7 @@ class ApplicationRecord < ActiveRecord::Base
|
||||||
def self.auto_sync_debounce
|
def self.auto_sync_debounce
|
||||||
@auto_sync_paused = true
|
@auto_sync_paused = true
|
||||||
result = yield
|
result = yield
|
||||||
result.update_attributes!(updated_at: Time.now)
|
result.update!(updated_at: Time.now)
|
||||||
@auto_sync_paused = false
|
@auto_sync_paused = false
|
||||||
result.broadcast!
|
result.broadcast!
|
||||||
result
|
result
|
||||||
|
@ -94,7 +95,7 @@ class ApplicationRecord < ActiveRecord::Base
|
||||||
|
|
||||||
def manually_sync!
|
def manually_sync!
|
||||||
device && (device.auto_sync_transaction do
|
device && (device.auto_sync_transaction do
|
||||||
update_attributes!(updated_at: Time.now)
|
update!(updated_at: Time.now)
|
||||||
end)
|
end)
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
|
@ -118,8 +118,8 @@ class Device < ApplicationRecord
|
||||||
end_t = violation.ends_at
|
end_t = violation.ends_at
|
||||||
# Some log validation errors will result in until_time being `nil`.
|
# Some log validation errors will result in until_time being `nil`.
|
||||||
if (throttled_until.nil? || end_t > throttled_until)
|
if (throttled_until.nil? || end_t > throttled_until)
|
||||||
reload.update_attributes!(throttled_until: end_t,
|
reload.update!(throttled_until: end_t,
|
||||||
throttled_at: Time.now)
|
throttled_at: Time.now)
|
||||||
refresh_cache
|
refresh_cache
|
||||||
cooldown = end_t.in_time_zone(self.timezone || "UTC").strftime("%I:%M%p")
|
cooldown = end_t.in_time_zone(self.timezone || "UTC").strftime("%I:%M%p")
|
||||||
info = [violation.explanation, cooldown]
|
info = [violation.explanation, cooldown]
|
||||||
|
@ -131,7 +131,7 @@ class Device < ApplicationRecord
|
||||||
if throttled_until.present?
|
if throttled_until.present?
|
||||||
old_time = throttled_until
|
old_time = throttled_until
|
||||||
reload # <= WHY!?! TODO: Find out why it crashes without this.
|
reload # <= WHY!?! TODO: Find out why it crashes without this.
|
||||||
.update_attributes!(throttled_until: nil, throttled_at: nil)
|
.update!(throttled_until: nil, throttled_at: nil)
|
||||||
refresh_cache
|
refresh_cache
|
||||||
cooldown_notice(THROTTLE_OFF, old_time, "info")
|
cooldown_notice(THROTTLE_OFF, old_time, "info")
|
||||||
end
|
end
|
||||||
|
@ -203,7 +203,7 @@ class Device < ApplicationRecord
|
||||||
|
|
||||||
last_sent_at = device.mqtt_rate_limit_email_sent_at || 4.years.ago
|
last_sent_at = device.mqtt_rate_limit_email_sent_at || 4.years.ago
|
||||||
if last_sent_at < 1.day.ago
|
if last_sent_at < 1.day.ago
|
||||||
device.update_attributes!(mqtt_rate_limit_email_sent_at: Time.now)
|
device.update!(mqtt_rate_limit_email_sent_at: Time.now)
|
||||||
device.tell(TOO_MANY_CONNECTIONS, ["fatal_email"])
|
device.tell(TOO_MANY_CONNECTIONS, ["fatal_email"])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ class FarmwareInstallation < ApplicationRecord
|
||||||
known_error = KNOWN_PROBLEMS[error.class]
|
known_error = KNOWN_PROBLEMS[error.class]
|
||||||
description = \
|
description = \
|
||||||
known_error || (OTHER_PROBLEM % error.class)
|
known_error || (OTHER_PROBLEM % error.class)
|
||||||
update_attributes!(package_error: description,
|
update!(package_error: description,
|
||||||
package: nil)
|
package: nil)
|
||||||
unless known_error.present?
|
unless known_error.present?
|
||||||
raise error
|
raise error
|
||||||
|
@ -54,7 +54,7 @@ class FarmwareInstallation < ApplicationRecord
|
||||||
string = string_io.read(MAX_JSON_SIZE)
|
string = string_io.read(MAX_JSON_SIZE)
|
||||||
json = JSON.parse(string)
|
json = JSON.parse(string)
|
||||||
pkg_name = json.fetch("package")
|
pkg_name = json.fetch("package")
|
||||||
update_attributes!(package: pkg_name, package_error: nil)
|
update!(package: pkg_name, package_error: nil)
|
||||||
rescue => error
|
rescue => error
|
||||||
maybe_recover_from_fetch_error(error)
|
maybe_recover_from_fetch_error(error)
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,8 @@ class FbosConfig < ApplicationRecord
|
||||||
class MissingSerial < StandardError; end
|
class MissingSerial < StandardError; end
|
||||||
|
|
||||||
belongs_to :device
|
belongs_to :device
|
||||||
after_save :maybe_sync_nerves, on: [:create, :update]
|
after_create :maybe_sync_nerves
|
||||||
|
after_update :maybe_sync_nerves
|
||||||
|
|
||||||
FIRMWARE_HARDWARE = [
|
FIRMWARE_HARDWARE = [
|
||||||
NOT_SET = nil,
|
NOT_SET = nil,
|
||||||
|
|
|
@ -28,7 +28,7 @@ class GlobalConfig < ApplicationRecord
|
||||||
# Remove it if the demo tour does not require it.
|
# Remove it if the demo tour does not require it.
|
||||||
"MQTT_WS" => SessionToken::MQTT_WS,
|
"MQTT_WS" => SessionToken::MQTT_WS,
|
||||||
}.map do |(key, value)|
|
}.map do |(key, value)|
|
||||||
self.find_or_create_by(key: key).update_attributes(key: key, value: value)
|
self.find_or_create_by(key: key).update(key: key, value: value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Memoized version of every GlobalConfig, with key/values layed out in a hash.
|
# Memoized version of every GlobalConfig, with key/values layed out in a hash.
|
||||||
|
|
|
@ -40,16 +40,9 @@ class Image < ApplicationRecord
|
||||||
# Worst case scenario for 1280x1280 BMP.
|
# Worst case scenario for 1280x1280 BMP.
|
||||||
GCS_BUCKET_NAME = ENV["GCS_BUCKET"]
|
GCS_BUCKET_NAME = ENV["GCS_BUCKET"]
|
||||||
|
|
||||||
# ========= DEPRECATED PAPERCLIP STUFF =========
|
|
||||||
# has_attached_file :attachment, CONFIG
|
|
||||||
# validates_attachment_content_type :attachment,
|
|
||||||
# content_type: CONTENT_TYPES
|
|
||||||
# ========= /DEPRECATED PAPERCLIP STUFF ========
|
|
||||||
has_one_attached :attachment
|
has_one_attached :attachment
|
||||||
|
|
||||||
def set_attachment_by_url(url)
|
def set_attachment_by_url(url)
|
||||||
# File
|
|
||||||
# URI::HTTPS
|
|
||||||
attachment.attach(io: open(url), filename: "image_#{self.id}")
|
attachment.attach(io: open(url), filename: "image_#{self.id}")
|
||||||
self.attachment_processed_at = Time.now
|
self.attachment_processed_at = Time.now
|
||||||
self
|
self
|
||||||
|
|
|
@ -11,7 +11,9 @@ class ToolSlot < Point
|
||||||
MIN_PULLOUT = PULLOUT_DIRECTIONS.min
|
MIN_PULLOUT = PULLOUT_DIRECTIONS.min
|
||||||
PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\
|
PULLOUT_ERR = "must be a value between #{MIN_PULLOUT} and #{MAX_PULLOUT}. "\
|
||||||
"%{value} is not valid."
|
"%{value} is not valid."
|
||||||
IN_USE = "already in use by another tool slot"
|
IN_USE = "already in use by another tool slot. "\
|
||||||
|
"Please un-assign the tool from its current slot"\
|
||||||
|
" before reassigning."
|
||||||
|
|
||||||
belongs_to :tool
|
belongs_to :tool
|
||||||
validates_uniqueness_of :tool,
|
validates_uniqueness_of :tool,
|
||||||
|
|
|
@ -26,7 +26,7 @@ module Auth
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
@user.update_attributes(agreed_to_terms_at: Time.now) if agree_to_terms
|
@user.update(agreed_to_terms_at: Time.now) if agree_to_terms
|
||||||
SessionToken.as_json(@user, aud, fbos_version)
|
SessionToken.as_json(@user, aud, fbos_version)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module Auth
|
module Auth
|
||||||
# The API supports a number of authentication strategies (Cookies, Bot token,
|
# The API supports a number of authentication strategies (Cookies, Bot token,
|
||||||
# JWT). This service helps determine which auth strategy to use.
|
# JWT). This service helps determine which auth strategy to use.
|
||||||
class FromJWT < Mutations::Command
|
class FromJwt < Mutations::Command
|
||||||
required { string :jwt }
|
required { string :jwt }
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
|
|
|
@ -5,7 +5,7 @@ module Configs
|
||||||
GOOD = 5556
|
GOOD = 5556
|
||||||
|
|
||||||
required do
|
required do
|
||||||
duck :target, methods: [:update_attributes!]
|
duck :target, methods: [:update!]
|
||||||
duck :update_attrs, methods: [:deep_symbolize_keys]
|
duck :update_attrs, methods: [:deep_symbolize_keys]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ module DeviceCerts
|
||||||
SendNervesHubInfoJob.perform_later(device_id: device.id,
|
SendNervesHubInfoJob.perform_later(device_id: device.id,
|
||||||
serial_number: serial_number,
|
serial_number: serial_number,
|
||||||
tags: tags)
|
tags: tags)
|
||||||
return device.update_attributes!(serial_number: serial_number) && device
|
return device.update!(serial_number: serial_number) && device
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,7 +24,7 @@ module Devices
|
||||||
# when we were using MongoDB. This can be
|
# when we were using MongoDB. This can be
|
||||||
# safely removed now. - RC 11-APR-19
|
# safely removed now. - RC 11-APR-19
|
||||||
old_device = user.device
|
old_device = user.device
|
||||||
user.update_attributes!(device_id: device.id) # Detach from old one.
|
user.update!(device_id: device.id) # Detach from old one.
|
||||||
# Remove userless devices.
|
# Remove userless devices.
|
||||||
old_device.destroy! if old_device && device.users.count < 1
|
old_device.destroy! if old_device && device.users.count < 1
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ module Devices
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
user.update_attributes!(device: Devices::Create.run!(user: user))
|
user.update!(device: Devices::Create.run!(user: user))
|
||||||
device.destroy! if device.reload.users.count < 1
|
device.destroy! if device.reload.users.count < 1
|
||||||
end
|
end
|
||||||
true
|
true
|
||||||
|
|
|
@ -20,7 +20,7 @@ module Devices
|
||||||
|
|
||||||
def run_it
|
def run_it
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
device.update_attributes!(name: "FarmBot")
|
device.update!(name: "FarmBot")
|
||||||
Device::SINGULAR_RESOURCES.keys.map do |resource|
|
Device::SINGULAR_RESOURCES.keys.map do |resource|
|
||||||
device.send(resource).destroy!
|
device.send(resource).destroy!
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,14 +2,14 @@ module Devices
|
||||||
module Seeders
|
module Seeders
|
||||||
class AbstractExpress < AbstractGenesis
|
class AbstractExpress < AbstractGenesis
|
||||||
def settings_device_name
|
def settings_device_name
|
||||||
device.update_attributes!(name: "FarmBot Express")
|
device.update!(name: "FarmBot Express")
|
||||||
end
|
end
|
||||||
|
|
||||||
def sensors_soil_sensor; end
|
def sensors_soil_sensor; end
|
||||||
def sensors_tool_verification; end
|
def sensors_tool_verification; end
|
||||||
|
|
||||||
def settings_enable_encoders
|
def settings_enable_encoders
|
||||||
device.firmware_config.update_attributes!(encoder_enabled_x: 0,
|
device.firmware_config.update!(encoder_enabled_x: 0,
|
||||||
encoder_enabled_y: 0,
|
encoder_enabled_y: 0,
|
||||||
encoder_enabled_z: 0)
|
encoder_enabled_z: 0)
|
||||||
end
|
end
|
||||||
|
@ -17,7 +17,7 @@ module Devices
|
||||||
def settings_firmware
|
def settings_firmware
|
||||||
device
|
device
|
||||||
.fbos_config
|
.fbos_config
|
||||||
.update_attributes!(firmware_hardware: FbosConfig::EXPRESS_K10)
|
.update!(firmware_hardware: FbosConfig::EXPRESS_K10)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tool_slots_slot_1
|
def tool_slots_slot_1
|
||||||
|
@ -91,11 +91,11 @@ module Devices
|
||||||
def sequences_unmount_tool; end
|
def sequences_unmount_tool; end
|
||||||
|
|
||||||
def settings_default_map_size_y
|
def settings_default_map_size_y
|
||||||
device.web_app_config.update_attributes!(map_size_y: 1_200)
|
device.web_app_config.update!(map_size_y: 1_200)
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_hide_sensors
|
def settings_hide_sensors
|
||||||
device.web_app_config.update_attributes!(hide_sensors: true)
|
device.web_app_config.update!(hide_sensors: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -18,11 +18,11 @@ module Devices
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_device_name
|
def settings_device_name
|
||||||
device.update_attributes!(name: "FarmBot Genesis")
|
device.update!(name: "FarmBot Genesis")
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_enable_encoders
|
def settings_enable_encoders
|
||||||
device.firmware_config.update_attributes!(encoder_enabled_x: 1,
|
device.firmware_config.update!(encoder_enabled_x: 1,
|
||||||
encoder_enabled_y: 1,
|
encoder_enabled_y: 1,
|
||||||
encoder_enabled_z: 1)
|
encoder_enabled_z: 1)
|
||||||
end
|
end
|
||||||
|
@ -146,15 +146,15 @@ module Devices
|
||||||
def settings_firmware
|
def settings_firmware
|
||||||
device
|
device
|
||||||
.fbos_config
|
.fbos_config
|
||||||
.update_attributes!(firmware_hardware: FbosConfig::FARMDUINO)
|
.update!(firmware_hardware: FbosConfig::FARMDUINO)
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_default_map_size_x
|
def settings_default_map_size_x
|
||||||
device.web_app_config.update_attributes!(map_size_x: 2_900)
|
device.web_app_config.update!(map_size_x: 2_900)
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_default_map_size_y
|
def settings_default_map_size_y
|
||||||
device.web_app_config.update_attributes!(map_size_y: 1_400)
|
device.web_app_config.update!(map_size_y: 1_400)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pin_bindings_button_1
|
def pin_bindings_button_1
|
||||||
|
|
|
@ -72,7 +72,7 @@ module Devices
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_hide_sensors
|
def settings_hide_sensors
|
||||||
device.web_app_config.update_attributes!(hide_sensors: false)
|
device.web_app_config.update!(hide_sensors: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def plants
|
def plants
|
||||||
|
|
|
@ -55,10 +55,10 @@ module Devices
|
||||||
.map { |p| p.merge(device: device) }
|
.map { |p| p.merge(device: device) }
|
||||||
.map { |p| Alerts::Create.run!(p) }
|
.map { |p| Alerts::Create.run!(p) }
|
||||||
device
|
device
|
||||||
.update_attributes!(fbos_version: READ_COMMENT_ABOVE)
|
.update!(fbos_version: READ_COMMENT_ABOVE)
|
||||||
device
|
device
|
||||||
.web_app_config
|
.web_app_config
|
||||||
.update_attributes!(discard_unsaved: true)
|
.update!(discard_unsaved: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,15 +2,15 @@ module Devices
|
||||||
module Seeders
|
module Seeders
|
||||||
class ExpressXlOneZero < AbstractExpress
|
class ExpressXlOneZero < AbstractExpress
|
||||||
def settings_device_name
|
def settings_device_name
|
||||||
device.update_attributes!(name: "FarmBot Express XL")
|
device.update!(name: "FarmBot Express XL")
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_default_map_size_x
|
def settings_default_map_size_x
|
||||||
device.web_app_config.update_attributes!(map_size_x: 6_000)
|
device.web_app_config.update!(map_size_x: 6_000)
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_default_map_size_y
|
def settings_default_map_size_y
|
||||||
device.web_app_config.update_attributes!(map_size_y: 2_400)
|
device.web_app_config.update!(map_size_y: 2_400)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ module Devices
|
||||||
def settings_firmware
|
def settings_firmware
|
||||||
device
|
device
|
||||||
.fbos_config
|
.fbos_config
|
||||||
.update_attributes!(firmware_hardware: FbosConfig::FARMDUINO_K14)
|
.update!(firmware_hardware: FbosConfig::FARMDUINO_K14)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ module Devices
|
||||||
def settings_firmware
|
def settings_firmware
|
||||||
device
|
device
|
||||||
.fbos_config
|
.fbos_config
|
||||||
.update_attributes!(firmware_hardware: FbosConfig::ARDUINO)
|
.update!(firmware_hardware: FbosConfig::ARDUINO)
|
||||||
end
|
end
|
||||||
|
|
||||||
def peripherals_lighting; end
|
def peripherals_lighting; end
|
||||||
|
|
|
@ -4,19 +4,19 @@ module Devices
|
||||||
def settings_firmware
|
def settings_firmware
|
||||||
device
|
device
|
||||||
.fbos_config
|
.fbos_config
|
||||||
.update_attributes!(firmware_hardware: FbosConfig::FARMDUINO_K14)
|
.update!(firmware_hardware: FbosConfig::FARMDUINO_K14)
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_device_name
|
def settings_device_name
|
||||||
device.update_attributes!(name: "FarmBot Genesis XL")
|
device.update!(name: "FarmBot Genesis XL")
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_default_map_size_x
|
def settings_default_map_size_x
|
||||||
device.web_app_config.update_attributes!(map_size_x: 5_900)
|
device.web_app_config.update!(map_size_x: 5_900)
|
||||||
end
|
end
|
||||||
|
|
||||||
def settings_default_map_size_y
|
def settings_default_map_size_y
|
||||||
device.web_app_config.update_attributes!(map_size_y: 2_900)
|
device.web_app_config.update!(map_size_y: 2_900)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ module Devices
|
||||||
class Sync < Mutations::Command
|
class Sync < Mutations::Command
|
||||||
SEL = "SELECT id, updated_at FROM"
|
SEL = "SELECT id, updated_at FROM"
|
||||||
WHERE = "WHERE device_id = "
|
WHERE = "WHERE device_id = "
|
||||||
|
FORMAT = "%Y-%m-%d %H:%M:%S.%5N"
|
||||||
|
|
||||||
def self.basic_query(plural_resource, where = WHERE)
|
def self.basic_query(plural_resource, where = WHERE)
|
||||||
[SEL, plural_resource, where].join(" ")
|
[SEL, plural_resource, where].join(" ")
|
||||||
|
|
|
@ -21,7 +21,7 @@ module Devices
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
p = inputs.except(:device).merge(mounted_tool_data)
|
p = inputs.except(:device).merge(mounted_tool_data)
|
||||||
device.update_attributes!(p)
|
device.update!(p)
|
||||||
device
|
device
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ module FarmEvents
|
||||||
FarmEvents => ->() { farm_event },
|
FarmEvents => ->() { farm_event },
|
||||||
Regimens => ->() { regimen },
|
Regimens => ->() { regimen },
|
||||||
}
|
}
|
||||||
options.fetch(self.class.parent).call()
|
options.fetch(self.class.module_parent).call()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,7 +33,7 @@ module FarmEvents
|
||||||
FarmEvent.auto_sync_debounce do
|
FarmEvent.auto_sync_debounce do
|
||||||
FarmEvent.transaction do
|
FarmEvent.transaction do
|
||||||
handle_body_field
|
handle_body_field
|
||||||
farm_event.update_attributes!(p)
|
farm_event.update!(p)
|
||||||
farm_event
|
farm_event
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ module FarmwareEnvs
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
farmware_env.update_attributes!(inputs.except(:farmware_env)) && farmware_env
|
farmware_env.update!(inputs.except(:farmware_env)) && farmware_env
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ module PasswordResets
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
user.update_attributes!(password: password,
|
user.update!(password: password,
|
||||||
password_confirmation: password_confirmation)
|
password_confirmation: password_confirmation)
|
||||||
Auth::CreateToken.run!(email: user.email,
|
Auth::CreateToken.run!(email: user.email,
|
||||||
password: password,
|
password: password,
|
||||||
|
|
|
@ -11,7 +11,7 @@ module Peripherals
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
peripheral.update_attributes!(inputs.except(:peripheral, :device))
|
peripheral.update!(inputs.except(:peripheral, :device))
|
||||||
peripheral
|
peripheral
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ module PinBindings
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
x = inputs.except(:pin_binding, :device)
|
x = inputs.except(:pin_binding, :device)
|
||||||
pin_binding.update_attributes!(x) && pin_binding
|
pin_binding.update!(x) && pin_binding
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ module PlantTemplates
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
plant_template.update_attributes!(update_params)
|
plant_template.update!(update_params)
|
||||||
plant_template
|
plant_template
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ module PointGroups
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate
|
def validate
|
||||||
validate_point_ids if point_ids.any?
|
validate_point_ids if point_ids.present?
|
||||||
validate_sort_type
|
validate_sort_type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ module PointGroups
|
||||||
PointGroup.transaction do
|
PointGroup.transaction do
|
||||||
PointGroupItem.transaction do
|
PointGroupItem.transaction do
|
||||||
maybe_reconcile_points
|
maybe_reconcile_points
|
||||||
point_group.update_attributes!(update_attributes)
|
point_group.update!(update_attributes)
|
||||||
point_group.reload # <= Because PointGroupItem caching?
|
point_group.reload # <= Because PointGroupItem caching?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -38,19 +38,26 @@ module PointGroups
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_reconcile_points
|
def maybe_reconcile_points
|
||||||
|
# Nil means "ignore"
|
||||||
|
# [] means "reset"
|
||||||
|
return if point_ids.nil?
|
||||||
|
|
||||||
# STEP 0: Setup
|
# STEP 0: Setup
|
||||||
@old_point_ids = Set.new(point_group.point_group_items.pluck(:id))
|
@old_point_ids = Set.new(point_group.point_group_items.pluck(:point_id))
|
||||||
@new_point_ids = Set.new(point_ids)
|
@new_point_ids = Set.new(point_ids)
|
||||||
@dont_delete = @new_point_ids & @old_point_ids
|
@dont_delete = @new_point_ids & @old_point_ids
|
||||||
@do_delete = (@old_point_ids - @dont_delete).to_a
|
@do_delete = (@old_point_ids - @dont_delete).to_a
|
||||||
|
|
||||||
# STEP 1: "Garbage Collection" of PGIs that are no longer used.
|
# STEP 1: "Garbage Collection" of PGIs that are no longer used.
|
||||||
PointGroupItem.where(id: @do_delete).map(&:destroy!)
|
PointGroupItem
|
||||||
|
.where(point_group_id: point_group.id)
|
||||||
|
.where(point_id: @do_delete)
|
||||||
|
.destroy_all
|
||||||
|
|
||||||
# STEP 2: Create missing PGIs
|
# STEP 2: Create missing PGIs
|
||||||
@do_create = (@new_point_ids - @dont_delete)
|
@do_create = (@new_point_ids - @dont_delete)
|
||||||
PointGroupItem.create!(@do_create.to_a.uniq.map do |id|
|
PointGroupItem.create!(@do_create.to_a.uniq.map do |point_id|
|
||||||
{ point_id: id, point_group_id: point_group.id }
|
{ point_id: point_id, point_group_id: point_group.id }
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
require_relative "../../lib/hstore_filter"
|
require_relative "../../lib/mutations/hstore_filter.rb"
|
||||||
# WHY??? ^
|
|
||||||
module Points
|
module Points
|
||||||
class Create < Mutations::Command
|
class Create < Mutations::Command
|
||||||
# WHY 1000?:
|
# WHY 1000?:
|
||||||
|
|
|
@ -73,7 +73,7 @@ module Points
|
||||||
# a fresh session_id, the frontend will
|
# a fresh session_id, the frontend will
|
||||||
# think it is an "echo" and cancel it out.
|
# think it is an "echo" and cancel it out.
|
||||||
# """ - Rick
|
# """ - Rick
|
||||||
x.update_attributes!(updated_at: Time.now)
|
x.update!(updated_at: Time.now)
|
||||||
x.broadcast!(SecureRandom.uuid)
|
x.broadcast!(SecureRandom.uuid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,47 +1,47 @@
|
||||||
require_relative "../../lib/hstore_filter"
|
# require_relative "../../lib/hstore_filter"
|
||||||
# WHY??? ^
|
# WHY??? ^
|
||||||
module Points
|
module Points
|
||||||
class Query < Mutations::Command
|
class Query < Mutations::Command
|
||||||
H_QUERY = "meta -> :key = :value"
|
H_QUERY = "meta -> :key = :value"
|
||||||
|
|
||||||
required do
|
required do
|
||||||
duck :points, method: [:where]
|
duck :points, method: [:where]
|
||||||
end
|
end
|
||||||
|
|
||||||
optional do
|
optional do
|
||||||
float :radius
|
float :radius
|
||||||
float :x
|
float :x
|
||||||
float :y
|
float :y
|
||||||
float :z
|
float :z
|
||||||
hstore :meta
|
hstore :meta
|
||||||
string :name
|
string :name
|
||||||
string :pointer_type, in: Point::POINTER_KINDS
|
string :pointer_type, in: Point::POINTER_KINDS
|
||||||
string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES
|
string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES
|
||||||
string :openfarm_slug
|
string :openfarm_slug
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
search_results
|
search_results
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_results
|
def search_results
|
||||||
@search_results ||= conditions.reduce(points) do |collection, query|
|
@search_results ||= conditions.reduce(points) do |collection, query|
|
||||||
collection.where(query)
|
collection.where(query)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def conditions
|
|
||||||
@conditions ||= regular_conditions + meta_conditions
|
|
||||||
end
|
|
||||||
|
|
||||||
def meta_conditions
|
|
||||||
@meta_conditions ||= (meta || {}).map do |(k,v)|
|
|
||||||
[H_QUERY, {key: k, value: v}]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def regular_conditions
|
|
||||||
@regular_conditions ||= [inputs.except(:points, :meta)]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conditions
|
||||||
|
@conditions ||= regular_conditions + meta_conditions
|
||||||
|
end
|
||||||
|
|
||||||
|
def meta_conditions
|
||||||
|
@meta_conditions ||= (meta || {}).map do |(k, v)|
|
||||||
|
[H_QUERY, { key: k, value: v }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def regular_conditions
|
||||||
|
@regular_conditions ||= [inputs.except(:points, :meta)]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
require_relative "../../lib/hstore_filter"
|
# require_relative "../../lib/hstore_filter"
|
||||||
|
|
||||||
module Points
|
module Points
|
||||||
class Update < Mutations::Command
|
class Update < Mutations::Command
|
||||||
required do
|
required do
|
||||||
model :device, class: Device
|
model :device, class: Device
|
||||||
model :point, class: Point
|
model :point, class: Point
|
||||||
end
|
end
|
||||||
|
|
||||||
optional do
|
optional do
|
||||||
integer :tool_id, nils: true, empty_is_nil: true
|
integer :tool_id, nils: true, empty_is_nil: true
|
||||||
float :x
|
float :x
|
||||||
float :y
|
float :y
|
||||||
float :z
|
float :z
|
||||||
float :radius
|
float :radius
|
||||||
string :name
|
string :name
|
||||||
string :openfarm_slug
|
string :openfarm_slug
|
||||||
integer :pullout_direction, in: ToolSlot::PULLOUT_DIRECTIONS
|
integer :pullout_direction, in: ToolSlot::PULLOUT_DIRECTIONS
|
||||||
string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES
|
string :plant_stage, in: CeleryScriptSettingsBag::PLANT_STAGES
|
||||||
time :planted_at
|
time :planted_at
|
||||||
hstore :meta
|
hstore :meta
|
||||||
boolean :gantry_mounted
|
boolean :gantry_mounted
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -27,10 +27,10 @@ module Points
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
Point.transaction { point.update_attributes!(inputs.except(:point)) && point }
|
Point.transaction { point.update!(inputs.except(:point)) && point }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def new_tool_id?
|
def new_tool_id?
|
||||||
raw_inputs.key?("tool_id")
|
raw_inputs.key?("tool_id")
|
||||||
|
|
|
@ -27,7 +27,7 @@ module Regimens
|
||||||
RegimenItem.new(ri).tap { |r| r.validate! }
|
RegimenItem.new(ri).tap { |r| r.validate! }
|
||||||
end
|
end
|
||||||
handle_body_field
|
handle_body_field
|
||||||
regimen.update_attributes!(inputs.slice(:name, :color, :regimen_items))
|
regimen.update!(inputs.slice(:name, :color, :regimen_items))
|
||||||
regimen
|
regimen
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ module SavedGardens
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
saved_garden.update_attributes!(inputs.except(:saved_garden))
|
saved_garden.update!(inputs.except(:saved_garden))
|
||||||
saved_garden
|
saved_garden
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Sensors
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
sensor.update_attributes!(inputs.except(:sensor))
|
sensor.update!(inputs.except(:sensor))
|
||||||
sensor
|
sensor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ module Sequences
|
||||||
Sequence.auto_sync_debounce do
|
Sequence.auto_sync_debounce do
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
sequence.migrated_nodes = true
|
sequence.migrated_nodes = true
|
||||||
sequence.update_attributes!(inputs.except(*BLACKLIST))
|
sequence.update!(inputs.except(*BLACKLIST))
|
||||||
CeleryScript::StoreCelery.run!(sequence: sequence,
|
CeleryScript::StoreCelery.run!(sequence: sequence,
|
||||||
args: args,
|
args: args,
|
||||||
body: body)
|
body: body)
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Tools
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
tool.update_attributes!(inputs.except(:tool)) && tool
|
tool.update!(inputs.except(:tool)) && tool
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ module Users
|
||||||
required { model :user, class: User }
|
required { model :user, class: User }
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
user.update_attributes!(confirmed_at: Time.now,
|
user.update!(confirmed_at: Time.now,
|
||||||
email: user.unconfirmed_email,
|
email: user.unconfirmed_email,
|
||||||
unconfirmed_email: nil)
|
unconfirmed_email: nil)
|
||||||
fbos_vers = Gem::Version.new("99.9.9") # Not relevant here, stubbing out.
|
fbos_vers = Gem::Version.new("99.9.9") # Not relevant here, stubbing out.
|
||||||
|
|
|
@ -21,7 +21,7 @@ module Users
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
maybe_perform_password_reset
|
maybe_perform_password_reset
|
||||||
user.update_attributes!(calculated_update)
|
user.update!(calculated_update)
|
||||||
user.reload
|
user.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ module WebcamFeeds
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
webcam_feed.update_attributes!(inputs.except(:webcam_feed)) && webcam_feed
|
webcam_feed.update!(inputs.except(:webcam_feed)) && webcam_feed
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require_relative "../app/models/transport.rb"
|
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_relative "../app/lib/celery_script/cs_heap"
|
||||||
require "rails/all"
|
require "rails/all"
|
||||||
|
|
||||||
# Require the gems listed in Gemfile, including any gems
|
# Require the gems listed in Gemfile, including any gems
|
||||||
|
@ -14,11 +14,13 @@ module FarmBot
|
||||||
REDIS_URL = ENV.fetch(REDIS_ENV_KEY, "redis://redis:6379/0")
|
REDIS_URL = ENV.fetch(REDIS_ENV_KEY, "redis://redis:6379/0")
|
||||||
gcs_enabled =
|
gcs_enabled =
|
||||||
%w[ GOOGLE_CLOUD_KEYFILE_JSON GCS_PROJECT GCS_BUCKET ].all? { |s| ENV.key? s }
|
%w[ GOOGLE_CLOUD_KEYFILE_JSON GCS_PROJECT GCS_BUCKET ].all? { |s| ENV.key? s }
|
||||||
|
config.load_defaults 6.0
|
||||||
config.active_storage.service = gcs_enabled ?
|
config.active_storage.service = gcs_enabled ?
|
||||||
:google : :local
|
:google : :local
|
||||||
config.cache_store = :redis_cache_store, { url: REDIS_URL }
|
config.cache_store = :redis_cache_store, { url: REDIS_URL }
|
||||||
config.middleware.use Rack::Attack
|
config.middleware.use Rack::Attack
|
||||||
config.active_record.schema_format = :sql
|
config.active_record.schema_format = :sql
|
||||||
|
config.active_record.belongs_to_required_by_default = false
|
||||||
config.active_job.queue_adapter = :delayed_job
|
config.active_job.queue_adapter = :delayed_job
|
||||||
config.action_dispatch.perform_deep_munge = false
|
config.action_dispatch.perform_deep_munge = false
|
||||||
I18n.enforce_available_locales = false
|
I18n.enforce_available_locales = false
|
||||||
|
|
|
@ -10,7 +10,7 @@ class DeprecateDeviceSerialNumberTable < ActiveRecord::Migration[5.2]
|
||||||
|
|
||||||
def change
|
def change
|
||||||
DeviceSerialNumber.preload(:devices) do |x|
|
DeviceSerialNumber.preload(:devices) do |x|
|
||||||
x.device.update_attributes!(serial_number: x.serial_number)
|
x.device.update!(serial_number: x.serial_number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,9 +29,10 @@ if Rails.env == "development"
|
||||||
SensorReading,
|
SensorReading,
|
||||||
FarmwareInstallation,
|
FarmwareInstallation,
|
||||||
Tool,
|
Tool,
|
||||||
Device,
|
|
||||||
Delayed::Job,
|
Delayed::Job,
|
||||||
Delayed::Backend::ActiveRecord::Job,
|
Delayed::Backend::ActiveRecord::Job,
|
||||||
|
Fragment,
|
||||||
|
Device,
|
||||||
].map(&:delete_all)
|
].map(&:delete_all)
|
||||||
Users::Create.run!(name: "Test",
|
Users::Create.run!(name: "Test",
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
|
@ -43,7 +44,7 @@ if Rails.env == "development"
|
||||||
User.update_all(confirmed_at: Time.now,
|
User.update_all(confirmed_at: Time.now,
|
||||||
agreed_to_terms_at: Time.now)
|
agreed_to_terms_at: Time.now)
|
||||||
u = User.last
|
u = User.last
|
||||||
u.update_attributes(device: Devices::Create.run!(user: u))
|
u.update(device: Devices::Create.run!(user: u))
|
||||||
# === Parameterized Sequence stuff
|
# === Parameterized Sequence stuff
|
||||||
json = JSON.parse(File.read("spec/lib/celery_script/ast_fixture5.json"), symbolize_names: true)
|
json = JSON.parse(File.read("spec/lib/celery_script/ast_fixture5.json"), symbolize_names: true)
|
||||||
Sequences::Create.run!(json, device: u.device)
|
Sequences::Create.run!(json, device: u.device)
|
||||||
|
|
|
@ -13,4 +13,5 @@ export const fakeDesignerState = (): DesignerState => ({
|
||||||
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
||||||
currentPoint: undefined,
|
currentPoint: undefined,
|
||||||
openedSavedGarden: undefined,
|
openedSavedGarden: undefined,
|
||||||
|
tryGroupSortType: undefined,
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,9 @@ jest.mock("../../../config_storage/actions", () => ({
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { mount, shallow } from "enzyme";
|
import { mount, shallow } from "enzyme";
|
||||||
import { DevWidget, DevWidgetFERow, DevWidgetFBOSRow } from "../dev_widget";
|
import {
|
||||||
|
DevWidget, DevWidgetFERow, DevWidgetFBOSRow, DevWidgetDelModeRow
|
||||||
|
} from "../dev_widget";
|
||||||
import { DevSettings } from "../dev_support";
|
import { DevSettings } from "../dev_support";
|
||||||
import { setWebAppConfigValue } from "../../../config_storage/actions";
|
import { setWebAppConfigValue } from "../../../config_storage/actions";
|
||||||
|
|
||||||
|
@ -52,4 +54,12 @@ describe("<DevWidget />", () => {
|
||||||
wrapper.find("button").simulate("click");
|
wrapper.find("button").simulate("click");
|
||||||
expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", "{}");
|
expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", "{}");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("disables delete mode", () => {
|
||||||
|
delete mockDevSettings[DevSettings.FUTURE_FE_FEATURES];
|
||||||
|
mockDevSettings[DevSettings.QUICK_DELETE_MODE] = "true";
|
||||||
|
const wrapper = mount(<DevWidgetDelModeRow />);
|
||||||
|
wrapper.find("button").simulate("click");
|
||||||
|
expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", "{}");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace devStorage {
|
||||||
export enum Key {
|
export enum Key {
|
||||||
FUTURE_FE_FEATURES = "FUTURE_FE_FEATURES",
|
FUTURE_FE_FEATURES = "FUTURE_FE_FEATURES",
|
||||||
FBOS_VERSION_OVERRIDE = "FBOS_VERSION_OVERRIDE",
|
FBOS_VERSION_OVERRIDE = "FBOS_VERSION_OVERRIDE",
|
||||||
|
QUICK_DELETE_MODE = "QUICK_DELETE_MODE",
|
||||||
}
|
}
|
||||||
type Storage = { [K in Key]: string };
|
type Storage = { [K in Key]: string };
|
||||||
|
|
||||||
|
@ -64,4 +65,11 @@ export namespace DevSettings {
|
||||||
export const setMaxFbosVersionOverride = () =>
|
export const setMaxFbosVersionOverride = () =>
|
||||||
devStorage.setItem(FBOS_VERSION_OVERRIDE, MAX_FBOS_VERSION_OVERRIDE);
|
devStorage.setItem(FBOS_VERSION_OVERRIDE, MAX_FBOS_VERSION_OVERRIDE);
|
||||||
|
|
||||||
|
export const QUICK_DELETE_MODE = devStorage.Key.QUICK_DELETE_MODE;
|
||||||
|
export const quickDeleteEnabled = () =>
|
||||||
|
!!devStorage.getItem(QUICK_DELETE_MODE);
|
||||||
|
export const enableQuickDelete = () =>
|
||||||
|
devStorage.setItem(QUICK_DELETE_MODE, "true");
|
||||||
|
export const disableQuickDelete = () =>
|
||||||
|
devStorage.removeItem(QUICK_DELETE_MODE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,22 @@ export const DevWidgetFBOSRow = () => {
|
||||||
</Row>;
|
</Row>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DevWidgetDelModeRow = () =>
|
||||||
|
<Row>
|
||||||
|
<Col xs={8}>
|
||||||
|
<label>
|
||||||
|
{"Enable quick delete mode"}
|
||||||
|
</label>
|
||||||
|
</Col>
|
||||||
|
<Col xs={4}>
|
||||||
|
<ToggleButton
|
||||||
|
toggleValue={DevSettings.quickDeleteEnabled()}
|
||||||
|
toggleAction={DevSettings.quickDeleteEnabled()
|
||||||
|
? DevSettings.disableQuickDelete
|
||||||
|
: DevSettings.enableQuickDelete} />
|
||||||
|
</Col>
|
||||||
|
</Row>;
|
||||||
|
|
||||||
export const DevWidget = ({ dispatch }: { dispatch: Function }) =>
|
export const DevWidget = ({ dispatch }: { dispatch: Function }) =>
|
||||||
<Widget>
|
<Widget>
|
||||||
<WidgetHeader title={"Dev options"}>
|
<WidgetHeader title={"Dev options"}>
|
||||||
|
@ -62,6 +78,7 @@ export const DevWidget = ({ dispatch }: { dispatch: Function }) =>
|
||||||
</WidgetHeader>
|
</WidgetHeader>
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<DevWidgetFERow />
|
<DevWidgetFERow />
|
||||||
|
<DevWidgetDelModeRow />
|
||||||
<DevWidgetFBOSRow />
|
<DevWidgetFBOSRow />
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
</Widget>;
|
</Widget>;
|
||||||
|
|
|
@ -974,6 +974,7 @@ export enum Actions {
|
||||||
CHOOSE_LOCATION = "CHOOSE_LOCATION",
|
CHOOSE_LOCATION = "CHOOSE_LOCATION",
|
||||||
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
|
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
|
||||||
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
||||||
|
TRY_SORT_TYPE = "TRY_SORT_TYPE",
|
||||||
|
|
||||||
// Regimens
|
// Regimens
|
||||||
PUSH_WEEK = "PUSH_WEEK",
|
PUSH_WEEK = "PUSH_WEEK",
|
||||||
|
|
|
@ -303,6 +303,20 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.quick-del {
|
||||||
|
&:hover {
|
||||||
|
background: lighten($red, 10%) !important;
|
||||||
|
border: none;
|
||||||
|
box-shadow: inset 0px 0px 0px 4px $darkest_red !important;
|
||||||
|
&:after {
|
||||||
|
content: "x";
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
color: $darkest_red;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-control {
|
.block-control {
|
||||||
|
|
|
@ -1469,3 +1469,16 @@ textarea {
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,.2);
|
box-shadow: 0 0 10px rgba(0,0,0,.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sort-path-info-bar {
|
||||||
|
background: lightgray;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
&:hover {
|
||||||
|
background: darken(lightgray, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -101,6 +101,7 @@
|
||||||
.farmware-button{
|
.farmware-button{
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0rem;
|
top: 0rem;
|
||||||
|
z-index: 9;
|
||||||
height: 0px;
|
height: 0px;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,7 +209,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.farmware-list-items,
|
.farmware-list-items,
|
||||||
.sequence-list-items,
|
.sequence-list-item,
|
||||||
.regimen-list {
|
.regimen-list {
|
||||||
button {
|
button {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -220,6 +220,7 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
float: left;
|
float: left;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i {
|
i {
|
||||||
|
@ -228,7 +229,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sequence-list-items {
|
.sequence-list-item {
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
let mockReleaseNoteData = {};
|
let mockReleaseNoteResponse = Promise.resolve({ data: "" });
|
||||||
jest.mock("axios", () => ({
|
jest.mock("axios", () => ({
|
||||||
get: jest.fn(() => Promise.resolve(mockReleaseNoteData))
|
get: jest.fn(() => mockReleaseNoteResponse)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../../../api/crud", () => ({
|
jest.mock("../../../api/crud", () => ({
|
||||||
|
@ -53,24 +53,44 @@ describe("<FarmbotOsSettings />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fetches OS release notes", async () => {
|
it("fetches OS release notes", async () => {
|
||||||
mockReleaseNoteData = { data: "intro\n\n# v6\n\n* note" };
|
mockReleaseNoteResponse = Promise.resolve({
|
||||||
|
data: "intro\n\n# v6\n\n* note"
|
||||||
|
});
|
||||||
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
|
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
|
||||||
{...fakeProps()} />);
|
{...fakeProps()} />);
|
||||||
await expect(axios.get).toHaveBeenCalledWith(
|
await expect(axios.get).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("RELEASE_NOTES.md"));
|
expect.stringContaining("RELEASE_NOTES.md"));
|
||||||
expect(osSettings.instance().state.osReleaseNotesHeading)
|
expect(osSettings.instance().osReleaseNotes.heading)
|
||||||
.toEqual("FarmBot OS v6");
|
.toEqual("FarmBot OS v6");
|
||||||
expect(osSettings.instance().state.osReleaseNotes)
|
expect(osSettings.instance().osReleaseNotes.notes)
|
||||||
.toEqual("* note");
|
.toEqual("* note");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't fetch OS release notes", async () => {
|
it("doesn't fetch OS release notes", async () => {
|
||||||
mockReleaseNoteData = { data: "empty notes" };
|
mockReleaseNoteResponse = Promise.resolve({ data: "" });
|
||||||
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
|
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
|
||||||
{...fakeProps()} />);
|
{...fakeProps()} />);
|
||||||
await expect(axios.get).toHaveBeenCalledWith(
|
await expect(axios.get).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("RELEASE_NOTES.md"));
|
expect.stringContaining("RELEASE_NOTES.md"));
|
||||||
expect(osSettings.instance().state.osReleaseNotes)
|
expect(osSettings.instance().state.allOsReleaseNotes)
|
||||||
|
.toEqual("");
|
||||||
|
expect(osSettings.instance().osReleaseNotes.heading)
|
||||||
|
.toEqual("FarmBot OS v6");
|
||||||
|
expect(osSettings.instance().osReleaseNotes.notes)
|
||||||
|
.toEqual("Could not get release notes.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors while fetching OS release notes", async () => {
|
||||||
|
mockReleaseNoteResponse = Promise.reject({ error: "" });
|
||||||
|
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
|
||||||
|
{...fakeProps()} />);
|
||||||
|
await expect(axios.get).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("RELEASE_NOTES.md"));
|
||||||
|
expect(osSettings.instance().state.allOsReleaseNotes)
|
||||||
|
.toEqual("");
|
||||||
|
expect(osSettings.instance().osReleaseNotes.heading)
|
||||||
|
.toEqual("FarmBot OS v6");
|
||||||
|
expect(osSettings.instance().osReleaseNotes.notes)
|
||||||
.toEqual("Could not get release notes.");
|
.toEqual("Could not get release notes.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,4 +102,11 @@ describe("<FarmbotOsSettings />", () => {
|
||||||
.simulate("change", { currentTarget: { value: newName } });
|
.simulate("change", { currentTarget: { value: newName } });
|
||||||
expect(edit).toHaveBeenCalledWith(p.deviceAccount, { name: newName });
|
expect(edit).toHaveBeenCalledWith(p.deviceAccount, { name: newName });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("displays boot sequence selector", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.shouldDisplay = () => true;
|
||||||
|
const osSettings = shallow(<FarmbotOsSettings {...p} />);
|
||||||
|
expect(osSettings.find("BootSequenceSelector").length).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,27 +29,31 @@ const OS_RELEASE_NOTES_URL =
|
||||||
|
|
||||||
export class FarmbotOsSettings
|
export class FarmbotOsSettings
|
||||||
extends React.Component<FarmbotOsProps, FarmbotOsState> {
|
extends React.Component<FarmbotOsProps, FarmbotOsState> {
|
||||||
state = { osReleaseNotesHeading: "", osReleaseNotes: "" };
|
state: FarmbotOsState = { allOsReleaseNotes: "" };
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchReleaseNotes(OS_RELEASE_NOTES_URL,
|
this.fetchReleaseNotes(OS_RELEASE_NOTES_URL);
|
||||||
(this.props.bot.hardware.informational_settings
|
|
||||||
.controller_version || "6").split(".")[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchReleaseNotes = (url: string, osMajorVersion: string) => {
|
get osMajorVersion() {
|
||||||
|
return (this.props.bot.hardware.informational_settings
|
||||||
|
.controller_version || "6").split(".")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchReleaseNotes = (url: string) => {
|
||||||
axios
|
axios
|
||||||
.get<string>(url)
|
.get<string>(url)
|
||||||
.then(resp => {
|
.then(resp => this.setState({ allOsReleaseNotes: resp.data }))
|
||||||
const osReleaseNotes = resp.data
|
.catch(() => this.setState({ allOsReleaseNotes: "" }));
|
||||||
.split("# v")
|
}
|
||||||
.filter(x => x.startsWith(osMajorVersion))[0]
|
|
||||||
.split("\n\n").slice(1).join("\n");
|
get osReleaseNotes() {
|
||||||
const osReleaseNotesHeading = "FarmBot OS v" + osMajorVersion;
|
const notes = (this.state.allOsReleaseNotes
|
||||||
this.setState({ osReleaseNotesHeading, osReleaseNotes });
|
.split("# v")
|
||||||
})
|
.filter(x => x.startsWith(this.osMajorVersion))[0] || "")
|
||||||
.catch(() =>
|
.split("\n\n").slice(1).join("\n") || t("Could not get release notes.");
|
||||||
this.setState({ osReleaseNotes: "Could not get release notes." }));
|
const heading = "FarmBot OS v" + this.osMajorVersion;
|
||||||
|
return { heading, notes };
|
||||||
}
|
}
|
||||||
|
|
||||||
changeBot = (e: React.FormEvent<HTMLInputElement>) => {
|
changeBot = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
@ -119,8 +123,8 @@ export class FarmbotOsSettings
|
||||||
|| this.props.isValidFbosConfig}>
|
|| this.props.isValidFbosConfig}>
|
||||||
<FarmbotOsRow
|
<FarmbotOsRow
|
||||||
bot={this.props.bot}
|
bot={this.props.bot}
|
||||||
osReleaseNotesHeading={this.state.osReleaseNotesHeading}
|
osReleaseNotesHeading={this.osReleaseNotes.heading}
|
||||||
osReleaseNotes={this.state.osReleaseNotes}
|
osReleaseNotes={this.osReleaseNotes.notes}
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
sourceFbosConfig={sourceFbosConfig}
|
sourceFbosConfig={sourceFbosConfig}
|
||||||
shouldDisplay={this.props.shouldDisplay}
|
shouldDisplay={this.props.shouldDisplay}
|
||||||
|
|
|
@ -170,8 +170,7 @@ export interface FarmbotOsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FarmbotOsState {
|
export interface FarmbotOsState {
|
||||||
osReleaseNotesHeading: string;
|
allOsReleaseNotes: string;
|
||||||
osReleaseNotes: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface McuInputBoxProps {
|
export interface McuInputBoxProps {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import { BotPosition } from "../../devices/interfaces";
|
import { BotPosition } from "../../devices/interfaces";
|
||||||
import { fakeCropLiveSearchResult } from "../../__test_support__/fake_crop_search_result";
|
import { fakeCropLiveSearchResult } from "../../__test_support__/fake_crop_search_result";
|
||||||
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
|
||||||
|
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||||
|
|
||||||
describe("designer reducer", () => {
|
describe("designer reducer", () => {
|
||||||
const oldState = fakeDesignerState;
|
const oldState = fakeDesignerState;
|
||||||
|
@ -112,4 +113,14 @@ describe("designer reducer", () => {
|
||||||
const newState = designer(state, action);
|
const newState = designer(state, action);
|
||||||
expect(newState.cropSearchInProgress).toEqual(false);
|
expect(newState.cropSearchInProgress).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("starts group sort type trial", () => {
|
||||||
|
const state = oldState();
|
||||||
|
state.tryGroupSortType = undefined;
|
||||||
|
const action: ReduxAction<PointGroupSortType | undefined> = {
|
||||||
|
type: Actions.TRY_SORT_TYPE, payload: "random"
|
||||||
|
};
|
||||||
|
const newState = designer(state, action);
|
||||||
|
expect(newState.tryGroupSortType).toEqual("random");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { AxisNumberProperty, BotSize, TaggedPlant } from "./map/interfaces";
|
||||||
import { SelectionBoxData } from "./map/background";
|
import { SelectionBoxData } from "./map/background";
|
||||||
import { GetWebAppConfigValue } from "../config_storage/actions";
|
import { GetWebAppConfigValue } from "../config_storage/actions";
|
||||||
import {
|
import {
|
||||||
ExecutableType, PlantPointer
|
ExecutableType, PlantPointer, PointGroupSortType
|
||||||
} from "farmbot/dist/resources/api_resources";
|
} from "farmbot/dist/resources/api_resources";
|
||||||
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
|
||||||
import { TimeSettings } from "../interfaces";
|
import { TimeSettings } from "../interfaces";
|
||||||
|
@ -108,6 +108,7 @@ export interface DesignerState {
|
||||||
chosenLocation: BotPosition;
|
chosenLocation: BotPosition;
|
||||||
currentPoint: CurrentPointPayl | undefined;
|
currentPoint: CurrentPointPayl | undefined;
|
||||||
openedSavedGarden: string | undefined;
|
openedSavedGarden: string | undefined;
|
||||||
|
tryGroupSortType: PointGroupSortType | "nn" | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaggedExecutable = TaggedSequence | TaggedRegimen;
|
export type TaggedExecutable = TaggedSequence | TaggedRegimen;
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
} from "./layers/plants/plant_actions";
|
} from "./layers/plants/plant_actions";
|
||||||
import { chooseLocation } from "../move_to";
|
import { chooseLocation } from "../move_to";
|
||||||
import { GroupOrder } from "../point_groups/group_order_visual";
|
import { GroupOrder } from "../point_groups/group_order_visual";
|
||||||
|
import { NNPath } from "../point_groups/paths";
|
||||||
|
|
||||||
export class GardenMap extends
|
export class GardenMap extends
|
||||||
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
React.Component<GardenMapProps, Partial<GardenMapState>> {
|
||||||
|
@ -347,6 +348,8 @@ export class GardenMap extends
|
||||||
GroupOrder = () => <GroupOrder
|
GroupOrder = () => <GroupOrder
|
||||||
plants={this.props.plants}
|
plants={this.props.plants}
|
||||||
mapTransformProps={this.mapTransformProps} />
|
mapTransformProps={this.mapTransformProps} />
|
||||||
|
NNPath = () => <NNPath plants={this.props.plants}
|
||||||
|
mapTransformProps={this.mapTransformProps} />
|
||||||
Bugs = () => showBugs() ? <Bugs mapTransformProps={this.mapTransformProps}
|
Bugs = () => showBugs() ? <Bugs mapTransformProps={this.mapTransformProps}
|
||||||
botSize={this.props.botSize} /> : <g />
|
botSize={this.props.botSize} /> : <g />
|
||||||
|
|
||||||
|
@ -370,6 +373,7 @@ export class GardenMap extends
|
||||||
<this.TargetCoordinate />
|
<this.TargetCoordinate />
|
||||||
<this.DrawnPoint />
|
<this.DrawnPoint />
|
||||||
<this.GroupOrder />
|
<this.GroupOrder />
|
||||||
|
<this.NNPath />
|
||||||
<this.Bugs />
|
<this.Bugs />
|
||||||
</svg>
|
</svg>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -54,6 +54,7 @@ export interface GardenPlantProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
plant: Readonly<TaggedPlant>;
|
plant: Readonly<TaggedPlant>;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
editing: boolean;
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
zoomLvl: number;
|
zoomLvl: number;
|
||||||
activeDragXY: BotPosition | undefined;
|
activeDragXY: BotPosition | undefined;
|
||||||
|
|
|
@ -14,6 +14,7 @@ describe("<GardenPlant/>", () => {
|
||||||
mapTransformProps: fakeMapTransformProps(),
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
plant: fakePlant(),
|
plant: fakePlant(),
|
||||||
selected: false,
|
selected: false,
|
||||||
|
editing: false,
|
||||||
multiselected: false,
|
multiselected: false,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class GardenPlant extends
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { selected, dragging, plant, multiselected, mapTransformProps,
|
const { selected, dragging, plant, multiselected, mapTransformProps,
|
||||||
activeDragXY, zoomLvl, animate } = this.props;
|
activeDragXY, zoomLvl, animate, editing } = this.props;
|
||||||
const { id, radius, x, y } = plant.body;
|
const { id, radius, x, y } = plant.body;
|
||||||
const { icon } = this.state;
|
const { icon } = this.state;
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ export class GardenPlant extends
|
||||||
fill={Color.soilCloud}
|
fill={Color.soilCloud}
|
||||||
fillOpacity={0} />}
|
fillOpacity={0} />}
|
||||||
|
|
||||||
{multiselected &&
|
{multiselected && !editing &&
|
||||||
<g id="selected-plant-indicator">
|
<g id="selected-plant-indicator">
|
||||||
<Circle
|
<Circle
|
||||||
className={`plant-indicator ${animate ? "animate" : ""}`}
|
className={`plant-indicator ${animate ? "animate" : ""}`}
|
||||||
|
|
|
@ -32,6 +32,7 @@ export function PlantLayer(props: PlantLayerProps) {
|
||||||
mapTransformProps={mapTransformProps}
|
mapTransformProps={mapTransformProps}
|
||||||
plant={p}
|
plant={p}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
|
editing={editing}
|
||||||
multiselected={multiselected}
|
multiselected={multiselected}
|
||||||
dragging={selected && dragging && editing}
|
dragging={selected && dragging && editing}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
|
|
|
@ -8,6 +8,13 @@ jest.mock("../../actions", () => ({
|
||||||
toggleHoveredPlant: jest.fn()
|
toggleHoveredPlant: jest.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let mockDev = false;
|
||||||
|
jest.mock("../../../account/dev/dev_support", () => ({
|
||||||
|
DevSettings: {
|
||||||
|
futureFeaturesEnabled: () => mockDev,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { GroupDetailActive } from "../group_detail_active";
|
import { GroupDetailActive } from "../group_detail_active";
|
||||||
import { mount, shallow } from "enzyme";
|
import { mount, shallow } from "enzyme";
|
||||||
|
@ -81,4 +88,19 @@ describe("<GroupDetailActive/>", () => {
|
||||||
el.componentWillUnmount && el.componentWillUnmount();
|
el.componentWillUnmount && el.componentWillUnmount();
|
||||||
expect(clearInterval).toHaveBeenCalledWith(123);
|
expect(clearInterval).toHaveBeenCalledWith(123);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows paths", () => {
|
||||||
|
mockDev = true;
|
||||||
|
const p = fakeProps();
|
||||||
|
p.plants = [fakePlant(), fakePlant()];
|
||||||
|
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||||
|
expect(wrapper.text().toLowerCase()).toContain("optimized");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't show paths", () => {
|
||||||
|
mockDev = false;
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = mount(<GroupDetailActive {...p} />);
|
||||||
|
expect(wrapper.text().toLowerCase()).not.toContain("optimized");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { mount, shallow } from "enzyme";
|
||||||
|
import { PathInfoBar, nn, NNPath, PathInfoBarProps } from "../paths";
|
||||||
|
import {
|
||||||
|
fakePlant, fakePointGroup
|
||||||
|
} from "../../../__test_support__/fake_state/resources";
|
||||||
|
import {
|
||||||
|
fakeMapTransformProps
|
||||||
|
} from "../../../__test_support__/map_transform_props";
|
||||||
|
import { Actions } from "../../../constants";
|
||||||
|
import { edit } from "../../../api/crud";
|
||||||
|
import { error } from "../../../toast/toast";
|
||||||
|
|
||||||
|
describe("<PathInfoBar />", () => {
|
||||||
|
const fakeProps = (): PathInfoBarProps => ({
|
||||||
|
sortTypeKey: "random",
|
||||||
|
dispatch: jest.fn(),
|
||||||
|
group: fakePointGroup(),
|
||||||
|
pathData: { random: 123 },
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hovers path", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = shallow(<PathInfoBar {...p} />);
|
||||||
|
wrapper.simulate("mouseEnter");
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.TRY_SORT_TYPE, payload: "random"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unhovers path", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = shallow(<PathInfoBar {...p} />);
|
||||||
|
wrapper.simulate("mouseLeave");
|
||||||
|
expect(p.dispatch).toHaveBeenCalledWith({
|
||||||
|
type: Actions.TRY_SORT_TYPE, payload: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selects path", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
const wrapper = shallow(<PathInfoBar {...p} />);
|
||||||
|
wrapper.simulate("click");
|
||||||
|
expect(edit).toHaveBeenCalledWith(p.group, { sort_type: "random" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selects new path", () => {
|
||||||
|
const p = fakeProps();
|
||||||
|
p.sortTypeKey = "nn";
|
||||||
|
const wrapper = shallow(<PathInfoBar {...p} />);
|
||||||
|
wrapper.simulate("click");
|
||||||
|
expect(edit).not.toHaveBeenCalled();
|
||||||
|
expect(error).toHaveBeenCalledWith("Not supported yet.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("nearest neighbor algorithm", () => {
|
||||||
|
it("returns optimized array", () => {
|
||||||
|
const p1 = fakePlant();
|
||||||
|
p1.body.x = 100;
|
||||||
|
p1.body.y = 100;
|
||||||
|
const p2 = fakePlant();
|
||||||
|
p2.body.x = 200;
|
||||||
|
p2.body.y = 200;
|
||||||
|
const p3 = fakePlant();
|
||||||
|
p3.body.x = 175;
|
||||||
|
p3.body.y = 1000;
|
||||||
|
const p4 = fakePlant();
|
||||||
|
p4.body.x = 1000;
|
||||||
|
p4.body.y = 150;
|
||||||
|
const points = nn([p4, p2, p3, p1]);
|
||||||
|
expect(points).toEqual([p1, p2, p3, p4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("<NNPath />", () => {
|
||||||
|
const fakeProps = () => ({
|
||||||
|
plants: [],
|
||||||
|
mapTransformProps: fakeMapTransformProps(),
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't render optimized path", () => {
|
||||||
|
const wrapper = mount(<NNPath {...fakeProps()} />);
|
||||||
|
expect(wrapper.html()).toEqual("<g></g>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders optimized path", () => {
|
||||||
|
localStorage.setItem("try_it", "ok");
|
||||||
|
const wrapper = mount(<NNPath {...fakeProps()} />);
|
||||||
|
expect(wrapper.html()).not.toEqual("<g></g>");
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,6 +13,8 @@ import { TaggedPlant } from "../map/interfaces";
|
||||||
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";
|
import { PointGroupSortSelector, sortGroupBy } from "./point_group_sort_selector";
|
||||||
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||||
import { PointGroupItem } from "./point_group_item";
|
import { PointGroupItem } from "./point_group_item";
|
||||||
|
import { Paths } from "./paths";
|
||||||
|
import { DevSettings } from "../../account/dev/dev_support";
|
||||||
|
|
||||||
interface GroupDetailActiveProps {
|
interface GroupDetailActiveProps {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
@ -97,6 +99,11 @@ export class GroupDetailActive
|
||||||
<div className="groups-list-wrapper">
|
<div className="groups-list-wrapper">
|
||||||
{this.icons}
|
{this.icons}
|
||||||
</div>
|
</div>
|
||||||
|
{DevSettings.futureFeaturesEnabled() &&
|
||||||
|
<Paths
|
||||||
|
points={this.props.plants}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
group={this.props.group} />}
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
className="groups-delete-btn"
|
className="groups-delete-btn"
|
||||||
dispatch={this.props.dispatch}
|
dispatch={this.props.dispatch}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { isUndefined } from "lodash";
|
||||||
import { sortGroupBy } from "./point_group_sort_selector";
|
import { sortGroupBy } from "./point_group_sort_selector";
|
||||||
import { Color } from "../../ui";
|
import { Color } from "../../ui";
|
||||||
import { transformXY } from "../map/util";
|
import { transformXY } from "../map/util";
|
||||||
|
import { nn } from "./paths";
|
||||||
|
|
||||||
export interface GroupOrderProps {
|
export interface GroupOrderProps {
|
||||||
plants: TaggedPlant[];
|
plants: TaggedPlant[];
|
||||||
|
@ -14,23 +15,41 @@ export interface GroupOrderProps {
|
||||||
|
|
||||||
const sortedPointCoordinates =
|
const sortedPointCoordinates =
|
||||||
(plants: TaggedPlant[]): { x: number, y: number }[] => {
|
(plants: TaggedPlant[]): { x: number, y: number }[] => {
|
||||||
const group = fetchGroupFromUrl(store.getState().resources.index);
|
const { resources } = store.getState();
|
||||||
|
const group = fetchGroupFromUrl(resources.index);
|
||||||
if (isUndefined(group)) { return []; }
|
if (isUndefined(group)) { return []; }
|
||||||
const groupPlants = plants
|
const groupPlants = plants
|
||||||
.filter(p => group.body.point_ids.includes(p.body.id || 0));
|
.filter(p => group.body.point_ids.includes(p.body.id || 0));
|
||||||
return sortGroupBy(group.body.sort_type, groupPlants)
|
const groupSortType = resources.consumers.farm_designer.tryGroupSortType
|
||||||
.map(p => ({ x: p.body.x, y: p.body.y }));
|
|| group.body.sort_type;
|
||||||
|
const sorted = groupSortType == "nn"
|
||||||
|
? nn(groupPlants)
|
||||||
|
: sortGroupBy(groupSortType, groupPlants);
|
||||||
|
return sorted.map(p => ({ x: p.body.x, y: p.body.y }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupOrder = (props: GroupOrderProps) => {
|
export interface PointsPathLineProps {
|
||||||
const points = sortedPointCoordinates(props.plants);
|
orderedPoints: { x: number, y: number }[];
|
||||||
return <g id="group-order"
|
mapTransformProps: MapTransformProps;
|
||||||
stroke={Color.mediumGray} strokeWidth={3} strokeDasharray={12}>
|
color?: Color;
|
||||||
{points.map((p, i) => {
|
dash?: number;
|
||||||
const prev = i > 0 ? points[i - 1] : p;
|
strokeWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PointsPathLine = (props: PointsPathLineProps) =>
|
||||||
|
<g id="group-order"
|
||||||
|
stroke={props.color || Color.mediumGray}
|
||||||
|
strokeWidth={props.strokeWidth || 3}
|
||||||
|
strokeDasharray={props.dash || 12}>
|
||||||
|
{props.orderedPoints.map((p, i) => {
|
||||||
|
const prev = i > 0 ? props.orderedPoints[i - 1] : p;
|
||||||
const one = transformXY(prev.x, prev.y, props.mapTransformProps);
|
const one = transformXY(prev.x, prev.y, props.mapTransformProps);
|
||||||
const two = transformXY(p.x, p.y, props.mapTransformProps);
|
const two = transformXY(p.x, p.y, props.mapTransformProps);
|
||||||
return <line key={i} x1={one.qx} y1={one.qy} x2={two.qx} y2={two.qy} />;
|
return <line key={i} x1={one.qx} y1={one.qy} x2={two.qx} y2={two.qy} />;
|
||||||
})}
|
})}
|
||||||
</g>;
|
</g>;
|
||||||
};
|
|
||||||
|
export const GroupOrder = (props: GroupOrderProps) =>
|
||||||
|
<PointsPathLine
|
||||||
|
orderedPoints={sortedPointCoordinates(props.plants)}
|
||||||
|
mapTransformProps={props.mapTransformProps} />;
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { TaggedPlant, MapTransformProps } from "../map/interfaces";
|
||||||
|
import { sortGroupBy, sortOptionsTable } from "./point_group_sort_selector";
|
||||||
|
import { sortBy } from "lodash";
|
||||||
|
import { PointsPathLine } from "./group_order_visual";
|
||||||
|
import { Color } from "../../ui";
|
||||||
|
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||||
|
import { t } from "../../i18next_wrapper";
|
||||||
|
import { Actions } from "../../constants";
|
||||||
|
import { edit } from "../../api/crud";
|
||||||
|
import { TaggedPointGroup } from "farmbot";
|
||||||
|
import { error } from "../../toast/toast";
|
||||||
|
|
||||||
|
const xy = (point: TaggedPlant) => ({ x: point.body.x, y: point.body.y });
|
||||||
|
|
||||||
|
const distance = (p1: { x: number, y: number }, p2: { x: number, y: number }) =>
|
||||||
|
Math.pow(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2), 0.5);
|
||||||
|
|
||||||
|
const pathDistance = (points: TaggedPlant[]) => {
|
||||||
|
let total = 0;
|
||||||
|
let prev: { x: number, y: number } | undefined = undefined;
|
||||||
|
points.map(xy)
|
||||||
|
.map(p => {
|
||||||
|
prev ? total += distance(p, prev) : 0;
|
||||||
|
prev = p;
|
||||||
|
});
|
||||||
|
return Math.round(total);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findNearest =
|
||||||
|
(from: { x: number, y: number }, available: TaggedPlant[]) => {
|
||||||
|
const distances = available.map(p => ({
|
||||||
|
point: p, distance: distance(xy(p), from)
|
||||||
|
}));
|
||||||
|
return sortBy(distances, "distance")[0].point;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nn = (points: TaggedPlant[]) => {
|
||||||
|
let available = points.slice(0);
|
||||||
|
const ordered: TaggedPlant[] = [];
|
||||||
|
let from = { x: 0, y: 0 };
|
||||||
|
points.map(() => {
|
||||||
|
const nearest = findNearest(from, available);
|
||||||
|
ordered.push(nearest);
|
||||||
|
from = { x: nearest.body.x, y: nearest.body.y };
|
||||||
|
available = available.filter(p => p.uuid !== nearest.uuid);
|
||||||
|
});
|
||||||
|
return ordered;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SORT_TYPES: (PointGroupSortType | "nn")[] = [
|
||||||
|
"random", "xy_ascending", "xy_descending", "yx_ascending", "yx_descending"];
|
||||||
|
|
||||||
|
export interface PathInfoBarProps {
|
||||||
|
sortTypeKey: PointGroupSortType | "nn";
|
||||||
|
dispatch: Function;
|
||||||
|
group: TaggedPointGroup;
|
||||||
|
pathData: { [key: string]: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PathInfoBar = (props: PathInfoBarProps) => {
|
||||||
|
const { sortTypeKey, dispatch, group } = props;
|
||||||
|
const pathLength = props.pathData[sortTypeKey];
|
||||||
|
const maxLength = Math.max(...Object.values(props.pathData));
|
||||||
|
const normalizedLength = pathLength / maxLength * 100;
|
||||||
|
const sortLabel =
|
||||||
|
sortTypeKey == "nn" ? "Optimized" : sortOptionsTable()[sortTypeKey];
|
||||||
|
return <div className={"sort-path-info-bar"}
|
||||||
|
onMouseEnter={() =>
|
||||||
|
dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })}
|
||||||
|
onMouseLeave={() =>
|
||||||
|
dispatch({ type: Actions.TRY_SORT_TYPE, payload: undefined })}
|
||||||
|
onClick={() =>
|
||||||
|
sortTypeKey == "nn"
|
||||||
|
? error(t("Not supported yet."))
|
||||||
|
: dispatch(edit(group, { sort_type: sortTypeKey }))}
|
||||||
|
style={{ width: `${normalizedLength}%` }}>
|
||||||
|
{`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PathsProps {
|
||||||
|
points: TaggedPlant[];
|
||||||
|
dispatch: Function;
|
||||||
|
group: TaggedPointGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PathsState {
|
||||||
|
pathData: { [key: string]: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Paths extends React.Component<PathsProps, PathsState> {
|
||||||
|
state: PathsState = { pathData: {} };
|
||||||
|
|
||||||
|
generatePathData = (points: TaggedPlant[]) => {
|
||||||
|
SORT_TYPES.map((sortType: PointGroupSortType) =>
|
||||||
|
this.state.pathData[sortType] =
|
||||||
|
pathDistance(sortGroupBy(sortType, points)));
|
||||||
|
this.state.pathData.nn = pathDistance(nn(points));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.state.pathData.nn) { this.generatePathData(this.props.points); }
|
||||||
|
return <div>
|
||||||
|
<label>{t("Path lengths by sort type")}</label>
|
||||||
|
{SORT_TYPES.concat("nn").map(st =>
|
||||||
|
<PathInfoBar key={st}
|
||||||
|
sortTypeKey={st}
|
||||||
|
dispatch={this.props.dispatch}
|
||||||
|
group={this.props.group}
|
||||||
|
pathData={this.state.pathData} />)}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NNPathProps {
|
||||||
|
plants: TaggedPlant[];
|
||||||
|
mapTransformProps: MapTransformProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NNPath = (props: NNPathProps) =>
|
||||||
|
localStorage.getItem("try_it") == "ok"
|
||||||
|
? <PointsPathLine
|
||||||
|
color={Color.blue}
|
||||||
|
strokeWidth={2}
|
||||||
|
dash={1}
|
||||||
|
orderedPoints={nn(props.plants).map(xy)}
|
||||||
|
mapTransformProps={props.mapTransformProps} />
|
||||||
|
: <g />;
|
|
@ -11,7 +11,7 @@ interface Props {
|
||||||
value: PointGroupSortType;
|
value: PointGroupSortType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsTable = (): Record<PointGroupSortType, string> => ({
|
export const sortOptionsTable = (): Record<PointGroupSortType, string> => ({
|
||||||
"random": t("Random Order"),
|
"random": t("Random Order"),
|
||||||
"xy_ascending": t("X/Y, Ascending"),
|
"xy_ascending": t("X/Y, Ascending"),
|
||||||
"xy_descending": t("X/Y, Descending"),
|
"xy_descending": t("X/Y, Descending"),
|
||||||
|
@ -21,7 +21,7 @@ const optionsTable = (): Record<PointGroupSortType, string> => ({
|
||||||
|
|
||||||
const optionPlusDescriptions = () =>
|
const optionPlusDescriptions = () =>
|
||||||
(Object
|
(Object
|
||||||
.entries(optionsTable()) as [PointGroupSortType, string][])
|
.entries(sortOptionsTable()) as [PointGroupSortType, string][])
|
||||||
.map(x => ({ label: x[1], value: x[0] }));
|
.map(x => ({ label: x[1], value: x[0] }));
|
||||||
|
|
||||||
const optionList =
|
const optionList =
|
||||||
|
@ -32,7 +32,7 @@ export const isSortType = (x: unknown): x is PointGroupSortType => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const selected = (value: PointGroupSortType) => ({
|
const selected = (value: PointGroupSortType) => ({
|
||||||
label: t(optionsTable()[value] || value),
|
label: t(sortOptionsTable()[value] || value),
|
||||||
value: value
|
value: value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ export function PointGroupSortSelector(p: Props) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<FBSelect
|
<FBSelect
|
||||||
|
key={p.value}
|
||||||
list={optionPlusDescriptions()}
|
list={optionPlusDescriptions()}
|
||||||
selectedItem={selected(p.value as PointGroupSortType)}
|
selectedItem={selected(p.value as PointGroupSortType)}
|
||||||
onChange={sortTypeChange(p.onChange)} />
|
onChange={sortTypeChange(p.onChange)} />
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { cloneDeep } from "lodash";
|
||||||
import { TaggedResource } from "farmbot";
|
import { TaggedResource } from "farmbot";
|
||||||
import { Actions } from "../constants";
|
import { Actions } from "../constants";
|
||||||
import { BotPosition } from "../devices/interfaces";
|
import { BotPosition } from "../devices/interfaces";
|
||||||
|
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
|
||||||
|
|
||||||
export let initialState: DesignerState = {
|
export let initialState: DesignerState = {
|
||||||
selectedPlants: undefined,
|
selectedPlants: undefined,
|
||||||
|
@ -19,6 +20,7 @@ export let initialState: DesignerState = {
|
||||||
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
chosenLocation: { x: undefined, y: undefined, z: undefined },
|
||||||
currentPoint: undefined,
|
currentPoint: undefined,
|
||||||
openedSavedGarden: undefined,
|
openedSavedGarden: undefined,
|
||||||
|
tryGroupSortType: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export let designer = generateReducer<DesignerState>(initialState)
|
export let designer = generateReducer<DesignerState>(initialState)
|
||||||
|
@ -69,4 +71,8 @@ export let designer = generateReducer<DesignerState>(initialState)
|
||||||
.add<string | undefined>(Actions.CHOOSE_SAVED_GARDEN, (s, { payload }) => {
|
.add<string | undefined>(Actions.CHOOSE_SAVED_GARDEN, (s, { payload }) => {
|
||||||
s.openedSavedGarden = payload;
|
s.openedSavedGarden = payload;
|
||||||
return s;
|
return s;
|
||||||
|
})
|
||||||
|
.add<PointGroupSortType | undefined>(Actions.TRY_SORT_TYPE, (s, { payload }) => {
|
||||||
|
s.tryGroupSortType = payload;
|
||||||
|
return s;
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function findBySlug(
|
||||||
return crop || {
|
return crop || {
|
||||||
crop: {
|
crop: {
|
||||||
name: startCase((slug || t("Name")).split("-").join(" ")),
|
name: startCase((slug || t("Name")).split("-").join(" ")),
|
||||||
slug: "slug",
|
slug: slug || "slug",
|
||||||
binomial_name: t("Binomial Name"),
|
binomial_name: t("Binomial Name"),
|
||||||
common_names: [t("Common Names")],
|
common_names: [t("Common Names")],
|
||||||
description: t("Description"),
|
description: t("Description"),
|
||||||
|
|
|
@ -13,20 +13,24 @@ const mockResponse: { promise: Promise<{}> } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock("axios", () => ({
|
jest.mock("axios", () => ({
|
||||||
get: () => mockResponse.promise
|
get: jest.fn(() => mockResponse.promise)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unmock("../cached_crop");
|
jest.unmock("../cached_crop");
|
||||||
import { cachedCrop } from "../cached_crop";
|
import { cachedCrop } from "../cached_crop";
|
||||||
|
import axios from "axios";
|
||||||
|
import { times } from "lodash";
|
||||||
|
|
||||||
describe("cachedIcon()", () => {
|
describe("cachedIcon()", () => {
|
||||||
it("does an HTTP request if the icon can't be found locally", async () => {
|
it("does an HTTP request if the icon can't be found locally", async () => {
|
||||||
|
times(10, () => cachedCrop("lettuce"));
|
||||||
const item1 = await cachedCrop("lettuce");
|
const item1 = await cachedCrop("lettuce");
|
||||||
expect(item1.svg_icon).toContain("<svg>Wow</svg>");
|
expect(item1.svg_icon).toContain("<svg>Wow</svg>");
|
||||||
const item2 = await cachedCrop("lettuce");
|
const item2 = await cachedCrop("lettuce");
|
||||||
expect(item2.slug).toBe(item1.slug);
|
expect(item2.slug).toBe(item1.slug);
|
||||||
expect(item2.svg_icon).toBe(item1.svg_icon);
|
expect(item2.svg_icon).toBe(item1.svg_icon);
|
||||||
expect(item2.spread).toBe(undefined);
|
expect(item2.spread).toBe(undefined);
|
||||||
|
expect(axios.get).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles unexpected responses from OpenFarm", async () => {
|
it("handles unexpected responses from OpenFarm", async () => {
|
||||||
|
|
|
@ -37,7 +37,8 @@ function localStorageIconSet(icon: OFIcon): void {
|
||||||
* and the garlic icon is not cached locally, and you try to render 10 garlic
|
* and the garlic icon is not cached locally, and you try to render 10 garlic
|
||||||
* icons in the first 100ms, and HTTP requests take more than 100ms, you will
|
* icons in the first 100ms, and HTTP requests take more than 100ms, you will
|
||||||
* end up performing 10 HTTP requests at application start time. Not very
|
* end up performing 10 HTTP requests at application start time. Not very
|
||||||
* efficient */
|
* efficient.
|
||||||
|
* SOLUTION: Keep a record of open requests to avoid duplicate requests. */
|
||||||
const promiseCache: Dictionary<Promise<Readonly<OFCropAttrs>>> = {};
|
const promiseCache: Dictionary<Promise<Readonly<OFCropAttrs>>> = {};
|
||||||
|
|
||||||
const cacheTheIcon = (slug: string) =>
|
const cacheTheIcon = (slug: string) =>
|
||||||
|
@ -60,6 +61,8 @@ const cacheTheIcon = (slug: string) =>
|
||||||
|
|
||||||
function HTTPIconFetch(slug: string) {
|
function HTTPIconFetch(slug: string) {
|
||||||
const url = OpenFarmAPI.OFBaseURL + slug;
|
const url = OpenFarmAPI.OFBaseURL + slug;
|
||||||
|
// Avoid duplicate requests.
|
||||||
|
if (promiseCache[url]) { return promiseCache[url]; }
|
||||||
promiseCache[url] = axios
|
promiseCache[url] = axios
|
||||||
.get<OFCropResponse>(url)
|
.get<OFCropResponse>(url)
|
||||||
.then(cacheTheIcon(slug), cacheTheIcon(slug));
|
.then(cacheTheIcon(slug), cacheTheIcon(slug));
|
||||||
|
|
|
@ -8,7 +8,13 @@ jest.mock("../set_active_sequence_by_name", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../../api/crud", () => ({
|
jest.mock("../../api/crud", () => ({
|
||||||
init: jest.fn()
|
init: jest.fn(),
|
||||||
|
destroy: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mockDev = false;
|
||||||
|
jest.mock("../../account/dev/dev_support", () => ({
|
||||||
|
DevSettings: { quickDeleteEnabled: () => mockDev, }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
@ -17,7 +23,7 @@ import { SequencesList } from "../sequences_list";
|
||||||
import { fakeSequence } from "../../__test_support__/fake_state/resources";
|
import { fakeSequence } from "../../__test_support__/fake_state/resources";
|
||||||
import { SequencesListProps } from "../interfaces";
|
import { SequencesListProps } from "../interfaces";
|
||||||
import { Actions } from "../../constants";
|
import { Actions } from "../../constants";
|
||||||
import { init } from "../../api/crud";
|
import { init, destroy } from "../../api/crud";
|
||||||
import { push } from "../../history";
|
import { push } from "../../history";
|
||||||
import { resourceUsageList } from "../../resources/in_use";
|
import { resourceUsageList } from "../../resources/in_use";
|
||||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||||
|
@ -25,6 +31,7 @@ import { resourceReducer } from "../../resources/reducer";
|
||||||
import { resourceReady } from "../../sync/actions";
|
import { resourceReady } from "../../sync/actions";
|
||||||
import { setActiveSequenceByName } from "../set_active_sequence_by_name";
|
import { setActiveSequenceByName } from "../set_active_sequence_by_name";
|
||||||
import { inputEvent } from "../../__test_support__/fake_input_event";
|
import { inputEvent } from "../../__test_support__/fake_input_event";
|
||||||
|
import { Link } from "../../link";
|
||||||
|
|
||||||
describe("<SequencesList />", () => {
|
describe("<SequencesList />", () => {
|
||||||
const fakeSequences = () => {
|
const fakeSequences = () => {
|
||||||
|
@ -113,8 +120,28 @@ describe("<SequencesList />", () => {
|
||||||
|
|
||||||
it("opens sequence", () => {
|
it("opens sequence", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const wrapper = shallow(<SequencesList {...p} />);
|
const wrapper = mount(<SequencesList {...p} />);
|
||||||
wrapper.find("Link").first().simulate("click");
|
wrapper.find(Link).first().simulate("click");
|
||||||
expect(setActiveSequenceByName).toHaveBeenCalled();
|
expect(setActiveSequenceByName).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("doesn't delete sequence", () => {
|
||||||
|
mockDev = false;
|
||||||
|
const p = fakeProps([fakeSequence()]);
|
||||||
|
const wrapper = mount(<SequencesList {...p} />);
|
||||||
|
const button = wrapper.find("button").last();
|
||||||
|
expect(button.hasClass("quick-del")).toBeFalsy();
|
||||||
|
button.simulate("click");
|
||||||
|
expect(destroy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes sequence", () => {
|
||||||
|
mockDev = true;
|
||||||
|
const p = fakeProps([fakeSequence()]);
|
||||||
|
const wrapper = mount(<SequencesList {...p} />);
|
||||||
|
const button = wrapper.find("button").last();
|
||||||
|
expect(button.hasClass("quick-del")).toBeTruthy();
|
||||||
|
button.simulate("click");
|
||||||
|
expect(destroy).toHaveBeenCalledWith(p.sequences[0].uuid);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { SequencesListProps, SequencesListState } from "./interfaces";
|
||||||
import { sortResourcesById, urlFriendly, lastUrlChunk } from "../util";
|
import { sortResourcesById, urlFriendly, lastUrlChunk } from "../util";
|
||||||
import { Row, Col } from "../ui/index";
|
import { Row, Col } from "../ui/index";
|
||||||
import { TaggedSequence } from "farmbot";
|
import { TaggedSequence } from "farmbot";
|
||||||
import { init } from "../api/crud";
|
import { init, destroy } from "../api/crud";
|
||||||
import { Content } from "../constants";
|
import { Content } from "../constants";
|
||||||
import { StepDragger, NULL_DRAGGER_ID } from "../draggable/step_dragger";
|
import { StepDragger, NULL_DRAGGER_ID } from "../draggable/step_dragger";
|
||||||
import { Link } from "../link";
|
import { Link } from "../link";
|
||||||
|
@ -13,48 +13,79 @@ import { UUID, VariableNameSet } from "../resources/interfaces";
|
||||||
import { variableList } from "./locals_list/variable_support";
|
import { variableList } from "./locals_list/variable_support";
|
||||||
import { t } from "../i18next_wrapper";
|
import { t } from "../i18next_wrapper";
|
||||||
import { EmptyStateWrapper, EmptyStateGraphic } from "../ui/empty_state_wrapper";
|
import { EmptyStateWrapper, EmptyStateGraphic } from "../ui/empty_state_wrapper";
|
||||||
|
import { DevSettings } from "../account/dev/dev_support";
|
||||||
|
|
||||||
const filterFn = (searchTerm: string) => (seq: TaggedSequence): boolean => seq
|
const filterFn = (searchTerm: string) => (seq: TaggedSequence): boolean =>
|
||||||
.body
|
seq.body.name.toLowerCase().includes(searchTerm);
|
||||||
.name
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchTerm);
|
|
||||||
|
|
||||||
const sequenceList = (props: {
|
interface SequenceButtonWrapperProps {
|
||||||
dispatch: Function,
|
ts: TaggedSequence;
|
||||||
resourceUsage: Record<UUID, boolean | undefined>,
|
dispatch: Function;
|
||||||
sequenceMetas: Record<UUID, VariableNameSet | undefined>
|
variableData: VariableNameSet | undefined;
|
||||||
}) =>
|
children: React.ReactChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sequence list item wrapper for drag action and link to sequence. */
|
||||||
|
const SequenceButtonWrapper = (props: SequenceButtonWrapperProps) =>
|
||||||
|
<div className="sequence-list-item" key={props.ts.uuid}>
|
||||||
|
<StepDragger
|
||||||
|
dispatch={props.dispatch}
|
||||||
|
step={{
|
||||||
|
kind: "execute",
|
||||||
|
args: { sequence_id: props.ts.body.id || 0 },
|
||||||
|
body: variableList(props.variableData)
|
||||||
|
}}
|
||||||
|
intent="step_splice"
|
||||||
|
draggerId={NULL_DRAGGER_ID}>
|
||||||
|
<Link
|
||||||
|
to={`/app/sequences/${urlFriendly(props.ts.body.name) || ""}`}
|
||||||
|
key={props.ts.uuid}
|
||||||
|
onClick={setActiveSequenceByName}>
|
||||||
|
{props.children}
|
||||||
|
</Link>
|
||||||
|
</StepDragger>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
interface SequenceButtonProps {
|
||||||
|
ts: TaggedSequence;
|
||||||
|
inUse: boolean;
|
||||||
|
deleteFunc?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sequence list item label and indicators. */
|
||||||
|
const SequenceButton = (props: SequenceButtonProps) => {
|
||||||
|
const { color, name } = props.ts.body;
|
||||||
|
const css = [`fb-button`, `block`, `full-width`, `${color || "purple"}`];
|
||||||
|
lastUrlChunk() === urlFriendly(name) && css.push("active");
|
||||||
|
props.deleteFunc && css.push("quick-del");
|
||||||
|
const nameWithSaveIndicator = name + (props.ts.specialStatus ? "*" : "");
|
||||||
|
return <button className={css.join(" ")} draggable={true}
|
||||||
|
onClick={props.deleteFunc}>
|
||||||
|
<label>{nameWithSaveIndicator}</label>
|
||||||
|
{props.inUse &&
|
||||||
|
<i className="in-use fa fa-hdd-o" title={t(Content.IN_USE)} />}
|
||||||
|
</button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SequenceListItemProps {
|
||||||
|
dispatch: Function;
|
||||||
|
resourceUsage: Record<UUID, boolean | undefined>;
|
||||||
|
sequenceMetas: Record<UUID, VariableNameSet | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SequenceListItem = (props: SequenceListItemProps) =>
|
||||||
(ts: TaggedSequence) => {
|
(ts: TaggedSequence) => {
|
||||||
const css =
|
const inUse = !!props.resourceUsage[ts.uuid];
|
||||||
[`fb-button`, `block`, `full-width`, `${ts.body.color || "purple"}`];
|
const variableData = props.sequenceMetas[ts.uuid];
|
||||||
lastUrlChunk() === urlFriendly(ts.body.name) && css.push("active");
|
const deleteSeq = () => props.dispatch(destroy(ts.uuid));
|
||||||
const { uuid } = ts;
|
|
||||||
const nameWithSaveIndicator = ts.body.name + (ts.specialStatus ? "*" : "");
|
|
||||||
const inUse = !!props.resourceUsage[uuid];
|
|
||||||
const variableData = props.sequenceMetas[uuid];
|
|
||||||
|
|
||||||
return <div className="sequence-list-items" key={uuid}>
|
return <div className="sequence-list-item" key={ts.uuid}>
|
||||||
<StepDragger
|
{DevSettings.quickDeleteEnabled()
|
||||||
dispatch={props.dispatch}
|
? <SequenceButton ts={ts} inUse={inUse} deleteFunc={deleteSeq} />
|
||||||
step={{
|
: <SequenceButtonWrapper
|
||||||
kind: "execute",
|
ts={ts} dispatch={props.dispatch} variableData={variableData}>
|
||||||
args: { sequence_id: ts.body.id || 0 },
|
<SequenceButton ts={ts} inUse={inUse} />
|
||||||
body: variableList(variableData)
|
</SequenceButtonWrapper>}
|
||||||
}}
|
|
||||||
intent="step_splice"
|
|
||||||
draggerId={NULL_DRAGGER_ID}>
|
|
||||||
<Link
|
|
||||||
to={`/app/sequences/${urlFriendly(ts.body.name) || ""}`}
|
|
||||||
key={uuid}
|
|
||||||
onClick={setActiveSequenceByName}>
|
|
||||||
<button className={css.join(" ")} draggable={true}>
|
|
||||||
<label>{nameWithSaveIndicator}</label>
|
|
||||||
{inUse &&
|
|
||||||
<i className="in-use fa fa-hdd-o" title={t(Content.IN_USE)} />}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</StepDragger>
|
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -126,7 +157,9 @@ export class SequencesList extends
|
||||||
<div className="sequence-list">
|
<div className="sequence-list">
|
||||||
{sortResourcesById(sequences)
|
{sortResourcesById(sequences)
|
||||||
.filter(filterFn(searchTerm))
|
.filter(filterFn(searchTerm))
|
||||||
.map(sequenceList({ dispatch, resourceUsage, sequenceMetas }))}
|
.map(SequenceListItem({
|
||||||
|
dispatch, resourceUsage, sequenceMetas
|
||||||
|
}))}
|
||||||
</div>}
|
</div>}
|
||||||
</EmptyStateWrapper>
|
</EmptyStateWrapper>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -141,7 +141,7 @@ namespace :api do
|
||||||
puts "Setting new support target to #{data.to_s}"
|
puts "Setting new support target to #{data.to_s}"
|
||||||
GlobalConfig # Set the new oldest support version.
|
GlobalConfig # Set the new oldest support version.
|
||||||
.find_by(key: "FBOS_END_OF_LIFE_VERSION")
|
.find_by(key: "FBOS_END_OF_LIFE_VERSION")
|
||||||
.update_attributes!(value: data.to_s)
|
.update!(value: data.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ FRACTION_DELIM = "/"
|
||||||
def open_json(url)
|
def open_json(url)
|
||||||
begin
|
begin
|
||||||
JSON.parse(open(url).read)
|
JSON.parse(open(url).read)
|
||||||
rescue OpenURI::HTTPError => exception
|
rescue *[OpenURI::HTTPError, SocketError] => exception
|
||||||
puts exception.message
|
puts exception.message
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
|
@ -80,7 +80,7 @@ end
|
||||||
|
|
||||||
# Fetch a page of build coverage report results.
|
# Fetch a page of build coverage report results.
|
||||||
def fetch_builds_for_page(page_number)
|
def fetch_builds_for_page(page_number)
|
||||||
open_json("#{LATEST_COV_URL}?page=#{page_number}")["builds"]
|
open_json("#{LATEST_COV_URL}?page=#{page_number}")["builds"] || []
|
||||||
end
|
end
|
||||||
|
|
||||||
# Number of coverage build data pages required to fetch the desired build count.
|
# Number of coverage build data pages required to fetch the desired build count.
|
||||||
|
|
|
@ -86,7 +86,7 @@ describe Api::FbosConfigsController do
|
||||||
it 'resets everything to the defaults' do
|
it 'resets everything to the defaults' do
|
||||||
sign_in user
|
sign_in user
|
||||||
old_conf = device.fbos_config
|
old_conf = device.fbos_config
|
||||||
old_conf.update_attributes(arduino_debug_messages: 23)
|
old_conf.update(arduino_debug_messages: 23)
|
||||||
delete :destroy, params: {}
|
delete :destroy, params: {}
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
new_conf = device.reload.fbos_config
|
new_conf = device.reload.fbos_config
|
||||||
|
|
|
@ -138,7 +138,7 @@ describe Api::FirmwareConfigsController do
|
||||||
it 'resets everything to the defaults' do
|
it 'resets everything to the defaults' do
|
||||||
sign_in user
|
sign_in user
|
||||||
old_conf = device.firmware_config
|
old_conf = device.firmware_config
|
||||||
old_conf.update_attributes(pin_guard_5_pin_nr: 23)
|
old_conf.update(pin_guard_5_pin_nr: 23)
|
||||||
delete :destroy, params: {}
|
delete :destroy, params: {}
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
new_conf = device.reload.firmware_config
|
new_conf = device.reload.firmware_config
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue