Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app): factory mode desktop toggle #14911

Merged
merged 9 commits into from
Apr 16, 2024
1 change: 1 addition & 0 deletions app/src/assets/localization/en/anonymous.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"module_calibration_get_started": "<block>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.</block><block>The calibration adapter came with your module. The pipette probe came with your pipette.</block>",
"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",
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/localization/en/branded.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"module_calibration_get_started": "<block>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.</block><block>The calibration adapter came with your module. The pipette probe came with your Flex pipette.</block>",
"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",
Expand Down
9 changes: 9 additions & 0 deletions app/src/assets/localization/en/device_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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.",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 3 additions & 10 deletions app/src/atoms/Slideout/MultiSlideout.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Dispatch>()

const { settings } = useRobotSettingsQuery().data ?? {}
const oemModeSetting = (settings ?? []).find(
(setting: RobotSettingsField) => setting?.id === 'enableOEMMode'
)
const isOEMMode = oemModeSetting?.value ?? null

const [currentStep, setCurrentStep] = React.useState<number>(1)
const [toggleValue, setToggleValue] = React.useState<boolean>(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<Element> = () => {
setToggleValue(toggleValue => !toggleValue)
}

const handleCompleteClick: React.MouseEventHandler<Element> = () => {
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 (
<MultiSlideout
title={currentStep === 1 ? t('enter_password') : t('manage_oem_settings')}
maxSteps={2}
currentStep={currentStep}
onCloseClick={onCloseClick}
isExpanded={isExpanded}
footer={
<>
{currentStep === 1 ? (
<PrimaryButton onClick={handleSubmitFactoryPassword} width="100%">
{t('shared:next')}
</PrimaryButton>
) : null}
{currentStep === 2 ? (
<PrimaryButton onClick={handleCompleteClick} width="100%">
{t('complete_and_restart_robot')}
</PrimaryButton>
) : null}
</>
}
>
{currentStep === 1 ? (
<Flex flexDirection={DIRECTION_COLUMN}>
<Controller
control={control}
name="passwordInput"
render={({ field, fieldState }) => (
<InputField
id="passwordInput"
name="passwordInput"
type="text"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
field.onChange(e)
trigger('passwordInput')
}}
value={field.value}
error={fieldState.error?.message && ' '}
onBlur={field.onBlur}
title={t('enter_factory_password')}
/>
)}
/>
{errors.passwordInput != null ? (
<StyledText
as="label"
color={COLORS.red50}
marginTop={SPACING.spacing4}
>
{errors.passwordInput.message}
</StyledText>
) : null}
</Flex>
) : null}
{currentStep === 2 ? (
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText
css={TYPOGRAPHY.pSemiBold}
paddingBottom={SPACING.spacing4}
>
{t('oem_mode')}
</StyledText>
<Flex alignItems={ALIGN_CENTER} gridGap={SPACING.spacing6}>
<ToggleButton
label="oem_mode_toggle"
toggledOn={toggleValue}
onClick={handleToggleClick}
/>
<StyledText as="p" marginBottom={SPACING.spacing4}>
{toggleValue ? t('on') : t('off')}
</StyledText>
</Flex>
<StyledText as="p">{t('branded:oem_mode_description')}</StyledText>
</Flex>
) : null}
</MultiSlideout>
)
}
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<boolean>>
}

export function FactoryMode({
isRobotBusy,
setShowFactoryModeSlideout,
}: FactoryModeProps): JSX.Element {
const { t } = useTranslation('device_settings')

return (
<Flex
alignItems={ALIGN_CENTER}
justifyContent={JUSTIFY_SPACE_BETWEEN}
marginTop={SPACING.spacing24}
>
<Box width="70%">
<StyledText as="p" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
{t('factory_mode')}
</StyledText>
</Box>
<TertiaryButton
disabled={isRobotBusy}
marginLeft={SPACING_AUTO}
onClick={() => {
setShowFactoryModeSlideout(true)
}}
>
{t('setup_mode')}
</TertiaryButton>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DeviceReset,
DisplayRobotName,
EnableStatusLight,
FactoryMode,
GantryHoming,
LegacySettings,
OpenJupyterControl,
Expand All @@ -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'
Expand Down Expand Up @@ -72,6 +74,10 @@ export function RobotSettingsAdvanced({
showDeviceResetModal,
setShowDeviceResetModal,
] = React.useState<boolean>(false)
const [
showFactoryModeSlideout,
setShowFactoryModeSlideout,
] = React.useState<boolean>(false)

const isRobotBusy = useIsRobotBusy({ poll: true })
const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName)
Expand Down Expand Up @@ -131,6 +137,13 @@ export function RobotSettingsAdvanced({
robotName={robotName}
/>
)}
{showFactoryModeSlideout && (
<FactoryModeSlideout
isExpanded={showFactoryModeSlideout}
onCloseClick={() => setShowFactoryModeSlideout(false)}
robotName={robotName}
/>
)}
{showDeviceResetSlideout && (
<DeviceResetSlideout
isExpanded={showDeviceResetSlideout}
Expand Down Expand Up @@ -195,6 +208,15 @@ export function RobotSettingsAdvanced({
isRobotBusy={isRobotBusy || isEstopNotDisengaged}
onUpdateStart={() => handleUpdateBuildroot(robot)}
/>
{isFlex ? (
<>
<Divider marginY={SPACING.spacing16} />
<FactoryMode
isRobotBusy={isRobotBusy || isEstopNotDisengaged}
setShowFactoryModeSlideout={setShowFactoryModeSlideout}
/>
</>
) : null}
<Troubleshooting
robotName={robotName}
isEstopNotDisengaged={isEstopNotDisengaged}
Expand Down
Loading