Skip to content

Commit

Permalink
fix(app): prevent user from proceeding if uploaded protocol has no st…
Browse files Browse the repository at this point in the history
…eps (#4381)

Closes #3121
  • Loading branch information
mcous authored Nov 6, 2019
1 parent 086723d commit a8344e9
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 59 deletions.
8 changes: 4 additions & 4 deletions app/src/components/FileInfo/InstrumentItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { Icon } from '@opentrons/components'
import styles from './styles.css'
import type { PipetteCompatibility } from './useInstrumentMountInfo'

type Props = {
type Props = {|
compatibility?: PipetteCompatibility,
mount?: string,
children: React.Node,
hidden?: boolean,
}
|}

export default function ModuleItem(props: Props) {
export default function InstrumentItem(props: Props) {
if (props.hidden) return null
return (
<div className={styles.instrument_item}>
Expand All @@ -26,7 +26,7 @@ export default function ModuleItem(props: Props) {
)
}

function StatusIcon(props: { match: boolean }) {
function StatusIcon(props: {| match: boolean |}) {
const { match } = props

const iconName = match ? 'check-circle' : 'checkbox-blank-circle-outline'
Expand Down
55 changes: 35 additions & 20 deletions app/src/components/FileInfo/ProtocolPipettesCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import InstrumentItem from './InstrumentItem'
import { SectionContentHalf } from '../layout'
import InfoSection from './InfoSection'
import MissingItemWarning from './MissingItemWarning'
import useInstrumentMountInfo from './useInstrumentMountInfo'
import useInstrumentMountInfo, {
MATCH,
INEXACT_MATCH,
} from './useInstrumentMountInfo'

import styles from './styles.css'

import type { Dispatch } from '../../types'
import type { Robot } from '../../discovery'
import type { Robot } from '../../discovery/types'

type Props = {| robot: Robot |}

Expand All @@ -27,41 +31,52 @@ const inexactPipetteSupportArticle =
const TITLE = 'Required Pipettes'

function ProtocolPipettes(props: Props) {
const dispatch: Dispatch = useDispatch()
const dispatch = useDispatch<Dispatch>()
const infoByMount = useInstrumentMountInfo(props.robot.name)

React.useEffect(() => {
dispatch(fetchPipettes(props.robot))
}, [dispatch, props.robot])

const changePipetteUrl = `/robots/${props.robot.name}/instruments`

const allPipettesMatch = every(infoByMount, ({ compatibility }) =>
['match', 'inexact_match'].includes(compatibility)
[MATCH, INEXACT_MATCH].includes(compatibility)
)

const someInexactMatches = some(
infoByMount,
({ compatibility }) => compatibility === 'inexact_match'
({ compatibility }) => compatibility === INEXACT_MATCH
)

const pipetteItemProps = PIPETTE_MOUNTS.map(mount => {
const info = infoByMount[mount]

return info.protocol
? {
compatibility: info.compatibility,
mount: info.protocol.mount,
hidden: !info.protocol.name,
displayName: info.protocol.displayName,
}
: null
}).filter(Boolean)

if (pipetteItemProps.length === 0) return null

return (
<InfoSection title={TITLE}>
<SectionContentHalf>
{PIPETTE_MOUNTS.map(mount => {
const info = infoByMount[mount]
if (!info) return null
const { protocol, compatibility } = info
return (
<InstrumentItem
key={protocol.mount}
compatibility={compatibility}
mount={protocol.mount}
hidden={!protocol.name}
>
{protocol.displayName}
</InstrumentItem>
)
}).filter(Boolean)}
{pipetteItemProps.map(itemProps => (
<InstrumentItem
key={itemProps.mount}
compatibility={itemProps.compatibility}
mount={itemProps.mount}
hidden={itemProps.hidden}
>
{itemProps.displayName}
</InstrumentItem>
))}
</SectionContentHalf>
{!allPipettesMatch && (
<MissingItemWarning
Expand Down
20 changes: 14 additions & 6 deletions app/src/components/FileInfo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,31 @@ import styles from './styles.css'

import type { Robot } from '../../discovery'

type Props = {
const NO_STEPS_MESSAGE = `This protocol has no steps in it - there's nothing for your robot to do! Your protocol needs at least one aspirate/dispense to import properly`

type Props = {|
robot: Robot,
sessionLoaded: ?boolean,
sessionLoaded: boolean,
sessionHasSteps: boolean,
uploadError: ?{ message: string },
}
|}

export default function FileInfo(props: Props) {
const { robot, sessionLoaded, uploadError } = props
const { robot, sessionLoaded, sessionHasSteps } = props
const uploadError = sessionHasSteps
? props.uploadError
: { message: NO_STEPS_MESSAGE }

return (
<div className={styles.file_info_container}>
<InformationCard />
<ProtocolPipettesCard robot={robot} />
<ProtocolModulesCard robot={robot} />
<ProtocolLabwareCard />
{uploadError && <UploadError uploadError={uploadError} />}
{sessionLoaded && <Continue />}
{sessionLoaded && uploadError && (
<UploadError uploadError={uploadError} />
)}
{sessionLoaded && !uploadError && <Continue />}
</div>
)
}
64 changes: 38 additions & 26 deletions app/src/components/FileInfo/useInstrumentMountInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,39 @@ import isEmpty from 'lodash/isEmpty'
import {
selectors as robotSelectors,
constants as robotConstants,
type Pipette,
type Mount,
} from '../../robot'
import {
getPipettesState,
type Pipette as ActualPipette,
} from '../../robot-api'
import { getPipettesState } from '../../robot-api'
import {
getPipetteModelSpecs,
getPipetteNameSpecs,
type PipetteModelSpecs,
} from '@opentrons/shared-data'

import type { PipetteModelSpecs } from '@opentrons/shared-data'
import type { State } from '../../types'
import type { Pipette, Mount } from '../../robot/types'
import type { Pipette as ActualPipette } from '../../robot-api/types'

export type PipetteCompatibility = 'match' | 'inexact_match' | 'incompatible'

type InstrumentMountInfo = {|
actual: {|
export type InstrumentMountInfo = {|
actual: null | {|
...ActualPipette,
displayName: string,
modelSpecs: ?PipetteModelSpecs,
|},
protocol: {|
...$Exact<Pipette>,
protocol: null | {|
...$Shape<$Exact<Pipette>>,
displayName: string,
|},
compatibility: PipetteCompatibility,
|}

const { PIPETTE_MOUNTS } = robotConstants

export const MATCH: 'match' = 'match'
export const INCOMPATIBLE: 'incompatible' = 'incompatible'
export const INEXACT_MATCH: 'inexact_match' = 'inexact_match'

function pipettesAreInexactMatch(
protocolInstrName: ?string,
actualModelSpecs: ?PipetteModelSpecs
Expand All @@ -45,18 +49,21 @@ function pipettesAreInexactMatch(
function useInstrumentMountInfo(
robotName: string
): { [Mount]: InstrumentMountInfo } {
const protocolInstruments = useSelector(robotSelectors.getPipettes)
const protocolInstruments = useSelector<State, Array<Pipette>>(
robotSelectors.getPipettes
)
const actualInstruments = useSelector(state =>
getPipettesState(state, robotName)
)

const instrumentInfoByMount = PIPETTE_MOUNTS.reduce((acc, mount) => {
const protocolInstrument = protocolInstruments.find(i => i.mount === mount)
const actualInstrument = actualInstruments[mount]
const requestedAs = protocolInstrument?.requestedAs

const actualModelSpecs = getPipetteModelSpecs(actualInstrument?.model || '')
const requestedDisplayName = protocolInstrument?.requestedAs
? getPipetteNameSpecs(protocolInstrument?.requestedAs)?.displayName
const requestedDisplayName = requestedAs
? getPipetteNameSpecs(requestedAs)?.displayName
: protocolInstrument?.modelSpecs?.displayName

const protocolInstrName =
Expand All @@ -65,25 +72,30 @@ function useInstrumentMountInfo(

const perfectMatch = protocolInstrName === actualInstrName

let compatibility: PipetteCompatibility = 'incompatible'
let compatibility: PipetteCompatibility = INCOMPATIBLE
if (perfectMatch || isEmpty(protocolInstrument)) {
compatibility = 'match'
compatibility = MATCH
} else if (pipettesAreInexactMatch(protocolInstrName, actualModelSpecs)) {
compatibility = 'inexact_match'
compatibility = INEXACT_MATCH
}

return {
...acc,
[mount]: {
protocol: {
...protocolInstrument,
displayName: requestedDisplayName || 'N/A',
},
actual: {
...actualInstrument,
modelSpecs: actualModelSpecs,
displayName: actualModelSpecs?.displayName || 'N/A',
},
protocol: protocolInstrument
? {
...protocolInstrument,
displayName: requestedDisplayName || protocolInstrument.name,
}
: null,
actual:
actualInstrument && actualModelSpecs
? {
...actualInstrument,
modelSpecs: actualModelSpecs,
displayName: actualModelSpecs.displayName,
}
: null,
compatibility,
},
}
Expand Down
6 changes: 5 additions & 1 deletion app/src/components/nav-bar/NavButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Props = {| ...ContextRouter, name: string |}
function NavButton(props: Props) {
const { name } = props
const isProtocolLoaded = useSelector(robotSelectors.getSessionIsLoaded)
const isProtocolRunnable = useSelector(robotSelectors.getCommands).length > 0
const isProtocolRunning = useSelector(robotSelectors.getIsRunning)
const isProtocolDone = useSelector(robotSelectors.getIsDone)
const connectedRobot = useSelector(getConnectedRobot)
Expand Down Expand Up @@ -70,6 +71,7 @@ function NavButton(props: Props) {
<GenericNavButton
disabled={
!isProtocolLoaded ||
!isProtocolRunnable ||
isProtocolRunning ||
isProtocolDone ||
incompatiblePipettes
Expand All @@ -85,7 +87,9 @@ function NavButton(props: Props) {
case 'run':
return (
<GenericNavButton
disabled={!isProtocolLoaded || incompatiblePipettes}
disabled={
!isProtocolLoaded || !isProtocolRunnable || incompatiblePipettes
}
tooltipComponent={
incompatiblePipettes ? incompatPipetteTooltip : null
}
Expand Down
7 changes: 5 additions & 2 deletions app/src/pages/Upload/FileInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import FileInfo from '../../components/FileInfo'

import type { Robot } from '../../discovery'

type Props = {
type Props = {|
robot: Robot,
filename: string,
uploadInProgress: boolean,
uploadError: ?{ message: string },
sessionLoaded: boolean,
}
sessionHasSteps: boolean,
|}

export default function FileInfoPage(props: Props) {
const {
Expand All @@ -22,6 +23,7 @@ export default function FileInfoPage(props: Props) {
uploadInProgress,
uploadError,
sessionLoaded,
sessionHasSteps,
} = props

return (
Expand All @@ -34,6 +36,7 @@ export default function FileInfoPage(props: Props) {
<FileInfo
robot={robot}
sessionLoaded={sessionLoaded}
sessionHasSteps={sessionHasSteps}
uploadError={uploadError}
/>
{uploadInProgress && <SpinnerModal message="Upload in Progress" />}
Expand Down
4 changes: 4 additions & 0 deletions app/src/pages/Upload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type SP = {|
uploadInProgress: boolean,
uploadError: ?{ message: string },
sessionLoaded: boolean,
sessionHasSteps: boolean,
|}

type Props = {| ...OP, ...SP, dispatch: Dispatch |}
Expand All @@ -39,6 +40,7 @@ function mapStateToProps(state: State): SP {
uploadInProgress: robotSelectors.getSessionLoadInProgress(state),
uploadError: robotSelectors.getUploadError(state),
sessionLoaded: robotSelectors.getSessionIsLoaded(state),
sessionHasSteps: robotSelectors.getCommands(state).length > 0,
}
}

Expand All @@ -49,6 +51,7 @@ function UploadPage(props: Props) {
uploadInProgress,
uploadError,
sessionLoaded,
sessionHasSteps,
match: { path },
} = props

Expand All @@ -75,6 +78,7 @@ function UploadPage(props: Props) {
uploadInProgress={uploadInProgress}
uploadError={uploadError}
sessionLoaded={sessionLoaded}
sessionHasSteps={sessionHasSteps}
/>
)}
/>
Expand Down

0 comments on commit a8344e9

Please sign in to comment.