commit
1a35dfb136
|
@ -25,3 +25,7 @@ public/webpack
|
|||
public/webpack/*
|
||||
scratchpad.rb
|
||||
tmp
|
||||
|
||||
# Ignore master key for decrypting credentials and more.
|
||||
/config/master.key
|
||||
config/credentials.yml.enc
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
#########################
|
||||
## Sass Lint Config File
|
||||
#########################
|
||||
# Linter Options
|
||||
options:
|
||||
# Don't merge default rules
|
||||
merge-default-rules: false
|
||||
# Raise an error if warnings are generated
|
||||
max-warnings: 0
|
||||
# File Options
|
||||
files:
|
||||
include: 'webpack/css/**/*.s+(a|c)ss'
|
||||
# Rule Configuration
|
||||
rules:
|
||||
extends-before-mixins: 2
|
||||
extends-before-declarations: 2
|
||||
placeholder-in-extend: 2
|
||||
mixins-before-declarations: 2
|
||||
no-warn: 1
|
||||
no-debug: 1
|
||||
no-ids: 2
|
||||
hex-notation:
|
||||
- 2
|
||||
-
|
||||
style: lowercase
|
||||
indentation:
|
||||
- 2
|
||||
-
|
||||
size: 2
|
||||
property-sort-order:
|
||||
- 1
|
||||
-
|
||||
order:
|
||||
- content
|
||||
- display
|
||||
- position
|
||||
- top
|
||||
- left
|
||||
- bottom
|
||||
- right
|
||||
- z-index
|
||||
- margin
|
||||
ignore-custom-properties: true
|
||||
variable-for-property:
|
||||
- 2
|
||||
-
|
||||
properties:
|
||||
- color
|
|
@ -32,6 +32,8 @@ before_script:
|
|||
- bundle exec rake db:create db:migrate
|
||||
script:
|
||||
- bundle exec rspec --fail-fast=3
|
||||
- npm run tslint
|
||||
- npm run sass-lint
|
||||
- npm run typecheck
|
||||
- npm run test-slow
|
||||
- npm run coverage
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -31,6 +31,7 @@ gem "webpack-rails"
|
|||
# Still working out the bugs. - RC 5 Jul 18
|
||||
gem "rabbitmq_http_api_client"
|
||||
gem "zero_downtime_migrations"
|
||||
# gem "digest-murmurhash"
|
||||
|
||||
group :development, :test do
|
||||
gem "thin"
|
||||
|
|
191
Gemfile.lock
191
Gemfile.lock
|
@ -7,25 +7,25 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (5.2.0)
|
||||
actionpack (= 5.2.0)
|
||||
actioncable (5.2.1)
|
||||
actionpack (= 5.2.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailer (5.2.0)
|
||||
actionpack (= 5.2.0)
|
||||
actionview (= 5.2.0)
|
||||
activejob (= 5.2.0)
|
||||
actionmailer (5.2.1)
|
||||
actionpack (= 5.2.1)
|
||||
actionview (= 5.2.1)
|
||||
activejob (= 5.2.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.0)
|
||||
actionview (= 5.2.0)
|
||||
activesupport (= 5.2.0)
|
||||
actionpack (5.2.1)
|
||||
actionview (= 5.2.1)
|
||||
activesupport (= 5.2.1)
|
||||
rack (~> 2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.0)
|
||||
activesupport (= 5.2.0)
|
||||
actionview (5.2.1)
|
||||
activesupport (= 5.2.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -35,20 +35,20 @@ GEM
|
|||
activemodel (>= 4.1, < 6)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (5.2.0)
|
||||
activesupport (= 5.2.0)
|
||||
activejob (5.2.1)
|
||||
activesupport (= 5.2.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.0)
|
||||
activesupport (= 5.2.0)
|
||||
activerecord (5.2.0)
|
||||
activemodel (= 5.2.0)
|
||||
activesupport (= 5.2.0)
|
||||
activemodel (5.2.1)
|
||||
activesupport (= 5.2.1)
|
||||
activerecord (5.2.1)
|
||||
activemodel (= 5.2.1)
|
||||
activesupport (= 5.2.1)
|
||||
arel (>= 9.0)
|
||||
activestorage (5.2.0)
|
||||
actionpack (= 5.2.0)
|
||||
activerecord (= 5.2.0)
|
||||
activestorage (5.2.1)
|
||||
actionpack (= 5.2.1)
|
||||
activerecord (= 5.2.1)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (5.2.0)
|
||||
activesupport (5.2.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
|
@ -58,14 +58,14 @@ GEM
|
|||
amq-protocol (2.3.0)
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
backports (3.11.3)
|
||||
backports (3.11.4)
|
||||
bcrypt (3.1.12)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
builder (3.2.3)
|
||||
bunny (2.11.0)
|
||||
amq-protocol (~> 2.3.0)
|
||||
capybara (3.4.2)
|
||||
capybara (3.7.1)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
|
@ -90,24 +90,25 @@ GEM
|
|||
debug_inspector (0.0.3)
|
||||
declarative (0.0.10)
|
||||
declarative-option (0.1.0)
|
||||
deep-cover (0.6.2)
|
||||
backports (>= 3.11.0)
|
||||
binding_of_caller
|
||||
deep-cover (0.6.4)
|
||||
bundler
|
||||
deep-cover-core (= 0.6.4)
|
||||
highline
|
||||
parser (~> 2.5.0)
|
||||
pry
|
||||
sass
|
||||
slop (~> 4.0)
|
||||
term-ansicolor
|
||||
terminal-table
|
||||
with_progress
|
||||
deep-cover-core (0.6.4)
|
||||
backports (>= 3.11.0)
|
||||
binding_of_caller
|
||||
parser (~> 2.5.0)
|
||||
pry
|
||||
terminal-table
|
||||
delayed_job (4.1.5)
|
||||
activesupport (>= 3.0, < 5.3)
|
||||
delayed_job_active_record (4.1.3)
|
||||
activerecord (>= 3.0, < 5.3)
|
||||
delayed_job (>= 3.0, < 5)
|
||||
devise (4.4.3)
|
||||
devise (4.5.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0, < 6.0)
|
||||
|
@ -121,10 +122,10 @@ GEM
|
|||
erubi (1.7.1)
|
||||
eventmachine (1.2.7)
|
||||
excon (0.62.0)
|
||||
factory_bot (4.10.0)
|
||||
factory_bot (4.11.0)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_bot_rails (4.10.0)
|
||||
factory_bot (~> 4.10.0)
|
||||
factory_bot_rails (4.11.0)
|
||||
factory_bot (~> 4.11.0)
|
||||
railties (>= 3.0.0)
|
||||
faker (1.9.1)
|
||||
i18n (>= 0.7)
|
||||
|
@ -135,12 +136,12 @@ GEM
|
|||
ffi (1.9.25)
|
||||
figaro (1.1.1)
|
||||
thor (~> 0.14)
|
||||
fog-core (2.1.0)
|
||||
fog-core (2.1.2)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
formatador (~> 0.2)
|
||||
mime-types
|
||||
fog-google (1.6.0)
|
||||
fog-google (1.7.1)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
|
@ -158,26 +159,26 @@ GEM
|
|||
formatador (0.2.5)
|
||||
globalid (0.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.23.4)
|
||||
google-api-client (0.23.8)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.5, < 0.7.0)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mime-types (~> 3.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
googleauth (0.6.2)
|
||||
signet (~> 0.9)
|
||||
googleauth (0.6.6)
|
||||
faraday (~> 0.12)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
logging (~> 2.0)
|
||||
memoist (~> 0.12)
|
||||
multi_json (~> 1.11)
|
||||
os (~> 0.9)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
hashdiff (0.3.7)
|
||||
hashie (3.5.7)
|
||||
highline (2.0.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.0.1)
|
||||
i18n (1.1.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.1.0)
|
||||
jsonapi-renderer (0.2.0)
|
||||
|
@ -186,10 +187,6 @@ GEM
|
|||
addressable (~> 2.3)
|
||||
letter_opener (1.6.0)
|
||||
launchy (~> 2.2)
|
||||
little-plugger (1.1.4)
|
||||
logging (2.2.2)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
lol_dba (2.1.5)
|
||||
actionpack (>= 3.0)
|
||||
activerecord (>= 3.0)
|
||||
|
@ -203,23 +200,23 @@ GEM
|
|||
mimemagic (~> 0.3.2)
|
||||
memoist (0.16.0)
|
||||
method_source (0.9.0)
|
||||
mime-types (3.1)
|
||||
mime-types (3.2.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mime-types-data (3.2018.0812)
|
||||
mimemagic (0.3.2)
|
||||
mini_mime (1.0.0)
|
||||
mini_mime (1.0.1)
|
||||
mini_portile2 (2.3.0)
|
||||
minitest (5.11.3)
|
||||
multi_json (1.13.1)
|
||||
multipart-post (2.0.0)
|
||||
mutations (0.8.2)
|
||||
mutations (0.8.3)
|
||||
activesupport
|
||||
nio4r (2.3.1)
|
||||
nokogiri (1.8.4)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (0.9.6)
|
||||
paperclip (6.0.0)
|
||||
os (1.0.0)
|
||||
paperclip (6.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
mime-types
|
||||
|
@ -227,10 +224,10 @@ GEM
|
|||
terrapin (~> 0.6.0)
|
||||
parser (2.5.1.2)
|
||||
ast (~> 2.4.0)
|
||||
passenger (5.3.3)
|
||||
passenger (5.3.4)
|
||||
rack
|
||||
rake (>= 0.8.1)
|
||||
pg (1.0.0)
|
||||
pg (1.1.2)
|
||||
polymorphic_constraints (1.0.0)
|
||||
rails
|
||||
pry (0.11.3)
|
||||
|
@ -238,7 +235,7 @@ GEM
|
|||
method_source (~> 0.9.0)
|
||||
pry-rails (0.3.6)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (3.0.2)
|
||||
public_suffix (3.0.3)
|
||||
rabbitmq_http_api_client (1.9.1)
|
||||
effin_utf8 (~> 1.0.0)
|
||||
faraday (~> 0.13.0)
|
||||
|
@ -251,18 +248,18 @@ GEM
|
|||
rack-cors (1.0.2)
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.2.0)
|
||||
actioncable (= 5.2.0)
|
||||
actionmailer (= 5.2.0)
|
||||
actionpack (= 5.2.0)
|
||||
actionview (= 5.2.0)
|
||||
activejob (= 5.2.0)
|
||||
activemodel (= 5.2.0)
|
||||
activerecord (= 5.2.0)
|
||||
activestorage (= 5.2.0)
|
||||
activesupport (= 5.2.0)
|
||||
rails (5.2.1)
|
||||
actioncable (= 5.2.1)
|
||||
actionmailer (= 5.2.1)
|
||||
actionpack (= 5.2.1)
|
||||
actionview (= 5.2.1)
|
||||
activejob (= 5.2.1)
|
||||
activemodel (= 5.2.1)
|
||||
activerecord (= 5.2.1)
|
||||
activestorage (= 5.2.1)
|
||||
activesupport (= 5.2.1)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.2.0)
|
||||
railties (= 5.2.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
|
@ -279,16 +276,13 @@ GEM
|
|||
rails_stdout_logging
|
||||
rails_serve_static_assets (0.0.5)
|
||||
rails_stdout_logging (0.0.5)
|
||||
railties (5.2.0)
|
||||
actionpack (= 5.2.0)
|
||||
activesupport (= 5.2.0)
|
||||
railties (5.2.1)
|
||||
actionpack (= 5.2.1)
|
||||
activesupport (= 5.2.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
rake (12.3.1)
|
||||
rb-fsevent (0.10.3)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
|
@ -299,45 +293,40 @@ GEM
|
|||
actionpack (>= 4.2.0, < 5.3)
|
||||
railties (>= 4.2.0, < 5.3)
|
||||
retriable (3.1.2)
|
||||
rollbar (2.16.3)
|
||||
rollbar (2.17.0)
|
||||
multi_json
|
||||
rspec (3.7.0)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
rspec-mocks (~> 3.7.0)
|
||||
rspec-core (3.7.1)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-expectations (3.7.0)
|
||||
rspec (3.8.0)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
rspec-mocks (~> 3.8.0)
|
||||
rspec-core (3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-expectations (3.8.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-mocks (3.7.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-mocks (3.8.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-rails (3.7.2)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-rails (3.8.0)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
rspec-mocks (~> 3.7.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-support (3.7.1)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
rspec-mocks (~> 3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-support (3.8.0)
|
||||
ruby-graphviz (1.2.3)
|
||||
ruby-progressbar (1.10.0)
|
||||
rubyzip (1.2.1)
|
||||
sass (3.5.7)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
rubyzip (1.2.2)
|
||||
scenic (1.4.1)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
secure_headers (6.0.0)
|
||||
selenium-webdriver (3.13.1)
|
||||
selenium-webdriver (3.14.0)
|
||||
childprocess (~> 0.5)
|
||||
rubyzip (~> 1.2)
|
||||
signet (0.8.1)
|
||||
signet (0.9.1)
|
||||
addressable (~> 2.3)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
|
@ -347,9 +336,9 @@ GEM
|
|||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
skylight (2.0.2)
|
||||
skylight-core (= 2.0.2)
|
||||
skylight-core (2.0.2)
|
||||
skylight (3.0.0)
|
||||
skylight-core (= 3.0.0)
|
||||
skylight-core (3.0.0)
|
||||
activesupport (>= 4.2.0)
|
||||
slop (4.6.2)
|
||||
sprockets (3.7.2)
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
# Mostly used for creation of jwt.pem- which is used to verify authenticity of
|
||||
# JSON Web Tokens
|
||||
class KeyGen
|
||||
PROD_KEY_FILE = "/keys/production.pem"
|
||||
KEY_FILE = "jwt.#{Rails.env}.pem"
|
||||
SAVE_PATH = (Rails.env == "production") ? PROD_KEY_FILE : KEY_FILE
|
||||
# SAVE_PATH = KEY_FILE
|
||||
SAVE_PATH = "jwt.#{Rails.env}.pem"
|
||||
|
||||
def self.try_file
|
||||
OpenSSL::PKey::RSA.new(File.read(SAVE_PATH)) if File.file?(SAVE_PATH)
|
||||
|
|
|
@ -30,9 +30,8 @@ module CeleryScriptSettingsBag
|
|||
set_servo_angle change_ownership dump_info)
|
||||
ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware)
|
||||
ALLOWED_CHAGES = %w(add remove update)
|
||||
RESOURCE_NAME = %w(images plants regimens peripherals
|
||||
corpuses logs sequences farm_events
|
||||
tool_slots tools points tokens users device)
|
||||
RESOURCE_NAME = %w(Device FarmEvent Image Log Peripheral Plant Point
|
||||
Regimen Sequence Tool ToolSlot User GenericPointer)
|
||||
ALLOWED_MESSAGE_TYPES = %w(success busy warn error info fun debug)
|
||||
ALLOWED_CHANNEL_NAMES = %w(ticker toast email espeak)
|
||||
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant)
|
||||
|
@ -45,7 +44,7 @@ module CeleryScriptSettingsBag
|
|||
read_status reboot sync take_photo)
|
||||
STEPS = %w(_if execute execute_script find_home move_absolute
|
||||
move_relative read_pin send_message take_photo wait
|
||||
write_pin )
|
||||
write_pin resource_update)
|
||||
BAD_ALLOWED_PIN_MODES = '"%s" is not a valid pin_mode. Allowed values: %s'
|
||||
BAD_LHS = 'Can not put "%s" into a left hand side (LHS) '\
|
||||
'argument. Allowed values: %s'
|
||||
|
@ -58,12 +57,13 @@ module CeleryScriptSettingsBag
|
|||
BAD_DATA_TYPE = '"%s" is not a valid data_type. Allowed values: %s'
|
||||
BAD_MESSAGE_TYPE = '"%s" is not a valid message_type. Allowed values: %s'
|
||||
BAD_MESSAGE = "Messages must be between 1 and 300 characters"
|
||||
BAD_RESOURCE_TYPE = '"%s" is not a valid resource_type. Allowed values: %s'
|
||||
BAD_TOOL_ID = 'Tool #%s does not exist.'
|
||||
BAD_PERIPH_ID = 'Peripheral #%s does not exist.'
|
||||
BAD_PACKAGE = '"%s" is not a valid package. Allowed values: %s'
|
||||
BAD_AXIS = '"%s" is not a valid axis. Allowed values: %s'
|
||||
BAD_POINTER_ID = "Bad point ID: %s"
|
||||
BAD_PIN_ID = "Can't find %s with id of %s"
|
||||
BAD_RESOURCE_ID = "Can't find %s with id of %s"
|
||||
NO_PIN_ID = "%s requires a valid pin number"
|
||||
BAD_POINTER_TYPE = '"%s" is not a type of point. Allowed values: %s'
|
||||
BAD_PIN_TYPE = '"%s" is not a type of pin. Allowed values: %s'
|
||||
|
@ -74,9 +74,9 @@ module CeleryScriptSettingsBag
|
|||
"BoxLed4" => BoxLed }
|
||||
CANT_ANALOG = "Analog modes are not supported for Box LEDs"
|
||||
ALLOWED_PIN_TYPES = PIN_TYPE_MAP.keys
|
||||
KLASS_LOOKUP = Point::POINTER_KINDS.reduce({}) do |acc, val|
|
||||
(acc[val] = Kernel.const_get(val)) && acc
|
||||
end
|
||||
# KLASS_LOOKUP =
|
||||
# Point::POINTER_KINDS.reduce({}) { |a, v| (a[v] = Kernel.const_get(v)) && a }
|
||||
RESOURCE_UPDATE_ARGS = [:resource_type, :resource_id, :label, :value]
|
||||
|
||||
Corpus = CeleryScript::Corpus
|
||||
.new
|
||||
|
@ -180,13 +180,19 @@ module CeleryScriptSettingsBag
|
|||
BAD_DATA_TYPE % [v.to_s, ALLOWED_DATA_TYPES.inspect]
|
||||
end
|
||||
end
|
||||
.arg(:resource_id, [Integer])
|
||||
.arg(:resource_type, [String]) do |n|
|
||||
within(RESOURCE_NAME, n) do |v|
|
||||
BAD_RESOURCE_TYPE % [v.to_s, RESOURCE_NAME]
|
||||
end
|
||||
end
|
||||
.node(:named_pin, [:pin_type, :pin_id]) do |node|
|
||||
args = HashWithIndifferentAccess.new(node.args)
|
||||
klass = PIN_TYPE_MAP.fetch(args[:pin_type].value)
|
||||
id = args[:pin_id].value
|
||||
node.invalidate!(NO_PIN_ID % [klass.name]) if (id == 0)
|
||||
bad_node = !klass.exists?(id)
|
||||
node.invalidate!(BAD_PIN_ID % [klass.name, id]) if bad_node
|
||||
no_resource(node, klass, id) if bad_node
|
||||
end
|
||||
.node(:nothing, [])
|
||||
.node(:tool, [:tool_id])
|
||||
|
@ -240,15 +246,37 @@ module CeleryScriptSettingsBag
|
|||
.node(:set_servo_angle, [:pin_number, :pin_value], [])
|
||||
.node(:change_ownership, [], [:pair])
|
||||
.node(:dump_info, [], [])
|
||||
.node(:resource_update, RESOURCE_UPDATE_ARGS) do |x|
|
||||
resource_type = x.args.fetch("resource_type").value
|
||||
resource_id = x.args.fetch("resource_id").value
|
||||
check_resource_type(x, resource_type, resource_id)
|
||||
end
|
||||
.node(:install_first_party_farmware, [])
|
||||
|
||||
ANY_ARG_NAME = Corpus.as_json[:args].pluck("name").map(&:to_s)
|
||||
ANY_NODE_NAME = Corpus.as_json[:nodes].pluck("name").map(&:to_s)
|
||||
|
||||
def self.no_resource(node, klass, resource_id)
|
||||
node.invalidate!(BAD_RESOURCE_ID % [klass.name, resource_id])
|
||||
end
|
||||
|
||||
def self.check_resource_type(node, resource_type, resource_id)
|
||||
case resource_type # <= Security critical code (for const_get'ing)
|
||||
when "Device"
|
||||
# When "resource_type" is "Device", resource_id always refers to
|
||||
# the current_device.
|
||||
# For convinience, we try to set it here, defaulting to 0
|
||||
node.args["resource_id"].instance_variable_set("@value", 0)
|
||||
when *RESOURCE_NAME.without("Device")
|
||||
klass = Kernel.const_get(resource_type)
|
||||
resource_ok = klass.exists?(resource_id)
|
||||
no_resource(node, klass, resource_id) unless resource_ok
|
||||
end
|
||||
end
|
||||
# Given an array of allowed values and a CeleryScript AST node, will DETERMINE
|
||||
# if the node contains a legal value. Throws exception and invalidates if not.
|
||||
def self.within(array, node)
|
||||
val = node&.value
|
||||
val = node.try(:value)
|
||||
node.invalidate!(yield(val)) if !array.include?(val)
|
||||
end
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ class GlobalConfig < ApplicationRecord
|
|||
"FBOS_END_OF_LIFE_VERSION" => "0.0.0",
|
||||
"MINIMUM_FBOS_VERSION" => "6.0.0"
|
||||
}.map do |(key, value)|
|
||||
self.find_or_create_by(key: key) do |conf|
|
||||
conf.assign_attributes(key: key, value: value)
|
||||
end
|
||||
self
|
||||
.find_or_create_by(key: key)
|
||||
.update_attributes(key: key, value: value)
|
||||
end
|
||||
|
||||
# Memoized version of every GlobalConfig, with key/values layed out in a hash.
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
module Devices
|
||||
class Update < Mutations::Command
|
||||
BAD_TOOL_ID = "Can't mount to tool #%s because it does not exist."
|
||||
|
||||
required do
|
||||
model :device, class: Device
|
||||
end
|
||||
|
@ -8,11 +10,35 @@ module Devices
|
|||
string :name
|
||||
string :timezone#, in: Device::TIMEZONES
|
||||
time :last_saw_mq
|
||||
integer :mounted_tool_id, nils: true
|
||||
end
|
||||
|
||||
def validate
|
||||
validate_tool_id if better_tool_id
|
||||
end
|
||||
|
||||
def execute
|
||||
device.update_attributes!(inputs.except(:device))
|
||||
p = inputs.except(:device).merge(mounted_tool_data)
|
||||
device.update_attributes!(p)
|
||||
device
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bad_tool_id
|
||||
add_error :mounted_tool_id, :mounted_tool_id, BAD_TOOL_ID % better_tool_id
|
||||
end
|
||||
|
||||
def validate_tool_id
|
||||
bad_tool_id unless device.tools.pluck(:id).include?(better_tool_id)
|
||||
end
|
||||
|
||||
def better_tool_id
|
||||
@better_tool_id ||= ((mounted_tool_id || 0) > 0) ? mounted_tool_id : nil
|
||||
end
|
||||
|
||||
def mounted_tool_data
|
||||
mounted_tool_id_present? ? {mounted_tool_id: better_tool_id} : {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,8 +31,8 @@ module Sequences
|
|||
|
||||
def validate
|
||||
validate_sequence
|
||||
regimens_cant_have_parameters
|
||||
farm_events_cant_have_parameters
|
||||
# regimens_cant_have_parameters
|
||||
# farm_events_cant_have_parameters
|
||||
raise Errors::Forbidden unless device.sequences.include?(sequence)
|
||||
end
|
||||
|
||||
|
@ -55,26 +55,29 @@ module Sequences
|
|||
Regimen => BASE + "the following Regimen(s) are using it: %{items}",
|
||||
}
|
||||
|
||||
def regimens_cant_have_parameters
|
||||
maybe_stop_parameter_use(resource: Regimen,
|
||||
items: Regimen
|
||||
.includes(:regimen_items)
|
||||
.where(regimen_items: {sequence_id: sequence.id})
|
||||
.map(&:fancy_name))
|
||||
end
|
||||
# TODO: Bring this back after "sequence variables" rollout. - RC 12 SEP 2018
|
||||
# def regimens_cant_have_parameters
|
||||
# maybe_stop_parameter_use(resource: Regimen,
|
||||
# items: Regimen
|
||||
# .includes(:regimen_items)
|
||||
# .where(regimen_items: {sequence_id: sequence.id})
|
||||
# .map(&:fancy_name))
|
||||
# end
|
||||
|
||||
def farm_events_cant_have_parameters
|
||||
maybe_stop_parameter_use(resource: FarmEvent,
|
||||
items: FarmEvent
|
||||
.where(executable: sequence)
|
||||
.map(&:fancy_name))
|
||||
end
|
||||
# TODO: Bring this back after "sequence variables" rollout. - RC 12 SEP 2018
|
||||
# def farm_events_cant_have_parameters
|
||||
# maybe_stop_parameter_use(resource: FarmEvent,
|
||||
# items: FarmEvent
|
||||
# .where(executable: sequence)
|
||||
# .map(&:fancy_name))
|
||||
# end
|
||||
|
||||
def maybe_stop_parameter_use(resource:, items:)
|
||||
add_error :sequence, :sequence, EXPL.fetch(resource) % {
|
||||
resource: resource,
|
||||
items: items.join(", ")
|
||||
} if items.present?
|
||||
end
|
||||
# TODO: Bring this back after "sequence variables" rollout. - RC 12 SEP 2018
|
||||
# def maybe_stop_parameter_use(resource:, items:)
|
||||
# add_error :sequence, :sequence, EXPL.fetch(resource) % {
|
||||
# resource: resource,
|
||||
# items: items.join(", ")
|
||||
# } if items.present?
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class DeviceSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :timezone, :last_saw_api, :last_saw_mq, :tz_offset_hrs,
|
||||
:fbos_version, :throttled_until, :throttled_at
|
||||
:fbos_version, :throttled_until, :throttled_at, :mounted_tool_id
|
||||
end
|
||||
|
|
|
@ -86,7 +86,8 @@ EXTRA_DOMAINS: staging.farm.bot,whatever.farm.bot
|
|||
# Most users will not want this enabled.
|
||||
RUN_CAPYBARA: "true"
|
||||
# Set this to "production" in most cases.
|
||||
# If you need help debugging issues, please delete this line.
|
||||
# Setting this line to "production" will disable debug backtraces.
|
||||
# Please delete this line if you are submitting a bug report on the forum/Github
|
||||
RAILS_ENV: "production"
|
||||
# Every server has a superuser.
|
||||
# Set this to something SECURE.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Devise.setup do |config|
|
||||
config.secret_key = ENV['DEVISE_SECRET']
|
||||
config.secret_key = ENV["DEVISE_SECRET"]
|
||||
config.mailer_sender = 'do-not-reply@farmbot.io'
|
||||
require 'devise/orm/active_record'
|
||||
config.case_insensitive_keys = [ :email ]
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class AddShowMotorPlotToWebAppConfigs < ActiveRecord::Migration[5.2]
|
||||
safety_assured
|
||||
def change
|
||||
add_column :web_app_configs,
|
||||
:show_motor_plot,
|
||||
:boolean,
|
||||
default: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
class AddMountedToolIdToDevice < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_reference :devices,
|
||||
:mounted_tool,
|
||||
null: true,
|
||||
foreign_key: { to_table: :tools }
|
||||
end
|
||||
end
|
|
@ -122,7 +122,8 @@ CREATE TABLE public.devices (
|
|||
last_saw_mq timestamp without time zone,
|
||||
fbos_version character varying(15),
|
||||
throttled_until timestamp without time zone,
|
||||
throttled_at timestamp without time zone
|
||||
throttled_at timestamp without time zone,
|
||||
mounted_tool_id bigint
|
||||
);
|
||||
|
||||
|
||||
|
@ -1216,7 +1217,8 @@ CREATE TABLE public.web_app_configs (
|
|||
photo_filter_end character varying,
|
||||
discard_unsaved boolean DEFAULT false,
|
||||
xy_swap boolean DEFAULT false,
|
||||
home_button_homing boolean DEFAULT false
|
||||
home_button_homing boolean DEFAULT false,
|
||||
show_motor_plot boolean DEFAULT false
|
||||
);
|
||||
|
||||
|
||||
|
@ -1715,6 +1717,13 @@ ALTER TABLE ONLY public.webcam_feeds
|
|||
CREATE INDEX delayed_jobs_priority ON public.delayed_jobs USING btree (priority, run_at);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_devices_on_mounted_tool_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_devices_on_mounted_tool_id ON public.devices USING btree (mounted_tool_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_devices_on_timezone; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2195,6 +2204,14 @@ ALTER TABLE ONLY public.token_issuances
|
|||
ADD CONSTRAINT fk_rails_e202a61188 FOREIGN KEY (device_id) REFERENCES public.devices(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: devices fk_rails_eef5afaff7; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.devices
|
||||
ADD CONSTRAINT fk_rails_eef5afaff7 FOREIGN KEY (mounted_tool_id) REFERENCES public.tools(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pin_bindings fk_rails_f72ee24d98; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2305,6 +2322,8 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||
('20180726165546'),
|
||||
('20180727152741'),
|
||||
('20180813185430'),
|
||||
('20180815143819');
|
||||
('20180815143819'),
|
||||
('20180829211322'),
|
||||
('20180910143055');
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
class CorpusEmitter
|
||||
PIPE = "\n | "
|
||||
PIPE = "\n | "
|
||||
|
||||
class CSArg
|
||||
TRANSLATIONS = {"integer" => "number",
|
||||
"string" => "string",
|
||||
"float" => "number" }
|
||||
"float" => "number",
|
||||
"boolean" => "boolean" }
|
||||
|
||||
attr_reader :name, :allowed_values
|
||||
|
||||
def initialize(name:, allowed_values:)
|
||||
|
@ -141,7 +143,7 @@ class CorpusEmitter
|
|||
result.push(enum_type :PlantStage, CeleryScriptSettingsBag::PLANT_STAGES)
|
||||
|
||||
File.open("latest_corpus.ts", "w") do |f|
|
||||
f.write(result.join.gsub("\n\n\n", "\n").gsub("\n\n", "\n").strip)
|
||||
f.write(result.join.gsub("\n\n\n", "\n").gsub("\n\n", "\n").gsub("\n\n", "\n").strip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
58
package.json
58
package.json
|
@ -17,7 +17,8 @@
|
|||
"test-slow": "jest --coverage --no-cache -w 4",
|
||||
"test": "jest --no-coverage --cache -w 5",
|
||||
"typecheck": "./node_modules/.bin/tsc --noEmit --jsx preserve",
|
||||
"tslint": "./node_modules/tslint/bin/tslint --project ."
|
||||
"tslint": "./node_modules/tslint/bin/tslint --project .",
|
||||
"sass-lint": "./node_modules/sass-lint/bin/sass-lint.js -c .sass-lint.yml -v -q"
|
||||
},
|
||||
"keywords": [
|
||||
"farmbot"
|
||||
|
@ -25,21 +26,21 @@
|
|||
"author": "farmbot.io",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"webpack-dev-server": "3.1.5"
|
||||
"webpack-dev-server": "3.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "2.3.1",
|
||||
"@blueprintjs/datetime": "2.0.3",
|
||||
"@blueprintjs/select": "^2.0.1",
|
||||
"@types/enzyme": "3.1.12",
|
||||
"@types/enzyme": "3.1.13",
|
||||
"@types/fastclick": "^1.0.28",
|
||||
"@types/history": "^4.6.1",
|
||||
"@types/i18next": "^8.4.2",
|
||||
"@types/history": "4.7.0",
|
||||
"@types/i18next": "8.4.5",
|
||||
"@types/jest": "23.3.1",
|
||||
"@types/lodash": "4.14.114",
|
||||
"@types/markdown-it": "^0.0.4",
|
||||
"@types/lodash": "4.14.116",
|
||||
"@types/markdown-it": "0.0.5",
|
||||
"@types/moxios": "^0.4.5",
|
||||
"@types/node": "10.5.3",
|
||||
"@types/node": "10.9.4",
|
||||
"@types/react": "16.3.14",
|
||||
"@types/react-color": "2.13.5",
|
||||
"@types/react-dom": "16.0.5",
|
||||
|
@ -50,51 +51,52 @@
|
|||
"browser-speech": "1.1.1",
|
||||
"coveralls": "3.0.2",
|
||||
"css-loader": "1.0.0",
|
||||
"enzyme": "^3.1.0",
|
||||
"enzyme-adapter-react-16": "^1.1.0",
|
||||
"farmbot": "6.5.0-rc4",
|
||||
"enzyme": "3.6.0",
|
||||
"enzyme-adapter-react-16": "1.5.0",
|
||||
"farmbot": "6.5.0",
|
||||
"farmbot-toastr": "^1.0.3",
|
||||
"fastclick": "^1.0.6",
|
||||
"file-loader": "1.1.11",
|
||||
"i18next": "11.5.0",
|
||||
"file-loader": "2.0.0",
|
||||
"i18next": "11.7.0",
|
||||
"imports-loader": "0.8.0",
|
||||
"jest": "23.4.1",
|
||||
"jest": "23.5.0",
|
||||
"json-loader": "0.5.7",
|
||||
"lodash": "4.17.10",
|
||||
"markdown-it": "^8.4.0",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"moment": "2.22.2",
|
||||
"moxios": "^0.4.0",
|
||||
"node-sass": "4.9.2",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.0",
|
||||
"node-sass": "4.9.3",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
"raf": "^3.4.0",
|
||||
"react": "16.4.1",
|
||||
"react": "16.4.2",
|
||||
"react-addons-css-transition-group": "^15.6.2",
|
||||
"react-addons-test-utils": "^15.6.2",
|
||||
"react-color": "2.14.1",
|
||||
"react-dom": "16.4.1",
|
||||
"react-dom": "16.4.2",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router": "^3",
|
||||
"react-test-renderer": "16.4.1",
|
||||
"react-test-renderer": "16.4.2",
|
||||
"react-transition-group": "^2.3.1",
|
||||
"redux": "4.0.0",
|
||||
"redux-immutable-state-invariant": "^2.1.0",
|
||||
"redux-thunk": "2.3.0",
|
||||
"rollbar-sourcemap-webpack-plugin": "^2.3.0",
|
||||
"sass-lint": "^1.12.1",
|
||||
"sass-loader": "7.1.0",
|
||||
"stats-webpack-plugin": "0.6.2",
|
||||
"style-loader": "0.21.0",
|
||||
"ts-jest": "23.0.1",
|
||||
"stats-webpack-plugin": "0.7.0",
|
||||
"style-loader": "0.23.0",
|
||||
"ts-jest": "23.1.4",
|
||||
"ts-lint": "^4.5.1",
|
||||
"ts-loader": "4.4.2",
|
||||
"ts-loader": "5.0.0",
|
||||
"tslint": "5.11.0",
|
||||
"typescript": "3.0.1",
|
||||
"url-loader": "1.0.1",
|
||||
"webpack": "4.16.4",
|
||||
"typescript": "3.0.3",
|
||||
"url-loader": "1.1.1",
|
||||
"webpack": "4.17.2",
|
||||
"webpack-uglify-js-plugin": "1.1.9",
|
||||
"weinre": "^2.0.0-pre-I0Z7U9OV",
|
||||
"which": "1.3.1",
|
||||
"yarn": "^1.9.2"
|
||||
"yarn": "1.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jscpd": "0.6.22",
|
||||
|
@ -124,7 +126,7 @@
|
|||
"./webpack/__test_support__/additional_mocks.ts"
|
||||
],
|
||||
"transform": {
|
||||
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
|
||||
".(ts|tsx)": "ts-jest"
|
||||
},
|
||||
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
|
||||
"moduleFileExtensions": [
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
require 'spec_helper'
|
||||
require "spec_helper"
|
||||
|
||||
# Api::DevicesController is the RESTful endpoint for managing device related
|
||||
# settings. Consumed by the Angular SPA on the front end.
|
||||
describe Api::DevicesController do
|
||||
|
||||
include Devise::Test::ControllerHelpers
|
||||
describe "#update" do
|
||||
|
||||
describe '#update' do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
let(:user2) { FactoryBot.create(:user) }
|
||||
let(:device) { user.device }
|
||||
let(:tool) { FactoryBot.create(:tool, device: user.device) }
|
||||
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
let(:user2) { FactoryBot.create(:user) }
|
||||
|
||||
it 'updates a Device' do
|
||||
it "updates a Device" do
|
||||
sign_in user
|
||||
fake_name = Faker::Name.name
|
||||
put :update, params: {id: user.device.id, name: fake_name}, session: { format: :json }
|
||||
put :update,
|
||||
params: {id: user.device.id, name: fake_name},
|
||||
session: { format: :json }
|
||||
# put path, params, options
|
||||
user.reload
|
||||
device = user.reload.device.reload
|
||||
|
@ -22,7 +24,7 @@ describe Api::DevicesController do
|
|||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it 'updates a Device Timezone wrong' do
|
||||
it "updates a Device Timezone wrong" do
|
||||
sign_in user
|
||||
before = user.device.timezone
|
||||
put :update,
|
||||
|
@ -36,7 +38,7 @@ describe Api::DevicesController do
|
|||
expect(user.device.timezone).to eq(before)
|
||||
end
|
||||
|
||||
it 'updates a Device timezone correctly' do
|
||||
it "updates a Device timezone correctly" do
|
||||
sign_in user
|
||||
fake_tz = Device::TIMEZONES.sample
|
||||
put :update, params: {id: user.device.id, timezone: fake_tz}, session: { format: :json }
|
||||
|
@ -45,5 +47,42 @@ describe Api::DevicesController do
|
|||
expect(device.timezone).to eq(fake_tz)
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "mounts a tool" do
|
||||
sign_in user
|
||||
put :update,
|
||||
params: {
|
||||
id: user.device.id,
|
||||
mounted_tool_id: tool.id
|
||||
},
|
||||
session: { format: :json }
|
||||
user.reload
|
||||
device = user.reload.device.reload
|
||||
expect(device.mounted_tool_id).to eq(tool.id)
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "performs referential integrity checks on mounted_tool_id" do
|
||||
sign_in user
|
||||
put :update,
|
||||
params: {
|
||||
id: user.device.id,
|
||||
mounted_tool_id: (FactoryBot.create(:tool).id + 1)
|
||||
},
|
||||
session: { format: :json }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:mounted_tool_id]).to include("Can't mount to tool")
|
||||
end
|
||||
|
||||
it "dismounts a tool" do
|
||||
sign_in user
|
||||
device.update_attributes!(mounted_tool_id: tool.id)
|
||||
expect(device.mounted_tool_id).to be
|
||||
put :update,
|
||||
params: { id: user.device.id, mounted_tool_id: 0 },
|
||||
session: { format: :json }
|
||||
expect(device.reload.mounted_tool_id).not_to be
|
||||
expect(json[:mounted_tool_id]).to be(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,6 +35,7 @@ describe Api::SequencesController do
|
|||
end
|
||||
|
||||
it 'disallows adding `parent` to sequences used as executable' do
|
||||
pending "Possibly broke"
|
||||
sign_in user
|
||||
sequence = FakeSequence.create(device: user.device)
|
||||
farm_ev = FactoryBot.create(:farm_event, device: user.device, executable: sequence)
|
||||
|
@ -46,6 +47,7 @@ describe Api::SequencesController do
|
|||
|
||||
|
||||
it 'disallows adding `parent` to sequences used in a regimen' do
|
||||
pending "Possibly broke"
|
||||
sign_in user
|
||||
sequence = FakeSequence.create(device: user.device)
|
||||
regimen = Regimens::Create.run!(device: user.device,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
FactoryBot.define do
|
||||
factory :diagnostic_dump do
|
||||
device
|
||||
fbos_version "123_fbos_version"
|
||||
fbos_commit "123_fbos_commit"
|
||||
firmware_commit "123_firmware_commit"
|
||||
network_interface "123_network_interface"
|
||||
fbos_dmesg_dump "123_fbos_dmesg_dump"
|
||||
firmware_state "123_firmware_state"
|
||||
fbos_version { "123_fbos_version" }
|
||||
fbos_commit { "123_fbos_commit" }
|
||||
firmware_commit { "123_firmware_commit" }
|
||||
network_interface { "123_network_interface" }
|
||||
fbos_dmesg_dump { "123_fbos_dmesg_dump" }
|
||||
firmware_state { "123_firmware_state" }
|
||||
ticket_identifier { rand(36**5).to_s(36) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
FactoryBot.define do
|
||||
factory :generic_pointer do
|
||||
radius 1.5
|
||||
radius { 1.5 }
|
||||
x { rand(1...550) }
|
||||
y { rand(1...550) }
|
||||
z { rand(1...550) }
|
||||
meta ({})
|
||||
meta {({})}
|
||||
device
|
||||
pointer_type GenericPointer.name
|
||||
pointer_type { GenericPointer.name }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :global_config do
|
||||
key "MyString"
|
||||
value "MyText"
|
||||
key { "MyString" }
|
||||
value { "MyText" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
FactoryBot.define do
|
||||
factory :image do
|
||||
attachment { StringIO.new(File.open("./spec/fixture.jpg").read) }
|
||||
meta ({x: 1, y: 2, z: 3})
|
||||
meta {({x: 1, y: 2, z: 3})}
|
||||
device
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,6 @@ FactoryBot.define do
|
|||
factory :log_dispatch do
|
||||
device
|
||||
log
|
||||
sent_at "2017-05-25 06:16:55"
|
||||
sent_at { "2017-05-25 06:16:55" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,6 @@ FactoryBot.define do
|
|||
sequence(:created_at) { |n| n.minutes.ago.utc }
|
||||
message { Faker::Company.bs }
|
||||
type { Log::TYPES.sample }
|
||||
channels ["toast"]
|
||||
channels { ["toast"] }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,6 @@ FactoryBot.define do
|
|||
factory :peripheral do
|
||||
device
|
||||
pin { count = (count + 1) % 50 }
|
||||
label "MyString"
|
||||
label { "MyString" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory :plant_template do
|
||||
openfarm_slug "lettuce"
|
||||
radius 1.5
|
||||
openfarm_slug { "lettuce" }
|
||||
radius { 1.5 }
|
||||
x { rand(1...550) }
|
||||
y { rand(1...550) }
|
||||
z { rand(1...550) }
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
FactoryBot.define do
|
||||
factory :plant do
|
||||
radius 1.5
|
||||
radius { 1.5 }
|
||||
x { rand(1...550) }
|
||||
y { rand(1...550) }
|
||||
z { rand(1...550) }
|
||||
meta ({})
|
||||
meta {({})}
|
||||
device
|
||||
openfarm_slug "lettuce"
|
||||
pointer_type "Plant"
|
||||
openfarm_slug { "lettuce" }
|
||||
pointer_type { "Plant" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ FactoryBot.define do
|
|||
factory :sensor do
|
||||
device
|
||||
pin { count = (count + 1) % 50 }
|
||||
label "MyString"
|
||||
mode 1
|
||||
label { "MyString" }
|
||||
mode { 1 }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,9 +3,9 @@ FactoryBot.define do
|
|||
x { rand(1...550) }
|
||||
y { rand(1...550) }
|
||||
z { rand(1...550) }
|
||||
meta ({})
|
||||
meta {({})}
|
||||
device
|
||||
tool
|
||||
pointer_type("ToolSlot")
|
||||
pointer_type { "ToolSlot" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
FactoryBot.define do
|
||||
factory :user do
|
||||
device
|
||||
name { Faker::Name.name }
|
||||
email { Faker::Internet.email }
|
||||
password { Faker::Internet.password(8) }
|
||||
name { Faker::Name.name }
|
||||
email { Faker::Internet.email }
|
||||
password { Faker::Internet.password(8) }
|
||||
confirmed_at { Time.now }
|
||||
after(:create) do |user|
|
||||
user.device ||= Devices::Create.run!(user: resp[:user])
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :webcam_feed do
|
||||
device
|
||||
url "http://placehold.it/320x240"
|
||||
url { "http://placehold.it/320x240" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,7 +27,7 @@ describe CeleryScript::Corpus do
|
|||
speed: 100
|
||||
}
|
||||
})
|
||||
check1 = CeleryScript::Checker.new(ok1, Sequence::Corpus, device)
|
||||
check1 = CeleryScript::Checker.new(ok1, corpus, device)
|
||||
expect(check1.valid?).to be_truthy
|
||||
|
||||
ok2 = CeleryScript::AstNode.new({
|
||||
|
@ -48,7 +48,7 @@ describe CeleryScript::Corpus do
|
|||
speed: 100
|
||||
}
|
||||
})
|
||||
check2 = CeleryScript::Checker.new(ok2, Sequence::Corpus, device)
|
||||
check2 = CeleryScript::Checker.new(ok2, corpus, device)
|
||||
expect(check2.valid?).to be_truthy
|
||||
end
|
||||
|
||||
|
@ -68,7 +68,7 @@ describe CeleryScript::Corpus do
|
|||
},
|
||||
}
|
||||
})
|
||||
check = CeleryScript::Checker.new(bad, Sequence::Corpus, device)
|
||||
check = CeleryScript::Checker.new(bad, corpus, device)
|
||||
expect(check.valid?).to be_falsey
|
||||
expect(check.error.message).to include("but got Integer")
|
||||
expect(check.error.message).to include("'location' within 'move_absolute'")
|
||||
|
@ -94,7 +94,7 @@ describe CeleryScript::Corpus do
|
|||
speed: 100
|
||||
}
|
||||
})
|
||||
check = CeleryScript::Checker.new(bad, Sequence::Corpus, device)
|
||||
check = CeleryScript::Checker.new(bad, corpus, device)
|
||||
expect(check.valid?).to be_falsey
|
||||
expect(check.error.message).to include("but got String")
|
||||
end
|
||||
|
@ -123,9 +123,7 @@ describe CeleryScript::Corpus do
|
|||
},
|
||||
"body": []
|
||||
})
|
||||
checker = CeleryScript::Checker.new(tree,
|
||||
CeleryScriptSettingsBag::Corpus,
|
||||
device)
|
||||
checker = CeleryScript::Checker.new(tree, corpus, device)
|
||||
expect(checker.error.message).to include("not a valid message_type")
|
||||
end
|
||||
|
||||
|
@ -145,9 +143,57 @@ describe CeleryScript::Corpus do
|
|||
}
|
||||
]
|
||||
})
|
||||
checker = CeleryScript::Checker.new(tree,
|
||||
CeleryScriptSettingsBag::Corpus,
|
||||
device)
|
||||
checker = CeleryScript::Checker.new(tree, corpus, device)
|
||||
expect(checker.error.message).to include("not a valid channel_name")
|
||||
end
|
||||
|
||||
it "validates tool_ids" do
|
||||
ast = { "kind": "tool", "args": { "tool_id": 0 } };
|
||||
checker = CeleryScript::Checker.new(CeleryScript::AstNode.new(ast),
|
||||
corpus,
|
||||
device)
|
||||
expect(checker.valid?).to be(false)
|
||||
expect(checker.error.message).to include("Tool #0 does not exist.")
|
||||
end
|
||||
|
||||
it "Validates resource_update nodes" do
|
||||
ast = { "kind": "resource_update",
|
||||
"args": { "resource_type" => "Device",
|
||||
"resource_id" => 23, # Mutated to "0" later..
|
||||
"label" => "mounted_tool_id",
|
||||
"value" => 1 } }
|
||||
checker = CeleryScript::Checker.new(CeleryScript::AstNode.new(ast), corpus, device)
|
||||
expect(checker.valid?).to be(true)
|
||||
expect(checker.tree.args["resource_id"].value).to eq(0)
|
||||
end
|
||||
|
||||
it "rejects bogus resource_updates" do
|
||||
fake_id = FakeSequence.create().id + 1
|
||||
expect(Sequence.exists?(fake_id)).to be(false)
|
||||
ast = { "kind": "resource_update",
|
||||
"args": { "resource_type" => "Sequence",
|
||||
"resource_id" => fake_id,
|
||||
"label" => "foo",
|
||||
"value" => "Should Fail" } }
|
||||
hmm = CeleryScript::AstNode.new(ast)
|
||||
expect(hmm.args.fetch("resource_id").value).to eq(fake_id)
|
||||
checker = CeleryScript::Checker.new(hmm, corpus, device)
|
||||
expect(checker.valid?).to be(false)
|
||||
expect(checker.error.message)
|
||||
.to eq("Can't find Sequence with id of #{fake_id}")
|
||||
end
|
||||
|
||||
it "rejects bogus resource_types" do
|
||||
ast = { "kind": "resource_update",
|
||||
"args": { "resource_type" => "CanOpener",
|
||||
"resource_id" => 0,
|
||||
"label" => "foo",
|
||||
"value" => "Should Fail" } }
|
||||
checker = CeleryScript::Checker.new(CeleryScript::AstNode.new(ast),
|
||||
corpus,
|
||||
device)
|
||||
expect(checker.valid?).to be(false)
|
||||
expect(checker.error.message)
|
||||
.to include('"CanOpener" is not a valid resource_type.')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,7 +20,8 @@ end
|
|||
|
||||
describe "Pin Binding updates" do
|
||||
it "enforces mutual exclusivity" do
|
||||
[Point, Tool, PinBinding, Sequence, Device].map(&:destroy_all)
|
||||
Device.update_all(mounted_tool_id: nil)
|
||||
[Point, Tool, PinBinding, Sequence].map(&:destroy_all)
|
||||
device = FactoryBot.create(:device)
|
||||
PinBinding.create!(device: device)
|
||||
Sequence.create!(device: device, name: "test")
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
describe Sequences::Update do
|
||||
it "does not allow you to modify other peoples sequences" do
|
||||
theirs = FakeSequence.create()
|
||||
you = FactoryBot.create(:device)
|
||||
K = Sequences::Update
|
||||
evil_params = {sequence: theirs, device: you, name: "impossible", body: []}
|
||||
expect { K.run!(evil_params) }.to raise_error(Errors::Forbidden)
|
||||
end
|
||||
end
|
|
@ -1,10 +1,8 @@
|
|||
import { ReactWrapper, ShallowWrapper } from "enzyme";
|
||||
import { range } from "lodash";
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
export function getProp(i: ReactWrapper<any, {}>, key: string): any {
|
||||
return i.props()[key];
|
||||
}
|
||||
export const getProp =
|
||||
<T, K extends keyof T>(i: ReactWrapper<T, {}>, key: K): T[K] => i.props()[key];
|
||||
|
||||
/** Simulate a click and check button text for a button in a wrapper. */
|
||||
export function clickButton(
|
||||
|
|
|
@ -30,6 +30,7 @@ const fakeProps = (): AppProps => {
|
|||
firmwareConfig: undefined,
|
||||
xySwap: false,
|
||||
animate: false,
|
||||
getConfigValue: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -7,13 +7,13 @@ jest.mock("farmbot", () => {
|
|||
|
||||
import { fetchNewDevice } from "../device";
|
||||
import { auth } from "../__test_support__/fake_state/token";
|
||||
import { get } from "lodash";
|
||||
|
||||
describe("fetchNewDevice", () => {
|
||||
it("returns an instance of FarmBot", async () => {
|
||||
const bot = await fetchNewDevice(auth);
|
||||
expect(bot).toBeInstanceOf(mockFarmbot);
|
||||
// We use this for debugging in local dev env
|
||||
// tslint:disable-next-line:no-any
|
||||
expect((global as any)["current_bot"]).toBeDefined();
|
||||
expect(get(global, "current_bot")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,14 +8,7 @@ jest.mock("axios", () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
jest.mock("../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
clear: jest.fn(),
|
||||
deprecatedGetBool: jest.fn(),
|
||||
}
|
||||
};
|
||||
});
|
||||
jest.mock("../session", () => ({ Session: { clear: jest.fn(), } }));
|
||||
|
||||
import { maybeRefreshToken } from "../refresh_token";
|
||||
import { API } from "../api/index";
|
||||
|
|
|
@ -6,8 +6,6 @@ let mockAuth: AuthState | undefined = undefined;
|
|||
jest.mock("../session", () => ({
|
||||
Session: {
|
||||
fetchStoredToken: jest.fn(() => mockAuth),
|
||||
deprecatedGetNum: () => undefined,
|
||||
deprecatedGetBool: () => undefined,
|
||||
getAll: () => undefined,
|
||||
clear: jest.fn()
|
||||
}
|
||||
|
|
|
@ -1,24 +1,3 @@
|
|||
import { fakeWebAppConfig } from "../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../__test_support__/fake_state";
|
||||
|
||||
const mockConfig = fakeWebAppConfig();
|
||||
jest.mock("../resources/selectors_by_kind", () => ({
|
||||
getWebAppConfig: () => mockConfig
|
||||
}));
|
||||
|
||||
jest.mock("../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockState = fakeState();
|
||||
jest.mock("../redux/store", () => ({
|
||||
store: {
|
||||
dispatch: jest.fn(),
|
||||
getState: () => mockState,
|
||||
}
|
||||
}));
|
||||
|
||||
import {
|
||||
isNumericSetting,
|
||||
isBooleanSetting,
|
||||
|
@ -27,7 +6,6 @@ import {
|
|||
Session,
|
||||
} from "../session";
|
||||
import { auth } from "../__test_support__/fake_state/token";
|
||||
import { edit, save } from "../api/crud";
|
||||
|
||||
describe("fetchStoredToken", () => {
|
||||
it("can't fetch token", () => {
|
||||
|
@ -61,26 +39,6 @@ describe("safeBooleanSetting", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("setBool", () => {
|
||||
it("sets bool", () => {
|
||||
Session.setBool("x_axis_inverted", false);
|
||||
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
|
||||
x_axis_inverted: false
|
||||
});
|
||||
expect(save).toHaveBeenCalledWith(mockConfig.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invertBool", () => {
|
||||
it("inverts bool", () => {
|
||||
Session.invertBool("x_axis_inverted");
|
||||
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
|
||||
x_axis_inverted: true
|
||||
});
|
||||
expect(save).toHaveBeenCalledWith(mockConfig.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeNumericSetting", () => {
|
||||
it("safely returns num", () => {
|
||||
expect(() => safeNumericSetting("no")).toThrow();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const mock = {
|
||||
response: {
|
||||
// tslint:disable-next-line:no-any
|
||||
data: (undefined as any) // Mutable
|
||||
data: (undefined as undefined | {}) // Mutable
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -35,8 +34,7 @@ describe("requestAccountExport", () => {
|
|||
|
||||
it("downloads the data synchronously (when API has no email support)", async () => {
|
||||
mock.response.data = {};
|
||||
// tslint:disable-next-line:no-any
|
||||
window.URL = window.URL || ({} as any);
|
||||
window.URL = window.URL || ({} as typeof window.URL);
|
||||
window.URL.createObjectURL = jest.fn();
|
||||
window.URL.revokeObjectURL = jest.fn();
|
||||
const a = await requestAccountExport();
|
||||
|
|
|
@ -79,7 +79,9 @@ export class Account extends React.Component<Props, State> {
|
|||
<ChangePassword />
|
||||
</Row>
|
||||
<Row>
|
||||
<LabsFeatures />
|
||||
<LabsFeatures
|
||||
dispatch={this.props.dispatch}
|
||||
getConfigValue={this.props.getConfigValue} />
|
||||
</Row>
|
||||
<Row>
|
||||
<DeleteAccount onClick={deleteAcct} />
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { User } from "../auth/interfaces";
|
||||
import { TaggedUser } from "farmbot";
|
||||
import { GetWebAppConfigValue } from "../config_storage/actions";
|
||||
|
||||
export interface Props {
|
||||
user: TaggedUser;
|
||||
dispatch: Function;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
/** JSON form that gets POSTed to the API when user updates their info. */
|
||||
|
|
|
@ -3,7 +3,7 @@ import { fetchLabFeatures } from "../labs_features_list_data";
|
|||
describe("fetchLabFeatures", () => {
|
||||
window.location.reload = jest.fn();
|
||||
it("basically just initializes stuff", () => {
|
||||
const val = fetchLabFeatures();
|
||||
const val = fetchLabFeatures(jest.fn());
|
||||
expect(val.length).toBe(9);
|
||||
expect(val[0].value).toBeFalsy();
|
||||
const { callback } = val[0];
|
||||
|
|
|
@ -1,20 +1,5 @@
|
|||
const mockStorj: Dictionary<boolean> = {};
|
||||
|
||||
jest.mock("../../../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
deprecatedGetBool: (k: string) => {
|
||||
mockStorj[k] = !!mockStorj[k];
|
||||
return mockStorj[k];
|
||||
},
|
||||
invertBool: (k: string) => {
|
||||
mockStorj[k] = !mockStorj[k];
|
||||
return mockStorj[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { Dictionary } from "farmbot";
|
||||
import { maybeToggleFeature, LabsFeature } from "../labs_features_list_data";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
|
@ -29,7 +14,7 @@ describe("maybeToggleFeature()", () => {
|
|||
storageKey: BooleanSetting.stub_config,
|
||||
confirmationMessage: "are you sure?"
|
||||
};
|
||||
const out = maybeToggleFeature(data);
|
||||
const out = maybeToggleFeature(x => mockStorj[x], jest.fn())(data);
|
||||
expect(data.value).toBeFalsy();
|
||||
expect(out).toBeUndefined();
|
||||
expect(window.confirm).toHaveBeenCalledWith(data.confirmationMessage);
|
||||
|
@ -44,7 +29,7 @@ describe("maybeToggleFeature()", () => {
|
|||
storageKey: BooleanSetting.stub_config,
|
||||
confirmationMessage: "are you sure?"
|
||||
};
|
||||
const out = maybeToggleFeature(data);
|
||||
const out = maybeToggleFeature(x => mockStorj[x], jest.fn())(data);
|
||||
out ?
|
||||
expect(out.value).toBeTruthy() : fail("out === undefined. Thats bad");
|
||||
expect(out).toBeTruthy();
|
||||
|
@ -52,7 +37,7 @@ describe("maybeToggleFeature()", () => {
|
|||
|
||||
it("Does not require consent when going from true to false", () => {
|
||||
window.confirm = jest.fn(() => true);
|
||||
const output = maybeToggleFeature({
|
||||
const output = maybeToggleFeature(x => mockStorj[x], jest.fn())({
|
||||
name: "Example",
|
||||
value: (mockStorj[BooleanSetting.stub_config] = true),
|
||||
description: "I stub this.",
|
||||
|
@ -71,7 +56,7 @@ describe("maybeToggleFeature()", () => {
|
|||
description: "I stub this.",
|
||||
storageKey: BooleanSetting.stub_config
|
||||
};
|
||||
const out = maybeToggleFeature(data);
|
||||
const out = maybeToggleFeature(x => mockStorj[x], jest.fn())(data);
|
||||
out ?
|
||||
expect(out.value).toBeTruthy() : fail("out === undefined. Thats bad");
|
||||
expect(out).toBeTruthy();
|
||||
|
|
|
@ -9,7 +9,7 @@ const mockFeatures = [
|
|||
];
|
||||
|
||||
const mocks = {
|
||||
"maybeToggleFeature": jest.fn(),
|
||||
"maybeToggleFeature": jest.fn(() => jest.fn()),
|
||||
"fetchLabFeatures": jest.fn(() => mockFeatures)
|
||||
};
|
||||
|
||||
|
@ -21,7 +21,9 @@ import { LabsFeatures } from "../labs_features";
|
|||
|
||||
describe("<LabsFeatures/>", () => {
|
||||
it("triggers the correct callback on click", () => {
|
||||
const el = mount(<LabsFeatures />);
|
||||
const el = mount(<LabsFeatures
|
||||
dispatch={jest.fn()}
|
||||
getConfigValue={jest.fn()} />);
|
||||
expect(mocks.fetchLabFeatures.mock.calls.length).toBeGreaterThan(0);
|
||||
el.find("button").simulate("click");
|
||||
expect(mockFeatures[0].callback).toHaveBeenCalled();
|
||||
|
|
|
@ -4,20 +4,29 @@ import { LabsFeaturesList } from "./labs_features_list_ui";
|
|||
import { maybeToggleFeature } from "./labs_features_list_data";
|
||||
import { t } from "i18next";
|
||||
import { ToolTips } from "../../constants";
|
||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
||||
export class LabsFeatures extends React.Component<{}, {}> {
|
||||
interface LabsFeaturesProps {
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
export class LabsFeatures extends React.Component<LabsFeaturesProps, {}> {
|
||||
state = {};
|
||||
|
||||
render() {
|
||||
const { getConfigValue, dispatch } = this.props;
|
||||
return <Widget className="peripherals-widget">
|
||||
<WidgetHeader title={t("App Settings")}
|
||||
helpText={ToolTips.LABS}>
|
||||
</WidgetHeader>
|
||||
<WidgetBody>
|
||||
<LabsFeaturesList onToggle={(x) => {
|
||||
maybeToggleFeature(x);
|
||||
this.forceUpdate();
|
||||
}} />
|
||||
<LabsFeaturesList
|
||||
getConfigValue={getConfigValue}
|
||||
onToggle={x => {
|
||||
maybeToggleFeature(getConfigValue, dispatch)(x);
|
||||
this.forceUpdate();
|
||||
}} />
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Session } from "../../session";
|
||||
import { t } from "i18next";
|
||||
import { BooleanConfigKey } from "../../config_storage/web_app_configs";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { Content } from "../../constants";
|
||||
import { VirtualTrail } from "../../farm_designer/map/virtual_farmbot/bot_trail";
|
||||
import { GetWebAppConfigValue, setWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
||||
export interface LabsFeature {
|
||||
/** Toggle label. */
|
||||
|
@ -21,88 +22,91 @@ export interface LabsFeature {
|
|||
callback?(): void;
|
||||
}
|
||||
|
||||
export const fetchLabFeatures = (): LabsFeature[] => ([
|
||||
{
|
||||
name: t("Internationalize Web App"),
|
||||
description: t("Turn off to set Web App to English."),
|
||||
storageKey: BooleanSetting.disable_i18n,
|
||||
value: false,
|
||||
displayInvert: true,
|
||||
callback: () => window.location.reload()
|
||||
},
|
||||
{
|
||||
name: t("Confirm Sequence step deletion"),
|
||||
description: t(Content.CONFIRM_STEP_DELETION),
|
||||
storageKey: BooleanSetting.confirm_step_deletion,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Hide Webcam widget"),
|
||||
description: t(Content.HIDE_WEBCAM_WIDGET),
|
||||
storageKey: BooleanSetting.hide_webcam_widget,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Dynamic map size"),
|
||||
description: t(Content.DYNAMIC_MAP_SIZE),
|
||||
storageKey: BooleanSetting.dynamic_map,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Double default map dimensions"),
|
||||
description: t(Content.DOUBLE_MAP_DIMENSIONS),
|
||||
storageKey: BooleanSetting.map_xl,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Display plant animations"),
|
||||
description: t(Content.PLANT_ANIMATIONS),
|
||||
storageKey: BooleanSetting.disable_animations,
|
||||
value: false,
|
||||
displayInvert: true
|
||||
},
|
||||
{
|
||||
name: t("Read speak logs in browser"),
|
||||
description: t(Content.BROWSER_SPEAK_LOGS),
|
||||
storageKey: BooleanSetting.enable_browser_speak,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Discard Unsaved Changes"),
|
||||
description: t(Content.DISCARD_UNSAVED_CHANGES),
|
||||
storageKey: BooleanSetting.discard_unsaved,
|
||||
value: false,
|
||||
confirmationMessage: t(Content.DISCARD_UNSAVED_CHANGES_CONFIRM)
|
||||
},
|
||||
{
|
||||
name: t("Display virtual FarmBot trail"),
|
||||
description: t(Content.VIRTUAL_TRAIL),
|
||||
storageKey: BooleanSetting.display_trail,
|
||||
value: false,
|
||||
callback: () => sessionStorage.setItem("virtualTrailRecords", "[]")
|
||||
},
|
||||
].map(fetchRealValue));
|
||||
export const fetchLabFeatures =
|
||||
(getConfigValue: GetWebAppConfigValue): LabsFeature[] => ([
|
||||
{
|
||||
name: t("Internationalize Web App"),
|
||||
description: t("Turn off to set Web App to English."),
|
||||
storageKey: BooleanSetting.disable_i18n,
|
||||
value: false,
|
||||
displayInvert: true,
|
||||
callback: () => window.location.reload()
|
||||
},
|
||||
{
|
||||
name: t("Confirm Sequence step deletion"),
|
||||
description: t(Content.CONFIRM_STEP_DELETION),
|
||||
storageKey: BooleanSetting.confirm_step_deletion,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Hide Webcam widget"),
|
||||
description: t(Content.HIDE_WEBCAM_WIDGET),
|
||||
storageKey: BooleanSetting.hide_webcam_widget,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Dynamic map size"),
|
||||
description: t(Content.DYNAMIC_MAP_SIZE),
|
||||
storageKey: BooleanSetting.dynamic_map,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Double default map dimensions"),
|
||||
description: t(Content.DOUBLE_MAP_DIMENSIONS),
|
||||
storageKey: BooleanSetting.map_xl,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Display plant animations"),
|
||||
description: t(Content.PLANT_ANIMATIONS),
|
||||
storageKey: BooleanSetting.disable_animations,
|
||||
value: false,
|
||||
displayInvert: true
|
||||
},
|
||||
{
|
||||
name: t("Read speak logs in browser"),
|
||||
description: t(Content.BROWSER_SPEAK_LOGS),
|
||||
storageKey: BooleanSetting.enable_browser_speak,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: t("Discard Unsaved Changes"),
|
||||
description: t(Content.DISCARD_UNSAVED_CHANGES),
|
||||
storageKey: BooleanSetting.discard_unsaved,
|
||||
value: false,
|
||||
confirmationMessage: t(Content.DISCARD_UNSAVED_CHANGES_CONFIRM)
|
||||
},
|
||||
{
|
||||
name: t("Display virtual FarmBot trail"),
|
||||
description: t(Content.VIRTUAL_TRAIL),
|
||||
storageKey: BooleanSetting.display_trail,
|
||||
value: false,
|
||||
callback: () => sessionStorage.setItem(VirtualTrail.records, "[]")
|
||||
},
|
||||
].map(fetchSettingValue(getConfigValue)));
|
||||
|
||||
/** Always allow toggling from true => false (deactivate).
|
||||
* Require a disclaimer when going from false => true (activate). */
|
||||
export const maybeToggleFeature =
|
||||
(x: LabsFeature): LabsFeature | undefined => {
|
||||
return (x.value
|
||||
|| !x.confirmationMessage
|
||||
|| window.confirm(x.confirmationMessage)) ?
|
||||
toggleFeatureValue(x) : undefined;
|
||||
};
|
||||
|
||||
/** Stub this when testing if need be. */
|
||||
const fetchVal = (k: BooleanConfigKey) => !!Session.deprecatedGetBool(k);
|
||||
(getConfigValue: GetWebAppConfigValue, dispatch: Function) =>
|
||||
(x: LabsFeature): LabsFeature | undefined =>
|
||||
(x.value
|
||||
|| !x.confirmationMessage
|
||||
|| window.confirm(x.confirmationMessage)) ?
|
||||
toggleFeatureValue(getConfigValue, dispatch)(x) : undefined;
|
||||
|
||||
/** Takes a `LabFeature` (probably one with an uninitialized fallback / default
|
||||
* value) and sets it to the _real_ value that's in the API. */
|
||||
const fetchRealValue = (x: LabsFeature): LabsFeature => {
|
||||
return { ...x, value: fetchVal(x.storageKey) };
|
||||
};
|
||||
const fetchSettingValue = (getConfigValue: GetWebAppConfigValue) =>
|
||||
(x: LabsFeature): LabsFeature => {
|
||||
return { ...x, value: !!getConfigValue(x.storageKey) };
|
||||
};
|
||||
|
||||
/** Toggle the `.value` of a `LabsToggle` object */
|
||||
const toggleFeatureValue = (x: LabsFeature) => {
|
||||
return { ...x, value: Session.invertBool(x.storageKey) };
|
||||
};
|
||||
const toggleFeatureValue =
|
||||
(getConfigValue: GetWebAppConfigValue, dispatch: Function) =>
|
||||
(x: LabsFeature) => {
|
||||
const value = !getConfigValue(x.storageKey);
|
||||
dispatch(setWebAppConfigValue(x.storageKey, value));
|
||||
return { ...x, value };
|
||||
};
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import * as React from "react";
|
||||
import { fetchLabFeatures, LabsFeature } from "./labs_features_list_data";
|
||||
import { KeyValShowRow } from "../../controls/key_val_show_row";
|
||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
||||
interface LabsFeaturesListProps {
|
||||
onToggle(feature: LabsFeature): void;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
export function LabsFeaturesList(props: LabsFeaturesListProps) {
|
||||
return <div>
|
||||
{fetchLabFeatures().map((p, i) => {
|
||||
{fetchLabFeatures(props.getConfigValue).map((p, i) => {
|
||||
const displayValue = p.displayInvert ? !p.value : p.value;
|
||||
return <KeyValShowRow key={i}
|
||||
label={p.name}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { Everything } from "../interfaces";
|
||||
import { Props } from "./interfaces";
|
||||
import { getUserAccountSettings } from "../resources/selectors";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
const user = getUserAccountSettings(props.resources.index);
|
||||
|
||||
return {
|
||||
user,
|
||||
dispatch: () => { throw new Error("NEVER SHOULD HAPPEN"); }
|
||||
dispatch: () => { throw new Error("NEVER SHOULD HAPPEN"); },
|
||||
getConfigValue: getWebAppConfigValue(() => props),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ describe("API", () => {
|
|||
[API.current.pointSearchPath, BASE + "/api/points/search"],
|
||||
[API.current.sensorReadingPath, BASE + "/api/sensor_readings"],
|
||||
[API.current.farmwareEnvPath, BASE + "/api/farmware_envs"],
|
||||
[API.current.plantTemplatePath, BASE + "/api/plant_templates"],
|
||||
[API.current.plantTemplatePath, BASE + "/api/plant_templates/"],
|
||||
[API.current.diagnosticDumpsPath, BASE + "/api/diagnostic_dumps/"],
|
||||
[API.current.farmwareInstallationPath, BASE + "/api/farmware_installations"],
|
||||
].map(x => expect(x[0]).toEqual(x[1]));
|
||||
|
|
|
@ -144,7 +144,7 @@ export class API {
|
|||
(gardenId: number) => `${this.savedGardensPath}/${gardenId}/apply`;
|
||||
get exportDataPath() { return `${this.baseUrl}/api/export_data`; }
|
||||
/** /api/plant_templates/:id */
|
||||
get plantTemplatePath() { return `${this.baseUrl}/api/plant_templates`; }
|
||||
get plantTemplatePath() { return `${this.baseUrl}/api/plant_templates/`; }
|
||||
/** /api/diagnostic_dumps/:id */
|
||||
get diagnosticDumpsPath() { return `${this.baseUrl}/api/diagnostic_dumps/`; }
|
||||
/** /api/farmware_installations/:id */
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { UnsafeError } from "../interfaces";
|
||||
import { findByUuid } from "../resources/reducer";
|
||||
import { generateUuid } from "../resources/util";
|
||||
import { defensiveClone } from "../util";
|
||||
import { defensiveClone, unpackUUID } from "../util";
|
||||
import { EditResourceParams } from "./interfaces";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
import { SequenceBodyItem } from "farmbot/dist";
|
||||
|
@ -225,6 +225,7 @@ export function urlFor(tag: ResourceName) {
|
|||
FirmwareConfig: API.current.firmwareConfigPath,
|
||||
DiagnosticDump: API.current.diagnosticDumpsPath,
|
||||
SavedGarden: API.current.savedGardensPath,
|
||||
PlantTemplate: API.current.plantTemplatePath,
|
||||
};
|
||||
const url = OPTIONS[tag];
|
||||
if (url) {
|
||||
|
@ -247,7 +248,7 @@ export function updateViaAjax(payl: AjaxUpdatePayload) {
|
|||
let url = urlFor(kind);
|
||||
if (body.id) {
|
||||
verb = "put";
|
||||
if (!SINGULAR_RESOURCE.includes(payl.uuid.split(".")[0] as ResourceName)) {
|
||||
if (!SINGULAR_RESOURCE.includes(unpackUUID(payl.uuid).kind)) {
|
||||
url += body.id;
|
||||
}
|
||||
} else {
|
||||
|
@ -276,7 +277,8 @@ const MUST_CONFIRM_LIST: ResourceName[] = [
|
|||
"Point",
|
||||
"Sequence",
|
||||
"Regimen",
|
||||
"Image"
|
||||
"Image",
|
||||
"SavedGarden",
|
||||
];
|
||||
|
||||
const confirmationChecker = (resource: TaggedResource, force = false) =>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ResourceName } from "farmbot";
|
||||
import { startTracking } from "../connectivity/data_consistency";
|
||||
import { unpackUUID } from "../util";
|
||||
|
||||
const BLACKLIST: ResourceName[] = [
|
||||
"DiagnosticDump",
|
||||
|
@ -16,6 +17,6 @@ const BLACKLIST: ResourceName[] = [
|
|||
];
|
||||
|
||||
export function maybeStartTracking(uuid: string) {
|
||||
const ignore = BLACKLIST.includes(uuid.split(".")[0] as ResourceName);
|
||||
const ignore = BLACKLIST.includes(unpackUUID(uuid).kind);
|
||||
ignore || startTracking(uuid);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { validBotLocationData, validFwConfig } from "./util";
|
|||
import { BooleanSetting } from "./session_keys";
|
||||
import { getPathArray } from "./history";
|
||||
import { FirmwareConfig } from "./config_storage/firmware_configs";
|
||||
import { getWebAppConfigValue } from "./config_storage/actions";
|
||||
import { getWebAppConfigValue, GetWebAppConfigValue } from "./config_storage/actions";
|
||||
import { takeSortedLogs } from "./logs/state_to_props";
|
||||
|
||||
/** Remove 300ms delay on touch devices - https://github.com/ftlabs/fastclick */
|
||||
|
@ -44,6 +44,7 @@ export interface AppProps {
|
|||
xySwap: boolean;
|
||||
firmwareConfig: FirmwareConfig | undefined;
|
||||
animate: boolean;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
function mapStateToProps(props: Everything): AppProps {
|
||||
|
@ -64,6 +65,7 @@ function mapStateToProps(props: Everything): AppProps {
|
|||
xySwap: !!webAppConfigValue(BooleanSetting.xy_swap),
|
||||
firmwareConfig: validFwConfig(getFirmwareConfig(props.resources.index)),
|
||||
animate: !webAppConfigValue(BooleanSetting.disable_animations),
|
||||
getConfigValue: webAppConfigValue,
|
||||
};
|
||||
}
|
||||
/** Time at which the app gives up and asks the user to refresh */
|
||||
|
@ -111,7 +113,8 @@ export class App extends React.Component<AppProps, {}> {
|
|||
user={this.props.user}
|
||||
bot={this.props.bot}
|
||||
dispatch={this.props.dispatch}
|
||||
logs={this.props.logs} />
|
||||
logs={this.props.logs}
|
||||
getConfigValue={this.props.getConfigValue} />
|
||||
{!syncLoaded && <LoadingPlant animate={this.props.animate} />}
|
||||
{syncLoaded && this.props.children}
|
||||
{!(["controls", "account", "regimens"].includes(currentPage)) &&
|
||||
|
|
|
@ -19,8 +19,6 @@ jest.mock("axios", () => ({
|
|||
jest.mock("../../session", () => ({
|
||||
Session: {
|
||||
fetchStoredToken: jest.fn(),
|
||||
deprecatedGetNum: () => undefined,
|
||||
deprecatedGetBool: () => undefined,
|
||||
getAll: () => undefined,
|
||||
clear: jest.fn()
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import { store } from "../redux/store";
|
||||
import { BooleanConfigKey, NumberConfigKey } from "../config_storage/web_app_configs";
|
||||
import { edit, save } from "../api/crud";
|
||||
import { getWebAppConfig } from "../resources/selectors_by_kind";
|
||||
|
||||
/**
|
||||
* HISTORICAL CONTEXT: We once stored user settings (like map zoom level) in
|
||||
* localStorage and would retrieve values via `Session.getBool("zoom_level")`
|
||||
*
|
||||
* PROBLEM: localStorage is no longer used. Many parts of the app were accessing
|
||||
* values in places that did not have access to the Redux store.
|
||||
*
|
||||
* SOLUTION: Create a temporary shim that will "cheat" and directly call Redux
|
||||
* store without a lot of boilerplate props passing.
|
||||
*
|
||||
* WHY NOT JUST INLINE THESE FUNCTIONS?: It's easier to stub out calls in tests
|
||||
* that already exist.
|
||||
*/
|
||||
|
||||
/** Avoid using this function in new places. Pass props instead. */
|
||||
export function getBoolViaRedux(key: BooleanConfigKey): boolean | undefined {
|
||||
const conf = getWebAppConfig(store.getState().resources.index);
|
||||
return conf && conf.body[key];
|
||||
}
|
||||
|
||||
/** Avoid using this function in new places. Pass props instead. */
|
||||
export function setBoolViaRedux(key: BooleanConfigKey, val: boolean) {
|
||||
const conf = getWebAppConfig(store.getState().resources.index);
|
||||
if (conf) {
|
||||
store.dispatch(edit(conf, { [key]: val }));
|
||||
// tslint:disable-next-line:no-any
|
||||
store.dispatch(save(conf.uuid) as any);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/** Avoid using this function in new places. Pass props instead. */
|
||||
export function getNumViaRedux(key: NumberConfigKey): number | undefined {
|
||||
const conf = getWebAppConfig(store.getState().resources.index);
|
||||
return conf && conf.body[key];
|
||||
}
|
||||
|
||||
/** Avoid using this function in new places. Pass props instead. */
|
||||
export function setNumViaRedux(key: NumberConfigKey, val: number): number {
|
||||
const conf = getWebAppConfig(store.getState().resources.index);
|
||||
if (conf) {
|
||||
store.dispatch(edit(conf, { [key]: val }));
|
||||
// tslint:disable-next-line:no-any
|
||||
store.dispatch(save(conf.uuid) as any);
|
||||
}
|
||||
return val;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
BooleanConfigKey as BooleanWebAppConfigKey,
|
||||
NumberConfigKey as NumberWebAppConfigKey,
|
||||
StringConfigKey as StringWebAppConfigKey
|
||||
StringConfigKey as StringWebAppConfigKey,
|
||||
WebAppConfig
|
||||
} from "./web_app_configs";
|
||||
import { GetState } from "../redux/interfaces";
|
||||
import { edit, save } from "../api/crud";
|
||||
|
@ -12,7 +13,7 @@ export function toggleWebAppBool(key: BooleanWebAppConfigKey) {
|
|||
return (dispatch: Function, getState: GetState) => {
|
||||
const conf = getWebAppConfig(getState().resources.index);
|
||||
if (conf) {
|
||||
const val = !conf.body[key];
|
||||
const val = !(conf.body as WebAppConfig)[key];
|
||||
dispatch(edit(conf, { [key]: val }));
|
||||
dispatch(save(conf.uuid));
|
||||
} else {
|
||||
|
@ -33,7 +34,7 @@ export type GetWebAppConfigValue = (k: WebAppConfigKey) => WebAppConfigValue;
|
|||
export function getWebAppConfigValue(getState: GetState) {
|
||||
return (key: WebAppConfigKey): WebAppConfigValue => {
|
||||
const conf = getWebAppConfig(getState().resources.index);
|
||||
return conf && conf.body[key];
|
||||
return conf && (conf.body as WebAppConfig)[key];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ export interface WebAppConfig {
|
|||
discard_unsaved: boolean;
|
||||
xy_swap: boolean;
|
||||
home_button_homing: boolean;
|
||||
show_motor_plot: boolean;
|
||||
}
|
||||
|
||||
export type NumberConfigKey = "id"
|
||||
|
@ -89,4 +90,5 @@ export type BooleanConfigKey = "confirm_step_deletion"
|
|||
|"show_images"
|
||||
|"discard_unsaved"
|
||||
|"xy_swap"
|
||||
|"home_button_homing";
|
||||
|"home_button_homing"
|
||||
|"show_motor_plot";
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from "../auto_sync";
|
||||
import { destroyOK } from "../../resources/actions";
|
||||
import { SkipMqttData, BadMqttData, UpdateMqttData, DeleteMqttData } from "../interfaces";
|
||||
import { unpackUUID } from "../../util";
|
||||
|
||||
describe("handleInbound()", () => {
|
||||
const dispatch = jest.fn();
|
||||
|
@ -50,7 +51,7 @@ describe("handleInbound()", () => {
|
|||
it("handles DELETE when the record is in system", () => {
|
||||
const i = getState().resources.index.byKind.Sequence;
|
||||
// Pick an ID that we know will be in the DB
|
||||
const id = parseInt(Object.values(i)[0].split(".")[1], 10);
|
||||
const id = unpackUUID(Object.values(i)[0]).remoteId || -1;
|
||||
const fixtr: DeleteMqttData = { status: "DELETE", kind: "Sequence", id };
|
||||
handleInbound(dispatch, getState, fixtr);
|
||||
expect(dispatch).toHaveBeenCalled();
|
||||
|
|
|
@ -12,6 +12,7 @@ import { fakeState } from "../../__test_support__/fake_state";
|
|||
import { GetState } from "../../redux/interfaces";
|
||||
import { SyncPayload, UpdateMqttData, Reason } from "../interfaces";
|
||||
import { storeUUID } from "../data_consistency";
|
||||
import { unpackUUID } from "../../util";
|
||||
|
||||
function toBinary(input: object): Buffer {
|
||||
return Buffer.from(JSON.stringify(input), "utf8");
|
||||
|
@ -62,8 +63,10 @@ describe("handleCreateOrUpdate", () => {
|
|||
const dispatch = jest.fn();
|
||||
const getState = jest.fn(fakeState) as GetState;
|
||||
const { index } = getState().resources;
|
||||
const fakeId = Object.values(index.byKind.Sequence)[0].split(".")[1];
|
||||
myPayload.id = parseInt(fakeId, 10);
|
||||
|
||||
const fakeId =
|
||||
unpackUUID(Object.values(index.byKind.Sequence)[0]).remoteId || -1;
|
||||
myPayload.id = fakeId;
|
||||
myPayload.kind = "Sequence";
|
||||
handleCreateOrUpdate(dispatch, getState, myPayload);
|
||||
expect(dispatch).toHaveBeenCalled();
|
||||
|
|
|
@ -31,7 +31,7 @@ set(window, "outstanding_requests", outstandingRequests);
|
|||
const PLACEHOLDER = "placeholder";
|
||||
|
||||
/** Max wait in MS before clearing out. */
|
||||
const MAX_WAIT = 3500;
|
||||
const MAX_WAIT = 11000;
|
||||
|
||||
/**
|
||||
* PROBLEM: You save a sequence and click "RUN" very fast. The remote device
|
||||
|
@ -71,7 +71,7 @@ export function startTracking(uuid = PLACEHOLDER) {
|
|||
}
|
||||
storeUUID(cleanID);
|
||||
getDevice().on(cleanID, () => stopTracking(cleanID));
|
||||
setTimeout(stop, MAX_WAIT);
|
||||
setTimeout(() => stopTracking(uuid), MAX_WAIT);
|
||||
}
|
||||
|
||||
export function stopTracking(uuid: string) {
|
||||
|
|
|
@ -37,7 +37,8 @@ export namespace ToolTips {
|
|||
few sequences to verify that everything works as expected.`);
|
||||
|
||||
export const PIN_BINDINGS =
|
||||
trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is activated.`);
|
||||
trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is
|
||||
activated.`);
|
||||
|
||||
export const PIN_BINDING_WARNING =
|
||||
trim(`Warning: Binding to a pin without a physical button and
|
||||
|
@ -49,10 +50,10 @@ export namespace ToolTips {
|
|||
|
||||
// Hardware Settings: Homing and Calibration
|
||||
export const HOMING =
|
||||
trim(`(Alpha) If encoders or end-stops are enabled, home axis (find zero).`);
|
||||
trim(`If encoders or end-stops are enabled, home axis (find zero).`);
|
||||
|
||||
export const CALIBRATION =
|
||||
trim(`(Alpha) If encoders or end-stops are enabled, home axis and determine
|
||||
trim(`If encoders or end-stops are enabled, home axis and determine
|
||||
maximum.`);
|
||||
|
||||
export const SET_ZERO_POSITION =
|
||||
|
@ -107,8 +108,8 @@ export namespace ToolTips {
|
|||
trim(`The number of motor steps required to move the axis one millimeter.`);
|
||||
|
||||
export const ALWAYS_POWER_MOTORS =
|
||||
trim(`Keep power applied to motors. Prevents slipping from gravity in certain
|
||||
situations.`);
|
||||
trim(`Keep power applied to motors. Prevents slipping from gravity in
|
||||
certain situations.`);
|
||||
|
||||
export const INVERT_MOTORS =
|
||||
trim(`Invert direction of motor during calibration.`);
|
||||
|
@ -118,23 +119,23 @@ export namespace ToolTips {
|
|||
|
||||
// Hardware Settings: Encoders and Endstops
|
||||
export const ENABLE_ENCODERS =
|
||||
trim(`(Alpha) Enable use of rotary encoders during calibration and homing.`);
|
||||
trim(`Enable use of rotary encoders during calibration and homing.`);
|
||||
|
||||
export const ENCODER_POSITIONING =
|
||||
trim(`[EXPERIMENTAL] Use encoders for positioning.`);
|
||||
trim(`Use encoders for positioning.`);
|
||||
|
||||
export const INVERT_ENCODERS =
|
||||
trim(`(Alpha) Reverse the direction of encoder position reading.`);
|
||||
trim(`Reverse the direction of encoder position reading.`);
|
||||
|
||||
export const MAX_MISSED_STEPS =
|
||||
trim(`(Alpha) Number of steps missed (determined by encoder) before motor is
|
||||
trim(`Number of steps missed (determined by encoder) before motor is
|
||||
considered to have stalled.`);
|
||||
|
||||
export const ENCODER_MISSED_STEP_DECAY =
|
||||
trim(`(Alpha) Reduction to missed step total for every good step.`);
|
||||
trim(`Reduction to missed step total for every good step.`);
|
||||
|
||||
export const ENCODER_SCALING =
|
||||
trim(`(Alpha) encoder scaling factor = 10000 * (motor resolution * microsteps)
|
||||
trim(`encoder scaling factor = 10000 * (motor resolution * microsteps)
|
||||
/ (encoder resolution).`);
|
||||
|
||||
export const ENABLE_ENDSTOPS =
|
||||
|
@ -254,6 +255,12 @@ export namespace ToolTips {
|
|||
trim(`Snaps a photo using the device camera. Select the camera type on the
|
||||
Device page.`);
|
||||
|
||||
export const MARK_AS =
|
||||
trim(`The Mark As step allows FarmBot to programmatically edit the
|
||||
properties of the UTM, plants, and weeds from within a sequence.
|
||||
For example, you can mark a plant as "planted" during a seeding
|
||||
sequence or delete a weed after removing it.`);
|
||||
|
||||
// Regimens
|
||||
export const BULK_SCHEDULER =
|
||||
trim(`Add sequences to your regimen by selecting a sequence from the
|
||||
|
@ -522,8 +529,8 @@ export namespace Content {
|
|||
click "+" in the Regimens panel to create a new one.`);
|
||||
|
||||
export const NO_PARAMETERS = trim(`Can't directly use this sequence in a
|
||||
regimen. Consider wrapping it in a parent sequence that calls it via "execute"
|
||||
instead."`);
|
||||
regimen. Consider wrapping it in a parent sequence that calls it via
|
||||
"execute" instead.`);
|
||||
|
||||
// Farm Designer
|
||||
export const OUTSIDE_PLANTING_AREA =
|
||||
|
@ -538,6 +545,10 @@ export namespace Content {
|
|||
trim(`Click and drag to draw a point or use the inputs and press
|
||||
update. Press CREATE POINT to save, or the back arrow to exit.`);
|
||||
|
||||
export const BOX_SELECT_DESCRIPTION =
|
||||
trim(`Drag a box around the plants you would like to select.
|
||||
Press the back arrow to exit.`);
|
||||
|
||||
// Farm Events
|
||||
export const REGIMEN_TODAY_SKIPPED_ITEM_RISK =
|
||||
trim(`You are scheduling a regimen to run today. Be aware that
|
||||
|
@ -634,6 +645,7 @@ export enum Actions {
|
|||
OF_SEARCH_RESULTS_OK = "OF_SEARCH_RESULTS_OK",
|
||||
CHOOSE_LOCATION = "CHOOSE_LOCATION",
|
||||
SET_CURRENT_POINT_DATA = "SET_CURRENT_POINT_DATA",
|
||||
CHOOSE_SAVED_GARDEN = "CHOOSE_SAVED_GARDEN",
|
||||
|
||||
// Regimens
|
||||
PUSH_WEEK = "PUSH_WEEK",
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
jest.mock("moment", () => () => ({ unix: () => 1020 }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { MotorPositionPlot, MotorPositionHistory } from "../motor_position_plot";
|
||||
import { BotLocationData, BotPosition } from "../../../devices/interfaces";
|
||||
|
||||
describe("<MotorPositionPlot />", () => {
|
||||
const fakePosition = (): BotPosition => ({ x: 0, y: 0, z: 0 });
|
||||
const fakeLocationData = (): BotLocationData => ({
|
||||
position: fakePosition(),
|
||||
scaled_encoders: fakePosition(),
|
||||
raw_encoders: fakePosition()
|
||||
});
|
||||
|
||||
const fakeProps = () => ({
|
||||
locationData: fakeLocationData(),
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<MotorPositionPlot {...fakeProps()} />);
|
||||
["x", "y", "z", "position", "seconds ago", "120", "100"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
});
|
||||
|
||||
it("renders motor position", () => {
|
||||
const location1 = fakeLocationData();
|
||||
const location2 = fakeLocationData();
|
||||
location2.position.x = 100;
|
||||
sessionStorage.setItem(MotorPositionHistory.array, JSON.stringify([
|
||||
{ timestamp: 1000, locationData: location1 },
|
||||
{ timestamp: 1010, locationData: location2 },
|
||||
]));
|
||||
const wrapper = mount(<MotorPositionPlot {...fakeProps()} />);
|
||||
expect(wrapper.html()).toContain("M 120,0 L 120,0 L 110,-12.5 L 100,0");
|
||||
expect(wrapper.html()).toContain("M 120,0 L 120,0 L 110,0 L 100,0");
|
||||
});
|
||||
|
||||
it("handles undefined data", () => {
|
||||
const location1 = fakeLocationData();
|
||||
const location2 = fakeLocationData();
|
||||
location2.position.x = undefined;
|
||||
sessionStorage.setItem(MotorPositionHistory.array, JSON.stringify([
|
||||
{ timestamp: 1000, locationData: location1 },
|
||||
{ timestamp: 1010, locationData: location2 },
|
||||
]));
|
||||
const wrapper = mount(<MotorPositionPlot {...fakeProps()} />);
|
||||
expect(wrapper.html()).not.toContain("M 120,0 L 120,0 L 110,-12.5 L 100,0");
|
||||
expect(wrapper.html()).toContain("M 120,0 L 120,0 L 110,0 L 100,0");
|
||||
});
|
||||
});
|
|
@ -13,7 +13,7 @@ jest.mock("../../../config_storage/actions", () => {
|
|||
});
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { Move } from "../move";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { MoveProps } from "../interfaces";
|
||||
|
@ -81,4 +81,10 @@ describe("<Move />", () => {
|
|||
payload: 1
|
||||
});
|
||||
});
|
||||
|
||||
it("displays motor position plot", () => {
|
||||
mockConfig.show_motor_plot = true;
|
||||
const wrapper = shallow(<Move {...fakeProps()} />);
|
||||
expect(wrapper.html()).toContain("motor-position-plot");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { moveWidgetSetting } from "../settings_menu";
|
||||
import { moveWidgetSetting, MoveWidgetSettingsMenu } from "../settings_menu";
|
||||
|
||||
describe("moveWidgetSetting()", () => {
|
||||
it("renders setting", () => {
|
||||
|
@ -13,3 +13,18 @@ describe("moveWidgetSetting()", () => {
|
|||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
});
|
||||
});
|
||||
|
||||
describe("<MoveWidgetSettingsMenu />", () => {
|
||||
const fakeProps = () => ({
|
||||
toggle: jest.fn(),
|
||||
getValue: jest.fn(),
|
||||
});
|
||||
|
||||
it("displays motor plot toggle", () => {
|
||||
const noToggle = mount(<MoveWidgetSettingsMenu {...fakeProps()} />);
|
||||
expect(noToggle.text()).not.toContain("Motor position plot");
|
||||
localStorage.setItem("FUTURE_FEATURES", "true");
|
||||
const wrapper = mount(<MoveWidgetSettingsMenu {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("Motor position plot");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
import * as React from "react";
|
||||
import * as _ from "lodash";
|
||||
import { Xyz, LocationName, Dictionary } from "farmbot";
|
||||
import * as moment from "moment";
|
||||
import { BotLocationData, BotPosition } from "../../devices/interfaces";
|
||||
import { trim } from "../../util";
|
||||
import { t } from "i18next";
|
||||
|
||||
const HEIGHT = 50;
|
||||
const HISTORY_LENGTH_SECONDS = 120;
|
||||
const BORDER_WIDTH = 15;
|
||||
const BORDERS = BORDER_WIDTH * 2;
|
||||
const MAX_X = HISTORY_LENGTH_SECONDS;
|
||||
const DEFAULT_Y_MAX = 100;
|
||||
|
||||
const COLOR_LOOKUP: Dictionary<string> = {
|
||||
x: "red", y: "green", z: "blue"
|
||||
};
|
||||
const LINEWIDTH_LOOKUP: Dictionary<number> = {
|
||||
position: 0.5, scaled_encoders: 0.25
|
||||
};
|
||||
|
||||
export enum MotorPositionHistory {
|
||||
array = "motorPositionHistoryArray",
|
||||
}
|
||||
|
||||
type Entry = {
|
||||
timestamp: number,
|
||||
locationData: Record<LocationName, BotPosition>
|
||||
};
|
||||
|
||||
type Paths = Record<LocationName, Record<Xyz, string>>;
|
||||
|
||||
const getArray = (): Entry[] =>
|
||||
JSON.parse(_.get(sessionStorage, MotorPositionHistory.array, "[]"));
|
||||
|
||||
const getReversedArray = (): Entry[] => _.cloneDeep(getArray()).reverse();
|
||||
|
||||
const getLastEntry = (): Entry | undefined => {
|
||||
const array = getArray();
|
||||
return array[array.length - 1];
|
||||
};
|
||||
|
||||
const findYLimit = (): number => {
|
||||
const array = getArray();
|
||||
const arrayAbsMax = _.max(array.map(entry =>
|
||||
_.max(["position", "scaled_encoders"].map((name: LocationName) =>
|
||||
_.max(["x", "y", "z"].map((axis: Xyz) =>
|
||||
Math.abs(entry.locationData[name][axis] || 0) + 1))))));
|
||||
return Math.max(_.ceil(arrayAbsMax || 0, -2), DEFAULT_Y_MAX);
|
||||
};
|
||||
|
||||
const updateArray = (update: Entry): Entry[] => {
|
||||
const arr = getArray();
|
||||
const last = getLastEntry();
|
||||
if (update && _.isNumber(update.locationData.position.x) &&
|
||||
(!last || !_.isEqual(last.timestamp, update.timestamp))) {
|
||||
arr.push(update);
|
||||
}
|
||||
const newArray = _.takeRight(arr, 100)
|
||||
.filter(x => {
|
||||
const entryAge = (last ? last.timestamp : moment().unix()) - x.timestamp;
|
||||
return entryAge <= HISTORY_LENGTH_SECONDS;
|
||||
});
|
||||
sessionStorage.setItem(MotorPositionHistory.array, JSON.stringify(newArray));
|
||||
return newArray;
|
||||
};
|
||||
|
||||
const newPaths = (): Paths => ({
|
||||
position: { x: "", y: "", z: "" },
|
||||
scaled_encoders: { x: "", y: "", z: "" },
|
||||
raw_encoders: { x: "", y: "", z: "" }
|
||||
});
|
||||
|
||||
const getPaths = (): Paths => {
|
||||
const last = getLastEntry();
|
||||
const maxY = findYLimit();
|
||||
const paths = newPaths();
|
||||
if (last) {
|
||||
getReversedArray().map(entry => {
|
||||
["position", "scaled_encoders"].map((name: LocationName) => {
|
||||
["x", "y", "z"].map((axis: Xyz) => {
|
||||
const lastPos = last.locationData[name][axis];
|
||||
const pos = entry.locationData[name][axis];
|
||||
if (_.isNumber(lastPos) && _.isFinite(lastPos)
|
||||
&& _.isNumber(maxY) && _.isNumber(pos)) {
|
||||
if (!paths[name][axis].startsWith("M")) {
|
||||
const yStart = -lastPos / maxY * HEIGHT / 2;
|
||||
paths[name][axis] = `M ${MAX_X},${yStart} `;
|
||||
}
|
||||
const x = MAX_X - (last.timestamp - entry.timestamp);
|
||||
const y = -pos / maxY * HEIGHT / 2;
|
||||
paths[name][axis] += `L ${x},${y} `;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
const TitleLegend = () => {
|
||||
const titleY = -(HEIGHT + BORDER_WIDTH) / 2;
|
||||
const legendX = HISTORY_LENGTH_SECONDS / 4;
|
||||
return <g id="title_with_legend">
|
||||
<text fill={COLOR_LOOKUP.x} fontWeight={"bold"}
|
||||
x={legendX - 10} y={titleY}>{"X"}</text>
|
||||
<text fill={COLOR_LOOKUP.y} fontWeight={"bold"}
|
||||
x={legendX} y={titleY}>{"Y"}</text>
|
||||
<text fill={COLOR_LOOKUP.z} fontWeight={"bold"}
|
||||
x={legendX + 10} y={titleY}>{"Z"}</text>
|
||||
<text fontWeight={"bold"}
|
||||
x={HISTORY_LENGTH_SECONDS / 2} y={titleY}>{t("Position (mm)")}</text>
|
||||
</g>;
|
||||
};
|
||||
|
||||
const YAxisLabels = () => {
|
||||
const maxY = findYLimit();
|
||||
return <g id="y_axis_labels">
|
||||
{[maxY, maxY / 2, 0, -maxY / 2, -maxY].map(yPosition =>
|
||||
<g key={"y_axis_label_" + yPosition}>
|
||||
<text x={MAX_X + BORDER_WIDTH / 2} y={-yPosition / maxY * HEIGHT / 2}>
|
||||
{yPosition}
|
||||
</text>
|
||||
<text x={-BORDER_WIDTH / 2} y={-yPosition / maxY * HEIGHT / 2}>
|
||||
{yPosition}
|
||||
</text>
|
||||
</g>)}
|
||||
</g>;
|
||||
};
|
||||
|
||||
const XAxisLabels = () =>
|
||||
<g id="x_axis_labels">
|
||||
<text x={HISTORY_LENGTH_SECONDS / 2} y={HEIGHT / 2 + BORDER_WIDTH / 1.25}
|
||||
fontStyle={"italic"}>
|
||||
{t("seconds ago")}
|
||||
</text>
|
||||
{_.range(0, HISTORY_LENGTH_SECONDS + 1, 20).map(secondsAgo =>
|
||||
<text key={"x_axis_label_" + secondsAgo}
|
||||
x={MAX_X - secondsAgo} y={HEIGHT / 2 + BORDER_WIDTH / 3}>
|
||||
{secondsAgo}
|
||||
</text>)}
|
||||
</g>;
|
||||
|
||||
const PlotBackground = () =>
|
||||
<g id="plot_background">
|
||||
<rect fill="white" x={0} y={-HEIGHT / 2} width={"100%"} height={"100%"} />
|
||||
<line x1={0} y1={0} x2={MAX_X} y2={0} strokeWidth={0.25} stroke={"grey"} />
|
||||
</g>;
|
||||
|
||||
const PlotLines = ({ locationData }: { locationData: BotLocationData }) => {
|
||||
updateArray({ timestamp: moment().unix(), locationData });
|
||||
const paths = getPaths();
|
||||
return <g id="plot_lines">
|
||||
{["position", "scaled_encoders"].map((name: LocationName) =>
|
||||
["x", "y", "z"].map((axis: Xyz) =>
|
||||
<path key={name + axis} fill={"none"}
|
||||
stroke={COLOR_LOOKUP[axis]} strokeWidth={LINEWIDTH_LOOKUP[name]}
|
||||
strokeLinecap={"round"} strokeLinejoin={"round"}
|
||||
d={paths[name][axis]} />))}
|
||||
</g>;
|
||||
};
|
||||
|
||||
export const MotorPositionPlot = (props: { locationData: BotLocationData }) => {
|
||||
return <svg
|
||||
className="motor-position-plot-border"
|
||||
style={{ marginTop: "2rem" }}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox={trim(`${-BORDER_WIDTH} ${-HEIGHT / 2 - BORDER_WIDTH}
|
||||
${HISTORY_LENGTH_SECONDS + BORDERS} ${HEIGHT + BORDERS}`)}>
|
||||
<TitleLegend />
|
||||
<YAxisLabels />
|
||||
<XAxisLabels />
|
||||
<svg
|
||||
className="motor-position-plot"
|
||||
width={HISTORY_LENGTH_SECONDS}
|
||||
height={HEIGHT}
|
||||
x={0}
|
||||
y={-HEIGHT / 2}
|
||||
viewBox={`0 ${-HEIGHT / 2} ${HISTORY_LENGTH_SECONDS} ${HEIGHT}`}>
|
||||
<PlotBackground />
|
||||
<PlotLines locationData={props.locationData} />
|
||||
</svg>
|
||||
</svg>;
|
||||
};
|
|
@ -14,6 +14,8 @@ import { MoveProps } from "./interfaces";
|
|||
import { MoveWidgetSettingsMenu } from "./settings_menu";
|
||||
import { JogControlsGroup } from "./jog_controls_group";
|
||||
import { BotPositionRows } from "./bot_position_rows";
|
||||
import { MotorPositionPlot } from "./motor_position_plot";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
|
||||
export class Move extends React.Component<MoveProps, {}> {
|
||||
|
||||
|
@ -30,9 +32,12 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
<WidgetHeader
|
||||
title={t("Move")}
|
||||
helpText={ToolTips.MOVE}>
|
||||
<MoveWidgetSettingsMenu
|
||||
toggle={this.toggle}
|
||||
getValue={this.getValue} />
|
||||
<Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
<MoveWidgetSettingsMenu
|
||||
toggle={this.toggle}
|
||||
getValue={this.getValue} />
|
||||
</Popover>
|
||||
<EStopButton
|
||||
bot={this.props.bot}
|
||||
user={this.props.user} />
|
||||
|
@ -55,6 +60,8 @@ export class Move extends React.Component<MoveProps, {}> {
|
|||
arduinoBusy={this.props.arduinoBusy}
|
||||
firmware_version={informational_settings.firmware_version} />
|
||||
</MustBeOnline>
|
||||
{this.props.getWebAppConfigVal(BooleanSetting.show_motor_plot) &&
|
||||
<MotorPositionPlot locationData={locationData} />}
|
||||
</WidgetBody>
|
||||
</Widget>;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
import {
|
||||
BooleanConfigKey as BooleanWebAppConfigKey
|
||||
} from "../../config_storage/web_app_configs";
|
||||
|
@ -24,29 +23,34 @@ export const MoveWidgetSettingsMenu = ({ toggle, getValue }: {
|
|||
getValue: GetWebAppBool
|
||||
}) => {
|
||||
const Setting = moveWidgetSetting(toggle, getValue);
|
||||
return <Popover position={Position.BOTTOM_RIGHT}>
|
||||
<i className="fa fa-gear" />
|
||||
<div className="move-settings-menu">
|
||||
<p>{t("Invert Jog Buttons")}</p>
|
||||
<Setting label={t("X Axis")} setting={BooleanSetting.x_axis_inverted} />
|
||||
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
|
||||
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
|
||||
return <div className="move-settings-menu">
|
||||
<p>{t("Invert Jog Buttons")}</p>
|
||||
<Setting label={t("X Axis")} setting={BooleanSetting.x_axis_inverted} />
|
||||
<Setting label={t("Y Axis")} setting={BooleanSetting.y_axis_inverted} />
|
||||
<Setting label={t("Z Axis")} setting={BooleanSetting.z_axis_inverted} />
|
||||
|
||||
<p>{t("Display Encoder Data")}</p>
|
||||
<Setting
|
||||
label={t("Scaled encoder position")}
|
||||
setting={BooleanSetting.scaled_encoders} />
|
||||
<Setting
|
||||
label={t("Raw encoder position")}
|
||||
setting={BooleanSetting.raw_encoders} />
|
||||
<p>{t("Display Encoder Data")}</p>
|
||||
<Setting
|
||||
label={t("Scaled encoder position")}
|
||||
setting={BooleanSetting.scaled_encoders} />
|
||||
<Setting
|
||||
label={t("Raw encoder position")}
|
||||
setting={BooleanSetting.raw_encoders} />
|
||||
|
||||
<p>{t("Swap jog buttons (and rotate map)")}</p>
|
||||
<Setting label={t("x and y axis")} setting={BooleanSetting.xy_swap} />
|
||||
<p>{t("Swap jog buttons (and rotate map)")}</p>
|
||||
<Setting label={t("x and y axis")} setting={BooleanSetting.xy_swap} />
|
||||
|
||||
<p>{t("Home button behavior")}</p>
|
||||
<Setting
|
||||
label={t("perform homing (find home)")}
|
||||
setting={BooleanSetting.home_button_homing} />
|
||||
</div>
|
||||
</Popover>;
|
||||
<p>{t("Home button behavior")}</p>
|
||||
<Setting
|
||||
label={t("perform homing (find home)")}
|
||||
setting={BooleanSetting.home_button_homing} />
|
||||
|
||||
{localStorage.getItem("FUTURE_FEATURES") &&
|
||||
<div>
|
||||
<p>{t("Motor position plot")}</p>
|
||||
<Setting
|
||||
label={t("show")}
|
||||
setting={BooleanSetting.show_motor_plot} />
|
||||
</div>}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -97,7 +97,7 @@ export class Peripherals extends React.Component<PeripheralsProps, PeripheralSta
|
|||
<button
|
||||
className="fb-button gray"
|
||||
onClick={this.toggle}
|
||||
hidden={!!status && isEditing}>
|
||||
disabled={!!status && isEditing}>
|
||||
{!isEditing && t("Edit")}
|
||||
{isEditing && t("Back")}
|
||||
</button>
|
||||
|
|
|
@ -85,7 +85,7 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
|
|||
<button
|
||||
className="fb-button gray"
|
||||
onClick={this.toggle}
|
||||
hidden={!!status && isEditing}>
|
||||
disabled={!!status && isEditing}>
|
||||
{!isEditing && t("Edit")}
|
||||
{isEditing && t("Back")}
|
||||
</button>
|
||||
|
|
|
@ -27,7 +27,7 @@ describe("<WebcamPanel/>", () => {
|
|||
wrapper.setState({ activeMenu: "edit" });
|
||||
const text = allButtonText(wrapper);
|
||||
expect(text.toLowerCase()).not.toContain("edit");
|
||||
clickButton(wrapper, 2, "view");
|
||||
clickButton(wrapper, 0, "back");
|
||||
expect(wrapper.instance().state.activeMenu).toEqual("show");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,9 +30,10 @@ export function Edit(props: WebcamPanelProps) {
|
|||
return <Widget>
|
||||
<WidgetHeader title="Edit" helpText={ToolTips.WEBCAM}>
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={props.init}>
|
||||
<i className="fa fa-plus" />
|
||||
className="fb-button gray"
|
||||
disabled={unsaved.length > 0}
|
||||
onClick={props.onToggle}>
|
||||
{t("Back")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button green"
|
||||
|
@ -40,9 +41,9 @@ export function Edit(props: WebcamPanelProps) {
|
|||
{t("Save")}{unsaved.length > 0 ? "*" : ""}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button gray"
|
||||
onClick={props.onToggle}>
|
||||
{t("View")}
|
||||
className="fb-button green"
|
||||
onClick={props.init}>
|
||||
<i className="fa fa-plus" />
|
||||
</button>
|
||||
</WidgetHeader>
|
||||
<div className="widget-body">
|
||||
|
|
|
@ -68,19 +68,19 @@
|
|||
&:hover {
|
||||
background: $white !important;
|
||||
}
|
||||
:first-child {
|
||||
:first-child {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
width: 73%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pt-popover-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
position: relative;
|
||||
* {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
color: $white;
|
||||
}
|
||||
.mobile-menu {
|
||||
z-index: 9999;
|
||||
background: $dark_gray;
|
||||
color: $white;
|
||||
z-index: 9999;
|
||||
height: 100vh;
|
||||
outline: none;
|
||||
animation: slide-in 0.2s;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.fb-button {
|
||||
position: relative;
|
||||
font-size: 1rem;
|
||||
float: right;
|
||||
text-transform: uppercase;
|
||||
|
@ -7,7 +8,6 @@
|
|||
padding: .2rem .8rem;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
position: relative;
|
||||
color: $off_white;
|
||||
transition: all 0.1s ease-in-out !important;
|
||||
&.disabled,
|
||||
|
@ -25,6 +25,7 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
.btn-spinner {
|
||||
margin: 0.2rem 0px 0 0.6rem;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
animation: rotate 0.8s infinite linear;
|
||||
|
@ -32,7 +33,6 @@
|
|||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
float: right;
|
||||
margin: 0.2rem 0px 0 0.6rem;
|
||||
&.sync {
|
||||
border-color: $dark_gray;
|
||||
border-right-color: transparent;
|
||||
|
@ -200,40 +200,39 @@
|
|||
}
|
||||
|
||||
.fb-toggle-button {
|
||||
height: 1.8rem !important;
|
||||
position: relative;
|
||||
height: 1.8rem !important;
|
||||
border-bottom: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0.3rem 0.10rem;
|
||||
border-radius: 10px;
|
||||
width: 5rem;
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
transition: all 0.4s ease;
|
||||
&.yellow {
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.1rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
height: 1.6rem;
|
||||
width: 1.6rem;
|
||||
background: $white;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0.1rem;
|
||||
box-shadow: inset rgba(0, 0, 0, 0.03) 0px 0px 3px 3px;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.1rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
height: 1.6rem;
|
||||
width: 1.6rem;
|
||||
border-radius: 50%;
|
||||
top: 0.1rem;
|
||||
margin: 0 auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 0px 0 rgba(0, 0, 0, 0.04), 0 4px 9px rgba(0, 0, 0, 0.13), 0 3px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
@ -243,12 +242,12 @@
|
|||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.1rem;
|
||||
right: 0.2rem;
|
||||
height: 1.6rem;
|
||||
width: 1.6rem;
|
||||
background: $white;
|
||||
border-radius: 50%;
|
||||
top: 0.1rem;
|
||||
right: 0.2rem;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 0px 0 rgba(0, 0, 0, 0.04), 0 4px 9px rgba(0, 0, 0, 0.13), 0 3px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
&:hover {
|
||||
|
@ -264,12 +263,12 @@
|
|||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.1rem;
|
||||
left: 0.2rem;
|
||||
height: 1.6rem;
|
||||
width: 1.6rem;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
top: 0.1rem;
|
||||
left: 0.2rem;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 0px 0 rgba(0, 0, 0, 0.04), 0 4px 9px rgba(0, 0, 0, 0.13), 0 3px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
&.dim {
|
||||
|
@ -282,9 +281,12 @@
|
|||
}
|
||||
|
||||
.plus-button {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 5rem;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
border-radius: 50%;
|
||||
bottom: 5rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: $off_white;
|
||||
|
@ -292,9 +294,6 @@
|
|||
text-align: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 3px 0px 0px $dark_green;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
i {
|
||||
line-height: 5rem;
|
||||
font-size: 2.8rem;
|
||||
|
|
|
@ -4,9 +4,10 @@ $translucent2: rgba(0, 0, 0, 0.6);
|
|||
$white: #fff;
|
||||
$off_white: #f4f4f4;
|
||||
$light_gray: #ddd;
|
||||
$gray: #cccccc;
|
||||
$gray: #ccc;
|
||||
$medium_light_gray: #bcbcbc;
|
||||
$medium_gray: #666666;
|
||||
$medium_gray: #666;
|
||||
$placeholder_gray: #999;
|
||||
$dark_gray: #434343;
|
||||
$black: #000;
|
||||
$light_blue: #cdf;
|
||||
|
@ -22,13 +23,13 @@ $yellow: #fd6;
|
|||
$gold: #b90;
|
||||
$beige: #fec;
|
||||
$light_brown: #e9d5c3;
|
||||
$brown: #CA8;
|
||||
$brown: #ca8;
|
||||
$dark_brown: #783f04;
|
||||
$light_orange: #fc9;
|
||||
$orange: #fa0;
|
||||
$dark_orange: #e93;
|
||||
$light_purple: #bad;
|
||||
$purple: #C68ED2;
|
||||
$purple: #c68ed2;
|
||||
$light_magenta: #ead1dc;
|
||||
$magenta: #a64d79;
|
||||
$pink: #ebb;
|
||||
|
@ -114,4 +115,4 @@ $darkest_red: #900;
|
|||
.fun,
|
||||
.saucer-fun {
|
||||
background: $dark_blue !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
.farm-designer {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.farm-designer-map {
|
||||
min-width: 100%;
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
padding: 16rem 2rem 2rem 2rem; // at zoom = 1.0: 160px 20px 20px 20px
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
@ -25,11 +25,11 @@
|
|||
}
|
||||
text::selection {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#drop-area-svg {
|
||||
.drop-area-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
|
@ -70,27 +70,27 @@
|
|||
margin-bottom: 2rem;
|
||||
}
|
||||
label {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
bottom: -5px;
|
||||
right: 15px;
|
||||
margin-top: 0 !important;
|
||||
padding: 0.4rem 0.6rem 0.2rem;
|
||||
bottom: -5px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: $white;
|
||||
position: absolute;
|
||||
font-size: 1.2rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crop-info-overlay {
|
||||
display: flex;
|
||||
position: relative;
|
||||
bottom: 3rem;
|
||||
z-index: 2;
|
||||
background-color: $dark_gray;
|
||||
font-style: italic;
|
||||
color: $off_white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* align horizontal */
|
||||
align-items: center;
|
||||
|
@ -101,17 +101,17 @@
|
|||
|
||||
.thin-search-wrapper {
|
||||
.text-input-wrapper {
|
||||
margin: 1rem;
|
||||
position: relative;
|
||||
margin: 1rem;
|
||||
border-bottom: 1px solid #000;
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
background: #000;
|
||||
width: 1px;
|
||||
height: 10px;
|
||||
bottom: 0;
|
||||
}
|
||||
&:before {
|
||||
left: 0;
|
||||
|
@ -135,7 +135,7 @@
|
|||
background: transparent !important;
|
||||
}
|
||||
&::-webkit-input-placeholder {
|
||||
color: #999;
|
||||
color: $placeholder_gray;
|
||||
}
|
||||
}
|
||||
.select-results-container {
|
||||
|
@ -160,14 +160,15 @@
|
|||
.plant-search-item {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
&:hover, &.hovered {
|
||||
&:hover,
|
||||
&.hovered {
|
||||
background: darken($light_green, 10%);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
img {
|
||||
margin: 0 1rem 0 0;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
}
|
||||
.plant-search-item-age {
|
||||
|
@ -178,12 +179,12 @@
|
|||
float: right;
|
||||
}
|
||||
.plant-search-item-name {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
width: 8em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -270,9 +271,17 @@
|
|||
}
|
||||
|
||||
@keyframes water-spray-animation {
|
||||
0% { transform: scale(0.7) rotate(0deg); opacity: 0;}
|
||||
50% { opacity: 1;}
|
||||
100% { transform: scale(1.1) rotate(10deg); opacity: 0;}
|
||||
0% {
|
||||
transform: scale(0.7) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.1) rotate(10deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vacuum {
|
||||
|
@ -287,8 +296,56 @@
|
|||
}
|
||||
|
||||
@keyframes vacuum-animation {
|
||||
0% { transform: scale(1); opacity: 0;}
|
||||
100% { transform: scale(0); opacity: 1;}
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.saved-garden-indicator {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
z-index: 3;
|
||||
padding: 2rem;
|
||||
background: rgba(256, 256, 256, .75);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 1px 4px #555;
|
||||
text-align: center;
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
button {
|
||||
margin: 0.5rem;
|
||||
float: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.saved-garden-list {
|
||||
margin: -15px;
|
||||
.saved-garden-row {
|
||||
padding: 0.25rem;
|
||||
button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.saved-garden-info div {
|
||||
cursor: pointer;
|
||||
padding-right: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: $light_gray;
|
||||
}
|
||||
&.selected {
|
||||
background: $gray;
|
||||
p {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.garden-map-legend {
|
||||
|
@ -303,12 +360,12 @@
|
|||
transform: translateX(-140px);
|
||||
}
|
||||
.content {
|
||||
background: rgba(256, 256, 256, .75);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
background: rgba(256, 256, 256, .75);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
>*+* {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
@ -347,27 +404,26 @@
|
|||
}
|
||||
.farmbot-origin {
|
||||
.quadrants {
|
||||
border: 1px solid $dark_gray;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid $dark_gray;
|
||||
}
|
||||
.quadrant {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px), linear-gradient(rgba(0, 0, 0, 0.05) 2px, transparent 2px), linear-gradient(90deg, rgba(0, 0, 0, 0.05) 2px, transparent 2px);
|
||||
background-size: 4px 4px, 4px 4px, 100px 100px, 100px 100px;
|
||||
cursor: pointer;
|
||||
border: 1px solid $dark_gray;
|
||||
width: 50%;
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&.selected {
|
||||
box-shadow: inset 0 0 8px $dark_gray;
|
||||
}
|
||||
// Quadrant 1
|
||||
} // Quadrant 1
|
||||
&:nth-child(2) {
|
||||
&:before {
|
||||
top: 0;
|
||||
|
@ -377,8 +433,7 @@
|
|||
top: 8px;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
// Quadrant 2
|
||||
} // Quadrant 2
|
||||
&:nth-child(1) {
|
||||
&:before {
|
||||
top: 0;
|
||||
|
@ -388,19 +443,17 @@
|
|||
top: 8px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
// Quadrant 3
|
||||
} // Quadrant 3
|
||||
&:nth-child(3) {
|
||||
&:before {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
&:after {
|
||||
bottom: 8px;
|
||||
left: 16px;
|
||||
bottom: 8px;
|
||||
}
|
||||
}
|
||||
// Quadrant 4
|
||||
} // Quadrant 4
|
||||
&:nth-child(4) {
|
||||
&:before {
|
||||
bottom: 0;
|
||||
|
@ -440,10 +493,10 @@
|
|||
}
|
||||
}
|
||||
.menu-pullout {
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
left: -4.5rem;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s ease;
|
||||
text-shadow: 0px 1px 1px #555;
|
||||
&.active {
|
||||
|
@ -464,12 +517,12 @@
|
|||
}
|
||||
}
|
||||
span {
|
||||
position: absolute;
|
||||
top: 0.6rem;
|
||||
left: -4.6rem;
|
||||
transition-delay: 0.6s;
|
||||
transition: all 0.4s ease;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -4.6rem;
|
||||
top: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.panel-header,
|
||||
.farm-designer-panels {
|
||||
width: 30rem;
|
||||
position: fixed;
|
||||
top: 8.9rem;
|
||||
width: 30rem;
|
||||
}
|
||||
|
||||
@keyframes panel-pullout {
|
||||
|
@ -74,6 +74,7 @@
|
|||
.panel-tabs {
|
||||
display: flex;
|
||||
a {
|
||||
display: block;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
|
@ -81,7 +82,6 @@
|
|||
line-height: 5rem;
|
||||
height: 5rem;
|
||||
color: $light_gray;
|
||||
display: block;
|
||||
&.active {
|
||||
border-bottom: 3px solid $white;
|
||||
font-weight: bold;
|
||||
|
@ -106,17 +106,18 @@
|
|||
padding-left: 1.4rem;
|
||||
padding-right: 2rem;
|
||||
.back-arrow {
|
||||
display: inline-block;
|
||||
color: $off_white;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.8rem;
|
||||
margin-top: -1.8rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display: inline-block;
|
||||
color: $white;
|
||||
font-size: 1.8rem;
|
||||
margin-top: 0.4rem;
|
||||
|
@ -124,7 +125,6 @@
|
|||
width: 10em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
height: 2rem;
|
||||
}
|
||||
.right-button {
|
||||
|
@ -245,8 +245,8 @@
|
|||
|
||||
.panel-header {
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
z-index: 2;
|
||||
width: 300px;
|
||||
.panel-header-description,
|
||||
.crop-info-description {
|
||||
font-size: 1.2rem;
|
||||
|
@ -261,10 +261,10 @@
|
|||
|
||||
.crop-info-panel {
|
||||
.panel-header {
|
||||
position: inherit;
|
||||
background-size: 144% !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: top center !important;
|
||||
position: inherit;
|
||||
.back-arrow {
|
||||
margin-top: -0.2rem;
|
||||
vertical-align: bottom;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.farm-event-panel {
|
||||
.fa-calendar {
|
||||
font-size: 2rem;
|
||||
margin: 0.5rem 0 0 1.6rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.panel-content {
|
||||
padding: 6rem 1rem 6rem 0;
|
||||
|
@ -48,8 +48,8 @@
|
|||
|
||||
.farm-event-data-block {
|
||||
display: flex;
|
||||
padding: 0.8rem;
|
||||
position: relative;
|
||||
padding: 0.8rem;
|
||||
font-size: 1.2rem;
|
||||
align-items: center;
|
||||
background: $white;
|
||||
|
@ -61,8 +61,8 @@
|
|||
}
|
||||
i {
|
||||
position: absolute;
|
||||
right: 0.8rem;
|
||||
top: 1rem;
|
||||
right: 0.8rem;
|
||||
opacity: 0;
|
||||
}
|
||||
&:after {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Google Fonts
|
||||
@import url(https://fonts.googleapis.com/css?family=Roboto:400,400italic,700,700italic,100,100italic);
|
||||
@import 'colors';
|
||||
@import url("https://fonts.googleapis.com/css?family=Roboto:400,400italic,700,700italic,100,100italic");
|
||||
@import "colors";
|
||||
// Font Variables
|
||||
$roboto: 'Roboto',
|
||||
Arial,
|
||||
|
@ -8,8 +8,8 @@ Helvetica,
|
|||
sans-serif;
|
||||
body,
|
||||
html {
|
||||
font-family: $roboto;
|
||||
font-weight: 400;
|
||||
font-family: $roboto;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -20,13 +20,13 @@ h5,
|
|||
h6,
|
||||
p,
|
||||
fieldset {
|
||||
font-family: $roboto;
|
||||
font-weight: 400;
|
||||
font-family: $roboto;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
color: $dark_gray;
|
||||
line-height: 1.4rem;
|
||||
margin-bottom: 0!important;
|
||||
}
|
||||
font-size: 1.1rem;
|
||||
color: $dark_gray;
|
||||
line-height: 1.4rem;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
|
|
@ -64,12 +64,12 @@ input[type=time] {
|
|||
|
||||
.markdown {
|
||||
p {
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
color: $white;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 13rem);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
text-transform: none;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
@ -82,12 +82,12 @@ fieldset {
|
|||
}
|
||||
|
||||
.saucer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
background: $dark_gray;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
border: 2px solid white;
|
||||
|
@ -104,10 +104,10 @@ fieldset {
|
|||
}
|
||||
|
||||
.saucer-connector {
|
||||
height: 3rem;
|
||||
width: 1rem;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
height: 3rem;
|
||||
width: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-top: -1rem;
|
||||
&.last {
|
||||
|
@ -134,11 +134,11 @@ fieldset {
|
|||
.chip-temp-display {
|
||||
position: relative;
|
||||
.saucer {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 1rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 2px;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
@ -146,11 +146,11 @@ fieldset {
|
|||
.wifi-strength-display {
|
||||
position: relative;
|
||||
.percent-bar {
|
||||
width: 25%;
|
||||
position: absolute;
|
||||
height: 1rem;
|
||||
left: 12rem;
|
||||
top: 2px;
|
||||
left: 12rem;
|
||||
height: 1rem;
|
||||
width: 25%;
|
||||
clip-path: polygon(0 85%, 100% 0, 100% 100%, 0% 100%);
|
||||
background-color: $light_gray;
|
||||
.percent-bar-fill {
|
||||
|
@ -194,6 +194,9 @@ a {
|
|||
cursor: pointer !important;
|
||||
&.fa-gear {
|
||||
color: $white;
|
||||
&.dark {
|
||||
color: $dark_gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,10 +212,10 @@ a {
|
|||
&:after {
|
||||
content: "\25BE";
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
right: 1rem;
|
||||
color: $dark_gray;
|
||||
font-size: 2rem;
|
||||
right: 1rem;
|
||||
top: 0.2rem;
|
||||
line-height: initial;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -225,13 +228,13 @@ a {
|
|||
}
|
||||
|
||||
.drag-drop-area {
|
||||
margin: 0.75rem 0;
|
||||
margin-right: 15px;
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
border-color: $light_gray;
|
||||
color: $gray;
|
||||
font-weight: bold;
|
||||
margin: 0.75rem 0;
|
||||
margin-right: 15px;
|
||||
padding: 1.25rem;
|
||||
background: $off_white;
|
||||
text-align: center;
|
||||
|
@ -306,9 +309,9 @@ a {
|
|||
color: $orange;
|
||||
}
|
||||
.fa-th-large {
|
||||
color: $dark_gray;
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
color: $dark_gray;
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.fb-button {
|
||||
margin-top: 0.5rem;
|
||||
|
@ -336,7 +339,8 @@ a {
|
|||
|
||||
.sensor-history-table {
|
||||
font-size: 1.2rem;
|
||||
th, td {
|
||||
th,
|
||||
td {
|
||||
width: 1%;
|
||||
}
|
||||
tr {
|
||||
|
@ -404,15 +408,15 @@ fieldset {
|
|||
.webcam-stream-valid {
|
||||
img {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
max-height: 650px;
|
||||
min-height: 300px;
|
||||
}
|
||||
iframe {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
max-height: 650px;
|
||||
min-height: 300px;
|
||||
border: none;
|
||||
|
@ -420,10 +424,10 @@ fieldset {
|
|||
}
|
||||
|
||||
.webcam-stream-unavailable p {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
transform: translate(-50%, -50%);
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
|
@ -432,8 +436,8 @@ fieldset {
|
|||
|
||||
.clear-webcam-url-btn {
|
||||
position: absolute !important;
|
||||
left: 2rem;
|
||||
top: 6.2rem;
|
||||
left: 2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
@ -534,19 +538,19 @@ ul {
|
|||
}
|
||||
|
||||
.coming-soon {
|
||||
opacity: 0.50;
|
||||
position: relative;
|
||||
opacity: 0.50;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.coming-soon:after {
|
||||
content: "Coming Soon!";
|
||||
background-color: $red;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 0;
|
||||
background-color: $red;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
|
@ -554,26 +558,26 @@ ul {
|
|||
|
||||
.unavailable {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.40;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
&.banner {
|
||||
&:after {
|
||||
content: "Not available when device is offline.";
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: -2.5%;
|
||||
z-index: 10;
|
||||
width: 105%;
|
||||
padding: 0.5rem;
|
||||
background-color: $dark_gray;
|
||||
opacity: 0.90;
|
||||
color: $off_white;
|
||||
font-size: 1.8rem;
|
||||
position: absolute;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
top: 25%;
|
||||
left: -2.5%;
|
||||
width: 105%;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -583,8 +587,8 @@ ul {
|
|||
}
|
||||
|
||||
.button-group {
|
||||
float: right;
|
||||
margin: -2rem 0 2rem;
|
||||
float: right;
|
||||
button:not(:first-of-type) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
@ -607,40 +611,48 @@ ul {
|
|||
}
|
||||
}
|
||||
|
||||
.motor-position-plot-border {
|
||||
text {
|
||||
font-size: 0.4rem;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.controls-popup,
|
||||
.controls-popup-menu-outer {
|
||||
position: fixed;
|
||||
bottom: 3rem;
|
||||
right: 4rem;
|
||||
z-index: 2;
|
||||
background: $dark_gray;
|
||||
border-radius: 3rem;
|
||||
height: 6rem;
|
||||
width: 6rem;
|
||||
right: 4rem;
|
||||
bottom: 3rem;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.controls-popup {
|
||||
color: $off_white;
|
||||
i {
|
||||
position: fixed;
|
||||
bottom: 3rem;
|
||||
z-index: 3;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
bottom: 3rem;
|
||||
border-radius: 3rem;
|
||||
padding: 18px 20px;
|
||||
position: fixed;
|
||||
font-size: 2.4rem;
|
||||
transition: all 0.25s ease-in-out;
|
||||
&:hover {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
&.open {
|
||||
i {
|
||||
transform: rotate(-135deg);
|
||||
&:hover {
|
||||
background-color: rgba(0,0,0,0);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
.controls-popup-menu-outer {
|
||||
|
@ -649,7 +661,7 @@ ul {
|
|||
padding: 0.6rem 5rem 0rem 0rem;
|
||||
}
|
||||
.controls-popup-menu-inner {
|
||||
transition-delay: 0.25s!important;
|
||||
transition-delay: 0.25s !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@ -666,7 +678,7 @@ ul {
|
|||
|
||||
.controls-popup-menu-outer {
|
||||
transition: all 0.1 s ease-in-out;
|
||||
transition-delay: 0.2s!important;
|
||||
transition-delay: 0.2s !important;
|
||||
}
|
||||
|
||||
@keyframes page-transition {
|
||||
|
@ -679,10 +691,10 @@ ul {
|
|||
}
|
||||
|
||||
.empty-state-graphic {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
margin: auto;
|
||||
margin-top: 10%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.farmware-selection-panel {
|
||||
|
@ -701,7 +713,8 @@ ul {
|
|||
|
||||
.farmware-input-panel,
|
||||
.farmware-info-panel {
|
||||
label, h4 {
|
||||
label,
|
||||
h4 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
@ -716,7 +729,8 @@ ul {
|
|||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover, &.selected {
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: $medium_light_gray;
|
||||
p {
|
||||
font-weight: bold;
|
||||
|
@ -726,10 +740,10 @@ ul {
|
|||
|
||||
.farmware-button,
|
||||
.farmware-settings-menu {
|
||||
float: right !important;
|
||||
position: absolute !important;
|
||||
right: 3rem;
|
||||
top: 1rem;
|
||||
right: 3rem;
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
.logs {
|
||||
|
@ -781,7 +795,6 @@ ul {
|
|||
td:nth-child(1),
|
||||
td:nth-child(4) {
|
||||
min-width: 106px;
|
||||
|
||||
}
|
||||
td:nth-child(2),
|
||||
td:nth-child(3) {
|
||||
|
@ -798,7 +811,8 @@ ul {
|
|||
max-width: 250px;
|
||||
h1 {
|
||||
font-weight: 300;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
li:before {
|
||||
content: "• ";
|
||||
|
@ -857,9 +871,9 @@ ul {
|
|||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
.indicator {
|
||||
position: relative;
|
||||
background: $dark_gray;
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
span {
|
||||
position: relative;
|
||||
top: 0.15rem;
|
||||
|
@ -889,21 +903,22 @@ ul {
|
|||
}
|
||||
.fb-checkbox {
|
||||
display: inline;
|
||||
margin-right: 1rem;
|
||||
position: relative;
|
||||
margin-right: 1rem;
|
||||
&.partial:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
bottom: 1.2rem;
|
||||
border: solid $dark_gray;
|
||||
border-width: 0 0 3px 0;
|
||||
bottom: 1.2rem;
|
||||
left: 0.75rem;
|
||||
padding: 0.6rem 0.3rem;
|
||||
}
|
||||
}
|
||||
.pt-popover-wrapper, .pt-popover-target {
|
||||
margin-left: 1rem;
|
||||
.pt-popover-wrapper,
|
||||
.pt-popover-target {
|
||||
display: inline-block !important;
|
||||
margin-left: 1rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
@ -913,8 +928,8 @@ ul {
|
|||
margin-left: 3rem;
|
||||
i {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 0.55rem;
|
||||
right: 1rem;
|
||||
color: $light_gray;
|
||||
&:hover {
|
||||
color: $dark_gray;
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
.hotkey-guide {
|
||||
z-index: 99;
|
||||
position: relative;
|
||||
top: 24vh;
|
||||
margin: 0 auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
margin: 0 auto;
|
||||
width: 32rem;
|
||||
position: relative;
|
||||
outline: none;
|
||||
h3 {
|
||||
text-align: center;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
i {
|
||||
position: absolute;
|
||||
font-size: 2rem;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
}
|
||||
|
||||
.image-flipper-image {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
max-height: 650px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.image-flipper-left {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
bottom: -5px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
&:hover {
|
||||
|
@ -22,9 +22,9 @@
|
|||
|
||||
.image-flipper-right {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
|
@ -45,13 +45,13 @@
|
|||
}
|
||||
p {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
padding: 20% 1rem 0;
|
||||
line-height: 2.4rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
margin-left: 50px;
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
@ -59,13 +59,13 @@
|
|||
|
||||
.photos {
|
||||
.photos-footer {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
left: 2.5rem;
|
||||
bottom: -1rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
label {
|
||||
font-weight: normal;
|
||||
|
@ -93,9 +93,9 @@
|
|||
|
||||
.index-indicator {
|
||||
position: absolute;
|
||||
background: $white;
|
||||
height: 3px;
|
||||
top: 3rem;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: $white;
|
||||
height: 3px;
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ input:not([role="combobox"]) {
|
|||
&.bulk-day-selector {
|
||||
width: 10%;
|
||||
box-shadow: none;
|
||||
margin-bottom: 0.75rem!important;
|
||||
margin-top: 0.75rem!important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
margin-top: 0.75rem !important;
|
||||
height: 1.5rem;
|
||||
vertical-align: top;
|
||||
&.margin-left {
|
||||
|
@ -34,9 +34,9 @@ input:not([role="combobox"]) {
|
|||
}
|
||||
|
||||
.day-selector-wrapper {
|
||||
display: inline-block;
|
||||
width: 10%;
|
||||
height: 3rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.week-row {
|
||||
|
@ -56,8 +56,8 @@ select {
|
|||
}
|
||||
i {
|
||||
position: absolute;
|
||||
right: 0.65rem;
|
||||
top: 30%;
|
||||
right: 0.65rem;
|
||||
color: $dark_gray
|
||||
}
|
||||
&.dim {
|
||||
|
@ -77,7 +77,7 @@ select {
|
|||
}
|
||||
|
||||
.filter-search-item-none::after {
|
||||
content: "*";
|
||||
content: "*";
|
||||
}
|
||||
|
||||
.filter-search-heading-item {
|
||||
|
@ -92,23 +92,23 @@ select {
|
|||
|
||||
.fb-checkbox {
|
||||
input[type="checkbox"] {
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
border: 0.5px solid $gray;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background: $white;
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
cursor: pointer;
|
||||
&:checked:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
border: solid $dark_gray;
|
||||
border-width: 0 3px 3px 0;
|
||||
transform: rotate(45deg);
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
padding: 0.6rem 0.3rem;
|
||||
}
|
||||
}
|
||||
|
@ -119,11 +119,11 @@ select {
|
|||
&:checked:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.9rem;
|
||||
bottom: 0.5rem;
|
||||
border: solid $dark_gray;
|
||||
border-width: 0 4px 4px 0;
|
||||
transform: rotate(45deg);
|
||||
bottom: 0.5rem;
|
||||
left: 0.9rem;
|
||||
padding: 1rem 0.3rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
$duration: 2;
|
||||
|
||||
.loading-plant-div-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
padding-top: 10%;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding-top: 10%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-plant-svg-container {
|
||||
|
@ -56,7 +55,7 @@ $duration: 2;
|
|||
transform-box: fill-box;
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
animation: loading-plant-accent-pop $duration*0.5s cubic-bezier(0, 0, 0, 1.4) 1 ;
|
||||
animation: loading-plant-accent-pop $duration*0.5s cubic-bezier(0, 0, 0, 1.4) 1;
|
||||
}
|
||||
|
||||
@keyframes loading-plant-accent-pop {
|
||||
|
@ -77,9 +76,7 @@ $duration: 2;
|
|||
.loading-plant-circle {
|
||||
transform-origin: center;
|
||||
transform-box: fill-box;
|
||||
animation:
|
||||
loading-plant-circle-pop $duration*0.3s cubic-bezier(0, 0, 0, 1.4) 1,
|
||||
loading-plant-circle $duration*3s linear infinite $duration*0.3s;
|
||||
animation: loading-plant-circle-pop $duration*0.3s cubic-bezier(0, 0, 0, 1.4) 1, loading-plant-circle $duration*3s linear infinite $duration*0.3s;
|
||||
}
|
||||
|
||||
@keyframes loading-plant-circle-pop {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
.nav-wrapper {
|
||||
background: $dark_gray;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .2);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
background: $dark_gray;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
nav {
|
||||
max-width: 140rem;
|
||||
margin: 3.4rem auto 0;
|
||||
max-width: 140rem;
|
||||
button {
|
||||
margin: 1.8rem 1.8rem 0 0;
|
||||
font-size: 1.3rem !important;
|
||||
|
@ -35,12 +35,12 @@ nav {
|
|||
display: none;
|
||||
}
|
||||
.nav-links a {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-size: 1.2rem;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
padding: 2rem 1rem;
|
||||
letter-spacing: 1.2px;
|
||||
position: relative;
|
||||
transition: font-weight 0.2s ease;
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
|
@ -57,19 +57,19 @@ nav {
|
|||
}
|
||||
&:before {
|
||||
content: "";
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: $white;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: $dark_gray;
|
||||
}
|
||||
|
@ -95,16 +95,16 @@ nav {
|
|||
}
|
||||
}
|
||||
.pt-popover-content {
|
||||
width: 22rem;
|
||||
position: relative;
|
||||
width: 22rem;
|
||||
a:not(.app-version) {
|
||||
margin-bottom: 0.6rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.app-version {
|
||||
margin: 1rem -1rem -1rem;
|
||||
background: $dark_gray;
|
||||
color: $white;
|
||||
margin: 1rem -1rem -1rem;
|
||||
padding: 0.5rem 0 0 1rem;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
.farmware-input-panel,
|
||||
.sequence-editor-panel,
|
||||
.regimen-editor-panel {
|
||||
margin: -3rem -1.5rem -6rem;
|
||||
height: calc(100vh - 5rem);
|
||||
background: $light_gray;
|
||||
margin: -3rem -1.5rem -6rem;
|
||||
@media screen and (max-width: 768px) {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
@ -14,7 +14,8 @@
|
|||
margin-top: 0.4rem;
|
||||
}
|
||||
@media screen and (max-width: 974px) {
|
||||
h3, p {
|
||||
h3,
|
||||
p {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
|
|
@ -1,63 +1,63 @@
|
|||
$offset: 187;
|
||||
$duration: 1.3s;
|
||||
.spinner-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
padding-top: 16%;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding-top: 16%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: rotator $duration linear infinite;
|
||||
animation: rotator $duration linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotator {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-path {
|
||||
stroke-dasharray: $offset;
|
||||
stroke-dashoffset: 0;
|
||||
transform-origin: center;
|
||||
animation: dash $duration ease-in-out infinite, colors ($duration * 3) ease-in-out infinite;
|
||||
stroke-dasharray: $offset;
|
||||
stroke-dashoffset: 0;
|
||||
transform-origin: center;
|
||||
animation: dash $duration ease-in-out infinite, colors ($duration * 3) ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes colors {
|
||||
0% {
|
||||
stroke: #783F04;
|
||||
}
|
||||
25% {
|
||||
stroke: #EE6666;
|
||||
}
|
||||
50% {
|
||||
stroke: #66AA44;
|
||||
}
|
||||
75% {
|
||||
stroke: #274E13;
|
||||
}
|
||||
100% {
|
||||
stroke: #666666;
|
||||
}
|
||||
0% {
|
||||
stroke: #783f04;
|
||||
}
|
||||
25% {
|
||||
stroke: #ee6666;
|
||||
}
|
||||
50% {
|
||||
stroke: #66aa44;
|
||||
}
|
||||
75% {
|
||||
stroke: #274e13;
|
||||
}
|
||||
100% {
|
||||
stroke: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: $offset;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: $offset / 4;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: $offset;
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
0% {
|
||||
stroke-dashoffset: $offset;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: $offset / 4;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: $offset;
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* {
|
||||
margin: 0 auto;
|
||||
}
|
||||
background: linear-gradient(-135deg, #6DB1EC, #35A274);
|
||||
background: linear-gradient(-135deg, #6db1ec, #35a274);
|
||||
padding-top: 4rem;
|
||||
h1,
|
||||
h2 {
|
||||
|
@ -37,8 +37,8 @@
|
|||
max-width: 1024px;
|
||||
}
|
||||
.forgot-password {
|
||||
color: $blue;
|
||||
display: inline-block;
|
||||
color: $blue;
|
||||
}
|
||||
.tos {
|
||||
margin-bottom: 1.5rem;
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
}
|
||||
&:after {
|
||||
content: "";
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -48,11 +48,11 @@
|
|||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.saucer {
|
||||
margin: 0.2rem 0.6rem 0rem 0;
|
||||
float: left;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
border-radius: 50%;
|
||||
margin: 0.2rem 0.6rem 0rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
flex: 1;
|
||||
input,
|
||||
label {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
input {
|
||||
margin-left: 1rem;
|
||||
|
@ -36,17 +36,17 @@
|
|||
float: right;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
::-webkit-input-placeholder {
|
||||
::-webkit-input-placeholder {
|
||||
color: $dark_gray;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
::-moz-placeholder {
|
||||
::-moz-placeholder {
|
||||
color: $dark_gray;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
:-ms-input-placeholder {
|
||||
:-ms-input-placeholder {
|
||||
color: $dark_gray;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
|
@ -108,6 +108,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.step-up-down-arrows {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.step-control {
|
||||
margin-top: 0.3rem;
|
||||
margin-left: 2rem;
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
// Styles for TABLES
|
||||
table {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table.plain tr:nth-of-type(1n) {
|
||||
background: $off-white;
|
||||
border: .5rem solid $off-white;
|
||||
background: $off-white;
|
||||
border: .5rem solid $off-white;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
padding: .5rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
table thead tr th {
|
||||
padding: .5rem;
|
||||
}
|
||||
padding: .5rem;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
z-index: 999999;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: relative;
|
||||
box-shadow: 0px 1px 4px #555;
|
||||
pointer-events: all;
|
||||
padding: 1.8rem;
|
||||
cursor: pointer;
|
||||
min-height: 6rem;
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
@ -39,14 +39,16 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
&.yellow {
|
||||
.toast-title, .toast-message {
|
||||
.toast-title,
|
||||
.toast-message {
|
||||
color: $dark_gray;
|
||||
}
|
||||
}
|
||||
&.blue,
|
||||
&.green,
|
||||
&.red {
|
||||
.toast-title, .toast-message {
|
||||
.toast-title,
|
||||
.toast-message {
|
||||
color: $off_white;
|
||||
}
|
||||
}
|
||||
|
@ -66,19 +68,19 @@
|
|||
|
||||
.toast-loader {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
overflow: hidden;
|
||||
transform: rotate(180deg);
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.toast-loader-left,
|
||||
.toast-loader-right,
|
||||
.toast-loader-spinner {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -93,8 +95,8 @@
|
|||
|
||||
.toast-loader-right {
|
||||
right: 0;
|
||||
background: #666 !important;
|
||||
z-index: 1;
|
||||
background: #666 !important;
|
||||
opacity: 0;
|
||||
animation: show-hide 7s steps(1, end) reverse;
|
||||
border-radius: 0 100% 100% 0/ 0 50% 50% 0;
|
||||
|
@ -102,10 +104,10 @@
|
|||
|
||||
.toast-loader-spinner {
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: #666 !important;
|
||||
animation: spin 7s linear;
|
||||
transform-origin: center right;
|
||||
z-index: 2;
|
||||
border-radius: 100% 0 0 100%/ 50% 0 0 50%;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
&:hover,
|
||||
&:active {
|
||||
.help-text {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 17rem;
|
||||
bottom: -0.8rem;
|
||||
z-index: 999;
|
||||
padding: 2px;
|
||||
font-style: normal;
|
||||
|
@ -16,9 +19,6 @@
|
|||
border-radius: 3px;
|
||||
padding: .5rem .8rem;
|
||||
width: 250px;
|
||||
left: 17rem;
|
||||
bottom: -0.8rem;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
}
|
||||
|
||||
.move-amount {
|
||||
margin: 0;
|
||||
background-color: $white;
|
||||
border-right: 2px solid $off-white;
|
||||
color: $medium_gray;
|
||||
|
@ -22,7 +23,6 @@
|
|||
padding-right: 0;
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
margin: 0;
|
||||
float: left;
|
||||
&:hover {
|
||||
background-color: darken($white, 40%);
|
||||
|
@ -41,7 +41,7 @@
|
|||
border-right: 0px none;
|
||||
}
|
||||
&.move-amount-selected {
|
||||
background-color: $medium_gray!important;
|
||||
background-color: $medium_gray !important;
|
||||
color: $off_white;
|
||||
}
|
||||
}
|
||||
|
@ -55,12 +55,12 @@
|
|||
}
|
||||
|
||||
.arrow-button {
|
||||
margin: 0;
|
||||
background-color: $medium_gray;
|
||||
border-bottom: 2px solid $dark_gray;
|
||||
color: $off_white;
|
||||
font-size: 16px!important;
|
||||
font-size: 16px !important;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
*/
|
||||
|
||||
.widget-wrapper {
|
||||
position: relative;
|
||||
box-shadow: 0px 0px 10px $gray;
|
||||
margin-bottom: 3rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.widget-header {
|
||||
|
@ -60,17 +60,17 @@
|
|||
}
|
||||
a:hover {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
color: $white;
|
||||
}
|
||||
a:active {
|
||||
color: white;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
h5 {
|
||||
display: inline;
|
||||
color: $gray;
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
display: inline;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
it("renders settings", () => {
|
||||
const osSettings = mount(<FarmbotOsSettings {...fakeProps()} />);
|
||||
expect(osSettings.find("input").length).toBe(1);
|
||||
expect(osSettings.find("button").length).toBe(6);
|
||||
expect(osSettings.find("button").length).toBe(7);
|
||||
["NAME", "TIME ZONE", "LAST SEEN", "FARMBOT OS", "CAMERA", "FIRMWARE"]
|
||||
.map(string => expect(osSettings.text()).toContain(string));
|
||||
});
|
||||
|
@ -50,8 +50,10 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
{...fakeProps()} />);
|
||||
await expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("RELEASE_NOTES.md"));
|
||||
expect(osSettings.instance().state.osReleaseNotesHeading)
|
||||
.toEqual("FarmBot OS v6");
|
||||
expect(osSettings.instance().state.osReleaseNotes)
|
||||
.toEqual("# FarmBot OS v6\n* note");
|
||||
.toEqual("* note");
|
||||
});
|
||||
|
||||
it("doesn't fetch OS release notes", async () => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue