diff --git a/.ruby-version b/.ruby-version index 4f5e69734c..2aa5131992 100755 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.5 +3.4.7 diff --git a/Gemfile b/Gemfile index 7bac44de02..baa1181942 100755 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" -ruby "~> 3.4.5" +ruby "~> 3.4.7" gem "rails", "~> 6" gem "active_model_serializers" diff --git a/Gemfile.lock b/Gemfile.lock index 8f7a4554c7..6effdf408b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,8 +70,8 @@ GEM amq-protocol (2.3.4) base64 (0.3.0) bcrypt (3.1.20) - benchmark (0.4.1) - bigdecimal (3.2.2) + benchmark (0.5.0) + bigdecimal (3.3.1) builder (3.3.0) bunny (2.24.0) amq-protocol (~> 2.3) @@ -81,7 +81,7 @@ GEM climate_control (1.2.0) coderay (1.1.3) concurrent-ruby (1.3.5) - crack (1.0.0) + crack (1.0.1) bigdecimal rexml crass (1.0.6) @@ -91,7 +91,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0) database_cleaner-core (2.0.1) - date (3.4.1) + date (3.5.0) declarative (0.0.20) delayed_job (4.1.13) activesupport (>= 3.0, < 9.0) @@ -113,24 +113,24 @@ GEM drb (2.2.3) e2mmap (0.1.0) erubi (1.13.1) - factory_bot (6.5.5) + factory_bot (6.5.6) activesupport (>= 6.1.0) - factory_bot_rails (6.5.0) + factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) faker (3.5.2) i18n (>= 1.8.11, < 2) - faraday (2.13.4) + faraday (2.14.0) faraday-net_http (>= 2.0, < 3.5) json logger - faraday-follow_redirects (0.3.0) + faraday-follow_redirects (0.4.0) faraday (>= 1, < 3) faraday-net_http (3.4.1) net-http (>= 0.5.0) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) - google-apis-core (1.0.1) + google-apis-core (1.0.2) addressable (~> 2.8, >= 2.8.7) faraday (~> 2.13) faraday-follow_redirects (~> 0.3) @@ -138,9 +138,9 @@ GEM mini_mime (~> 1.1) representable (~> 3.0) retriable (~> 3.1) - google-apis-iamcredentials_v1 (0.24.0) + google-apis-iamcredentials_v1 (0.25.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.56.0) + google-apis-storage_v1 (0.57.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) @@ -159,7 +159,7 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.2.0) - googleauth (1.15.0) + googleauth (1.15.1) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) @@ -167,11 +167,11 @@ GEM multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hashdiff (1.2.0) + hashdiff (1.2.1) hashie (4.1.0) i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.13.2) + json (2.15.2) jsonapi-renderer (0.2.2) jwt (3.1.2) base64 @@ -196,22 +196,23 @@ GEM loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.1.0) method_source (1.1.0) mini_mime (1.1.5) - minitest (5.25.5) + minitest (5.26.0) multi_json (1.17.0) mutations (0.9.1) activesupport mutex_m (0.3.0) - net-http (0.6.0) + net-http (0.7.0) uri - net-imap (0.5.10) + net-imap (0.5.12) date net-protocol net-pop (0.1.2) @@ -220,15 +221,15 @@ GEM timeout net-smtp (0.5.1) net-protocol - nio4r (2.7.4) - nokogiri (1.18.9-aarch64-linux-gnu) + nio4r (2.7.5) + nokogiri (1.18.10-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-gnu) + nokogiri (1.18.10-x86_64-linux-gnu) racc (~> 1.4) orm_adapter (0.5.0) os (1.1.4) ostruct (0.6.3) - passenger (6.0.27) + passenger (6.1.0) rack (>= 1.6.13) rackup (>= 1.0.1) rake (>= 12.3.3) @@ -247,8 +248,8 @@ GEM hashie (~> 4.1) multi_json (~> 1.15) racc (1.8.1) - rack (2.2.17) - rack-attack (6.7.0) + rack (2.2.20) + rack-attack (6.8.0) rack (>= 1.0, < 4) rack-cors (2.0.2) rack (>= 2.0.0) @@ -290,7 +291,7 @@ GEM method_source rake (>= 12.2) thor (~> 1.0) - rake (13.3.0) + rake (13.3.1) rbtree (0.4.6) redis (4.8.1) representable (3.2.0) @@ -303,18 +304,18 @@ GEM actionpack (>= 5.2) railties (>= 5.2) retriable (3.1.2) - rexml (3.4.2) + rexml (3.4.4) rollbar (3.6.2) - rspec (3.13.1) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.5) + rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.5) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (6.1.5) @@ -325,7 +326,7 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.5) + rspec-support (3.13.6) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) scenic (1.9.0) @@ -361,20 +362,20 @@ GEM thor (1.4.0) thwait (0.2.0) e2mmap - timeout (0.4.3) + timeout (0.4.4) trailblazer-option (0.1.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) tzinfo-data (1.2025.2) tzinfo (>= 1.0.0) uber (0.1.0) - uri (1.0.3) + uri (1.1.0) valid_url (0.0.4) addressable rails warden (1.2.9) rack (>= 2.0.9) - webmock (3.25.1) + webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -438,7 +439,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.5p51 + ruby 3.4.7p58 BUNDLED WITH - 2.7.1 + 2.7.2 diff --git a/app/models/alert.rb b/app/models/alert.rb index e60abef525..3cabebc294 100644 --- a/app/models/alert.rb +++ b/app/models/alert.rb @@ -1,9 +1,9 @@ class Alert < ApplicationRecord belongs_to :device DEFAULTS = [ - SEED_DATA = { problem_tag: "api.seed_data.missing", priority: 400 }, - DOCUMENTATION = { problem_tag: "api.documentation.unread", priority: 300 }, - TOUR = { problem_tag: "api.tour.not_taken", priority: 200 }, + SEED_DATA = { problem_tag: "api.seed_data.missing", priority: 200 }, + DOCUMENTATION = { problem_tag: "api.documentation.unread", priority: 400 }, + TOUR = { problem_tag: "api.tour.not_taken", priority: 300 }, USER = { problem_tag: "api.user.not_welcomed", priority: 100 }, BULLETIN = { problem_tag: "api.bulletin.unread", priority: 100 }, DEMO = { problem_tag: "api.demo_account.in_use", priority: 100 }, diff --git a/app/mutations/devices/create.rb b/app/mutations/devices/create.rb index 5ff81f7300..5cbd221103 100644 --- a/app/mutations/devices/create.rb +++ b/app/mutations/devices/create.rb @@ -16,8 +16,6 @@ class Create < Mutations::Command def execute merge_default_values device = Device.create!({ name: "FarmBot" }.merge(inputs.except(:user))) - Alerts::Create.run!(Alert::SEED_DATA.merge(device: device)) - Alerts::Create.run!(Alert::TOUR.merge(device: device)) Alerts::Create.run!(Alert::USER.merge(device: device)) Alerts::Create.run!(Alert::DOCUMENTATION.merge(device: device)) diff --git a/app/mutations/devices/create_seed_data.rb b/app/mutations/devices/create_seed_data.rb index 66a9a0e40d..8efe56fe2e 100644 --- a/app/mutations/devices/create_seed_data.rb +++ b/app/mutations/devices/create_seed_data.rb @@ -34,6 +34,10 @@ class CreateSeedData < Mutations::Command end def execute + if device.account_seeded_at + return { done: "Device already has seed data." } + end + device.update(account_seeded_at: Time.now) self.delay.run_seeds! { done: "Loading resources now." } end diff --git a/app/mutations/devices/reset.rb b/app/mutations/devices/reset.rb index be57e5fd4b..7b5ddcbf16 100644 --- a/app/mutations/devices/reset.rb +++ b/app/mutations/devices/reset.rb @@ -20,7 +20,10 @@ def execute def run_it ActiveRecord::Base.transaction do - device.update!(name: "FarmBot", mounted_tool_id: nil) + device.update!(name: "FarmBot", + mounted_tool_id: nil, + setup_completed_at: nil, + account_seeded_at: nil) device.folders.update_all(parent_id: nil) Device::SINGULAR_RESOURCES.keys.map do |resource| device.send(resource).destroy! diff --git a/app/mutations/devices/seeders/abstract_seeder.rb b/app/mutations/devices/seeders/abstract_seeder.rb index e7deff99df..215a3db9f4 100644 --- a/app/mutations/devices/seeders/abstract_seeder.rb +++ b/app/mutations/devices/seeders/abstract_seeder.rb @@ -33,7 +33,6 @@ class AbstractSeeder :settings_default_map_size_y, :settings_device_name, :settings_change_firmware_config_defaults, - :settings_soil_height, :settings_firmware, :settings_gantry_height, :settings_hide_sensors, @@ -86,9 +85,7 @@ def initialize(device) @device = device end - def settings_hide_sensors - device.web_app_config.update!(hide_sensors: false) - end + def settings_hide_sensors; end def peripherals_lighting add_peripheral(7, ToolNames::LIGHTING) @@ -209,11 +206,6 @@ def settings_default_map_size_x; end def settings_default_map_size_y; end def settings_device_name; end def settings_change_firmware_config_defaults; end - def settings_soil_height; end - - def settings_soil_height - device.fbos_config.update!(soil_height: -500) - end def settings_three_d; end diff --git a/app/mutations/devices/seeders/demo_account_seeder.rb b/app/mutations/devices/seeders/demo_account_seeder.rb index 332503ca4e..5ec7b8f17a 100644 --- a/app/mutations/devices/seeders/demo_account_seeder.rb +++ b/app/mutations/devices/seeders/demo_account_seeder.rb @@ -8,7 +8,9 @@ class DemoAccountSeeder < AbstractSeeder "Genesis XL" => "Genesis_XL_Demo_Webcam.jpg", "Genesis" => "Genesis_Demo_Webcam.jpg", } - UNUSED_ALERTS = ["api.seed_data.missing", "api.user.not_welcomed"] + UNUSED_ALERTS = [ + Alert::USER[:problem_tag], + ] def feed(product_line) feed_name = "" @@ -151,6 +153,7 @@ def marketing_bulletin DEMO_ALERTS = [ Alert::DEMO, Alert::BULLETIN.merge(slug: "buy-a-farmbot", priority: 9999), + Alert::TOUR, ] DEMO_LOGS = [ diff --git a/app/mutations/devices/seeders/none.rb b/app/mutations/devices/seeders/none.rb index 0a0888962f..486866509d 100644 --- a/app/mutations/devices/seeders/none.rb +++ b/app/mutations/devices/seeders/none.rb @@ -35,7 +35,6 @@ def sequences_grid; end def sequences_dispense_water; end def settings_default_map_size_x; end def settings_default_map_size_y; end - def settings_soil_height; end def settings_gantry_height; end def settings_firmware; end def settings_hide_sensors; end diff --git a/app/mutations/devices/update.rb b/app/mutations/devices/update.rb index 087140fb64..4bcdc979f0 100644 --- a/app/mutations/devices/update.rb +++ b/app/mutations/devices/update.rb @@ -11,6 +11,7 @@ class Update < Mutations::Command string :timezone string :fb_order_number, nils: true string :setup_completed_at, nils: true + string :account_seeded_at, nils: true integer :mounted_tool_id, nils: true integer :ota_hour, nils: true float :lat, nils: true diff --git a/app/serializers/device_serializer.rb b/app/serializers/device_serializer.rb index 8e0d8547c7..3718ae3385 100644 --- a/app/serializers/device_serializer.rb +++ b/app/serializers/device_serializer.rb @@ -12,6 +12,7 @@ class DeviceSerializer < ApplicationSerializer :rpi, :serial_number, :setup_completed_at, + :account_seeded_at, :throttled_at, :throttled_until, :timezone, diff --git a/app/views/dashboard/_common_assets.html.erb b/app/views/dashboard/_common_assets.html.erb index 13181975ab..fe9f95bb88 100644 --- a/app/views/dashboard/_common_assets.html.erb +++ b/app/views/dashboard/_common_assets.html.erb @@ -18,5 +18,3 @@ window.process = { <%= render "addons" %> <%= javascript_include_tag *@js_assets %> - - diff --git a/app/views/dashboard/demo.html.erb b/app/views/dashboard/demo.html.erb index 216bad188c..041b092f1d 100644 --- a/app/views/dashboard/demo.html.erb +++ b/app/views/dashboard/demo.html.erb @@ -38,7 +38,7 @@ bottom: 60px; box-shadow: 0 0 15px rgba(0,0,0,0.4); color: #fff; - font-family: "Cabin", sans-serif; + font-family: "Cabin", "Cabin Fallback", sans-serif; font-size: 25px; font-weight: bold; padding: 15px 30px; diff --git a/app/views/layouts/dashboard.html.erb b/app/views/layouts/dashboard.html.erb index e8d42b0f60..bbecce5eca 100644 --- a/app/views/layouts/dashboard.html.erb +++ b/app/views/layouts/dashboard.html.erb @@ -1,5 +1,5 @@ - + @@ -18,8 +18,6 @@ text-align: center; width: 100%; padding-top: 10%; color: #434343; } <%= stylesheet_link_tag *@css_assets %> - - <% manifest_file = diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index 5a1da3f022..8078b5c3b0 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -1,9 +1,9 @@ - + <%= yield %> - \ No newline at end of file + diff --git a/config/application.rb b/config/application.rb index 1ee18cd100..c466f58c63 100755 --- a/config/application.rb +++ b/config/application.rb @@ -54,6 +54,14 @@ class Application < Rails::Application credentials: false, # No cookies. max_age: 0 end + unless ENV["GCS_BUCKET"] + allow do + origins ["#{ENV.fetch("API_HOST")}:#{API_PORT}", "localhost:#{API_PORT}"] + resource "*", + headers: :any, + methods: [:get, :options] + end + end end API_PORT = ENV["API_PORT"] Rails.application.routes.default_url_options[:host] = LOCAL_API_HOST diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 717b33b1d4..aea610c3d5 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -27,7 +27,7 @@ class Rack::Attack end ### Don't allow too many demo account requests ### - throttle("demo_accounts/ip", limit: 10, period: 10.minutes) do |req| + throttle("demo_accounts/ip", limit: 50, period: 1.hour) do |req| case req.path.downcase when "/demo", "/try_farmbot" req.ip diff --git a/db/migrate/20250925195004_change_soil_height_default.rb b/db/migrate/20250925195004_change_soil_height_default.rb new file mode 100644 index 0000000000..62cd9f668a --- /dev/null +++ b/db/migrate/20250925195004_change_soil_height_default.rb @@ -0,0 +1,5 @@ +class ChangeSoilHeightDefault < ActiveRecord::Migration[6.1] + def change + change_column_default(:fbos_configs, :soil_height, from: 0, to: -500) + end +end diff --git a/db/migrate/20250930204600_add_account_seeded_at_to_device.rb b/db/migrate/20250930204600_add_account_seeded_at_to_device.rb new file mode 100644 index 0000000000..6e6e45148c --- /dev/null +++ b/db/migrate/20250930204600_add_account_seeded_at_to_device.rb @@ -0,0 +1,9 @@ +class AddAccountSeededAtToDevice < ActiveRecord::Migration[6.1] + def up + add_column :devices, :account_seeded_at, :datetime + end + + def down + remove_column :devices, :account_seeded_at + end +end diff --git a/db/structure.sql b/db/structure.sql index 9a974c3f62..12e5713051 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -389,7 +389,8 @@ CREATE TABLE public.devices ( rpi character varying(3), max_log_age_in_days integer DEFAULT 0, max_sequence_count integer DEFAULT 0, - max_sequence_length integer DEFAULT 0 + max_sequence_length integer DEFAULT 0, + account_seeded_at timestamp without time zone ); @@ -576,7 +577,7 @@ CREATE TABLE public.fbos_configs ( update_channel character varying(7) DEFAULT 'stable'::character varying, boot_sequence_id integer, safe_height integer DEFAULT 0, - soil_height integer DEFAULT 0, + soil_height integer DEFAULT '-500'::integer, gantry_height integer DEFAULT 0, default_axis_order character varying(10) DEFAULT 'xy,z;high'::character varying ); @@ -3987,6 +3988,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20250502201109'), ('20250514203443'), ('20250722234106'), -('20250802174543'); +('20250802174543'), +('20250925195004'), +('20250930204600'); diff --git a/docker_configs/api.Dockerfile b/docker_configs/api.Dockerfile index b32180adff..699b9f5c23 100644 --- a/docker_configs/api.Dockerfile +++ b/docker_configs/api.Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.4.5 +FROM ruby:3.4.7 RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg > /dev/null && \ sh -c '. /etc/os-release; echo $VERSION_CODENAME; echo "deb http://apt.postgresql.org/pub/repos/apt/ $VERSION_CODENAME-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' && \ apt-get update -qq && apt-get install -y build-essential libpq-dev postgresql postgresql-contrib && \ diff --git a/frontend/__test_support__/fake_state/app.ts b/frontend/__test_support__/fake_state/app.ts index 0f4bf5251f..e48c7a2797 100644 --- a/frontend/__test_support__/fake_state/app.ts +++ b/frontend/__test_support__/fake_state/app.ts @@ -3,7 +3,6 @@ import { fakeMovementState } from "../fake_bot_data"; import { controlsState, curvesPanelState, - jobsState, metricPanelState, plantsPanelState, pointsPanelState, @@ -25,6 +24,5 @@ export const app: AppState = { toasts: {}, movement: fakeMovementState(), controls: controlsState(), - jobs: jobsState(), popups: popUpsState(), }; diff --git a/frontend/__test_support__/panel_state.ts b/frontend/__test_support__/panel_state.ts index 6a7f2387a8..506d9bff34 100644 --- a/frontend/__test_support__/panel_state.ts +++ b/frontend/__test_support__/panel_state.ts @@ -6,7 +6,6 @@ import { SequencesPanelState, MetricPanelState, CurvesPanelState, - JobsAndLogsState, ControlsState, PopupsState, } from "../interfaces"; @@ -75,11 +74,6 @@ export const controlsState = (): ControlsState => ({ webcams: false, }); -export const jobsState = (): JobsAndLogsState => ({ - jobs: true, - logs: false, -}); - export const popUpsState = (): PopupsState => ({ timeTravel: false, controls: false, diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index 5fe6a4432c..9860b0ea78 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -563,7 +563,12 @@ jest.mock("@react-three/drei", () => {
{name}
, Plane: (props: React.ComponentProps) => // @ts-expect-error geometry props not assignable to div -
{props.name} {props.url}
, +
{props.name} {props.url} + {props.children} +
, + Decal: (props: React.ComponentProps) => + // @ts-expect-error geometry props not assignable to div +
{props.name}
, Cylinder: ({ name }: { name: string }) =>
{name}
, Torus: ({ name }: { name: string }) => @@ -592,11 +597,19 @@ jest.mock("@react-three/drei", () => { PerspectiveCamera: ({ name }: { name: string }) =>
{name}
, useCursor: jest.fn(), - useTexture: jest.fn(() => ({ + useTexture: jest.fn(url => ({ wrapS: "", wrapT: "", repeat: { set: jest.fn() }, + image: url == "mock_load_error" + ? undefined + : { height: 2, width: 2 }, + source: url == "mock_load_error" + ? undefined + : { data: { height: 2, width: 2 } }, })), + RenderTexture: ({ children }: { children: ReactNode }) => +
{children}
, GizmoHelper: ({ name }: { name: string }) =>
{name}
, GizmoViewcube: ({ name }: { name: string }) => diff --git a/frontend/__tests__/reducer_test.ts b/frontend/__tests__/reducer_test.ts index 2524d59510..1b2386a9c8 100644 --- a/frontend/__tests__/reducer_test.ts +++ b/frontend/__tests__/reducer_test.ts @@ -3,7 +3,6 @@ import { appReducer } from "../reducer"; import { ControlsState, CurvesPanelState, - JobsAndLogsState, MetricPanelState, MovementState, PlantsPanelState, @@ -128,17 +127,6 @@ describe("resource reducer", () => { expect(newState.controls.webcams).toBeTruthy(); }); - it("sets jobs panel options", () => { - const payload: keyof JobsAndLogsState = "logs"; - const state = app; - const newState = appReducer(state, { - type: Actions.SET_JOBS_PANEL_OPTION, - payload, - }); - expect(newState.jobs.jobs).toBeFalsy(); - expect(newState.jobs.logs).toBeTruthy(); - }); - it("toggles popup", () => { const payload: keyof PopupsState = "controls"; const state = app; diff --git a/frontend/app.tsx b/frontend/app.tsx index 26b9a26477..8979d382af 100644 --- a/frontend/app.tsx +++ b/frontend/app.tsx @@ -213,12 +213,14 @@ export class RawApp extends React.Component { appState={this.props.appState} menuOpen={this.props.menuOpen} pings={this.props.pings} />} - {syncLoaded && this.props.children} - - - {syncLoaded && } - - +
+ {syncLoaded && this.props.children} + + + {syncLoaded && } + + +
{ const { location_data, informational_settings } = props.bot.hardware; - const locationData = validBotLocationData(location_data); + const locationData = validBotLocationData(location_data, + isExpress(props.firmwareHardware)); const botOnline = isBotOnlineFromState(props.bot); const { busy, locked } = props.bot.hardware.informational_settings; return
diff --git a/frontend/css/app/navbar.scss b/frontend/css/app/navbar.scss index d041fc42d0..a7edb3b914 100644 --- a/frontend/css/app/navbar.scss +++ b/frontend/css/app/navbar.scss @@ -10,8 +10,11 @@ background: linear-gradient(0deg, transparent, $translucent3 60%, $translucent5), linear-gradient(0deg, transparent 30%, $translucent3); } +.skip-nav-link { + display: none; +} + nav { - margin-top: 3rem; button { margin: 1.8rem 1.8rem 0 0; font-size: 1.3rem !important; @@ -177,8 +180,8 @@ nav { .nav-coordinates, .jobs-button, .setup-button, - .connectivity-button, - .nav-name { + .connectivity-button { + margin: 0; display: flex; position: relative; gap: 0.5rem; @@ -186,12 +189,15 @@ nav { height: 2.9rem; overflow: hidden; border-radius: 5px; + border: none; + cursor: pointer; + font-weight: bold; padding-left: 0.8rem; padding-right: 0.8rem; font-weight: bold; font-size: 1.1rem; line-height: 3rem; - color: $light_gray; + color: $off_white; background: $medium_gray; user-select: none; &:hover { @@ -255,6 +261,7 @@ nav { .connectivity-button { p { font-weight: bold; + max-width: 17rem; } } .connection-status-popover { @@ -273,81 +280,13 @@ nav { } } -.menu-popover { - .bp6-popover-content { - position: relative; - width: 22rem; - padding: 0; - font-size: 1.2rem; - letter-spacing: 1.2px; - color: var(--text-color); - i { - margin-right: 0.8rem; - } - a:not(.app-version) { - display: inline-block; - width: 100%; - text-transform: uppercase; - padding: 0.5rem 1rem; - } - img { - margin-right: 0.8rem; - } - .app-version { - padding: 0.5rem 1rem 0; - margin-top: 0.5rem; - border-top: 1px solid var(--border-color); - label { - font-weight: normal; - } - a { - text-align: right; - border-radius: 0.5rem; - } - } - .dark-mode-toggle { - padding: 0.5rem 1rem; - line-height: 1.8rem; - label { - font-weight: normal; - } - .fb-button { - margin: 0; - } - } - } - .bp6-popover-arrow { - visibility: hidden; - } - .fa-user { - width: 2.75rem; - height: 2.75rem; - padding: 0.75rem; - border-radius: 5px; - &:hover { - background: rgba(255, 255, 255, 0.2); - } - } -} - -body:has(.app.dark) { - .menu-popover { - .bp6-popover-content { - img { - filter: invert(0.75); - } - } - } -} - body:has(.app.light) { .nav-right { .time-travel-button, .nav-coordinates, .jobs-button, .setup-button, - .connectivity-button, - .nav-name { + .connectivity-button { &.hover { background: var(--main-bg); color: var(--text-color); @@ -410,13 +349,6 @@ body:has(.app.light) { } } -.nav-additional-menu { - padding: 0.5rem 0; - a:hover { - background: var(--secondary-bg); - } -} - .read-only-icon { .fa-ban { color: $red; diff --git a/frontend/css/app/static_pages.scss b/frontend/css/app/static_pages.scss index 75e438de97..a91ffedbdd 100644 --- a/frontend/css/app/static_pages.scss +++ b/frontend/css/app/static_pages.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .static-page { min-height: 100vh; @@ -9,13 +10,13 @@ padding: 8rem 2rem; h1, h2 { - font-family: "Cabin", Arial, Helvetica, sans-serif !important; + font-family: $cabin !important; font-weight: 100 !important; color: $white; text-shadow: 0 0 25px rgba(0, 0, 0, 0.1), 0 0 25px rgba(0, 0, 0, 0.1); } h1 { - font-family: "Inknut Antiqua" !important; + font-family: $inknut !important; font-weight: bold !important; font-size: 3.4rem; line-height: 3.6rem; @@ -242,7 +243,7 @@ background: $cyan; border-radius: 5px 5px 0 0; .btn-title { - font-family: "Inknut Antiqua"; + font-family: $inknut; font-weight: bold; margin-bottom: 0.5rem !important; padding-top: 0.75rem; @@ -329,7 +330,7 @@ } h1 { margin: 0; - font-family: "Inknut Antiqua" !important; + font-family: $inknut !important; font-weight: 600 !important; } } diff --git a/frontend/css/app/status_ticker.scss b/frontend/css/app/status_ticker.scss index 3971dbd4bf..7da0fbcace 100644 --- a/frontend/css/app/status_ticker.scss +++ b/frontend/css/app/status_ticker.scss @@ -2,10 +2,6 @@ @use "sass:color"; .ticker-list { - position: fixed; - top: 0; - left: 0; - right: 0; z-index: 3; background: var(--main-bg); backdrop-filter: var(--blur); @@ -98,8 +94,8 @@ // background: linear-gradient(to top, black 0%, transparent 30%); .status-ticker-message { - display: inline; - letter-spacing: 0.1rem; + letter-spacing: 0.05rem; + overflow: hidden; span { -webkit-animation: flash 1.5s ease-in; animation: flash 1.5s ease-in; diff --git a/frontend/css/app/toastr.scss b/frontend/css/app/toastr.scss index 3138189aaf..479b6e08cd 100644 --- a/frontend/css/app/toastr.scss +++ b/frontend/css/app/toastr.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .toast-container { display: flex; @@ -95,7 +96,7 @@ pointer-events: none; font-size: 1.6rem; font-weight: bold; - font-family: 'Inknut Antiqua', serif; + font-family: $inknut; } .toast-message { diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index cd3f8bd42b..381fb89bb4 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -14,7 +14,7 @@ } &.panel-closed-mobile, &.panel-closed { - top: 15rem; + top: 13rem; } } } @@ -535,8 +535,8 @@ } .garden-map-legend { - position: fixed; - top: 8rem; + position: absolute; + top: 7.5rem; right: -155px; z-index: 3; transition: all 0.3s ease; @@ -628,7 +628,6 @@ } button { margin: 0; - width: 5rem; } } .move-to-mode { diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index 7999514caf..860de63b0f 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -1,8 +1,9 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .farm-designer-panels { - position: fixed; + position: absolute; top: 7.5rem; width: 45rem; margin: 1rem; @@ -305,7 +306,7 @@ overflow-y: auto; overflow-x: hidden; width: calc(100% - 75px); - font-family: "Cabin", sans-serif; + font-family: $cabin; } .saving-indicator { position: absolute; @@ -601,7 +602,7 @@ display: inline-block; font-size: 1.2rem; font-weight: bold; - color: $black; + color: var(--text-color); } } .number-eq-criteria, diff --git a/frontend/css/farm_designer/three_d_garden.scss b/frontend/css/farm_designer/three_d_garden.scss index 29f597da3c..bc57e7ba20 100644 --- a/frontend/css/farm_designer/three_d_garden.scss +++ b/frontend/css/farm_designer/three_d_garden.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .three-d-garden-loading-container, .promo-loading-container { @@ -26,7 +27,7 @@ .three-d-garden-loading-text, .promo-loading-text { - font-family: 'Inknut Antiqua', sans-serif; + font-family: $inknut; font-size: 2rem; font-weight: bold; color: $off_white; @@ -131,7 +132,7 @@ .setting-title { color: $off_white; - font-family: 'Inknut Antiqua'; + font-family: $inknut; text-shadow: 0 0 10px black; line-height: 1.8rem; text-align: center; @@ -371,7 +372,7 @@ .title { margin: 0; - font-family: 'Inknut Antiqua'; + font-family: $inknut; font-size: 3.5rem; line-height: 5rem; font-weight: bold; @@ -477,7 +478,7 @@ font-size: 1rem; h2 { margin: 0; - font-family: 'Inknut Antiqua'; + font-family: $inknut; line-height: 100%; font-weight: bold; } diff --git a/frontend/css/global/fonts.scss b/frontend/css/global/fonts.scss index 3cf3c01a5c..3b6775d9ef 100644 --- a/frontend/css/global/fonts.scss +++ b/frontend/css/global/fonts.scss @@ -1,10 +1,39 @@ @use "../variables" as *; @use "sass:color"; -$cabin: 'Cabin', -Arial, -Helvetica, -sans-serif; +@import url("https://fonts.googleapis.com/css?family=Cabin:400,400italic,700,700italic,100,100italic&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Inknut+Antiqua:400,400italic,700,700italic,100,100italic&display=swap"); + +// Fallback font metrics tuned to match web fonts and reduce layout shift. +@font-face { + font-family: "Cabin Fallback"; + src: local("Arial"); + ascent-override: 96.5%; + descent-override: 25%; + line-gap-override: 0%; +} + +@font-face { + font-family: "Inknut Antiqua Fallback"; + src: local("Times New Roman"); + ascent-override: 170.3%; + descent-override: 87.6%; + line-gap-override: 0%; +} + +@font-face { + font-family: "FontAwesome"; + font-style: normal; + font-weight: normal; + font-display: swap; + src: url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"), + url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"); +} + +$cabin: 'Cabin', 'Cabin Fallback', Arial, Helvetica, sans-serif; + +$inknut: 'Inknut Antiqua', 'Inknut Antiqua Fallback', serif; body, html { diff --git a/frontend/css/global/global.scss b/frontend/css/global/global.scss index cec8aabc53..ae57563169 100644 --- a/frontend/css/global/global.scss +++ b/frontend/css/global/global.scss @@ -79,7 +79,7 @@ body { overflow: hidden; box-shadow: var(--box-shadow); label { - padding: 0.5rem 3rem; + padding: 0.5rem 1.5rem; cursor: pointer; &.selected { border-bottom: 3px solid var(--text-color); diff --git a/frontend/css/global/grids.scss b/frontend/css/global/grids.scss index 744e573dae..1cec6f84e5 100644 --- a/frontend/css/global/grids.scss +++ b/frontend/css/global/grids.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "./fonts" as *; .grid { display: grid; @@ -57,6 +58,10 @@ grid-template-columns: 1fr 1fr 1fr 1fr; } +.space-between { + justify-content: space-between; +} + .key-value-edit-row { grid-template-columns: 1fr 1fr auto; } @@ -85,7 +90,7 @@ fieldset { .panel-title { display: grid; - font-family: 'Inknut Antiqua', serif; + font-family: $inknut; font-weight: bold; grid-template-columns: auto 1fr auto; align-items: center; diff --git a/frontend/css/global/imports.scss b/frontend/css/global/imports.scss index 72cfc098c1..2ae73cd7a3 100644 --- a/frontend/css/global/imports.scss +++ b/frontend/css/global/imports.scss @@ -1,6 +1,3 @@ -// Google Fonts -@import url("https://fonts.googleapis.com/css?family=Cabin:400,400italic,700,700italic,100,100italic"); -@import url("https://fonts.googleapis.com/css?family=Inknut+Antiqua:400,400italic,700,700italic,100,100italic"); // Blueprint @import "~/node_modules/@blueprintjs/core/lib/css/blueprint.css"; @import "~/node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css"; diff --git a/frontend/css/global/saucers.scss b/frontend/css/global/saucers.scss index 80f26304dc..a9709a9030 100644 --- a/frontend/css/global/saucers.scss +++ b/frontend/css/global/saucers.scss @@ -9,6 +9,10 @@ background: $dark_gray; border-radius: 50%; cursor: pointer; + &.hollow { + background: transparent; + border: 2px solid var(--border-color); + } &.active { border: 2px solid $dark_gray; } diff --git a/frontend/css/panels/connectivity.scss b/frontend/css/panels/connectivity.scss index 7ae52dc507..db5f405d65 100644 --- a/frontend/css/panels/connectivity.scss +++ b/frontend/css/panels/connectivity.scss @@ -39,8 +39,10 @@ .connectivity { width: 600px; max-width: calc(100vw - 2rem); - max-height: calc(100vh - 10rem); + max-height: calc(100vh - 8.9rem); padding: 1rem; + position: relative; + overflow: scroll; .connectivity-content { table { font-size: 1.3rem; diff --git a/frontend/css/panels/events.scss b/frontend/css/panels/events.scss index be847e876f..27b445b012 100644 --- a/frontend/css/panels/events.scss +++ b/frontend/css/panels/events.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .farm-event { align-items: start!important; @@ -12,7 +13,7 @@ .farm-event-year { text-align: center; font-size: 2rem; - font-family: 'Inknut Antiqua', serif; + font-family: $inknut; font-weight: bold; } diff --git a/frontend/css/panels/jobs.scss b/frontend/css/panels/jobs.scss index 8ad9210965..ba26d4435d 100644 --- a/frontend/css/panels/jobs.scss +++ b/frontend/css/panels/jobs.scss @@ -2,7 +2,44 @@ @use "sass:color"; .jobs-and-logs { - margin-top: 1rem; + grid-template-rows: 14rem 23rem; + height: 37.5rem; + + .jobs-section, + .logs-section { + display: flex; + flex-direction: column; + overflow: hidden; + } + + .jobs-section { + border-bottom: 2px solid var(--border-color); + .jobs-tab { + flex: 1; + overflow-y: auto; + } + } + + .logs-section { + .logs-tab { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + .search-row { + margin: 0 1rem; + } + .logs-table-wrapper { + flex: 1; + display: flex; + flex-direction: column; + .logs-table { + max-height: none; + height: 100%; + } + } + } + } } .jobs-panel { @@ -16,13 +53,14 @@ } .jobs-tab { - overflow-y: scroll; - max-height: 26rem; + overflow-y: auto; + height: 100%; &.bp6-popover { margin-top: 1.5rem; } table { text-align: left; + border-radius: 0; p { padding: 1rem; } @@ -34,10 +72,10 @@ position: sticky; top: 0; z-index: 999; - background: var(--main-bg); } tr { transform: scale(1); + vertical-align: top; } th, td { @@ -69,9 +107,8 @@ .jobs-panel-portal { .bp6-popover-content { padding: 0; - width: min(500px, 100vw - 1rem); + width: min(500px, 100vw); max-height: calc(100vh - 10rem); overflow: hidden; - padding-top: 1rem; } } diff --git a/frontend/css/panels/location_info.scss b/frontend/css/panels/location_info.scss index c96538ff48..6afe81fa82 100644 --- a/frontend/css/panels/location_info.scss +++ b/frontend/css/panels/location_info.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .location-info-panel { .panel-content { @@ -20,7 +21,7 @@ } } h2 { - font-family: 'Inknut Antiqua'; + font-family: $inknut; font-weight: bold; margin: 0; font-size: 2rem; diff --git a/frontend/css/panels/logs.scss b/frontend/css/panels/logs.scss index 2ef734e984..aa27fa4b9f 100644 --- a/frontend/css/panels/logs.scss +++ b/frontend/css/panels/logs.scss @@ -3,13 +3,13 @@ .logs-table-wrapper { border: none; + overflow: scroll; .fa-trash { display: none; } - tr { - vertical-align: top; + tbody tr { &:hover { - background: $translucent2_white; + background: var(--secondary-bg); .fa-trash { display: inline; margin-left: 6px; @@ -21,8 +21,11 @@ } } } + tr { + vertical-align: top; + } .fa-filter { - border: 1px black solid; + border: 1px var(--border-color) solid; border-radius: 50%; padding: 0.25rem; width: 2rem; @@ -52,12 +55,6 @@ font-weight: bold; } } - .notice { - font-style: italic; - text-align: center; - font-size: 1.4rem; - padding: 1rem; - } } .logs-filter-menu { @@ -70,7 +67,7 @@ .line { position: absolute; width: 0.5rem; - height: 86.5%; + height: 85%; border-right: 1.5px $light_gray solid; .line-label { position: absolute; @@ -81,6 +78,10 @@ .bp6-slider-unlabeled { margin: 0 1rem; } + .current-version-only { + margin-left: 2.5rem; + margin-top: 0.5rem; + } } .logs-tab { @@ -90,9 +91,10 @@ } .logs-table { - display: block; - overflow: scroll; + display: table; max-height: 42rem; + margin-top: 0.5rem; + border-radius: 0; .log-verbosity-saucer .saucer { text-align: center; margin-left: 6px; @@ -103,15 +105,11 @@ button { float: none; } + table { + border-radius: 0; + } thead { text-align: left; - background: var(--main-bg); - } - thead, - th { - position: sticky; - top: 0; - z-index: 999; } td { word-break: break-word; @@ -132,6 +130,16 @@ td:nth-child(4) { white-space: nowrap; } + + .logs-retention-row { + td { + font-style: italic; + text-align: center; + padding: 0.7rem 1rem 1rem; + white-space: normal; + background: var(--secondary-bg); + } + } } .link-to-logs { diff --git a/frontend/css/panels/messages.scss b/frontend/css/panels/messages.scss index 238429e18b..5ed38eff96 100644 --- a/frontend/css/panels/messages.scss +++ b/frontend/css/panels/messages.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .problem-alerts { padding: 1rem; @@ -32,7 +33,7 @@ font-size: 1.5rem; font-weight: bold; margin: 0; - font-family: 'Inknut Antiqua'; + font-family: $inknut; } p { display: inline; diff --git a/frontend/css/panels/plants.scss b/frontend/css/panels/plants.scss index b0649d1537..2809cc2fef 100644 --- a/frontend/css/panels/plants.scss +++ b/frontend/css/panels/plants.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .crop-search-result-wrapper { .crop-search-results-wrapper { @@ -61,7 +62,7 @@ .panel-title { overflow: visible; .title { - font-family: "Inknut Antiqua"; + font-family: $inknut; font-weight: bold; } .bp6-popover-wrapper { diff --git a/frontend/css/panels/settings.scss b/frontend/css/panels/settings.scss index 8ed2274b1e..167e7d6dcd 100644 --- a/frontend/css/panels/settings.scss +++ b/frontend/css/panels/settings.scss @@ -1,6 +1,14 @@ @use "../variables" as *; @use "sass:color"; +.settings-panel { + .panel-top { + .dark-mode-toggle { + margin-right: 1rem; + } + } +} + .settings-panel-content { overflow-y: auto; overflow-x: hidden; @@ -94,6 +102,16 @@ } } } + .app-version { + border-top: 1.5px solid var(--border-color); + padding: 1rem 2.3rem; + margin: -1rem -1rem -1.3rem; + font-size: 1.3rem; + a { + font-weight: bold; + justify-self: end; + } + } } .setting { diff --git a/frontend/css/panels/setup_wizard.scss b/frontend/css/panels/setup_wizard.scss index d2506cf73c..db8e6fff91 100644 --- a/frontend/css/panels/setup_wizard.scss +++ b/frontend/css/panels/setup_wizard.scss @@ -1,5 +1,6 @@ @use "../variables" as *; @use "sass:color"; +@use "../global/fonts" as *; .setup-panel { .panel-top { @@ -54,7 +55,7 @@ .wizard-header { padding: 1.5rem; h1 { - font-family: 'Inknut Antiqua', serif; + font-family: $inknut; font-weight: bold; font-size: 2rem; margin: 0; @@ -62,6 +63,7 @@ } .wizard-section { h2 { + padding: 0.5rem 1.5rem; margin-top: 0; } .bp6-collapse-body { @@ -89,7 +91,6 @@ } h2, .wizard-step-header { - padding: 0.5rem 1.5rem; cursor: pointer; &.open { background: var(--secondary-bg); @@ -101,8 +102,11 @@ background: var(--secondary-bg); } } + .wizard-step-header { + padding: 0.5rem 1.5rem 0.5rem 4rem; + } .wizard-step-content { - padding: 0.5rem 1.5rem 2rem; + padding: 0.5rem 1.5rem 1rem 4rem; background: var(--secondary-bg); .markdown { display: contents; diff --git a/frontend/demo/demo_iframe.tsx b/frontend/demo/demo_iframe.tsx index 9d370c64fd..1ececa6b97 100644 --- a/frontend/demo/demo_iframe.tsx +++ b/frontend/demo/demo_iframe.tsx @@ -9,7 +9,7 @@ import { Path } from "../internal_urls"; import { FBSelect } from "../ui"; import { SEED_DATA_OPTIONS, SEED_DATA_OPTIONS_DDI } from "../messages/cards"; -interface State { +export interface DemoAccountState { error: Error | undefined; stage: string; productLine: string; @@ -27,8 +27,9 @@ export const EASTER_EGG = "BIRDS AREN'T REAL"; export const WAITING_ON_API = "Planting your demo garden..."; // APPLICATION CODE ============================== -export class DemoIframe extends React.Component<{}, State> { - state: State = { +export abstract class DemoAccountBase

+ extends React.Component { + state: DemoAccountState = { error: undefined, stage: t("DEMO THE APP"), productLine: "genesis_1.8", @@ -69,28 +70,29 @@ export class DemoIframe extends React.Component<{}, State> { this.connectMqtt().then(this.connectApi); }; - ok = () => { + protected seedDataSelect = (): React.ReactElement => { const selection = this.state.productLine; - return

- - - - x.value != "none")} - customNullLabel={t("Select a model")} - selectedItem={SEED_DATA_OPTIONS_DDI()[selection]} - onChange={ddi => this.setState({ productLine: "" + ddi.value })} /> -
; + return x.value != "none")} + customNullLabel={t("Select a model")} + selectedItem={SEED_DATA_OPTIONS_DDI()[selection]} + onChange={ddi => this.setState({ productLine: "" + ddi.value })} />; }; + protected demoButton = (className: string): React.ReactElement => { + return ; + }; + + protected abstract ok(): React.ReactNode; + no = () => { console.error(this.state.error); const message = JSON.stringify(this.state.error, undefined, 2); @@ -101,3 +103,16 @@ export class DemoIframe extends React.Component<{}, State> { return this.state.error ? this.no() : this.ok(); } } + +export class DemoIframe extends DemoAccountBase { + ok = () => { + return
+ + + {this.demoButton("demo-button")} + {this.seedDataSelect()} +
; + }; +} diff --git a/frontend/demo/lua_runner/__tests__/stubs_test.ts b/frontend/demo/lua_runner/__tests__/stubs_test.ts index f3b33fd427..1ca2e25ff4 100644 --- a/frontend/demo/lua_runner/__tests__/stubs_test.ts +++ b/frontend/demo/lua_runner/__tests__/stubs_test.ts @@ -14,7 +14,11 @@ jest.mock("../../../resources/getters", () => ({ getFbosConfig: () => mockFbosConfig, })); -import { getDefaultAxisOrder, getGardenSize, getSafeZ } from "../stubs"; +import { + getDefaultAxisOrder, + getGardenSize, + getSafeZ, +} from "../stubs"; describe("getGardenSize()", () => { it("gets garden size: axis lengths", () => { diff --git a/frontend/devices/__tests__/jobs_test.tsx b/frontend/devices/__tests__/jobs_test.tsx index b26a830cc7..dd48607970 100644 --- a/frontend/devices/__tests__/jobs_test.tsx +++ b/frontend/devices/__tests__/jobs_test.tsx @@ -9,8 +9,6 @@ import { fakeState } from "../../__test_support__/fake_state"; import { fakeBytesJob, fakePercentJob } from "../../__test_support__/fake_bot_data"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; import { bot } from "../../__test_support__/fake_state/bot"; -import { jobsState } from "../../__test_support__/panel_state"; -import { Actions } from "../../constants"; import { fakeDevice } from "../../__test_support__/resource_index_builder"; describe("", () => { @@ -45,36 +43,14 @@ describe("", () => { getConfigValue: jest.fn(), bot, fbosVersion: undefined, - jobsPanelState: jobsState(), jobs: {}, device: fakeDevice(), }); - it("renders jobs", () => { - const p = fakeProps(); - p.jobsPanelState.jobs = true; - p.jobsPanelState.logs = false; - const wrapper = mount(); - expect(wrapper.html()).toContain("jobs-tab"); - expect(wrapper.html()).not.toContain("logs-tab"); - }); - - it("renders logs", () => { - const p = fakeProps(); - p.jobsPanelState.jobs = false; - p.jobsPanelState.logs = true; - const wrapper = mount(); - expect(wrapper.html()).not.toContain("jobs-tab"); - expect(wrapper.html()).toContain("logs-tab"); - }); - - it("sets state", () => { - const p = fakeProps(); - const wrapper = mount(); - wrapper.instance().setPanelState("logs")(); - expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SET_JOBS_PANEL_OPTION, payload: "logs", - }); + it("renders jobs and logs", () => { + const wrapper = mount(); + expect(wrapper.find(".jobs-tab").length).toEqual(1); + expect(wrapper.find(".logs-tab").length).toEqual(1); }); }); diff --git a/frontend/devices/connectivity/__tests__/connectivity_test.tsx b/frontend/devices/connectivity/__tests__/connectivity_test.tsx index 65b1ebe2c3..4a0fca4915 100644 --- a/frontend/devices/connectivity/__tests__/connectivity_test.tsx +++ b/frontend/devices/connectivity/__tests__/connectivity_test.tsx @@ -132,6 +132,22 @@ describe("", () => { expect(wrapper.text().toLowerCase()).toContain("version: v1.0.0"); }); + it("displays order number", () => { + const p = fakeProps(); + p.metricPanelState.realtime = true; + p.device.body.fb_order_number = "FB1234"; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("order number: fb1234"); + }); + + it("displays order number as 'Unset' when undefined", () => { + const p = fakeProps(); + p.metricPanelState.realtime = true; + p.device.body.fb_order_number = undefined; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("order number: unset"); + }); + it("renders network tab", () => { mockIsMobile = true; const p = fakeProps(); @@ -168,7 +184,7 @@ describe("", () => { p.flags.botFirmware = false; const wrapper = mount(); expect(wrapper.find(".fix-firmware-buttons").length).toBeGreaterThan(0); - clickButton(wrapper, 1, "restart firmware"); + clickButton(wrapper, 2, "restart firmware"); }); it("doesn't display fix firmware buttons", () => { diff --git a/frontend/devices/connectivity/connectivity.tsx b/frontend/devices/connectivity/connectivity.tsx index 32ae7f5d59..c54b176eae 100644 --- a/frontend/devices/connectivity/connectivity.tsx +++ b/frontend/devices/connectivity/connectivity.tsx @@ -26,6 +26,7 @@ import { Actions } from "../../constants"; import { forceOnline } from "../must_be_online"; import { isMobile } from "../../screen_size"; import { NavigationContext } from "../../routes_helpers"; +import { logout } from "../../logout"; export interface ConnectivityProps { bot: BotState; @@ -78,7 +79,7 @@ export class Connectivity firmware_version, target, memory_usage, sync_status, video_devices, } = informational_settings; - const { id, fbos_version } = this.props.device.body; + const { id, fbos_version, fb_order_number } = this.props.device.body; return

{t("Device ID")}: {id}

+

{t("Order number")}: + {fb_order_number || t("Unset")}

{controller_version ?

{t("Version")}: { reformatFbosVersion(controller_version)}

@@ -177,14 +180,21 @@ export class Connectivity render() { const { realtime, network, history } = this.props.metricPanelState; return
-
-
- - - +
+
+
+ + + +
+
{realtime && } {network && } diff --git a/frontend/devices/jobs.tsx b/frontend/devices/jobs.tsx index 241365fda1..ed44acda6c 100644 --- a/frontend/devices/jobs.tsx +++ b/frontend/devices/jobs.tsx @@ -4,7 +4,7 @@ import { DesignerPanel, DesignerPanelContent, DesignerPanelTop, } from "../farm_designer/designer_panel"; import { Panel } from "../farm_designer/panel_header"; -import { Everything, JobsAndLogsState, TimeSettings } from "../interfaces"; +import { Everything, TimeSettings } from "../interfaces"; import { BytesProgress, Dictionary, JobProgress, PercentageProgress, TaggedDevice, TaggedLog, @@ -15,7 +15,6 @@ import moment from "moment"; import { betterCompact, formatTime } from "../util"; import { Color } from "../ui"; import { cloneDeep, round, sortBy } from "lodash"; -import { Actions } from "../constants"; import { BotState, SourceFbosConfig } from "./interfaces"; import { GetWebAppConfigValue } from "../config_storage/actions"; import { LogsPanel } from "../logs"; @@ -54,7 +53,6 @@ export interface JobsAndLogsProps { getConfigValue: GetWebAppConfigValue; bot: BotState; fbosVersion: string | undefined; - jobsPanelState: JobsAndLogsState; jobs: Dictionary; device: TaggedDevice; } @@ -62,12 +60,6 @@ export interface JobsAndLogsProps { export class JobsAndLogs extends React.Component { - setPanelState = (key: keyof JobsAndLogsState) => () => - this.props.dispatch({ - type: Actions.SET_JOBS_PANEL_OPTION, - payload: key, - }); - Jobs = () => { return
-
- - + return
+
+ +
+
+
- {jobs && } - {logs && }
; } } diff --git a/frontend/devices/reducer.ts b/frontend/devices/reducer.ts index 22eb20ff93..d9d17ffed3 100644 --- a/frontend/devices/reducer.ts +++ b/frontend/devices/reducer.ts @@ -146,6 +146,11 @@ export const botReducer = generateReducer(initialState()) }) .add>(Actions.DEMO_SET_POSITION, (s, { payload }) => { s.hardware.location_data.position = payload; + updateMotorHistoryArray({ + position: payload, + raw_encoders: { x: undefined, y: undefined, z: undefined }, + scaled_encoders: payload, + }); return s; }) .add<[string, PercentageProgress]>(Actions.DEMO_SET_JOB_PROGRESS, diff --git a/frontend/extras/__tests__/fallback_widget_test.tsx b/frontend/extras/__tests__/fallback_widget_test.tsx index df6eebe707..336245eb24 100644 --- a/frontend/extras/__tests__/fallback_widget_test.tsx +++ b/frontend/extras/__tests__/fallback_widget_test.tsx @@ -21,6 +21,6 @@ describe("", () => { p.helpText = "This is a fake widget."; const wrapper = shallow(); expect(wrapper.html()) - .toContain("", () => { const fakeProps = (): ThreeDGardenMapProps => ({ @@ -44,6 +53,10 @@ describe("", () => { negativeZ: false, mountedToolName: undefined, peripheralValues: [], + allPoints: [], + groups: [], + images: [], + cameraCalibrationData: fakeCameraCalibrationData(), }); it("converts props", () => { @@ -78,21 +91,30 @@ describe("", () => { expectedConfig.waterFlow = false; expectedConfig.animate = true; expectedConfig.ambient = 1; + expectedConfig.sun = 1; expectedConfig.bedBrightness = 1; + expectedConfig.soilBrightness = 1; expectedConfig.cableDebug = true; expectedConfig.eventDebug = true; expectedConfig.lightsDebug = true; + expectedConfig.surfaceDebug = true; expectedConfig.lowDetail = true; expectedConfig.solar = true; expectedConfig.stats = true; expectedConfig.heading = 1; expectedConfig.north = true; - expectedConfig.desk = false; + expectedConfig.desk = true; expectedConfig.laser = true; expectedConfig.threeAxes = true; expectedConfig.sunAzimuth = 1; expectedConfig.sunInclination = 1; expectedConfig.scene = "Lab"; + expectedConfig.plants = ""; + expectedConfig.axes = true; + expectedConfig.people = true; + expectedConfig.xyDimensions = true; + expectedConfig.zDimension = true; + expectedConfig.imgScale = 0.6; expect(ThreeDGarden).toHaveBeenCalledWith({ config: expectedConfig, @@ -108,8 +130,7 @@ describe("", () => { y: 201, }], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); }); @@ -122,8 +143,7 @@ describe("", () => { config: expect.objectContaining({ x: 0, y: 0, z: 0 }), threeDPlants: [], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); }); @@ -137,8 +157,7 @@ describe("", () => { config: expect.objectContaining({ negativeZ: true, x: 0, y: 0, z: -100 }), threeDPlants: [], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); }); @@ -153,12 +172,11 @@ describe("", () => { config: expect.objectContaining({ sunInclination: expect.any(Number), sunAzimuth: expect.any(Number), - sun: 75, + sun: 1, }), threeDPlants: [], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); const callArgs = (ThreeDGarden as jest.Mock).mock.calls[0][0]; expect(callArgs.config.sunInclination).toBeCloseTo(28.64788975654116, 4); @@ -175,12 +193,11 @@ describe("", () => { config: expect.objectContaining({ sunInclination: -1, sunAzimuth: -1, - sun: 75, + sun: -1, }), threeDPlants: [], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); }); @@ -197,8 +214,7 @@ describe("", () => { config: expect.objectContaining({ kitVersion }), threeDPlants: [], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); }); @@ -211,8 +227,7 @@ describe("", () => { config: expect.objectContaining({ waterFlow: true }), threeDPlants: [], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); }); @@ -233,8 +248,7 @@ describe("", () => { config: expect.objectContaining({ rotary: exp }), threeDPlants: [], addPlantProps: expect.any(Object), - mapPoints: [], - weeds: [], + ...EMPTY_PROPS, }, {}); }); }); diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index 27ae5410c6..7ad6c3a254 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -233,6 +233,10 @@ export class RawFarmDesigner mountedToolName={this.props.mountedToolInfo.name} botPosition={this.props.botLocationData.position} peripheralValues={this.props.peripheralValues} + allPoints={this.props.allPoints} + groups={this.props.groups} + images={this.props.latestImages} + cameraCalibrationData={this.props.cameraCalibrationData} getWebAppConfigValue={this.props.getConfigValue} /> :
", () => { p.designer.shownImages = [1]; const wrapper = shallow(); const layer = wrapper.find("#image-layer"); - expect(layer.find("MapImage").length).toEqual(2); + expect(layer.find("MapImage").length).toEqual(1); }); it("toggles visibility off", () => { diff --git a/frontend/farm_designer/map/layers/images/image_layer.tsx b/frontend/farm_designer/map/layers/images/image_layer.tsx index a2300c7b98..d395e4256b 100644 --- a/frontend/farm_designer/map/layers/images/image_layer.tsx +++ b/frontend/farm_designer/map/layers/images/image_layer.tsx @@ -2,7 +2,7 @@ import React from "react"; import { MapTransformProps } from "../../interfaces"; import { CameraCalibrationData, DesignerState } from "../../../interfaces"; import { TaggedImage } from "farmbot"; -import { MapImage } from "./map_image"; +import { cameraZCheck, MapImage } from "./map_image"; import { reverse, cloneDeep, some } from "lodash"; import { equals } from "../../../../util"; import { BooleanSetting, StringSetting } from "../../../../session_keys"; @@ -12,6 +12,48 @@ import { filterImagesByType, } from "../../../../photos/photo_filter_settings/util"; +export interface FilterImagesProps { + visible: boolean; + images: TaggedImage[] | undefined; + designer: DesignerState | undefined; + getConfigValue: GetWebAppConfigValue | undefined; + calibrationZ: string | undefined; +} + +export interface TaggedImagePlus extends TaggedImage { + highlighted: boolean; +} + +export const filterImages = (props: FilterImagesProps): TaggedImagePlus[] => { + const { visible, images, designer, getConfigValue, calibrationZ } = props; + if (!images || !visible || !designer || !getConfigValue) { return []; } + const { hiddenImages, shownImages, hideUnShownImages, alwaysHighlightImage, + hoveredMapImage, + } = designer; + const getFilterValue = parseFilterSetting(getConfigValue); + const imageFilterBegin = getFilterValue(StringSetting.photo_filter_begin); + const imageFilterEnd = getFilterValue(StringSetting.photo_filter_end); + const rangeOverride = alwaysHighlightImage || hideUnShownImages; + const hoveredImage: TaggedImage | undefined = + images.filter(img => hoveredMapImage && img.body.id == hoveredMapImage + || (alwaysHighlightImage && shownImages.includes(img.body.id || 0)))[0]; + const filteredImages = reverse(cloneDeep(images)) + .filter(img => + (rangeOverride && shownImages.includes(img.body.id || 0)) + || imageInRange(img, imageFilterBegin, imageFilterEnd)) + .filter(img => !imageIsHidden( + hiddenImages, shownImages, hideUnShownImages, img.body.id)) + .filter(filterImagesByType(designer)) + .filter(img => !img.body.attachment_url.includes("placeholder")) + .filter(img => !hoveredImage || (img.body.id != hoveredImage.body.id)) + .filter(img => cameraZCheck(img.body.meta.z, calibrationZ)) + .map(img => ({ ...img, highlighted: false })); + if (hoveredImage) { + filteredImages.push({ ...hoveredImage, highlighted: true }); + } + return filteredImages; +}; + export interface ImageLayerProps { visible: boolean; images: TaggedImage[]; @@ -32,46 +74,28 @@ export class ImageLayer extends React.Component { render() { const { visible, images, mapTransformProps, cameraCalibrationData, - getConfigValue, + getConfigValue, designer, } = this.props; - const { hiddenImages, shownImages, - hideUnShownImages, alwaysHighlightImage, hoveredMapImage, - } = this.props.designer; const cropImages = !!getConfigValue(BooleanSetting.crop_images); const clipImageLayer = !!getConfigValue(BooleanSetting.clip_image_layer); - const getFilterValue = parseFilterSetting(getConfigValue); - const imageFilterBegin = getFilterValue(StringSetting.photo_filter_begin); - const imageFilterEnd = getFilterValue(StringSetting.photo_filter_end); - const hoveredImage: TaggedImage | undefined = - images.filter(img => img.body.id == hoveredMapImage - || (alwaysHighlightImage && shownImages.includes(img.body.id || 0)))[0]; - const rangeOverride = alwaysHighlightImage || hideUnShownImages; return - {visible && - reverse(cloneDeep(images)) - .filter(img => - (rangeOverride && shownImages.includes(img.body.id || 0)) - || imageInRange(img, imageFilterBegin, imageFilterEnd)) - .filter(img => !imageIsHidden( - hiddenImages, shownImages, hideUnShownImages, img.body.id)) - .filter(filterImagesByType(this.props.designer)) - .map(img => - )} - {visible && hoveredImage && - } + {filterImages({ + visible, + designer, + images, + getConfigValue, + calibrationZ: cameraCalibrationData.calibrationZ, + }) + .map(img => + )} ; } } diff --git a/frontend/farm_designer/map/layers/images/map_image.tsx b/frontend/farm_designer/map/layers/images/map_image.tsx index 8e29222d00..cb91907c83 100644 --- a/frontend/farm_designer/map/layers/images/map_image.tsx +++ b/frontend/farm_designer/map/layers/images/map_image.tsx @@ -42,6 +42,7 @@ export const imageSizeCheck = x: parse(calibCenter.x), y: parse(calibCenter.y), }; + if (!calibrationCenter.x) { return true; } return isNumber(calibrationCenter.x) && isNumber(calibrationCenter.y) && isNumber(size.width) && isNumber(size.height) && ((Math.abs(size.width / 2 - calibrationCenter.x) < 5 @@ -165,16 +166,19 @@ const generateTransform = (props: TransformProps): string => { }; interface ParsedCalibrationData { - imageScale: number | undefined; - imageOffsetX: number | undefined; - imageOffsetY: number | undefined; - imageOrigin: string | undefined; - imageRotation: number | undefined; + imageScale: number; + imageOffsetX: number; + imageOffsetY: number; + imageOrigin: string; + imageRotation: number; + calibrationZ: number; + centerX: number; + centerY: number; } /** If calibration data exists, parse it, usually to a number. * Otherwise, return values for pre-calibration preview. */ -const parseCalibrationData = +export const parseCalibrationData = (props: CameraCalibrationData): ParsedCalibrationData => { const { scale, offset, origin, rotation } = props; const imageScale = parse(scale) || 0.6; @@ -183,9 +187,12 @@ const parseCalibrationData = const cleanOrigin = origin ? origin.split("\"").join("") : undefined; const imageOrigin = cleanOrigin ?? "TOP_LEFT"; const imageRotation = parse(rotation) ?? 0; + const calibrationZ = parse(props.calibrationZ) ?? 0; + const centerX = parse(props.center.x) ?? 0; + const centerY = parse(props.center.y) ?? 0; return { imageScale, imageOffsetX, imageOffsetY, imageOrigin, - imageRotation, + imageRotation, calibrationZ, centerX, centerY, }; }; diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx index d3589f59ea..2f61fd7262 100644 --- a/frontend/farm_designer/map/legend/garden_map_legend.tsx +++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx @@ -171,7 +171,6 @@ const LayerToggles = (props: LayerTogglesProps) => { submenuTitle={t("extras")} popover={} /> } + target={} content={popover} />} diff --git a/frontend/farm_designer/state_to_props.ts b/frontend/farm_designer/state_to_props.ts index ce0a7805bd..dfa7117ff9 100644 --- a/frontend/farm_designer/state_to_props.ts +++ b/frontend/farm_designer/state_to_props.ts @@ -102,6 +102,9 @@ export function mapStateToProps(props: Everything): FarmDesignerProps { flipped: isToolFlipped(mountedToolSlotInfo?.meta), }; + const groups = selectAllPointGroups(props.resources.index); + const allPoints = selectAllPoints(props.resources.index); + const peripheralValues = uniq(selectAllPeripherals(props.resources.index)) .map(x => { const label = x.body.label; @@ -134,7 +137,7 @@ export function mapStateToProps(props: Everything): FarmDesignerProps { designer: props.resources.consumers.farm_designer, genericPoints, weeds, - allPoints: selectAllPoints(props.resources.index), + allPoints, tools: selectAllTools(props.resources.index), toolSlots: joinToolsAndSlot(props.resources.index), hoveredPlant, @@ -151,7 +154,7 @@ export function mapStateToProps(props: Everything): FarmDesignerProps { getConfigValue, sensorReadings, sensors: selectAllSensors(props.resources.index), - groups: selectAllPointGroups(props.resources.index), + groups, mountedToolInfo, visualizedSequenceBody, logs: selectAllLogs(props.resources.index), diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx index 39bf69d728..f47d8e6ba9 100644 --- a/frontend/farm_designer/three_d_garden_map.tsx +++ b/frontend/farm_designer/three_d_garden_map.tsx @@ -7,9 +7,10 @@ import { import { clone } from "lodash"; import { BotPosition, SourceFbosConfig } from "../devices/interfaces"; import { - ConfigurationName, TaggedCurve, TaggedGenericPointer, TaggedWeedPointer, + ConfigurationName, TaggedCurve, TaggedGenericPointer, TaggedImage, TaggedPoint, + TaggedPointGroup, TaggedWeedPointer, } from "farmbot"; -import { DesignerState } from "./interfaces"; +import { CameraCalibrationData, DesignerState } from "./interfaces"; import { GetWebAppConfigValue } from "../config_storage/actions"; import { BooleanSetting } from "../session_keys"; import { SlotWithTool } from "../resources/interfaces"; @@ -20,6 +21,7 @@ import { isPeripheralActiveFunc } from "./map/layers/farmbot/bot_peripherals"; import { DeviceAccountSettings } from "farmbot/dist/resources/api_resources"; import { SCENES } from "../settings/three_d_settings"; import { get3DTime, latLng } from "../three_d_garden/time_travel"; +import { parseCalibrationData } from "./map/layers/images/map_image"; export interface ThreeDGardenMapProps { botSize: BotSize; @@ -40,6 +42,10 @@ export interface ThreeDGardenMapProps { mountedToolName: string | undefined; peripheralValues: PeripheralValues; device: DeviceAccountSettings; + allPoints: TaggedPoint[]; + groups: TaggedPointGroup[]; + images: TaggedImage[]; + cameraCalibrationData: CameraCalibrationData; } export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { @@ -88,6 +94,7 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { config.extraLegsX = getValue("extraLegsX"); config.extraLegsY = getValue("extraLegsY"); config.bedBrightness = getValue("bedBrightness"); + config.soilBrightness = getValue("soilBrightness"); config.clouds = !!getValue("clouds"); config.laser = !!getValue("laser"); config.stats = !!getValue("stats"); @@ -97,16 +104,24 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { config.eventDebug = !!getValue("eventDebug"); config.cableDebug = !!getValue("cableDebug"); config.lightsDebug = !!getValue("lightsDebug"); + config.surfaceDebug = !!getValue("surfaceDebug"); + config.sun = getValue("sun"); config.ambient = getValue("ambient"); config.heading = getValue("heading"); config.bounds = !!getValue("bounds"); config.grid = !!getValue("grid"); + config.tracks = !!getValue("tracks"); + config.cableCarriers = !!getValue("cableCarriers"); + config.axes = !!getValue("axes"); + config.xyDimensions = !!getValue("xyDimensions"); + config.zDimension = !!getValue("zDimension"); config.scene = SCENES[getValue("scene")]; - config.people = false; + config.people = !!getValue("people"); config.north = true; - config.desk = false; + config.desk = !!getValue("desk"); + config.plants = ""; const { latitude, longitude, valid } = latLng(props.device); if (valid) { @@ -134,6 +149,16 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { }; config.rotary = rotarySpeed(); + const camCalData = parseCalibrationData(props.cameraCalibrationData); + config.imgScale = camCalData.imageScale; + config.imgRotation = camCalData.imageRotation; + config.imgOffsetX = camCalData.imageOffsetX; + config.imgOffsetY = camCalData.imageOffsetY; + config.imgOrigin = camCalData.imageOrigin; + config.imgCalZ = camCalData.calibrationZ; + config.imgCenterX = camCalData.centerX; + config.imgCenterY = camCalData.centerY; + config.zoom = true; config.pan = true; config.rotate = !props.designer.threeDTopDownView; @@ -148,6 +173,9 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { weeds={props.weeds} toolSlots={props.toolSlots} mountedToolName={props.mountedToolName} + allPoints={props.allPoints} + groups={props.groups} + images={props.images} addPlantProps={{ gridSize: props.mapTransformProps.gridSize, dispatch: props.dispatch, diff --git a/frontend/front_page/__tests__/demo_login_option_test.tsx b/frontend/front_page/__tests__/demo_login_option_test.tsx new file mode 100644 index 0000000000..a87c62b5ee --- /dev/null +++ b/frontend/front_page/__tests__/demo_login_option_test.tsx @@ -0,0 +1,61 @@ +let mockResponse: string | Error = "12345"; +jest.mock("axios", () => ({ + post: jest.fn(() => + typeof mockResponse === "string" + ? Promise.resolve(mockResponse) + : Promise.reject(mockResponse)), +})); + +const mockMqttClient = { + on: jest.fn((ev: string, cb: Function) => ev == "connect" && cb()), + subscribe: jest.fn(), +}; + +jest.mock("mqtt", () => ({ connect: () => mockMqttClient })); + +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { shallow } from "enzyme"; +import { DemoLoginOption } from "../demo_login_option"; +import axios from "axios"; +import { MQTT_CHAN } from "../../demo/demo_iframe"; + +describe("", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("renders demo controls", () => { + mockResponse = "ok"; + render(); + expect(screen.getByRole("heading", { name: /demo the app/i })) + .toBeInTheDocument(); + expect(screen.getByRole("button", { name: /demo the app/i })) + .toBeInTheDocument(); + expect(screen.getByText(/farmbot model/i)).toBeInTheDocument(); + }); + + it("requests a demo account on click", async () => { + mockResponse = "ok"; + + render(); + const user = userEvent.setup(); + await user.click(screen.getByRole("button", { name: /demo the app/i })); + + await waitFor(() => + expect(mockMqttClient.subscribe) + .toHaveBeenCalledWith(MQTT_CHAN, expect.any(Function))); + await waitFor(() => + expect(axios.post).toHaveBeenCalledWith( + "/api/demo_account", + expect.objectContaining({ product_line: expect.any(String) }))); + }); + + it("changes model", () => { + const wrapper = shallow(); + expect(wrapper.state().productLine).toEqual("genesis_1.8"); + wrapper.find("FBSelect").simulate("change", { value: "express_1.2" }); + expect(wrapper.state().productLine).toEqual("express_1.2"); + }); +}); diff --git a/frontend/front_page/demo_login_option.tsx b/frontend/front_page/demo_login_option.tsx new file mode 100644 index 0000000000..9c14d893b2 --- /dev/null +++ b/frontend/front_page/demo_login_option.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { DemoAccountBase } from "../demo/demo_iframe"; +import { t } from "../i18next_wrapper"; +import { Widget, WidgetBody, WidgetHeader } from "../ui"; + +export class DemoLoginOption extends DemoAccountBase { + ok = () => { + return + + +
+
+ + {this.seedDataSelect()} +
+
+ {this.demoButton("fb-button dark-blue")} +
+
+
+
; + }; +} diff --git a/frontend/front_page/front_page.tsx b/frontend/front_page/front_page.tsx index 64d6d10745..c5d8c36445 100644 --- a/frontend/front_page/front_page.tsx +++ b/frontend/front_page/front_page.tsx @@ -16,9 +16,17 @@ import { get } from "lodash"; import { t } from "../i18next_wrapper"; import { ToastContainer } from "../toast/fb_toast"; import { Path } from "../internal_urls"; +import { DemoLoginOption } from "./demo_login_option"; export const DEFAULT_APP_PAGE = Path.app(); +const OrDivider = () => +
+
+ {t("OR")} +
+
; + export interface PartialFormEvent { currentTarget: { checked: boolean; @@ -218,11 +226,7 @@ export class FrontPage extends React.Component<{}, Partial> {

{t("Setup, customize, and control your garden from anywhere")}

-
-
- {t("OR")} -
-
+ > { set={(key, val) => this.setState({ [key]: val })}> {this.maybeRenderTos()} + +
; diff --git a/frontend/help/tours/__tests__/index_test.tsx b/frontend/help/tours/__tests__/index_test.tsx index bb0dbd962a..dd50da5357 100644 --- a/frontend/help/tours/__tests__/index_test.tsx +++ b/frontend/help/tours/__tests__/index_test.tsx @@ -137,7 +137,7 @@ describe("", () => { p.helpState.currentTourStep = undefined; mount(); expect(p.dispatch).toHaveBeenCalledWith({ - type: Actions.SET_JOBS_PANEL_OPTION, payload: "logs", + type: Actions.OPEN_POPUP, payload: "jobs", }); }); diff --git a/frontend/help/tours/data.tsx b/frontend/help/tours/data.tsx index 4924e7e392..632c7ad685 100644 --- a/frontend/help/tours/data.tsx +++ b/frontend/help/tours/data.tsx @@ -141,7 +141,6 @@ export const TOURS = ( url: undefined, dispatchActions: [ { type: Actions.OPEN_POPUP, payload: "jobs" }, - { type: Actions.SET_JOBS_PANEL_OPTION, payload: "jobs" }, ], }, { @@ -169,14 +168,6 @@ export const TOURS = ( { type: Actions.CLOSE_POPUP, payload: undefined }, ], }, - { - slug: "accountMenu", - title: t("Account Menu"), - content: TourContent.ACCOUNT_MENU, - beacons: undefined, - activeBeacons: [{ class: "nav-name", type: "soft", keep: true }], - url: undefined, - }, { slug: "end", title: t("What's next?"), @@ -450,7 +441,6 @@ export const TOURS = ( dispatchActions: [ { type: Actions.SET_PROFILE_OPEN, payload: false }, { type: Actions.OPEN_POPUP, payload: "jobs" }, - { type: Actions.SET_JOBS_PANEL_OPTION, payload: "logs" }, ], }, { diff --git a/frontend/interfaces.ts b/frontend/interfaces.ts index 4274bf565b..5c58d4cb9b 100644 --- a/frontend/interfaces.ts +++ b/frontend/interfaces.ts @@ -94,11 +94,6 @@ export interface MovementState { distance: Record; } -export interface JobsAndLogsState { - jobs: boolean; - logs: boolean; -} - export interface ControlsState { move: boolean; peripherals: boolean; diff --git a/frontend/logs/__tests__/index_test.tsx b/frontend/logs/__tests__/index_test.tsx index 1bb4e0911c..12e2cf27db 100644 --- a/frontend/logs/__tests__/index_test.tsx +++ b/frontend/logs/__tests__/index_test.tsx @@ -53,6 +53,8 @@ describe("", () => { .map(string => expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase())); verifyFilterState(wrapper, true); + expect(wrapper.find(".logs-retention-row").text().toLowerCase()) + .toContain("logs older than"); }); it("handles unknown log type", () => { @@ -254,8 +256,6 @@ describe("", () => { expect(wrapper.text()).toContain("moved"); expect(p.dispatch).toHaveBeenCalledWith( { type: Actions.OPEN_POPUP, payload: "jobs" }); - expect(p.dispatch).toHaveBeenCalledWith( - { type: Actions.SET_JOBS_PANEL_OPTION, payload: "logs" }); }); }); diff --git a/frontend/logs/components/filter_menu.tsx b/frontend/logs/components/filter_menu.tsx index fea7ca92fa..c5e5f1f30a 100644 --- a/frontend/logs/components/filter_menu.tsx +++ b/frontend/logs/components/filter_menu.tsx @@ -43,7 +43,7 @@ export const LogsFilterMenu = (props: LogsFilterMenuProps) => { const values = filterStateKeys(props.state) .map((key: keyof LogsState) => props.state[key]); const [value, setValue] = React.useState(round(mean(values))); - return
+ return
{range(0, 4).map(i =>
{ onChange={props.setFilterLevel(logType)} value={props.state[logType]} /> )} -
+
diff --git a/frontend/logs/components/logs_table.tsx b/frontend/logs/components/logs_table.tsx index 0079b91917..1deba7f682 100644 --- a/frontend/logs/components/logs_table.tsx +++ b/frontend/logs/components/logs_table.tsx @@ -96,6 +96,20 @@ const LOG_TABLE_CLASS = [ /** All log messages with select data in table form for display in the app. */ export const LogsTable = (props: LogsTableProps) => { + const retentionDays = props.device.body.max_log_age_in_days || 60; + const rows = filterByVerbosity(getFilterLevel(props.state), props.logs) + .filter(bySearchTerm(props.state.searchTerm, props.timeSettings)) + .filter(log => !props.state.currentFbosOnly || !props.fbosVersion || + logVersionMatch(log, props.fbosVersion)) + .map((log: TaggedLog) => + ); + return
@@ -107,25 +121,18 @@ export const LogsTable = (props: LogsTableProps) => { - {filterByVerbosity(getFilterLevel(props.state), props.logs) - .filter(bySearchTerm(props.state.searchTerm, props.timeSettings)) - .filter(log => !props.state.currentFbosOnly || !props.fbosVersion || - logVersionMatch(log, props.fbosVersion)) - .map((log: TaggedLog) => - )} + {rows} + + + + +
+ {t("Logs older than {{ days }} days are automatically deleted", { + days: retentionDays, + })} +
-

- {t("Logs older than {{ days }} days are automatically deleted", { - days: props.device.body.max_log_age_in_days || 60, - })} -

; }; diff --git a/frontend/logs/components/settings_menu.tsx b/frontend/logs/components/settings_menu.tsx index ce3c4a61ce..249232b248 100644 --- a/frontend/logs/components/settings_menu.tsx +++ b/frontend/logs/components/settings_menu.tsx @@ -53,7 +53,7 @@ const LogSetting = (props: LogSettingProps) => { const config = sourceFbosConfig(setting); const firmwareHardware = validFirmwareHardware( sourceFbosConfig("firmware_hardware").value); - return
+ return
@@ -105,8 +105,8 @@ export class LogsSettingsMenu extends React.Component { getConfigValue={getConfigValue} />; }; const { private_ip } = this.props.bot.hardware.informational_settings; - return
-
+ return
+
@@ -115,8 +115,10 @@ export class LogsSettingsMenu extends React.Component { className={getModifiedClassNameSpecifyDefault(this.props.markdown, true)} toggleAction={this.props.toggleMarkdown} />
- {t("Sequence logs:")} - {SEQUENCE_LOG_SETTINGS().map(p => )} +
+ {t("Sequence logs:")} + {SEQUENCE_LOG_SETTINGS().map(p => )} +
{DevSettings.futureFeaturesEnabled() && private_ip && } -
+
} content={> { BooleanSetting.disable_emergency_unlock_confirmation)} />
; - AccountMenu = () => { - const hasName = this.props.user?.body.name; - const firstName = hasName - ? `${hasName.split(" ")[0].slice(0, 9)} ▾` - : `${t("Menu")} ▾`; - return
- - :
- {firstName} -
} - content={} /> -
; - }; - ConnectionStatus = () => { const data = connectivityData({ bot: this.props.bot, @@ -196,13 +171,15 @@ export class NavBar extends React.Component> { popoverClassName={"connectivity-popover"} isOpen={isOpen} enforceFocus={false} - target={
+ target={
} + {!isMobile() &&

{this.props.device.body.name || t("FarmBot")}

} + } content={ > { const isPercent = job?.unit == "percent"; const percent = isPercent ? round(job.percent, 1) : ""; const activeText = !isMobile() ? jobNameLookup(job) : ""; - const inactiveText = !isMobile() ? t("idle") : t("jobs"); + const inactiveText = !isMobile() ? t("Idle") : t("jobs"); const jobProgress = isPercent ? `${percent}%` : ""; const isOpen = this.props.appState.popups.jobs; return
@@ -253,8 +230,10 @@ export class NavBar extends React.Component> { popoverClassName={"jobs-panel"} isOpen={isOpen} enforceFocus={false} - target={ + target={} content={> { AppNavLinks = () =>
+ onClick={this.toggleMobileMenu} /> @@ -296,7 +274,7 @@ export class NavBar extends React.Component> { @@ -320,8 +298,11 @@ export class NavBar extends React.Component> { "nav-wrapper", this.isStaff ? "red" : "", ].join(" ")}> + + {t("Skip to main content")} +