diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index b81231eae1c..293f4ea1fc3 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -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' @@ -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 diff --git a/app/src/App/__tests__/DesktopApp.test.tsx b/app/src/App/__tests__/DesktopApp.test.tsx index 68aef94cc67..bd33d7ead85 100644 --- a/app/src/App/__tests__/DesktopApp.test.tsx +++ b/app/src/App/__tests__/DesktopApp.test.tsx @@ -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' @@ -55,6 +56,7 @@ const mockBreadcrumbs = Breadcrumbs as jest.MockedFunction const mockUseSoftwareUpdatePoll = useSoftwareUpdatePoll as jest.MockedFunction< typeof useSoftwareUpdatePoll > +const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const render = (path = '/') => { return renderWithProviders( @@ -78,6 +80,7 @@ describe('DesktopApp', () => { mockAlerts.mockReturnValue(
Mock Alerts
) mockAppSettings.mockReturnValue(
Mock AppSettings
) mockBreadcrumbs.mockReturnValue(
Mock Breadcrumbs
) + mockUseIsOT3.mockReturnValue(true) }) afterEach(() => { jest.resetAllMocks() diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index b0f3a68383d..1ab8926f2d7 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -83,6 +83,7 @@ import { useIsRobotViewable, useTrackProtocolRunEvent, useRobotAnalyticsData, + useIsOT3, } from '../hooks' import { formatTimestamp } from '../utils' import { RunTimer } from './RunTimer' @@ -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(false) + const isOT3 = useIsOT3(robotName) + React.useEffect(() => { - if (estopStatus?.data.status !== DISENGAGED) { + if (estopStatus?.data.status !== DISENGAGED && estopError == null) { setShowEmergencyStopRunBanner(true) } }, [estopStatus?.data.status]) @@ -288,6 +291,8 @@ export function ProtocolRunHeader({ /> ) : null} {estopStatus?.data.status !== DISENGAGED && + estopError == null && + isOT3 && showEmergencyStopRunBanner ? ( +const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const ROBOT_NAME = 'otie' const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b' @@ -345,6 +347,7 @@ describe('ProtocolRunHeader', () => { when(mockUseRunCalibrationStatus) .calledWith(ROBOT_NAME, RUN_ID) .mockReturnValue({ complete: true }) + mockUseIsOT3.mockReturnValue(true) mockRunFailedModal.mockReturnValue(
mock RunFailedModal
) mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any) }) diff --git a/app/src/organisms/Devices/RobotOverflowMenu.tsx b/app/src/organisms/Devices/RobotOverflowMenu.tsx index 76a4c3eca21..920ca366a23 100644 --- a/app/src/organisms/Devices/RobotOverflowMenu.tsx +++ b/app/src/organisms/Devices/RobotOverflowMenu.tsx @@ -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' @@ -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 = e => { e.preventDefault() e.stopPropagation() @@ -78,14 +81,16 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { if (robot.status === CONNECTABLE && runId == null) { menuItems = ( <> - - {t('run_a_protocol')} - + {!isRobotBusy ? ( + + {t('run_a_protocol')} + + ) : null} {isRobotOnWrongVersionOfSoftware && ( {t('shared:a_software_update_is_available')} diff --git a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx index bd92ce69aa2..8b698da342d 100644 --- a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx @@ -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, @@ -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 @@ -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) => { return renderWithProviders( @@ -60,6 +65,7 @@ describe('RobotOverflowMenu', () => { autoUpdateDisabledReason: null, updateFromFileDisabledReason: null, }) + mockUseIsRobotBusy.mockReturnValue(false) }) afterEach(() => { jest.resetAllMocks() @@ -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') + }) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts index ac66a0caf5e..47b849d815d 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts +++ b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts @@ -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' @@ -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(() => { @@ -30,6 +48,7 @@ describe('useIsRobotBusy', () => { }, }, } as UseQueryResult) + mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any) }) afterEach(() => { @@ -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) + 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) + 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 }) diff --git a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts index 058b1525e0d..db3f675aeee 100644 --- a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts +++ b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts @@ -1,7 +1,9 @@ import { useAllSessionsQuery, useAllRunsQuery, + useEstopQuery, } from '@opentrons/react-api-client' +import { DISENGAGED } from '../../EmergencyStop' const ROBOT_STATUS_POLL_MS = 30000 @@ -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) ) } diff --git a/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx b/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx index 4bb4847f604..f17cafa9e5e 100644 --- a/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx +++ b/app/src/organisms/ProtocolDetails/__tests__/RobotConfigurationDetails.test.tsx @@ -151,7 +151,6 @@ describe('RobotConfigurationDetails', () => { robotType: OT2_STANDARD_MODEL, } const { queryByText } = render(props) - console.log(props.robotType) expect(queryByText('extension mount')).not.toBeInTheDocument() }) diff --git a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx b/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx index 68d13f988ae..7f435eb1ae6 100644 --- a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx +++ b/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx @@ -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 @@ -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 ( {estopStatus?.data.status !== DISENGAGED && + estopError == null && + isOT3 && isEmergencyStopModalDismissed ? (