Skip to content

Commit

Permalink
refactor(app): update H-S running modal command to stop active H-S mo…
Browse files Browse the repository at this point in the history
…dules (#11244)

* refactor(app): update H-S running modal command to stop active H-S modules

This PR will use the HS running modal before starting a run to stop all active heater shakes. It also fixes a bug where the wrong heater shaker was rendered when opening the wizard from the setup page.

closes #11194
  • Loading branch information
sakibh authored Aug 2, 2022
1 parent 28697e1 commit e09f782
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as React from 'react'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { renderHook } from '@testing-library/react-hooks'
import { HEATERSHAKER_MODULE_V1 } from '@opentrons/shared-data'
import { useProtocolDetailsForRun } from '../../hooks'
import { useHeaterShakerModuleIdsFromRun } from '../hooks'
import { RUN_ID_1 } from '../../../RunTimeControl/__fixtures__'

import type { Store } from 'redux'
import type { State } from '../../../../redux/types'

jest.mock('../../hooks')

const mockUseProtocolDetailsForRun = useProtocolDetailsForRun as jest.MockedFunction<
typeof useProtocolDetailsForRun
>

describe('useHeaterShakerModuleIdsFromRun', () => {
const store: Store<State> = createStore(jest.fn(), {})

beforeEach(() => {
store.dispatch = jest.fn()
})

afterEach(() => {
jest.restoreAllMocks()
})

it('should return a heater shaker module id from protocol analysis load command result', () => {
mockUseProtocolDetailsForRun.mockReturnValue({
protocolData: {
pipettes: {},
labware: {},
modules: {
heatershaker_id: {
model: HEATERSHAKER_MODULE_V1,
},
},
labwareDefinitions: {},
commands: [
{
id: 'mock_command_1',
createdAt: '2022-07-27T22:26:33.846399+00:00',
commandType: 'loadModule',
key: '286d7201-bfdc-4c2c-ae67-544367dbbabe',
status: 'succeeded',
params: {
model: HEATERSHAKER_MODULE_V1,
location: {
slotName: '1',
},
moduleId: 'heatershaker_id',
},
result: {
moduleId: 'heatershaker_id',
definition: {},
model: HEATERSHAKER_MODULE_V1,
serialNumber: 'fake-serial-number-1',
},
startedAt: '2022-07-27T22:26:33.875106+00:00',
completedAt: '2022-07-27T22:26:33.878079+00:00',
},
],
},
} as any)
const wrapper: React.FunctionComponent<{}> = ({ children }) => (
<Provider store={store}>{children}</Provider>
)
const { result } = renderHook(
() => useHeaterShakerModuleIdsFromRun(RUN_ID_1),
{ wrapper }
)

const moduleIdsFromRun = result.current
expect(moduleIdsFromRun.moduleIdsFromRun).toStrictEqual(['heatershaker_id'])
})

it('should return two heater shaker module ids if two modules are loaded in the protocol', () => {
mockUseProtocolDetailsForRun.mockReturnValue({
protocolData: {
pipettes: {},
labware: {},
modules: {
heatershaker_id: {
model: HEATERSHAKER_MODULE_V1,
},
},
labwareDefinitions: {},
commands: [
{
id: 'mock_command_1',
createdAt: '2022-07-27T22:26:33.846399+00:00',
commandType: 'loadModule',
key: '286d7201-bfdc-4c2c-ae67-544367dbbabe',
status: 'succeeded',
params: {
model: HEATERSHAKER_MODULE_V1,
location: {
slotName: '1',
},
moduleId: 'heatershaker_id_1',
},
result: {
moduleId: 'heatershaker_id_1',
definition: {},
model: HEATERSHAKER_MODULE_V1,
serialNumber: 'fake-serial-number-1',
},
startedAt: '2022-07-27T22:26:33.875106+00:00',
completedAt: '2022-07-27T22:26:33.878079+00:00',
},
{
id: 'mock_command_2',
createdAt: '2022-07-27T22:26:33.846399+00:00',
commandType: 'loadModule',
key: '286d7201-bfdc-4c2c-ae67-544367dbbabe',
status: 'succeeded',
params: {
model: HEATERSHAKER_MODULE_V1,
location: {
slotName: '1',
},
moduleId: 'heatershaker_id_2',
},
result: {
moduleId: 'heatershaker_id_2',
definition: {},
model: 'heaterShakerModuleV1_2',
serialNumber: 'fake-serial-number-2',
},
startedAt: '2022-07-27T22:26:33.875106+00:00',
completedAt: '2022-07-27T22:26:33.878079+00:00',
},
],
},
} as any)

const wrapper: React.FunctionComponent<{}> = ({ children }) => (
<Provider store={store}>{children}</Provider>
)
const { result } = renderHook(
() => useHeaterShakerModuleIdsFromRun(RUN_ID_1),
{ wrapper }
)

const moduleIdsFromRun = result.current
expect(moduleIdsFromRun.moduleIdsFromRun).toStrictEqual([
'heatershaker_id_1',
'heatershaker_id_2',
])
})
})
24 changes: 24 additions & 0 deletions app/src/organisms/Devices/HeaterShakerIsRunningModal/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useProtocolDetailsForRun } from '../hooks'

