From dfb572cd377b2b33f1587dcb3899c9421d747eaa Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Mon, 15 Apr 2024 20:53:33 -0400 Subject: [PATCH] feat(app): factory mode desktop toggle (#14911) adds the desktop advanced setting toggle to enable factory mode. closes PLAT-280, PLAT-282 --- app/src/assets/localization/en/anonymous.json | 1 + app/src/assets/localization/en/branded.json | 1 + .../localization/en/device_settings.json | 9 + app/src/atoms/Slideout/MultiSlideout.tsx | 13 +- .../FactoryModeSlideout.tsx | 168 ++++++++++++++++++ .../RobotSettings/AdvancedTab/FactoryMode.tsx | 50 ++++++ .../RobotSettings/AdvancedTab/index.ts | 1 + .../RobotSettings/RobotSettingsAdvanced.tsx | 22 +++ 8 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx create mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index 2bb4f67a4d7..5dcfd9bf237 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -33,6 +33,7 @@ "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your pipette.", "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact support.", "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your robot.", + "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", "opentrons_app_successfully_updated": "The app was successfully updated.", "opentrons_app_update": "app update", "opentrons_app_update_available": "App Update Available", diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index 6143400d541..13b53967aff 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -33,6 +33,7 @@ "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your Opentrons Flex.", + "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", "opentrons_app_update": "Opentrons App update", "opentrons_app_update_available": "Opentrons App Update Available", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index c6bd00ad70d..3aec18d24a6 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -50,6 +50,7 @@ "clear_option_runs_history_subtext": "Clears information about past runs of all protocols.", "clear_option_tip_length_calibrations": "Clear tip length calibrations", "cancel_software_update": "Cancel software update", + "complete_and_restart_robot": "Complete and restart robot", "confirm_device_reset_description": "This will permanently delete all protocol, calibration, and other data. You’ll have to redo initial setup before using the robot again.", "confirm_device_reset_heading": "Are you sure you want to reset your device?", "connect": "Connect", @@ -107,6 +108,7 @@ "enable_status_light": "Enable status light", "enable_status_light_description": "Turn on or off the strip of color LEDs on the front of the robot.", "engaged": "Engaged", + "enter_factory_password": "Enter factory password", "enter_network_name": "Enter network name", "enter_password": "Enter password", "estop": "E-stop", @@ -118,6 +120,7 @@ "ethernet": "Ethernet", "ethernet_connection_description": "Connect an Ethernet cable to the back of the robot and a network switch or hub.", "exit": "exit", + "factory_mode": "Factory Mode", "factory_reset": "Factory Reset", "factory_reset_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", "factory_reset_modal_description": "This data cannot be retrieved later.", @@ -140,6 +143,7 @@ "install_e_stop": "Install the E-stop", "installing_software": "Installing software...", "installing_update": "Installing update...", + "invalid_password": "Invalid password", "ip_address": "IP Address", "join_other_network": "Join other network", "join_other_network_error_message": "Must be 2–32 characters long", @@ -151,6 +155,7 @@ "launch_jupyter_notebook": "Launch Jupyter Notebook", "legacy_settings": "Legacy Settings", "mac_address": "MAC Address", + "manage_oem_settings": "Manage OEM settings", "minutes": "{{minute}} minutes", "missing_calibration": "Missing calibration", "model_and_serial": "Pipette Model and Serial", @@ -186,7 +191,10 @@ "not_connected_via_wifi": "Not connected via Wi-Fi", "not_connected_via_wired_usb": "Not connected via wired USB", "not_now": "Not now", + "oem_mode": "OEM Mode", + "off": "Off", "one_hour": "1 hour", + "on": "On", "other_networks": "Other Networks", "password": "Password", "password_error_message": "Must be at least 8 characters", @@ -252,6 +260,7 @@ "select_authentication_method": "Select authentication method for your selected network.", "sending_software": "Sending software...", "serial": "Serial", + "setup_mode": "Setup mode", "short_trash_bin": "Short trash bin", "short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)", "show": "Show", diff --git a/app/src/atoms/Slideout/MultiSlideout.tsx b/app/src/atoms/Slideout/MultiSlideout.tsx index 71ce02f6de6..73054a10a45 100644 --- a/app/src/atoms/Slideout/MultiSlideout.tsx +++ b/app/src/atoms/Slideout/MultiSlideout.tsx @@ -1,16 +1,9 @@ import * as React from 'react' import { Slideout } from './index' -interface MultiSlideoutProps { - title: string | React.ReactElement - children: React.ReactNode - onCloseClick: () => void - currentStep: number - maxSteps: number - // isExpanded is for collapse and expand animation - isExpanded?: boolean - footer?: React.ReactNode -} +import type { MultiSlideoutSpecs, SlideoutProps } from './index' + +type MultiSlideoutProps = SlideoutProps & MultiSlideoutSpecs export const MultiSlideout = (props: MultiSlideoutProps): JSX.Element => { const { diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx new file mode 100644 index 00000000000..d034e713373 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx @@ -0,0 +1,168 @@ +import * as React from 'react' +import { useDispatch } from 'react-redux' +import { useForm, Controller } from 'react-hook-form' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + Flex, + PrimaryButton, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { useRobotSettingsQuery } from '@opentrons/react-api-client' + +import { ToggleButton } from '../../../../../atoms/buttons' +import { InputField } from '../../../../../atoms/InputField' +import { MultiSlideout } from '../../../../../atoms/Slideout/MultiSlideout' +import { restartRobot } from '../../../../../redux/robot-admin' +import { updateSetting } from '../../../../../redux/robot-settings' + +import type { RobotSettingsField } from '@opentrons/api-client' +import type { Dispatch } from '../../../../../redux/types' + +interface FactoryModeSlideoutProps { + isExpanded: boolean + onCloseClick: () => void + robotName: string +} + +interface FormValues { + passwordInput: string +} + +export function FactoryModeSlideout({ + isExpanded, + onCloseClick, + robotName, +}: FactoryModeSlideoutProps): JSX.Element { + const { t } = useTranslation(['device_settings', 'shared', 'branded']) + + const dispatch = useDispatch() + + const { settings } = useRobotSettingsQuery().data ?? {} + const oemModeSetting = (settings ?? []).find( + (setting: RobotSettingsField) => setting?.id === 'enableOEMMode' + ) + const isOEMMode = oemModeSetting?.value ?? null + + const [currentStep, setCurrentStep] = React.useState(1) + const [toggleValue, setToggleValue] = React.useState(false) + + const { + handleSubmit, + control, + formState: { errors }, + trigger, + } = useForm({ + defaultValues: { + passwordInput: '', + }, + }) + const onSubmit = (data: FormValues): void => { + setCurrentStep(2) + } + + const handleSubmitFactoryPassword = (): void => { + // TODO: validation and errors: PLAT-281 + void handleSubmit(onSubmit)() + } + + const handleToggleClick: React.MouseEventHandler = () => { + setToggleValue(toggleValue => !toggleValue) + } + + const handleCompleteClick: React.MouseEventHandler = () => { + dispatch(updateSetting(robotName, 'enableOEMMode', toggleValue)) + dispatch(restartRobot(robotName)) + onCloseClick() + } + + React.useEffect(() => { + // initialize local state to OEM mode value + if (isOEMMode != null) { + setToggleValue(isOEMMode) + } + }, [isOEMMode]) + + return ( + + {currentStep === 1 ? ( + + {t('shared:next')} + + ) : null} + {currentStep === 2 ? ( + + {t('complete_and_restart_robot')} + + ) : null} + + } + > + {currentStep === 1 ? ( + + ( + ) => { + field.onChange(e) + trigger('passwordInput') + }} + value={field.value} + error={fieldState.error?.message && ' '} + onBlur={field.onBlur} + title={t('enter_factory_password')} + /> + )} + /> + {errors.passwordInput != null ? ( + + {errors.passwordInput.message} + + ) : null} + + ) : null} + {currentStep === 2 ? ( + + + {t('oem_mode')} + + + + + {toggleValue ? t('on') : t('off')} + + + {t('branded:oem_mode_description')} + + ) : null} + + ) +} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx new file mode 100644 index 00000000000..8d2fda7c386 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + Box, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING_AUTO, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { TertiaryButton } from '../../../../atoms/buttons' + +interface FactoryModeProps { + isRobotBusy: boolean + setShowFactoryModeSlideout: React.Dispatch> +} + +export function FactoryMode({ + isRobotBusy, + setShowFactoryModeSlideout, +}: FactoryModeProps): JSX.Element { + const { t } = useTranslation('device_settings') + + return ( + + + + {t('factory_mode')} + + + { + setShowFactoryModeSlideout(true) + }} + > + {t('setup_mode')} + + + ) +} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts index 86e45ab1f73..b53134df945 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts @@ -1,6 +1,7 @@ export * from './DeviceReset' export * from './DisplayRobotName' export * from './EnableStatusLight' +export * from './FactoryMode' export * from './GantryHoming' export * from './LegacySettings' export * from './OpenJupyterControl' diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx index 8772f9a383a..be9cdcd2be4 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -19,6 +19,7 @@ import { DeviceReset, DisplayRobotName, EnableStatusLight, + FactoryMode, GantryHoming, LegacySettings, OpenJupyterControl, @@ -39,6 +40,7 @@ import { import { RenameRobotSlideout } from './AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout' import { DeviceResetSlideout } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout' import { DeviceResetModal } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetModal' +import { FactoryModeSlideout } from './AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout' import { handleUpdateBuildroot } from './UpdateBuildroot' import { UNREACHABLE } from '../../../redux/discovery' import { getTopPortalEl } from '../../../App/portal' @@ -72,6 +74,10 @@ export function RobotSettingsAdvanced({ showDeviceResetModal, setShowDeviceResetModal, ] = React.useState(false) + const [ + showFactoryModeSlideout, + setShowFactoryModeSlideout, + ] = React.useState(false) const isRobotBusy = useIsRobotBusy({ poll: true }) const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) @@ -131,6 +137,13 @@ export function RobotSettingsAdvanced({ robotName={robotName} /> )} + {showFactoryModeSlideout && ( + setShowFactoryModeSlideout(false)} + robotName={robotName} + /> + )} {showDeviceResetSlideout && ( handleUpdateBuildroot(robot)} /> + {isFlex ? ( + <> + + + + ) : null}