Skip to content

Commit

Permalink
feat(api-client, app): add subsystem types and update flows (#12957)
Browse files Browse the repository at this point in the history
  • Loading branch information
smb2268 authored Jun 26, 2023
1 parent 4b55a76 commit aad18ed
Show file tree
Hide file tree
Showing 44 changed files with 628 additions and 68 deletions.
1 change: 1 addition & 0 deletions api-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export * from './robot'
export * from './runs'
export * from './server'
export * from './sessions'
export * from './subsystems'
export * from './system'
export * from './types'
23 changes: 21 additions & 2 deletions api-client/src/instruments/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type InstrumentData = PipetteData | GripperData

export type InstrumentData = PipetteData | GripperData | BadPipette | BadGripper
export interface GripperData {
data: {
jawState: string
Expand All @@ -13,6 +12,8 @@ export interface GripperData {
instrumentType: 'gripper'
mount: string
serialNumber: string
subsystem: 'gripper'
ok: true
}
export interface PipetteData {
data: {
Expand All @@ -30,6 +31,8 @@ export interface PipetteData {
instrumentType: 'pipette'
mount: string
serialNumber: string
subsystem: 'pipette_left' | 'pipette_right'
ok: true
}

export type InstrumentsData = InstrumentData[]
Expand All @@ -47,3 +50,19 @@ export interface Instruments {
export interface GetInstrumentsRequestParams {
refresh?: boolean
}

export interface BadPipette {
subsystem: 'pipette_left' | 'pipette_right'
status: string
update: string
ok: false
instrumentType: 'pipette'
}

export interface BadGripper {
subsystem: 'gripper'
status: string
update: string
ok: false
instrumentType: 'gripper'
}
4 changes: 4 additions & 0 deletions api-client/src/pipettes/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ export const pipetteDataLeftFixture = {
instrumentType: 'pipette',
mount: 'left',
serialNumber: 'abc',
subsystem: 'pipette_left',
ok: true,
}

export const pipetteResponseRightFixture = {
Expand All @@ -269,4 +271,6 @@ export const pipetteResponseRightFixture = {
instrumentType: 'pipette',
mount: 'right',
serialNumber: 'cba',
subsystem: 'pipette_right',
ok: true,
}
17 changes: 17 additions & 0 deletions api-client/src/subsystems/getSubsystemUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GET, request } from '../request'

import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
import type { SubsystemUpdateProgressData } from './types'

export function getSubsystemUpdate(
config: HostConfig,
updateId: string
): ResponsePromise<SubsystemUpdateProgressData> {
return request<SubsystemUpdateProgressData>(
GET,
`/subsystems/updates/all/${updateId}`,
null,
config
)
}
3 changes: 3 additions & 0 deletions api-client/src/subsystems/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { updateSubsystem } from './updateSubsystem'
export { getSubsystemUpdate } from './getSubsystemUpdate'
export * from './types'
19 changes: 19 additions & 0 deletions api-client/src/subsystems/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type Subsystem =
| 'gantry_x'
| 'gantry_y'
| 'head'
| 'pipette_left'
| 'pipette_right'
| 'gripper'
| 'rear_panel'

export interface SubsystemUpdateProgressData {
data: {
id: string
createdAt: string
subsystem: Subsystem
updateStatus: 'queued' | 'updating' | 'done'
updateProgress: number
updateError: string
}
}
17 changes: 17 additions & 0 deletions api-client/src/subsystems/updateSubsystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { POST, request } from '../request'

import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
import type { SubsystemUpdateProgressData } from './types'

export function updateSubsystem(
config: HostConfig,
subsystem: string
): ResponsePromise<SubsystemUpdateProgressData> {
return request<SubsystemUpdateProgressData>(
POST,
`/subsystems/updates/${subsystem}`,
null,
config
)
}
1 change: 1 addition & 0 deletions app/src/assets/localization/en/gripper_wizard_flows.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"connect_and_screw_in_gripper": "Connect and Screw In Gripper",
"continue": "Continue",
"detach_gripper": "Detach Gripper",
"firmware_updating": "A firmware update is required, instrument is updating...",
"get_started": "Get started",
"gripper_calibration": "Gripper Calibration",
"gripper_recalibration": "Gripper Recalibration",
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/localization/en/pipette_wizard_flows.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"detach": "Detaching Pipette",
"error_encountered": "Error encountered",
"exit_cal": "Exit calibration",
"firmware_updating": "A firmware update is required, instrument is updating...",
"gantry_empty_for_96_channel_success": "Now that both mounts are empty, you can begin the 96-Channel Pipette attachment process.",
"get_started_detach": "<block>To get started, remove labware from the deck and clean up the working area to make detachment easier. Also gather the needed equipment shown to the right.</block>",
"grab_screwdriver": "While continuing to hold in place, grab your 2.5mm driver and tighten screws as shown in the animation. Test the pipette attachment by giving it a wiggle before pressing continue",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as React from 'react'
import { waitFor } from '@testing-library/react'
import { renderWithProviders } from '@opentrons/components'
import {
useInstrumentsQuery,
useSubsystemUpdateQuery,
useUpdateSubsystemMutation,
} from '@opentrons/react-api-client'
import { i18n } from '../../../i18n'
import { FirmwareUpdateModal } from '../'
import {
BadPipette,
PipetteData,
SubsystemUpdateProgressData,
} from '@opentrons/api-client'

jest.mock('@opentrons/react-api-client')

const mockUseInstrumentQuery = useInstrumentsQuery as jest.MockedFunction<
typeof useInstrumentsQuery
>
const mockUseSubsystemUpdateQuery = useSubsystemUpdateQuery as jest.MockedFunction<
typeof useSubsystemUpdateQuery
>
const mockUseUpdateSubsystemMutation = useUpdateSubsystemMutation as jest.MockedFunction<
typeof useUpdateSubsystemMutation
>

const render = (props: React.ComponentProps<typeof FirmwareUpdateModal>) => {
return renderWithProviders(<FirmwareUpdateModal {...props} />, {
i18nInstance: i18n,
})[0]
}

describe('FirmwareUpdateModal', () => {
let props: React.ComponentProps<typeof FirmwareUpdateModal>
const refetch = jest.fn(() => Promise.resolve())
const updateSubsystem = jest.fn(() => Promise.resolve())
beforeEach(() => {
props = {
proceed: jest.fn(),
description: 'A firmware update is required, instrument is updating',
subsystem: 'pipette_left',
}
mockUseInstrumentQuery.mockReturnValue({
data: {
data: [
{
subsystem: 'pipette_left',
ok: false,
} as BadPipette,
],
},
refetch,
} as any)
mockUseSubsystemUpdateQuery.mockReturnValue({
data: {
data: {
id: 'update id',
updateStatus: 'done',
} as any,
} as SubsystemUpdateProgressData,
} as any)
mockUseUpdateSubsystemMutation.mockReturnValue({
data: {
data: {
id: 'update id',
updateStatus: 'in progress',
updateProgress: 20,
} as any,
} as SubsystemUpdateProgressData,
updateSubsystem,
} as any)
})
it('calls proceed if no update is needed', () => {
mockUseInstrumentQuery.mockReturnValue({
data: {
data: [
{
subsystem: 'pipette_left',
ok: true,
} as PipetteData,
],
},
} as any)
mockUseSubsystemUpdateQuery.mockReturnValue({} as any)
const { getByText } = render(props)
getByText('A firmware update is required, instrument is updating')
expect(props.proceed).toHaveBeenCalled()
})
it('calls update subsystem if update is needed', () => {
mockUseSubsystemUpdateQuery.mockReturnValue({} as any)
const { getByText } = render(props)
getByText('A firmware update is required, instrument is updating')
expect(updateSubsystem).toHaveBeenCalled()
})
it('calls refetch instruments and then proceed once update is complete', async () => {
const { getByText } = render(props)
getByText('A firmware update is required, instrument is updating')
await waitFor(() => expect(refetch).toHaveBeenCalled())
await waitFor(() => expect(props.proceed).toHaveBeenCalled())
})
})
110 changes: 110 additions & 0 deletions app/src/molecules/FirmwareUpdateModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react'
import { css } from 'styled-components'
import {
ALIGN_CENTER,
DIRECTION_COLUMN,
TYPOGRAPHY,
SPACING,
Flex,
RESPONSIVENESS,
JUSTIFY_CENTER,
BORDERS,
COLORS,
} from '@opentrons/components'
import {
useInstrumentsQuery,
useSubsystemUpdateQuery,
useUpdateSubsystemMutation,
} from '@opentrons/react-api-client'
import { ProgressBar } from '../../atoms/ProgressBar'
import { StyledText } from '../../atoms/text'
import { BadGripper, BadPipette, Subsystem } from '@opentrons/api-client'

interface FirmwareUpdateModalProps {
description: string
proceed: () => void
subsystem: Subsystem
}

const DESCRIPTION_STYLE = css`
${TYPOGRAPHY.h1Default}
margin-top: ${SPACING.spacing8};
margin-bottom: ${SPACING.spacing24};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
font-weight: ${TYPOGRAPHY.fontWeightBold};
font-size: ${TYPOGRAPHY.fontSize32};
margin-top: ${SPACING.spacing4};
margin-bottom: ${SPACING.spacing32};
margin-left: 4.5rem;
margin-right: 4.5rem;
text-align: ${TYPOGRAPHY.textAlignCenter};
line-height: ${TYPOGRAPHY.lineHeight42};
}
`
const MODAL_STYLE = css`
align-items: ${ALIGN_CENTER};
flex-direction: ${DIRECTION_COLUMN};
justify-content: ${JUSTIFY_CENTER};
padding: ${SPACING.spacing32};
height: 24.625rem;
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
height: 31.5625rem;
}
`
const OUTER_STYLES = css`
border-radius: ${BORDERS.borderRadiusSize4};
background: ${COLORS.medGreyEnabled};
width: 13.374rem;
`

export const FirmwareUpdateModal = (
props: FirmwareUpdateModalProps
): JSX.Element => {
const { proceed, subsystem, description } = props
const [updateId, setUpdateId] = React.useState('')
const {
data: attachedInstruments,
refetch: refetchInstruments,
} = useInstrumentsQuery()
const { updateSubsystem } = useUpdateSubsystemMutation({
onSuccess: data => {
setUpdateId(data.data.id)
},
})
const updateNeeded =
attachedInstruments?.data?.some(
(i): i is BadGripper | BadPipette => !i.ok && i.subsystem === subsystem
) ?? false
React.useEffect(() => {
if (!updateNeeded) {
proceed()
} else {
updateSubsystem(subsystem)
}
}, [])
const { data: updateData } = useSubsystemUpdateQuery(updateId)
const status = updateData?.data.updateStatus
const percentComplete = updateData?.data.updateProgress ?? 0

React.useEffect(() => {
if (status === 'done') {
refetchInstruments()
.then(() => {
proceed()
})
.catch(() => {
proceed()
})
}
}, [status, proceed, refetchInstruments])
return (
<Flex css={MODAL_STYLE}>
<StyledText css={DESCRIPTION_STYLE}>{description}</StyledText>
<ProgressBar
percentComplete={percentComplete}
outerStyles={OUTER_STYLES}
/>
</Flex>
)
}
Loading

0 comments on commit aad18ed

Please sign in to comment.