export interface ModuleIdsFromRun {
moduleIdsFromRun: string[]
}

export function useHeaterShakerModuleIdsFromRun(
runId: string | null
): ModuleIdsFromRun {
const { protocolData } = useProtocolDetailsForRun(runId)

const loadModuleCommands = protocolData?.commands.filter(
command =>
command.commandType === 'loadModule' &&
command.params.model === 'heaterShakerModuleV1'
)

const moduleIdsFromRun =
loadModuleCommands != null
? loadModuleCommands?.map(command => command.result.moduleId)
: []

return { moduleIdsFromRun }
}
41 changes: 27 additions & 14 deletions app/src/organisms/Devices/HeaterShakerIsRunningModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useCreateLiveCommandMutation } from '@opentrons/react-api-client'
import {
Icon,
Expand All @@ -10,12 +11,13 @@ import {
TYPOGRAPHY,
JUSTIFY_FLEX_END,
} from '@opentrons/components'
import { useTranslation } from 'react-i18next'
import { useAttachedModules } from '../hooks'
import { Modal } from '../../../atoms/Modal'
import { PrimaryButton, SecondaryButton } from '../../../atoms/buttons'
import { StyledText } from '../../../atoms/text'
import { HeaterShakerModule } from '../../../redux/modules/types'
import { HeaterShakerModuleCard } from '../HeaterShakerWizard/HeaterShakerModuleCard'
import { HEATERSHAKER_MODULE_TYPE } from '@opentrons/shared-data'

import type { HeaterShakerDeactivateShakerCreateCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/module'

Expand All @@ -31,6 +33,15 @@ export const HeaterShakerIsRunningModal = (
const { closeModal, module, startRun } = props
const { t } = useTranslation('heater_shaker')
const { createLiveCommand } = useCreateLiveCommandMutation()
const attachedModules = useAttachedModules()
const moduleIds = attachedModules
.filter(
(module): module is HeaterShakerModule =>
module.moduleType === HEATERSHAKER_MODULE_TYPE &&
module?.data != null &&
module.data.speedStatus !== 'idle'
)
.map(module => module.id)

const title = (
<Flex flexDirection={DIRECTION_ROW}>
Expand All @@ -45,25 +56,27 @@ export const HeaterShakerIsRunningModal = (
</Flex>
)

const stopShakeCommand: HeaterShakerDeactivateShakerCreateCommand = {
commandType: 'heaterShaker/deactivateShaker',
params: {
moduleId: module.id,
},
}

const handleContinueShaking = (): void => {
startRun()
closeModal()
}

const handleStopShake = (): void => {
createLiveCommand({
command: stopShakeCommand,
}).catch((e: Error) => {
console.error(
`error setting module status with command type ${stopShakeCommand.commandType}: ${e.message}`
)
moduleIds.forEach(moduleId => {
const stopShakeCommand: HeaterShakerDeactivateShakerCreateCommand = {
commandType: 'heaterShaker/deactivateShaker',
params: {
moduleId: moduleId,
},
}

createLiveCommand({
command: stopShakeCommand,
}).catch((e: Error) => {
console.error(
`error setting module status with command type ${stopShakeCommand.commandType}: ${e.message}`
)
})
})
handleContinueShaking()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { fireEvent } from '@testing-library/react'
import { renderWithProviders } from '@opentrons/components'
import { MemoryRouter } from 'react-router-dom'
import { i18n } from '../../../../i18n'
import { useAttachedModules } from '../../hooks'
import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__'
import heaterShakerCommands from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommands.json'
import { HeaterShakerWizard } from '..'
Expand All @@ -22,11 +21,6 @@ jest.mock('../AttachModule')
jest.mock('../AttachAdapter')
jest.mock('../PowerOn')
jest.mock('../TestShake')
jest.mock('../../hooks')

const mockUseAttachedModules = useAttachedModules as jest.MockedFunction<
typeof useAttachedModules
>

const mockIntroduction = Introduction as jest.MockedFunction<
typeof Introduction
Expand Down Expand Up @@ -60,8 +54,8 @@ describe('HeaterShakerWizard', () => {
beforeEach(() => {
props = {
onCloseClick: jest.fn(),
attachedModule: mockHeaterShaker,
}
mockUseAttachedModules.mockReturnValue([mockHeaterShaker])
mockIntroduction.mockReturnValue(<div>Mock Introduction</div>)
mockKeyParts.mockReturnValue(<div>Mock Key Parts</div>)
mockAttachModule.mockReturnValue(<div>Mock Attach Module</div>)
Expand Down Expand Up @@ -121,6 +115,7 @@ describe('HeaterShakerWizard', () => {
protocolLoadOrder: 1,
slotName: '1',
} as ProtocolModuleInfo,
attachedModule: mockHeaterShaker,
}
const { getByText, getByRole } = render(props)

Expand Down Expand Up @@ -148,8 +143,10 @@ describe('HeaterShakerWizard', () => {
})

it('renders power on component and the test shake button is disabled', () => {
mockUseAttachedModules.mockReturnValue([])

props = {
...props,
attachedModule: null,
}
const { getByText, getByRole } = render(props)

let button = getByRole('button', { name: 'Continue to attachment guide' })
Expand Down
30 changes: 12 additions & 18 deletions app/src/organisms/Devices/HeaterShakerWizard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import { useTranslation } from 'react-i18next'
import { getAdapterName } from '@opentrons/shared-data'
import { Portal } from '../../../App/portal'
import { Interstitial } from '../../../atoms/Interstitial/Interstitial'
import { HEATERSHAKER_MODULE_TYPE } from '../../../redux/modules'
import { PrimaryButton, SecondaryButton } from '../../../atoms/buttons'
import { Tooltip } from '../../../atoms/Tooltip'
import { useAttachedModules } from '../hooks'
import { Introduction } from './Introduction'
import { KeyParts } from './KeyParts'
import { AttachModule } from './AttachModule'
Expand All @@ -30,34 +28,28 @@ import type { ProtocolModuleInfo } from '../../Devices/ProtocolRun/utils/getProt
interface HeaterShakerWizardProps {
onCloseClick: () => unknown
moduleFromProtocol?: ProtocolModuleInfo
attachedModule: HeaterShakerModule | null
}

export const HeaterShakerWizard = (
props: HeaterShakerWizardProps
): JSX.Element | null => {
const { onCloseClick, moduleFromProtocol } = props
const { onCloseClick, moduleFromProtocol, attachedModule } = props
const { t } = useTranslation(['heater_shaker', 'shared'])
const [currentPage, setCurrentPage] = React.useState(0)
const { robotName } = useParams<NavRouteParams>()
const attachedModules = useAttachedModules()
const [targetProps, tooltipProps] = useHoverTooltip()
const heaterShaker =
attachedModules.find(
(module): module is HeaterShakerModule =>
module.moduleType === HEATERSHAKER_MODULE_TYPE
) ?? null

let isPrimaryCTAEnabled: boolean = true

if (currentPage === 3) {
isPrimaryCTAEnabled = Boolean(heaterShaker)
isPrimaryCTAEnabled = Boolean(attachedModule)
}
const labwareDef =
moduleFromProtocol != null ? moduleFromProtocol.nestedLabwareDef : null

let heaterShakerModel: ModuleModel
if (heaterShaker != null) {
heaterShakerModel = heaterShaker.moduleModel
if (attachedModule != null) {
heaterShakerModel = attachedModule.moduleModel
} else if (moduleFromProtocol != null) {
heaterShakerModel = moduleFromProtocol.moduleDef.model
}
Expand Down Expand Up @@ -86,18 +78,20 @@ export const HeaterShakerWizard = (
return <AttachModule moduleFromProtocol={moduleFromProtocol} />
case 3:
buttonContent = t('btn_thermal_adapter')
return <PowerOn attachedModule={heaterShaker} />
return <PowerOn attachedModule={attachedModule} />
case 4:
buttonContent = t('btn_test_shake')
return (
// heaterShaker should never be null because isPrimaryCTAEnabled would be disabled otherwise
heaterShaker != null ? <AttachAdapter module={heaterShaker} /> : null
// attachedModule should never be null because isPrimaryCTAEnabled would be disabled otherwise
attachedModule != null ? (
<AttachAdapter module={attachedModule} />
) : null
)
case 5:
buttonContent = t('complete')
return heaterShaker != null ? (
return attachedModule != null ? (
<TestShake
module={heaterShaker}
module={attachedModule}
setCurrentPage={setCurrentPage}
moduleFromProtocol={moduleFromProtocol}
/>
Expand Down
Loading

0 comments on commit e09f782

Please sign in to comment.