Skip to content

Commit

Permalink
feat(app): update use is robot busy for estop (#13359)
Browse files Browse the repository at this point in the history
* feat(app): update useIsRobotBusy hook for estop and fix banner rendering issue on OT-2
  • Loading branch information
koji authored Aug 22, 2023
1 parent 2d9a16b commit 64d64fc
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 18 deletions.
6 changes: 5 additions & 1 deletion app/src/App/DesktopApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { Navbar } from './Navbar'
import { EstopTakeover, EmergencyStopContext } from '../organisms/EmergencyStop'
import { OPENTRONS_USB } from '../redux/discovery'
import { appShellRequestor } from '../redux/shell/remote'
import { useRobot } from '../organisms/Devices/hooks'
import { useRobot, useIsOT3 } from '../organisms/Devices/hooks'
import { PortalRoot as ModalPortalRoot } from './portal'

import type { RouteProps, DesktopRouteParams } from './types'
Expand Down Expand Up @@ -141,6 +141,10 @@ function RobotControlTakeover(): JSX.Element | null {
const params = deviceRouteMatch?.params as DesktopRouteParams
const robotName = params?.robotName
const robot = useRobot(robotName)
const isOT3 = useIsOT3(robotName)

// E-stop is not supported on OT2
if (!isOT3) return null

if (deviceRouteMatch == null || robot == null || robotName == null)
return null
Expand Down
3 changes: 3 additions & 0 deletions app/src/App/__tests__/DesktopApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ProtocolRunDetails } from '../../pages/Devices/ProtocolRunDetails'
import { RobotSettings } from '../../pages/Devices/RobotSettings'
import { GeneralSettings } from '../../pages/AppSettings/GeneralSettings'
import { Alerts } from '../../organisms/Alerts'
import { useIsOT3 } from '../../organisms/Devices/hooks'
import { useSoftwareUpdatePoll } from '../hooks'
import { DesktopApp } from '../DesktopApp'

Expand Down Expand Up @@ -55,6 +56,7 @@ const mockBreadcrumbs = Breadcrumbs as jest.MockedFunction<typeof Breadcrumbs>
const mockUseSoftwareUpdatePoll = useSoftwareUpdatePoll as jest.MockedFunction<
typeof useSoftwareUpdatePoll
>
const mockUseIsOT3 = useIsOT3 as jest.MockedFunction<typeof useIsOT3>

const render = (path = '/') => {
return renderWithProviders(
Expand All @@ -78,6 +80,7 @@ describe('DesktopApp', () => {
mockAlerts.mockReturnValue(<div>Mock Alerts</div>)
mockAppSettings.mockReturnValue(<div>Mock AppSettings</div>)
mockBreadcrumbs.mockReturnValue(<div>Mock Breadcrumbs</div>)
mockUseIsOT3.mockReturnValue(true)
})
afterEach(() => {
jest.resetAllMocks()
Expand Down
9 changes: 7 additions & 2 deletions app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
useIsRobotViewable,
useTrackProtocolRunEvent,
useRobotAnalyticsData,
useIsOT3,
} from '../hooks'
import { formatTimestamp } from '../utils'
import { RunTimer } from './RunTimer'
Expand Down Expand Up @@ -142,15 +143,17 @@ export function ProtocolRunHeader({
runRecord?.data?.errors != null
? getHighestPriorityError(runRecord?.data?.errors)
: undefined
const { data: estopStatus } = useEstopQuery({
const { data: estopStatus, error: estopError } = useEstopQuery({
refetchInterval: ESTOP_POLL_MS,
})
const [
showEmergencyStopRunBanner,
setShowEmergencyStopRunBanner,
] = React.useState<boolean>(false)
const isOT3 = useIsOT3(robotName)

React.useEffect(() => {
if (estopStatus?.data.status !== DISENGAGED) {
if (estopStatus?.data.status !== DISENGAGED && estopError == null) {
setShowEmergencyStopRunBanner(true)
}
}, [estopStatus?.data.status])
Expand Down Expand Up @@ -288,6 +291,8 @@ export function ProtocolRunHeader({
/>
) : null}
{estopStatus?.data.status !== DISENGAGED &&
estopError == null &&
isOT3 &&
showEmergencyStopRunBanner ? (
<EmergencyStopRunBanner
setShowEmergencyStopRunBanner={setShowEmergencyStopRunBanner}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
useRunCreatedAtTimestamp,
useUnmatchedModulesForProtocol,
useIsRobotViewable,
useIsOT3,
} from '../../hooks'
import { useIsHeaterShakerInProtocol } from '../../../ModuleCard/hooks'
import { ConfirmAttachmentModal } from '../../../ModuleCard/ConfirmAttachmentModal'
Expand Down Expand Up @@ -192,6 +193,7 @@ const mockRunFailedModal = RunFailedModal as jest.MockedFunction<
const mockUseEstopQuery = useEstopQuery as jest.MockedFunction<
typeof useEstopQuery
>
const mockUseIsOT3 = useIsOT3 as jest.MockedFunction<typeof useIsOT3>

const ROBOT_NAME = 'otie'
const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b'
Expand Down Expand Up @@ -345,6 +347,7 @@ describe('ProtocolRunHeader', () => {
when(mockUseRunCalibrationStatus)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({ complete: true })
mockUseIsOT3.mockReturnValue(true)
mockRunFailedModal.mockReturnValue(<div>mock RunFailedModal</div>)
mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any)
})
Expand Down
21 changes: 13 additions & 8 deletions app/src/organisms/Devices/RobotOverflowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ChooseProtocolSlideout } from '../ChooseProtocolSlideout'
import { useCurrentRunId } from '../ProtocolUpload/hooks'
import { ConnectionTroubleshootingModal } from './ConnectionTroubleshootingModal'
import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks'
import { useIsRobotBusy } from './hooks'

import type { StyleProps } from '@opentrons/components'
import type { DiscoveredRobot } from '../../redux/discovery/types'
Expand Down Expand Up @@ -61,6 +62,8 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element {
const isRobotOnWrongVersionOfSoftware =
autoUpdateAction === 'upgrade' || autoUpdateAction === 'downgrade'

const isRobotBusy = useIsRobotBusy({ poll: true })

const handleClickRun: React.MouseEventHandler<HTMLButtonElement> = e => {
e.preventDefault()
e.stopPropagation()
Expand All @@ -78,14 +81,16 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element {
if (robot.status === CONNECTABLE && runId == null) {
menuItems = (
<>
<MenuItem
{...targetProps}
onClick={handleClickRun}
disabled={isRobotOnWrongVersionOfSoftware}
data-testid={`RobotOverflowMenu_${robot.name}_runProtocol`}
>
{t('run_a_protocol')}
</MenuItem>
{!isRobotBusy ? (
<MenuItem
{...targetProps}
onClick={handleClickRun}
disabled={isRobotOnWrongVersionOfSoftware}
data-testid={`RobotOverflowMenu_${robot.name}_runProtocol`}
>
{t('run_a_protocol')}
</MenuItem>
) : null}
{isRobotOnWrongVersionOfSoftware && (
<Tooltip tooltipProps={tooltipProps} whiteSpace="normal">
{t('shared:a_software_update_is_available')}
Expand Down
22 changes: 22 additions & 0 deletions app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ChooseProtocolSlideout } from '../../ChooseProtocolSlideout'
import { ConnectionTroubleshootingModal } from '../ConnectionTroubleshootingModal'
import { RobotOverflowMenu } from '../RobotOverflowMenu'
import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update'
import { useIsRobotBusy } from '../hooks'

import {
mockUnreachableRobot,
Expand All @@ -19,6 +20,7 @@ jest.mock('../../../redux/robot-update/selectors')
jest.mock('../../ProtocolUpload/hooks')
jest.mock('../../ChooseProtocolSlideout')
jest.mock('../ConnectionTroubleshootingModal')
jest.mock('../hooks')

const mockUseCurrentRunId = useCurrentRunId as jest.MockedFunction<
typeof useCurrentRunId
Expand All @@ -32,6 +34,9 @@ const mockConnectionTroubleshootingModal = ConnectionTroubleshootingModal as jes
const mockGetBuildrootUpdateDisplayInfo = getRobotUpdateDisplayInfo as jest.MockedFunction<
typeof getRobotUpdateDisplayInfo
>
const mockUseIsRobotBusy = useIsRobotBusy as jest.MockedFunction<
typeof useIsRobotBusy
>

const render = (props: React.ComponentProps<typeof RobotOverflowMenu>) => {
return renderWithProviders(
Expand Down Expand Up @@ -60,6 +65,7 @@ describe('RobotOverflowMenu', () => {
autoUpdateDisabledReason: null,
updateFromFileDisabledReason: null,
})
mockUseIsRobotBusy.mockReturnValue(false)
})
afterEach(() => {
jest.resetAllMocks()
Expand Down Expand Up @@ -103,4 +109,20 @@ describe('RobotOverflowMenu', () => {
const run = getByText('Run a protocol')
expect(run).toBeDisabled()
})

it('should only render robot settings when e-stop is pressed or disconnected', () => {
mockUseCurrentRunId.mockReturnValue(null)
mockGetBuildrootUpdateDisplayInfo.mockReturnValue({
autoUpdateAction: 'upgrade',
autoUpdateDisabledReason: null,
updateFromFileDisabledReason: null,
})

mockUseIsRobotBusy.mockReturnValue(true)
const { getByText, getByLabelText, queryByText } = render(props)
const btn = getByLabelText('RobotOverflowMenu_button')
fireEvent.click(btn)
expect(queryByText('Run a protocol')).not.toBeInTheDocument()
getByText('Robot settings')
})
})
73 changes: 73 additions & 0 deletions app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { UseQueryResult } from 'react-query'
import {
useAllSessionsQuery,
useAllRunsQuery,
useEstopQuery,
} from '@opentrons/react-api-client'
import {
DISENGAGED,
NOT_PRESENT,
PHYSICALLY_ENGAGED,
ENGAGED,
} from '../../../EmergencyStop'

import { useIsRobotBusy } from '../useIsRobotBusy'

Expand All @@ -11,12 +18,23 @@ import type { Sessions, Runs } from '@opentrons/api-client'
jest.mock('@opentrons/react-api-client')
jest.mock('../../../ProtocolUpload/hooks')

const mockEstopStatus = {
data: {
status: DISENGAGED,
leftEstopPhysicalStatus: DISENGAGED,
rightEstopPhysicalStatus: NOT_PRESENT,
},
}

const mockUseAllSessionsQuery = useAllSessionsQuery as jest.MockedFunction<
typeof useAllSessionsQuery
>
const mockUseAllRunsQuery = useAllRunsQuery as jest.MockedFunction<
typeof useAllRunsQuery
>
const mockUseEstopQuery = useEstopQuery as jest.MockedFunction<
typeof useEstopQuery
>

describe('useIsRobotBusy', () => {
beforeEach(() => {
Expand All @@ -30,6 +48,7 @@ describe('useIsRobotBusy', () => {
},
},
} as UseQueryResult<Runs, Error>)
mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any)
})

afterEach(() => {
Expand Down Expand Up @@ -70,6 +89,60 @@ describe('useIsRobotBusy', () => {
expect(result).toBe(false)
})

it('returns false when Estop status is disengaged', () => {
mockUseAllRunsQuery.mockReturnValue({
data: {
links: {
current: null,
},
},
} as any)
mockUseAllSessionsQuery.mockReturnValue(({
data: [
{
id: 'test',
createdAt: '2019-08-24T14:15:22Z',
details: {},
sessionType: 'calibrationCheck',
createParams: {},
},
],
links: {},
} as unknown) as UseQueryResult<Sessions, Error>)
const result = useIsRobotBusy()
expect(result).toBe(false)
})

it('returns true when Estop status is not disengaged', () => {
mockUseAllRunsQuery.mockReturnValue({
data: {
links: {
current: null,
},
},
} as any)
mockUseAllSessionsQuery.mockReturnValue(({
data: [
{
id: 'test',
createdAt: '2019-08-24T14:15:22Z',
details: {},
sessionType: 'calibrationCheck',
createParams: {},
},
],
links: {},
} as unknown) as UseQueryResult<Sessions, Error>)
const mockEngagedStatus = {
...mockEstopStatus,
status: PHYSICALLY_ENGAGED,
leftEstopPhysicalStatus: ENGAGED,
}
mockUseEstopQuery.mockReturnValue({ data: mockEngagedStatus } as any)
const result = useIsRobotBusy()
expect(result).toBe(false)
})

// TODO: kj 07/13/2022 This test is temporary pending but should be solved by another PR.
// it('should poll the run and sessions if poll option is true', async () => {
// const result = useIsRobotBusy({ poll: true })
Expand Down
7 changes: 6 additions & 1 deletion app/src/organisms/Devices/hooks/useIsRobotBusy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
useAllSessionsQuery,
useAllRunsQuery,
useEstopQuery,
} from '@opentrons/react-api-client'
import { DISENGAGED } from '../../EmergencyStop'

const ROBOT_STATUS_POLL_MS = 30000

Expand All @@ -16,9 +18,12 @@ export function useIsRobotBusy(
const robotHasCurrentRun =
useAllRunsQuery({}, queryOptions)?.data?.links?.current != null
const allSessionsQueryResponse = useAllSessionsQuery(queryOptions)
const { data: estopStatus, error: estopError } = useEstopQuery(queryOptions)

return (
robotHasCurrentRun ||
(allSessionsQueryResponse?.data?.data != null &&
allSessionsQueryResponse?.data?.data?.length !== 0)
allSessionsQueryResponse?.data?.data?.length !== 0) ||
(estopStatus?.data.status !== DISENGAGED && estopError == null)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ describe('RobotConfigurationDetails', () => {
robotType: OT2_STANDARD_MODEL,
}
const { queryByText } = render(props)
console.log(props.robotType)
expect(queryByText('extension mount')).not.toBeInTheDocument()
})

Expand Down
10 changes: 5 additions & 5 deletions app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import { InstrumentsAndModules } from '../../../organisms/Devices/InstrumentsAnd
import { RecentProtocolRuns } from '../../../organisms/Devices/RecentProtocolRuns'
import { EstopBanner } from '../../../organisms/Devices/EstopBanner'
import { DISENGAGED, useEstopContext } from '../../../organisms/EmergencyStop'

const ESTOP_STATUS_REFETCH_INTERVAL = 10000
import { useIsOT3 } from '../../../organisms/Devices/hooks'

interface DeviceDetailsComponentProps {
robotName: string
Expand All @@ -24,10 +23,9 @@ interface DeviceDetailsComponentProps {
export function DeviceDetailsComponent({
robotName,
}: DeviceDetailsComponentProps): JSX.Element {
const { data: estopStatus } = useEstopQuery({
refetchInterval: ESTOP_STATUS_REFETCH_INTERVAL,
})
const { data: estopStatus, error: estopError } = useEstopQuery()
const { isEmergencyStopModalDismissed } = useEstopContext()
const isOT3 = useIsOT3(robotName)

return (
<Box
Expand All @@ -38,6 +36,8 @@ export function DeviceDetailsComponent({
paddingBottom={SPACING.spacing48}
>
{estopStatus?.data.status !== DISENGAGED &&
estopError == null &&
isOT3 &&
isEmergencyStopModalDismissed ? (
<Flex marginBottom={SPACING.spacing16}>
<EstopBanner status={estopStatus?.data.status} />
Expand Down

0 comments on commit 64d64fc

Please sign in to comment.