Skip to content

Commit

Permalink
feat(app): Add deck map to module review modal (#1910)
Browse files Browse the repository at this point in the history
Closes #1737
  • Loading branch information
mcous authored Jul 23, 2018
1 parent 07ad9ff commit f2e63e3
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 125 deletions.
10 changes: 7 additions & 3 deletions app/src/components/ConnectModulesModal/Prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ const connectedAlertProps = {

export default function Prompt (props: Props) {
const {modulesMissing, onClick} = props

const alert = modulesMissing
? <AlertItem {...missingAlertProps}/>
: <AlertItem {...connectedAlertProps}/>

const message = modulesMissing
? 'Plug in and power up the required module via USB to your robot.'
: 'Module succesfully detected.'
? 'Plug in and power up the required module(s) via USB to your robot.'
: 'Module(s) successfully detected.'

const buttonText = modulesMissing
? 'try searching for missing module'
? 'try searching for missing module(s)'
: 'continue to labware setup'

return (
<div className={styles.prompt}>
{alert}
Expand Down
53 changes: 53 additions & 0 deletions app/src/components/ConnectModulesModal/ReviewModuleItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// @flow
import * as React from 'react'
import {connect} from 'react-redux'
import countBy from 'lodash/countBy'

import {makeGetRobotModules} from '../../http-api-client'
import {selectors as robotSelectors} from '../../robot'
import {ModuleItem} from '../DeckMap'

import type {LabwareComponentProps} from '@opentrons/components'
import type {State} from '../../types'
import type {SessionModule} from '../../robot'

type OP = LabwareComponentProps

type Props = {
module: ?SessionModule,
present: boolean
}

export default connect(makeMapStateToProps)(ReviewModuleItem)

function ReviewModuleItem (props: Props) {
if (!props.module) return null

return (
<ModuleItem module={props.module} present={props.present} review />
)
}

function makeMapStateToProps (): (state: State, ownProps: OP) => Props {
// TODO(mc, 2018-07-23): this logic is duplicated because can only get props
// into Deck.props.LabwareComponent via redux
const getRobotModules = makeGetRobotModules()

return (state, ownProps) => {
const robot = robotSelectors.getConnectedRobot(state)
const module = robotSelectors.getModulesBySlot(state)[ownProps.slot]
const sessionModules = robotSelectors.getModules(state)
const actualModulesCall = robot && getRobotModules(state, robot)
const actualModules =
actualModulesCall &&
actualModulesCall.response &&
actualModulesCall.response.modules

const requiredNames = countBy(sessionModules, 'name')
const actualNames = countBy(actualModules || [], 'name')
const present =
!module || requiredNames[module.name] === actualNames[module.name]

return {present, module}
}
}
86 changes: 79 additions & 7 deletions app/src/components/ConnectModulesModal/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,89 @@
// @flow
import * as React from 'react'
import {connect} from 'react-redux'
import countBy from 'lodash/countBy'

import {makeGetRobotModules, fetchModules} from '../../http-api-client'
import {selectors as robotSelectors, actions as robotActions} from '../../robot'

import {Deck} from '@opentrons/components'
import {RefreshWrapper} from '../Page'
import {Modal} from '../modals'
import Prompt from './Prompt'
import ReviewModuleItem from './ReviewModuleItem'

import type {State} from '../../types'
import type {RobotService, SessionModule} from '../../robot'
import type {Module} from '../../http-api-client'

type Props = {
modulesMissing: boolean,
onClick: () => mixed,
type OP = {
robot: RobotService
}

export default function ConnectModulesModal (props: Props) {
type SP = {
modulesRequired: boolean,
modulesMissing: boolean
}

type DP = {
setReviewed: () => mixed,
fetchModules: () => mixed
}

type Props = OP & SP & DP

export default connect(
makeMapStateToProps,
mapDispatchToProps
)(ConnectModulesModal)

function ConnectModulesModal (props: Props) {
if (!props.modulesRequired) return null

const {modulesMissing, setReviewed, fetchModules} = props
const onPromptClick = modulesMissing ? fetchModules : setReviewed

return (
<Modal>
<Prompt {...props}/>
</Modal>
<RefreshWrapper refresh={fetchModules}>
<Modal>
<Prompt modulesMissing={modulesMissing} onClick={onPromptClick} />
<Deck LabwareComponent={ReviewModuleItem} />
</Modal>
</RefreshWrapper>
)
}

function makeMapStateToProps (): (state: State, ownProps: OP) => SP {
const getRobotModules = makeGetRobotModules()

return (state, ownProps) => {
const sessionModules = robotSelectors.getModules(state)
const actualModulesCall = getRobotModules(state, ownProps.robot)
const actualModules =
actualModulesCall.response && actualModulesCall.response.modules

return {
modulesRequired: sessionModules.length !== 0,
modulesMissing: checkModulesMissing(sessionModules, actualModules)
}
}
}

function mapDispatchToProps (dispatch: Dispatch, ownProps: OP): DP {
return {
setReviewed: () => dispatch(robotActions.setModulesReviewed(true)),
fetchModules: () => dispatch(fetchModules(ownProps.robot))
}
}

function checkModulesMissing (
required: Array<SessionModule>,
actual: ?Array<Module>
): boolean {
const requiredNames = countBy(required, 'name')
const actualNames = countBy(actual, 'name')

return Object.keys(requiredNames).some(
n => requiredNames[n] !== actualNames[n]
)
}
4 changes: 2 additions & 2 deletions app/src/components/ConnectModulesModal/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
@apply --font-header-light;

font-weight: normal;
margin-bottom: 1rem;
margin-top: 1rem;
text-align: center;
}

.prompt_button {
display: block;
width: auto;
margin: 1rem auto;
margin: 1.5rem auto;
padding-left: 3rem;
padding-right: 3rem;
}
9 changes: 5 additions & 4 deletions app/src/components/DeckMap/ConnectedSlotItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type {State, Dispatch} from '../../types'
import {
selectors as robotSelectors,
actions as robotActions,
type Mount
type Mount,
type SessionModule
} from '../../robot'

import type {LabwareComponentProps} from '@opentrons/components'
Expand All @@ -20,14 +21,14 @@ type OP = LabwareComponentProps & {match: Match}
type SP = {
_calibrator?: ?Mount,
_labware?: $PropertyType<LabwareItemProps, 'labware'>,
module?: {}
module?: SessionModule
}

type DP = {dispatch: Dispatch}

type Props = LabwareComponentProps & {
labware?: $PropertyType<LabwareItemProps, 'labware'>,
module?: {},
module?: SessionModule,
}

export default withRouter(connect(mapStateToProps, null, mergeProps)(SlotItem))
Expand All @@ -38,7 +39,7 @@ function SlotItem (props: Props) {
return (
<React.Fragment>
{module && (
<ModuleItem />
<ModuleItem module={module} />
)}
{labware && (
<LabwareItem
Expand Down
51 changes: 48 additions & 3 deletions app/src/components/DeckMap/ModuleItem.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
// @flow
import * as React from 'react'
import cx from 'classnames'

import {Icon, LabwareContainer} from '@opentrons/components'
import type {SessionModule} from '../../robot'

import {Icon, LabwareContainer, CenteredTextSvg} from '@opentrons/components'
import styles from './styles.css'

export type ModuleItemProps = {
module: SessionModule,
review?: boolean,
present?: boolean,
}

const DIMENSIONS = {
x: -28.3,
y: -2.5,
width: 158.6,
height: 90.5
}

export default function ModuleItem () {
export default function ModuleItem (props: ModuleItemProps) {
return (
<LabwareContainer {...DIMENSIONS}>
<rect
Expand All @@ -22,13 +31,49 @@ export default function ModuleItem () {
height='100%'
fill='#000'
/>
<ModuleItemContents {...props} />
</LabwareContainer>
)
}

function ModuleItemContents (props: ModuleItemProps) {
if (!props.review) {
return (
<Icon
className={styles.module_icon}
x='8'
y='20'
width='16'
name='usb'
/>
</LabwareContainer>
)
}

// TODO(mc, 2018-07-23): displayName?
const {present, module: {name}} = props
const message = present
? (<tspan x='55%'>{name}</tspan>)
: (
<React.Fragment>
<tspan x='55%' dy='-6'>Missing:</tspan>
<tspan x='55%' dy='12'>{name}</tspan>
</React.Fragment>
)

const iconClassName = cx(styles.module_review_icon, {
[styles.module_review_icon_present]: present
})

return (
<React.Fragment>
<Icon
className={iconClassName}
x='8'
y='0'
width='16'
name={present ? 'check-circle' : 'alert-circle'}
/>
<CenteredTextSvg className={styles.module_review_text} text={message} />
</React.Fragment>
)
}
7 changes: 6 additions & 1 deletion app/src/components/DeckMap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import React from 'react'

import {Deck} from '@opentrons/components'
import ConnectedSlotItem from './ConnectedSlotItem'

import ModuleItem from './ModuleItem'
import LabwareItem from './LabwareItem'
import styles from './styles.css'

export default function DeckMap () {
Expand All @@ -15,3 +16,7 @@ export default function DeckMap () {
/>
)
}

export {LabwareItem, ModuleItem}
export type {LabwareItemProps} from './LabwareItem'
export type {ModuleItemProps} from './ModuleItem'
17 changes: 17 additions & 0 deletions app/src/components/DeckMap/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,20 @@
.module_icon {
color: var(--c-white);
}

.module_review_icon {
color: var(--c-orange);
}

.module_review_icon_present {
color: var(--c-green);
}

.module_review_text {
fill: currentColor;
font-size: 0.5rem;
font-weight: var(--fw-semibold);
color: var(--c-white);
text-transform: uppercase;
white-space: pre;
}
23 changes: 1 addition & 22 deletions app/src/components/InstrumentSettings/AttachedModulesCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,8 @@ type Props = OP & SP & DP

const TITLE = 'Modules'

// TODO(mc, 2018-07-19): remove this testing code when API returns modules
// const STUBBED_MODULE_DATA = [
// {
// name: 'tempdeck',
// model: 'temp_deck',
// serial: '123123124',
// fwVersion: '1.2.13',
// status: '86',
// displayName: 'Temperature Module'
// },
// {
// name: 'magdeck',
// model: 'mag_deck',
// serial: '123123124',
// fwVersion: '1.2.13',
// status: 'disengaged',
// displayName: 'Magnetic Bead Module'
// }
// ]

export default connect(makeSTP, DTP)(AttachedModulesCard)

// TODO (ka 2018-6-29): change this to a refresh card once we have endpoints
function AttachedModulesCard (props: Props) {
if (props.modulesFlag) {
return (
Expand Down Expand Up @@ -76,7 +55,7 @@ function makeSTP (): (state: State, ownProps: OP) => SP {

return {
modulesFlag: getModulesOn(state),
modules: modules /* || STUBBED_MODULE_DATA */,
modules: modules,
refreshing: modulesCall.inProgress
}
}
Expand Down
5 changes: 2 additions & 3 deletions app/src/components/ModuleItem/ModuleImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ function getModuleImg (name: string) {
return MODULE_IMGS[name]
}

// TODO (ka 2018-7-10): replace with design assets onces available
const MODULE_IMGS = {
'temp_deck': require('./images/[email protected]'),
'mag_deck': require('./images/[email protected]')
tempdeck: require('./images/[email protected]'),
magdeck: require('./images/[email protected]')
}
Loading

0 comments on commit f2e63e3

Please sign in to comment.