diff --git a/Gemfile.lock b/Gemfile.lock index 4e0d566ee..10959b320 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,7 +83,7 @@ GEM activesupport climate_control (1.2.0) coderay (1.1.3) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) crack (0.4.5) rexml crass (1.0.6) @@ -114,12 +114,12 @@ GEM docile (1.4.0) e2mmap (0.1.0) erubi (1.12.0) - factory_bot (6.4.4) + factory_bot (6.4.5) activesupport (>= 5.0.0) - factory_bot_rails (6.4.2) + factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faker (3.2.2) + faker (3.2.3) i18n (>= 1.8.11, < 2) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -148,34 +148,33 @@ GEM faraday (~> 1.0) globalid (1.2.1) activesupport (>= 6.1) - google-apis-core (0.11.2) + google-apis-core (0.12.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) httpclient (>= 2.8.1, < 3.a) mini_mime (~> 1.0) representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) - google-apis-core (>= 0.11.0, < 2.a) + google-apis-iamcredentials_v1 (0.18.0) + google-apis-core (>= 0.12.0, < 2.a) + google-apis-storage_v1 (0.33.0) + google-apis-core (>= 0.12.0, < 2.a) google-cloud-core (1.6.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (2.1.0) faraday (>= 1.0, < 3.a) google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-storage (1.48.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (~> 0.33) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.9.1) + googleauth (1.9.2) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) @@ -217,26 +216,26 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.5) - minitest (5.20.0) + minitest (5.21.2) multi_json (1.15.0) multipart-post (2.3.0) mutations (0.9.1) activesupport - net-imap (0.4.9) + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol nio4r (2.7.0) nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) os (1.1.4) - passenger (6.0.19) + passenger (6.0.20) rack rake (>= 0.8.1) pg (1.5.4) @@ -307,7 +306,7 @@ GEM railties (>= 5.2) retriable (3.1.2) rexml (3.2.6) - rollbar (3.4.2) + rollbar (3.5.1) rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) @@ -320,7 +319,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -378,7 +377,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) diff --git a/README.md b/README.md index f3746e31e..8dbd43400 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # FarmBot Web App [![codebeat badge](https://codebeat.co/badges/7f81859b-67fe-4bdb-b56f-050bfed35e9c)](https://codebeat.co/projects/github-com-farmbot-farmbot-web-app-staging) -[![codecov](https://codecov.io/gh/FarmBot/Farmbot-Web-App/branch/main/graph/badge.svg)](https://codecov.io/gh/FarmBot/Farmbot-Web-App) +[![codecov](https://codecov.io/gh/FarmBot/Farmbot-Web-App/branch/staging/graph/badge.svg)](https://codecov.io/gh/FarmBot/Farmbot-Web-App) [![Coverage Status](https://coveralls.io/repos/github/FarmBot/Farmbot-Web-App/badge.svg)](https://coveralls.io/github/FarmBot/Farmbot-Web-App) [![Maintainability](https://api.codeclimate.com/v1/badges/74091163d8a02bb8988f/maintainability)](https://codeclimate.com/github/FarmBot/Farmbot-Web-App/maintainability) diff --git a/app/models/transport.rb b/app/models/transport.rb index 78bb32730..e49202528 100644 --- a/app/models/transport.rb +++ b/app/models/transport.rb @@ -4,9 +4,10 @@ # change protocols class Transport OPTS = { read_timeout: 10, heartbeat: 10, log_level: "warn" } + CLOUDAMQP_ENV_KEY = ENV.fetch("WHERE_IS_CLOUDAMQP_URL", "CLOUDAMQP_URL") def self.amqp_url - @amqp_url ||= ENV["CLOUDAMQP_URL"] || + @amqp_url ||= ENV[CLOUDAMQP_ENV_KEY] || ENV["RABBITMQ_URL"] || "amqp://admin:#{ENV.fetch("ADMIN_PASSWORD")}@mqtt:5672" end diff --git a/app/mutations/sequences/publish.rb b/app/mutations/sequences/publish.rb index 460519436..f1bdcd71d 100644 --- a/app/mutations/sequences/publish.rb +++ b/app/mutations/sequences/publish.rb @@ -3,7 +3,7 @@ class Publish < Mutations::Command NOT_YOURS = "Can't publish sequences you didn't create." OK_KINDS = %w( axis axis_addition axis_overwrite calibrate channel channel_name coordinate emergency_lock execute execute_script - find_home identifier is_outdated label location_placeholder + find_home identifier is_outdated label location_placeholder lua message message_type milliseconds move move_absolute move_relative nothing number number_placeholder numeric op package pair parameter_application parameter_declaration diff --git a/example.env b/example.env index a479371b3..b08cfb702 100644 --- a/example.env +++ b/example.env @@ -98,9 +98,14 @@ GCS_BUCKET=GOOGLE_CLOUD_STORAGE_BUCKET_NAME_FOR_IMAGE_FILES GCS_ID=GOOGLE_CLOUD_STORAGE='interop' id # Most self hosting users will want to delete this. GCS_KEY=GOOGLE_CLOUD_STORAGE='interop' key +GCS_PROJECT= +GOOGLE_CLOUD_KEYFILE_JSON= # Can be deleted unless you are a Rollbar customer. ROLLBAR_ACCESS_TOKEN=____ ROLLBAR_CLIENT_TOKEN=____ +ROLLBAR_ENV= +# Can be deleted unless you are using codecov. +CODECOV_TOKEN= # This can be set to anything. # Most users can just delete it. # This is used for people writing modifications to the software, mostly. @@ -128,6 +133,10 @@ EXTRA_DOMAINS=staging.farm.bot,whatever.farm.bot # Include the protocol! (http vs. https) # DELETE THIS LINE if you are a self-hosted user. RABBIT_MGMT_URL=http://delete_this_line.com +# defaults to `CLOUDAMQP_URL` +WHERE_IS_CLOUDAMQP_URL= +CLOUDAMQP_URL= +RABBITMQ_URL= # Allow only certain users on the server. If the user's email domain is not # on the list of trusted domains, they can not use the server. # The example below only allows users with `@farmbot.io` or `@farm.bot` emails @@ -156,6 +165,8 @@ FEEDBACK_WEBHOOK_URL=http://localhost:3000/change_this # Email address of a "publisher account" that is used to # publish shared sequences via `rake sequences:publish ` AUTHORIZED_PUBLISHER=foo@bar.com +# URL to send release info to. +RELEASE_WEBHOOK_URL= # OpenAI API key. Delete this line if you don't have one. OPENAI_API_KEY= # OpenAI API sampling temperature. Optional. Float between 0 and 2. diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts index 678137292..c1e5e5564 100644 --- a/frontend/__test_support__/fake_state/resources.ts +++ b/frontend/__test_support__/fake_state/resources.ts @@ -36,9 +36,6 @@ import { } from "farmbot/dist/resources/api_resources"; import { MessageType } from "../../sequences/interfaces"; import { TaggedPointGroup } from "../../resources/interfaces"; -import { - BooleanConfigKey as BooleanWebAppConfigKey, -} from "farmbot/dist/resources/configs/web_app"; export const resources: Everything["resources"] = buildResourceIndex(); let idCounter = 1; @@ -336,7 +333,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig { display_map_missed_steps: false, display_trail: false, dynamic_map: false, - ["enable_3d_electronics_box_top" as BooleanWebAppConfigKey]: true, + enable_3d_electronics_box_top: true, encoder_figure: false, go_button_axes: "XY", hide_webcam_widget: false, diff --git a/frontend/constants.ts b/frontend/constants.ts index d2d3a4c00..cc9f55559 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -1105,6 +1105,9 @@ export namespace Content { FarmBot and make changes. If the original author of the sequence publishes a new version, you will have the option to upgrade your copy.`); + export const INCLUDES_LUA_WARNING = + trim(`This sequence includes Lua code. Review carefully before executing.`); + export const IMPORTED_SEQUENCE = trim(`This sequence was imported from a publicly shared sequence. If the original author publishes a new version, you may upgrade your copy. @@ -1475,7 +1478,7 @@ export namespace TourContent { lot of plants at once? Go to the next step of the tour!`); export const GRID_AND_ROW_PLANTING = - trim(`To add a grid or row of plants, scroll to the bottom of the panel, + trim(`To add a grid or row of plants, press the + GRID button, enter values into the grid and row planting fields and click PREVIEW. The previewed plants will show in the map in grayscale. Make adjustments as necessary and when you are happy with the preview, click SAVE. diff --git a/frontend/css/global.scss b/frontend/css/global.scss index 29b9c6180..d791f1144 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -974,6 +974,9 @@ hr { box-shadow: 0 0 10px $translucent5, 0 0 5px inset rgba(255, 255, 255, 0.2) !important; } } + &.hard { + border-radius: 5px; + } } a { @@ -2024,9 +2027,13 @@ ul { font-weight: 700; line-height: 1; text-align: center; + user-select: none; &.hovered { background: $black; } + &.unbound { + background: $placeholder_gray; + } } .led-label { max-width: 7rem; diff --git a/frontend/css/sequences.scss b/frontend/css/sequences.scss index b8c8fa11b..5bb4c28db 100644 --- a/frontend/css/sequences.scss +++ b/frontend/css/sequences.scss @@ -1481,6 +1481,9 @@ display: inline-block; margin-left: 1rem; } + p { + margin-left: 1rem; + } } .import-banner { diff --git a/frontend/css/static_pages.scss b/frontend/css/static_pages.scss index a7e4d0354..23a568fd3 100644 --- a/frontend/css/static_pages.scss +++ b/frontend/css/static_pages.scss @@ -14,6 +14,8 @@ 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-weight: bold !important; font-size: 3.4rem; line-height: 3.6rem; } @@ -25,6 +27,8 @@ } input { margin-bottom: 1rem; + font-family: revert; + font-size: revert; } .all-content-wrapper { max-width: 50rem; diff --git a/frontend/css/widgets.scss b/frontend/css/widgets.scss index 2d9e5413a..b7b388e18 100644 --- a/frontend/css/widgets.scss +++ b/frontend/css/widgets.scss @@ -29,6 +29,8 @@ >*:not(h5):not(.title-help-icon):not(.title-help) { margin-left: 1rem; } + border-top-left-radius: 5px; + border-top-right-radius: 5px; .title-help{ display: inline; .title-help-text { @@ -109,6 +111,8 @@ >.row:not(:first-of-type) { margin-top: 1rem; } + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; } .widget-footer { diff --git a/frontend/farm_designer/__tests__/panel_header_test.tsx b/frontend/farm_designer/__tests__/panel_header_test.tsx index 79355df98..59328c26a 100644 --- a/frontend/farm_designer/__tests__/panel_header_test.tsx +++ b/frontend/farm_designer/__tests__/panel_header_test.tsx @@ -20,7 +20,7 @@ import { shallow, mount, ReactWrapper } from "enzyme"; import { DesignerNavTabs } from "../panel_header"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { - fakeFarmwareInstallation, + fakeFarmwareInstallation, fakeWebAppConfig, } from "../../__test_support__/fake_state/resources"; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -64,6 +64,22 @@ describe("", () => { expectActive(wrapper, "zones"); }); + it("shows sensors tab", () => { + const config = fakeWebAppConfig(); + config.body.hide_sensors = false; + mockState.resources = buildResourceIndex([config]); + const wrapper = mount(); + expect(wrapper.html()).toContain("sensors"); + }); + + it("doesn't show sensors tab", () => { + const config = fakeWebAppConfig(); + config.body.hide_sensors = true; + mockState.resources = buildResourceIndex([config]); + const wrapper = mount(); + expect(wrapper.html()).not.toContain("sensors"); + }); + it("renders scroll indicator", () => { Object.defineProperty(document, "getElementsByClassName", { value: () => [{}, { scrollWidth: 100, scrollLeft: 0, clientWidth: 75 }], diff --git a/frontend/farm_designer/move_to.tsx b/frontend/farm_designer/move_to.tsx index e0b45c0e2..d53744fa7 100644 --- a/frontend/farm_designer/move_to.tsx +++ b/frontend/farm_designer/move_to.tsx @@ -131,7 +131,9 @@ export const chooseLocation = (props: { }) => { if (props.gardenCoords) { props.dispatch(chooseLocationAction({ - x: props.gardenCoords.x, y: props.gardenCoords.y, z: 0 + x: Math.max(0, props.gardenCoords.x), + y: Math.max(0, props.gardenCoords.y), + z: 0, })); } }; @@ -156,7 +158,7 @@ interface GoToThisLocationButtonState { export class GoToThisLocationButton extends React.Component { + GoToThisLocationButtonState> { state: GoToThisLocationButtonState = { open: false, setAsDefault: false }; toggle = (key: keyof GoToThisLocationButtonState) => () => diff --git a/frontend/farm_designer/panel_header.tsx b/frontend/farm_designer/panel_header.tsx index 6ba6dea40..8a41d1ae4 100644 --- a/frontend/farm_designer/panel_header.tsx +++ b/frontend/farm_designer/panel_header.tsx @@ -6,10 +6,6 @@ import { DevSettings } from "../settings/dev/dev_support"; import { getWebAppConfigValue } from "../config_storage/actions"; import { store } from "../redux/store"; import { BooleanSetting } from "../session_keys"; -import { - getFwHardwareValue, hasSensors, -} from "../settings/firmware/firmware_hardware_support"; -import { getFbosConfig } from "../resources/getters"; import { computeEditorUrlFromState } from "../nav/compute_editor_url_from_state"; import { compact } from "lodash"; import { selectAllFarmwareInstallations } from "../resources/selectors"; @@ -218,10 +214,7 @@ const displayScrollIndicator = () => { export const showSensors = () => { const getWebAppConfigVal = getWebAppConfigValue(store.getState); - const firmwareHardware = getFwHardwareValue(getFbosConfig( - store.getState().resources.index)); - return !getWebAppConfigVal(BooleanSetting.hide_sensors) - && hasSensors(firmwareHardware); + return !getWebAppConfigVal(BooleanSetting.hide_sensors); }; export const showFarmware = () => { diff --git a/frontend/front_page/__tests__/front_page_test.tsx b/frontend/front_page/__tests__/front_page_test.tsx index a2c642e89..83ccfee3a 100644 --- a/frontend/front_page/__tests__/front_page_test.tsx +++ b/frontend/front_page/__tests__/front_page_test.tsx @@ -79,9 +79,10 @@ describe("", () => { expect(location.assign).toHaveBeenCalledWith(DEFAULT_APP_PAGE); }); - it("updates login state", () => { + it("updates state", () => { const wrapper = mount(); - changeBlurableInput(wrapper, "email", 1); + wrapper.setState({ activePanel: "forgotPassword" }); + changeBlurableInput(wrapper, "email", 0); expect(wrapper.state().email).toEqual("email"); }); diff --git a/frontend/front_page/__tests__/login_test.tsx b/frontend/front_page/__tests__/login_test.tsx index dbe2bbcaa..18a936522 100644 --- a/frontend/front_page/__tests__/login_test.tsx +++ b/frontend/front_page/__tests__/login_test.tsx @@ -1,7 +1,6 @@ import React from "react"; import { mount, shallow } from "enzyme"; import { Login, LoginProps } from "../login"; -import { changeBlurableInput } from "../../__test_support__/helpers"; describe("", () => { const fakeProps = (): LoginProps => ({ @@ -17,14 +16,17 @@ describe("", () => { const wrapper = mount(); ["Email", "Password", "Forgot password?", "Login"] .map(string => expect(wrapper.text()).toContain(string)); - changeBlurableInput(wrapper, "email", 1); - expect(p.onEmailChange).toHaveBeenCalledWith( - { currentTarget: { value: "email" } }); - const input = shallow(wrapper.find("input").at(2).getElement()); - input.simulate("change", { target: { value: "password" } }); - input.simulate("blur", { currentTarget: { value: "password" } }); - expect(p.onLoginPasswordChange).toHaveBeenCalledWith( - { currentTarget: { value: "password" } }); + }); + + it("interacts with login options", () => { + const p = fakeProps(); + const wrapper = shallow(); + const e1 = { currentTarget: { value: "email" } }; + wrapper.find("input").first().simulate("change", e1); + expect(p.onEmailChange).toHaveBeenCalledWith(e1); + const e2 = { currentTarget: { value: "password" } }; + wrapper.find("input").last().simulate("change", e2); + expect(p.onLoginPasswordChange).toHaveBeenCalledWith(e2); wrapper.find("a").first().simulate("click"); expect(p.onToggleForgotPassword).toHaveBeenCalled(); }); @@ -32,12 +34,7 @@ describe("", () => { it("submits", () => { const p = fakeProps(); const wrapper = shallow(); - const e = { persist: jest.fn(), preventDefault: jest.fn() }; - jest.useFakeTimers(); - wrapper.find("form").simulate("submit", e); - expect(e.persist).toHaveBeenCalled(); - expect(e.preventDefault).toHaveBeenCalled(); - jest.runAllTimers(); - expect(p.onSubmit).toHaveBeenCalledWith(e); + wrapper.find("form").simulate("submit"); + expect(p.onSubmit).toHaveBeenCalled(); }); }); diff --git a/frontend/front_page/login.tsx b/frontend/front_page/login.tsx index d803f9126..edb373861 100644 --- a/frontend/front_page/login.tsx +++ b/frontend/front_page/login.tsx @@ -1,20 +1,10 @@ -import * as React from "react"; -import { - BlurableInput, - Col, - Widget, - WidgetBody, - WidgetHeader, - Row, -} from "../ui/index"; -import { BlurablePassword } from "../ui/blurable_password"; +import React from "react"; import { t } from "../i18next_wrapper"; +import { Col, Widget, WidgetBody, WidgetHeader, Row } from "../ui"; import { updatePageInfo } from "../util"; export interface LoginProps { - /** Attributes */ email: string | undefined; - /** Callbacks */ onToggleForgotPassword(): void; onSubmit(e: React.FormEvent): void; onEmailChange(e: React.SyntheticEvent): void; @@ -22,17 +12,6 @@ export interface LoginProps { } export class Login extends React.Component { - /** PROBLEM: only updates when when `blur` event happens. - * * No update when you push return key- that's a submit event. - * SOLUTION: Intercept the `submit` event and forcibly focus on a hidden - * input control, thereby triggering the blur event across all - * fields. - */ - private hiddenFieldRef: HTMLElement | undefined = undefined; - - /** CSS to hide the fake input field used to change focus. */ - HIDE_ME = { background: "transparent", border: "none", display: "node" }; - render() { const { email, @@ -46,35 +25,23 @@ export class Login extends React.Component { -
{ - e.persist(); - e.preventDefault(); - /** Force focus on fake input. Triggers blur on all inputs. */ - this.hiddenFieldRef && this.hiddenFieldRef.focus(); - /** Give React time to update stuff before triggering callback. */ - setTimeout(() => onSubmit(e), 3); - }}> -
- x && (this.hiddenFieldRef = x)} /> -
+ - + onChange={onEmailChange} /> - + diff --git a/frontend/help/tours/data.tsx b/frontend/help/tours/data.tsx index 749e253d7..4924e7e39 100644 --- a/frontend/help/tours/data.tsx +++ b/frontend/help/tours/data.tsx @@ -163,7 +163,7 @@ export const TOURS = ( title: t("E-STOP Button"), content: TourContent.ESTOP_BUTTON, beacons: undefined, - activeBeacons: [{ class: "e-stop-btn", type: "soft", keep: true }], + activeBeacons: [{ class: "e-stop-btn", type: "hard", keep: true }], url: undefined, dispatchActions: [ { type: Actions.CLOSE_POPUP, payload: undefined }, @@ -240,6 +240,7 @@ export const TOURS = ( content: TourContent.GRID_AND_ROW_PLANTING, beacons: undefined, activeBeacons: [ + { class: "plus-grid-btn", type: "soft", keep: true }, { class: "grid-and-row-planting", type: "soft" }, { class: "preview-button", type: "hard" }, { class: "save-button", type: "hard" }, diff --git a/frontend/nav/__tests__/nav_links_test.tsx b/frontend/nav/__tests__/nav_links_test.tsx index 6a8af8220..0cecf0af0 100644 --- a/frontend/nav/__tests__/nav_links_test.tsx +++ b/frontend/nav/__tests__/nav_links_test.tsx @@ -4,13 +4,6 @@ jest.mock("../../history", () => ({ getPathArray: jest.fn(() => mockPath.split("/")), })); -let mockHasSensors = false; -jest.mock("../../settings/firmware/firmware_hardware_support", () => ({ - hasSensors: () => mockHasSensors, - getFwHardwareValue: jest.fn(), - isExpress: jest.fn(), -})); - import { fakeState } from "../../__test_support__/fake_state"; const mockState = fakeState(); jest.mock("../../redux/store", () => ({ store: { getState: () => mockState } })); @@ -21,7 +14,7 @@ import { NavLinks } from "../nav_links"; import { NavLinksProps } from "../interfaces"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { - fakeFarmwareInstallation, + fakeFarmwareInstallation, fakeWebAppConfig, } from "../../__test_support__/fake_state/resources"; import { fakeHelpState } from "../../__test_support__/fake_designer_state"; @@ -60,16 +53,26 @@ describe("", () => { mockPath = Path.mock(Path.plants()); const wrapper = shallow(); expect(wrapper.find("Link").at(1).hasClass("active")).toBeTruthy(); - expect(wrapper.html().toLowerCase()).not.toContain("sensors"); }); it("shows sensors link", () => { - mockHasSensors = true; + const config = fakeWebAppConfig(); + config.body.hide_sensors = false; + mockState.resources = buildResourceIndex([config]); const p = fakeProps(); const wrapper = shallow(); expect(wrapper.html().toLowerCase()).toContain("sensors"); }); + it("doesn't show sensors link", () => { + const config = fakeWebAppConfig(); + config.body.hide_sensors = true; + mockState.resources = buildResourceIndex([config]); + const p = fakeProps(); + const wrapper = shallow(); + expect(wrapper.html().toLowerCase()).not.toContain("sensors"); + }); + it("doesn't show farmware link", () => { const farmware = fakeFarmwareInstallation(); farmware.body.package = "included"; diff --git a/frontend/plants/crop_info.tsx b/frontend/plants/crop_info.tsx index af975467f..ec7eaf6a6 100644 --- a/frontend/plants/crop_info.tsx +++ b/frontend/plants/crop_info.tsx @@ -394,7 +394,7 @@ export class RawCropInfo extends React.Component { svgIcon={svgToUrl(result.crop.svg_icon)} /> + target={} content={
diff --git a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx index da8e388e0..c558cd32e 100644 --- a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx +++ b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx @@ -96,7 +96,7 @@ import { execSequence } from "../../devices/actions"; import { clickButton } from "../../__test_support__/helpers"; import { fakeVariableNameSet } from "../../__test_support__/fake_variables"; import { DropAreaProps } from "../../draggable/interfaces"; -import { Actions, DeviceSetting } from "../../constants"; +import { Actions, Content, DeviceSetting } from "../../constants"; import { setWebAppConfigValue } from "../../config_storage/actions"; import { BooleanSetting } from "../../session_keys"; import { push } from "../../history"; @@ -448,6 +448,26 @@ describe("", () => { expect(wrapper.state().view).toEqual("public"); }); + it("shows warning", () => { + mockPath = Path.mock(Path.sequences("1")); + const p = fakeProps(); + p.sequence.body.sequence_version_id = 1; + p.sequence.body.body = [{ kind: "lua", args: { lua: "" } }]; + maybeTagStep(p.sequence.body.body[0]); + const wrapper = mount(); + expect(wrapper.text()).toContain(Content.INCLUDES_LUA_WARNING); + }); + + it("doesn't show warning", () => { + mockPath = Path.mock(Path.sequences("1")); + const p = fakeProps(); + p.sequence.body.sequence_version_id = 1; + p.sequence.body.body = [{ kind: "sync", args: {} }]; + maybeTagStep(p.sequence.body.body[0]); + const wrapper = mount(); + expect(wrapper.text()).not.toContain(Content.INCLUDES_LUA_WARNING); + }); + it("edits description", () => { mockPath = Path.mock(Path.sequences("1")); const p = fakeProps(); diff --git a/frontend/sequences/panel/__tests__/preview_test.tsx b/frontend/sequences/panel/__tests__/preview_test.tsx index 455bc9cff..d5eb45088 100644 --- a/frontend/sequences/panel/__tests__/preview_test.tsx +++ b/frontend/sequences/panel/__tests__/preview_test.tsx @@ -25,6 +25,7 @@ import { push } from "../../../history"; import { installSequence } from "../../actions"; import { Path } from "../../../internal_urls"; import { emptyState } from "../../../resources/reducer"; +import { Content } from "../../../constants"; describe("", () => { API.setBaseUrl(""); @@ -78,6 +79,22 @@ describe("", () => { expect(wrapper.text().toLowerCase()).not.toContain("error"); }); + it("shows warning", async () => { + const sequence = fakeSequence(); + sequence.body.body = [{ kind: "lua", args: { lua: "" } }]; + mockGet = Promise.resolve({ data: sequence.body }); + const wrapper = await mount(); + expect(wrapper.text()).toContain(Content.INCLUDES_LUA_WARNING); + }); + + it("doesn't show warning", async () => { + const sequence = fakeSequence(); + sequence.body.body = [{ kind: "sync", args: {} }]; + mockGet = Promise.resolve({ data: sequence.body }); + const wrapper = await mount(); + expect(wrapper.text()).not.toContain(Content.INCLUDES_LUA_WARNING); + }); + it("errors while loading sequence", async () => { mockGet = Promise.reject("Error"); const wrapper = await mount(); diff --git a/frontend/sequences/panel/preview_support.tsx b/frontend/sequences/panel/preview_support.tsx index a6ce8dae2..4d61768c9 100644 --- a/frontend/sequences/panel/preview_support.tsx +++ b/frontend/sequences/panel/preview_support.tsx @@ -146,6 +146,7 @@ interface ImportBannerProps { export const ImportBanner = (props: ImportBannerProps) => { const [importing, setImporting] = React.useState(false); const { sequence } = props; + const includesLua = sequence?.body.body?.map(x => x.kind).includes("lua"); return
@@ -159,6 +160,7 @@ export const ImportBanner = (props: ImportBannerProps) => { {importing ? t("importing") : t("import")} {importing && } } + {includesLua &&

{t(Content.INCLUDES_LUA_WARNING)}

}
; }; diff --git a/frontend/sequences/sequence_editor_middle_active.tsx b/frontend/sequences/sequence_editor_middle_active.tsx index 83786def8..c8bcfb0dd 100644 --- a/frontend/sequences/sequence_editor_middle_active.tsx +++ b/frontend/sequences/sequence_editor_middle_active.tsx @@ -752,6 +752,7 @@ export const ImportedBanner = (props: ImportedBannerProps) => { {currentVersionItem?.label}

; + const includesLua = props.sequence.body.body?.map(x => x.kind).includes("lua"); return versionId ?
@@ -762,6 +763,7 @@ export const ImportedBanner = (props: ImportedBannerProps) => { onClick={upgradeSequence(props.sequence.body.id, latestId)}> {revertAvailable ? t("revert changes") : t("upgrade to latest")} } + {includesLua &&

{t(Content.INCLUDES_LUA_WARNING)}

}
= { disable_i18n: false, display_trail: true, dynamic_map: false, - ["enable_3d_electronics_box_top" as Key]: true, + enable_3d_electronics_box_top: true, encoder_figure: false, go_button_axes: "XY", hide_webcam_widget: false, diff --git a/frontend/settings/pin_bindings/__tests__/model_test.tsx b/frontend/settings/pin_bindings/__tests__/model_test.tsx index a0fb7657e..630fae644 100644 --- a/frontend/settings/pin_bindings/__tests__/model_test.tsx +++ b/frontend/settings/pin_bindings/__tests__/model_test.tsx @@ -180,6 +180,22 @@ describe("", () => { expect(e.object.parent?.children[0].position.z).toEqual(131); }); + it("changes cursor: bound", () => { + const wrapper = mount(); + expect(document.body.style.cursor).toEqual("default"); + wrapper.find({ name: "action-group" }).first().simulate("pointermove"); + expect(document.body.style.cursor).toEqual("pointer"); + document.body.style.cursor = "default"; + }); + + it("changes cursor: unbound", () => { + const wrapper = mount(); + expect(document.body.style.cursor).toEqual("default"); + wrapper.find({ name: "action-group" }).last().simulate("pointermove"); + expect(document.body.style.cursor).toEqual("not-allowed"); + document.body.style.cursor = "default"; + }); + it("renders: off", () => { const p = fakeProps(); p.isEditing = true; diff --git a/frontend/settings/pin_bindings/model.tsx b/frontend/settings/pin_bindings/model.tsx index f8b4724c5..e38a40b14 100644 --- a/frontend/settings/pin_bindings/model.tsx +++ b/frontend/settings/pin_bindings/model.tsx @@ -2,7 +2,7 @@ /* eslint-disable no-null/no-null */ import React, { useRef } from "react"; import { - Cylinder, Html, PerspectiveCamera, useCursor, useGLTF, + Cylinder, Html, PerspectiveCamera, useGLTF, } from "@react-three/drei"; import { Canvas, ThreeEvent, useFrame } from "@react-three/fiber"; import { GLTF } from "three-stdlib"; @@ -10,7 +10,7 @@ import { BindingTargetDropdown, pinBindingLabel } from "./pin_binding_input_grou import { BoxTopBaseProps, PinBindingListItems } from "./interfaces"; import { setPinBinding, findBinding, triggerBinding } from "./actions"; import { BufferGeometry } from "three"; -import { debounce, isUndefined, some } from "lodash"; +import { debounce, some } from "lodash"; import { t } from "../../i18next_wrapper"; import { isExpress } from "../../settings/firmware/firmware_hardware_support"; import { ButtonPin } from "./list_and_label_support"; @@ -141,7 +141,7 @@ export const Model = (props: BoxTopBaseProps) => { const BUTTONS: ButtonOrLedItem[] = [ { - label: t("E-Stop"), + label: t("Button 1"), pinNumber: ButtonPin.estop, on: props.botOnline && !locked, position: -60, @@ -152,7 +152,7 @@ export const Model = (props: BoxTopBaseProps) => { ref: estop, }, { - label: t("Unlock"), + label: t("Button 2"), pinNumber: ButtonPin.unlock, blink: props.botOnline && locked, position: -30, @@ -259,10 +259,10 @@ export const Model = (props: BoxTopBaseProps) => { }; const [hovered, setHovered] = React.useState(); - useCursor(!isUndefined(hovered)); const leave = (e: ThreeEvent) => { setHovered(undefined); setZForAllInGroup(e, Z); + document.body.style.cursor = "default"; }; return @@ -296,7 +296,12 @@ export const Model = (props: BoxTopBaseProps) => { const binding = findPinBinding(pinNumber); const isHovered = hovered == pinNumber; const click = debounce(clickBinding(pinNumber)); - const enter = () => !props.isEditing && setHovered(pinNumber); + const setCursor = () => + document.body.style.cursor = binding ? "pointer" : "not-allowed"; + const enter = () => { + !props.isEditing && setHovered(pinNumber); + setCursor(); + }; return { material-color={0xcccccc} /> { if (!props.isEditing) { @@ -321,7 +328,7 @@ export const Model = (props: BoxTopBaseProps) => { position={[-30, btnPosition, Z]} rotation={[Math.PI / 2, 0, 0]} /> @@ -339,7 +346,11 @@ export const Model = (props: BoxTopBaseProps) => { resources={props.resources} sequenceIdInput={binding?.sequence_id} specialActionInput={binding?.special_action} /> - :

+ :

{getLabel(binding) || label}

} diff --git a/package.json b/package.json index a338b7cc5..629a25b1a 100644 --- a/package.json +++ b/package.json @@ -35,30 +35,30 @@ "@monaco-editor/react": "4.6.0", "@parcel/transformer-sass": "2.11.0", "@parcel/transformer-typescript-tsc": "2.11.0", - "@react-three/drei": "9.96.0", - "@react-three/fiber": "8.15.14", + "@react-three/drei": "9.96.3", + "@react-three/fiber": "8.15.15", "@types/lodash": "4.14.202", "@types/markdown-it": "13.0.7", - "@types/node": "20.11.5", + "@types/node": "20.11.6", "@types/promise-timeout": "1.3.3", "@types/react": "18.2.48", "@types/react-color": "3.0.11", "@types/react-dom": "18.2.18", "@types/three": "0.160.0", "@types/ws": "8.5.10", - "axios": "1.6.5", + "axios": "1.6.7", "bowser": "2.11.0", "browser-speech": "1.1.1", "events": "3.3.0", - "farmbot": "15.8.5", - "i18next": "23.7.16", + "farmbot": "15.8.7", + "i18next": "23.7.19", "lodash": "4.17.21", "markdown-it": "14.0.0", "markdown-it-emoji": "3.0.0", "moment": "2.30.1", "monaco-editor": "0.45.0", "mqtt": "5.1.4", - "npm": "10.3.0", + "npm": "10.4.0", "parcel": "2.11.0", "process": "0.11.10", "promise-timeout": "1.3.0", @@ -72,7 +72,7 @@ "redux-immutable-state-invariant": "2.1.0", "redux-thunk": "3.1.0", "takeme": "0.12.0", - "three": "0.160.0", + "three": "0.160.1", "typescript": "5.3.3", "url": "0.11.3", "xterm": "5.3.0" @@ -81,8 +81,8 @@ "@types/enzyme": "3.10.12", "@types/jest": "29.5.11", "@types/readable-stream": "4.0.10", - "@typescript-eslint/eslint-plugin": "6.19.0", - "@typescript-eslint/parser": "6.19.0", + "@typescript-eslint/eslint-plugin": "6.19.1", + "@typescript-eslint/parser": "6.19.1", "@wojtekmaj/enzyme-adapter-react-17": "0.8.0", "enzyme": "3.11.0", "eslint": "8.56.0", @@ -106,7 +106,7 @@ "react-test-renderer": "18.2.0", "sass": "1.70.0", "sass-lint": "1.13.1", - "ts-jest": "29.1.1", + "ts-jest": "29.1.2", "tslint": "6.1.3" } } diff --git a/spec/mutations/sequences/publish_spec.rb b/spec/mutations/sequences/publish_spec.rb index 8b69a8fee..23e6fb7a1 100644 --- a/spec/mutations/sequences/publish_spec.rb +++ b/spec/mutations/sequences/publish_spec.rb @@ -51,9 +51,9 @@ it "disallows denied nodes and args" do bad = FakeSequence.with_parameters(device: device, body: [ { - kind: "lua", + kind: "factory_reset", args: { - lua: "os.cmd('cat /etc/password')", + package: "farmbot_os" }, }, ]) @@ -61,7 +61,7 @@ device: device, copyright: "FarmBot, Inc. 2021") expected = "For security reasons, we can't publish sequences " \ - "that contain the following content: lua" + "that contain the following content: factory_reset" actual = problems.errors["sequence"].message expect(actual).to eq(expected) end diff --git a/ubuntu_example.sh b/ubuntu_example.sh index d39251db6..bd160fb1e 100644 --- a/ubuntu_example.sh +++ b/ubuntu_example.sh @@ -19,7 +19,7 @@ sudo apt remove docker docker.io containerd runc # Install docker and docker compose sudo apt update sudo apt install ca-certificates curl gnupg -y -. /etc/os-release +source /etc/os-release sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/$ID/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg