diff --git a/app/src/components/ConnectModulesModal/Prompt.js b/app/src/components/ConnectModulesModal/Prompt.js index 882f513c05d..6a24c618f08 100644 --- a/app/src/components/ConnectModulesModal/Prompt.js +++ b/app/src/components/ConnectModulesModal/Prompt.js @@ -25,15 +25,19 @@ const connectedAlertProps = { export default function Prompt (props: Props) { const {modulesMissing, onClick} = props + const alert = modulesMissing ? : + 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 (
{alert} diff --git a/app/src/components/ConnectModulesModal/ReviewModuleItem.js b/app/src/components/ConnectModulesModal/ReviewModuleItem.js new file mode 100644 index 00000000000..50268b4a985 --- /dev/null +++ b/app/src/components/ConnectModulesModal/ReviewModuleItem.js @@ -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 ( + + ) +} + +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} + } +} diff --git a/app/src/components/ConnectModulesModal/index.js b/app/src/components/ConnectModulesModal/index.js index 80db82c9003..21abc2b7898 100644 --- a/app/src/components/ConnectModulesModal/index.js +++ b/app/src/components/ConnectModulesModal/index.js @@ -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 ( - - - + + + + + + + ) +} + +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, + actual: ?Array +): boolean { + const requiredNames = countBy(required, 'name') + const actualNames = countBy(actual, 'name') + + return Object.keys(requiredNames).some( + n => requiredNames[n] !== actualNames[n] ) } diff --git a/app/src/components/ConnectModulesModal/styles.css b/app/src/components/ConnectModulesModal/styles.css index aa8ebda7282..2422c32351b 100644 --- a/app/src/components/ConnectModulesModal/styles.css +++ b/app/src/components/ConnectModulesModal/styles.css @@ -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; } diff --git a/app/src/components/DeckMap/ConnectedSlotItem.js b/app/src/components/DeckMap/ConnectedSlotItem.js index 7dc9fbcea8c..413d942ec75 100644 --- a/app/src/components/DeckMap/ConnectedSlotItem.js +++ b/app/src/components/DeckMap/ConnectedSlotItem.js @@ -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' @@ -20,14 +21,14 @@ type OP = LabwareComponentProps & {match: Match} type SP = { _calibrator?: ?Mount, _labware?: $PropertyType, - module?: {} + module?: SessionModule } type DP = {dispatch: Dispatch} type Props = LabwareComponentProps & { labware?: $PropertyType, - module?: {}, + module?: SessionModule, } export default withRouter(connect(mapStateToProps, null, mergeProps)(SlotItem)) @@ -38,7 +39,7 @@ function SlotItem (props: Props) { return ( {module && ( - + )} {labware && ( + + + ) +} + +function ModuleItemContents (props: ModuleItemProps) { + if (!props.review) { + return ( - + ) + } + + // TODO(mc, 2018-07-23): displayName? + const {present, module: {name}} = props + const message = present + ? ({name}) + : ( + + Missing: + {name} + + ) + + const iconClassName = cx(styles.module_review_icon, { + [styles.module_review_icon_present]: present + }) + + return ( + + + + ) } diff --git a/app/src/components/DeckMap/index.js b/app/src/components/DeckMap/index.js index c2b6adbd9a6..10f3c35467f 100644 --- a/app/src/components/DeckMap/index.js +++ b/app/src/components/DeckMap/index.js @@ -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 () { @@ -15,3 +16,7 @@ export default function DeckMap () { /> ) } + +export {LabwareItem, ModuleItem} +export type {LabwareItemProps} from './LabwareItem' +export type {ModuleItemProps} from './ModuleItem' diff --git a/app/src/components/DeckMap/styles.css b/app/src/components/DeckMap/styles.css index 88b728e1c04..ebd18fe5d7f 100644 --- a/app/src/components/DeckMap/styles.css +++ b/app/src/components/DeckMap/styles.css @@ -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; +} diff --git a/app/src/components/InstrumentSettings/AttachedModulesCard.js b/app/src/components/InstrumentSettings/AttachedModulesCard.js index 467560b41af..4e11e9f53bd 100644 --- a/app/src/components/InstrumentSettings/AttachedModulesCard.js +++ b/app/src/components/InstrumentSettings/AttachedModulesCard.js @@ -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 ( @@ -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 } } diff --git a/app/src/components/ModuleItem/ModuleImage.js b/app/src/components/ModuleItem/ModuleImage.js index 4598f34d424..3d53e2782ab 100644 --- a/app/src/components/ModuleItem/ModuleImage.js +++ b/app/src/components/ModuleItem/ModuleImage.js @@ -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/module-temp@3x.png'), - 'mag_deck': require('./images/module-mag@3x.png') + tempdeck: require('./images/module-temp@3x.png'), + magdeck: require('./images/module-mag@3x.png') } diff --git a/app/src/components/ReviewDeckModal/LabwareComponent.js b/app/src/components/ReviewDeckModal/LabwareComponent.js index 533cd98e4ca..8b0d8ffdadb 100644 --- a/app/src/components/ReviewDeckModal/LabwareComponent.js +++ b/app/src/components/ReviewDeckModal/LabwareComponent.js @@ -3,17 +3,18 @@ import * as React from 'react' import {connect} from 'react-redux' -import {selectors as robotSelectors} from '../../robot' +import {selectors as robotSelectors, type SessionModule} from '../../robot' import type {LabwareComponentProps} from '@opentrons/components' -import LabwareItem, {type LabwareItemProps} from '../DeckMap/LabwareItem' -import ModuleItem from '../DeckMap/ModuleItem' +import type {LabwareItemProps} from '../DeckMap' + +import {LabwareItem, ModuleItem} from '../DeckMap' type OP = LabwareComponentProps type SP = { labware: ?$PropertyType, - module: ?{} + module: ?SessionModule } type Props = OP & SP @@ -24,7 +25,7 @@ function LabwareComponent (props: Props) { return ( {props.module && ( - + )} {props.labware && ( = createSelector( getRobotApiState, (state) => state[MODULES] || {inProgress: false} + // TODO(mc, 2018-07-23): DEBUG code; remove soon + // () => ({ + // inProgress: false, + // error: null, + // request: null, + // response: { + // modules: [ + // { + // 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' + // } + // ] + // } + // }) ) return selector diff --git a/app/src/pages/Calibrate/Labware.js b/app/src/pages/Calibrate/Labware.js index 7f9606ad578..f93cb2231a2 100644 --- a/app/src/pages/Calibrate/Labware.js +++ b/app/src/pages/Calibrate/Labware.js @@ -4,23 +4,21 @@ import * as React from 'react' import {connect} from 'react-redux' import {Route, Redirect, withRouter, type Match} from 'react-router' import {push} from 'react-router-redux' -import countBy from 'lodash/countBy' import type {State, Dispatch} from '../../types' -import type {Labware, Robot, SessionModule} from '../../robot' -import { - selectors as robotSelectors, - actions as robotActions -} from '../../robot' +import type {Labware, Robot} from '../../robot' + +import {selectors as robotSelectors} from '../../robot' +import {makeGetRobotSettings} from '../../http-api-client' import {getModulesOn} from '../../config' -import type {Module} from '../../http-api-client' -import {makeGetRobotSettings, makeGetRobotModules, fetchModules} from '../../http-api-client' -import Page, {RefreshWrapper} from '../../components/Page' + +import Page from '../../components/Page' import CalibrateLabware from '../../components/CalibrateLabware' import SessionHeader from '../../components/SessionHeader' import ReviewDeckModal from '../../components/ReviewDeckModal' import ConfirmModal from '../../components/CalibrateLabware/ConfirmModal' import ConnectModulesModal from '../../components/ConnectModulesModal' + type OP = { match: Match } @@ -30,48 +28,36 @@ type SP = { deckPopulated: boolean, labware: ?Labware, calibrateToBottom: boolean, - _robot: ?Robot, - modulesMissing: boolean, + robot: ?Robot, reviewModules: ?boolean, } -type DP = {dispatch: Dispatch} +type DP = {onBackClick: () => mixed} -type Props = SP & OP & { - onBackClick: () => mixed, - fetchModules: () => mixed, - onReviewPromptClick: () => mixed, -} +type Props = OP & SP & DP -export default withRouter(connect(makeMapStateToProps, null, mergeProps)(SetupDeckPage)) +export default withRouter( + connect(makeMapStateToProps, mapDispatchToProps)(SetupDeckPage) +) function SetupDeckPage (props: Props) { const { + robot, calibrateToBottom, labware, deckPopulated, - modulesMissing, reviewModules, onBackClick, - fetchModules, - onReviewPromptClick, match: {url, params: {slot}} } = props return ( - - )}} - > + + )}}> - {reviewModules && ( - + {robot && reviewModules && ( + )} {(!deckPopulated && !reviewModules) && ( @@ -87,77 +73,47 @@ function SetupDeckPage (props: Props) { ) }} /> - + ) } function makeMapStateToProps (): (state: State, ownProps: OP) => SP { const getRobotSettings = makeGetRobotSettings() - const getRobotModules = makeGetRobotModules() return (state, ownProps) => { const {match: {url, params: {slot}}} = ownProps const labware = robotSelectors.getLabware(state) const currentLabware = labware.find((lw) => lw.slot === slot) - const _robot = robotSelectors.getConnectedRobot(state) + const robot = robotSelectors.getConnectedRobot(state) // TODO(mc, 2018-07-19): API selector for getting the response directly - const settingsResponse = _robot && getRobotSettings(state, _robot).response - const settings = settingsResponse && settingsResponse.settings - const flag = !!settings && settings.find((s) => s.id === 'calibrateToBottom') - const calibrateToBottom = !!flag && flag.value - - const modules = robotSelectors.getModules(state) + const settingsResp = robot && getRobotSettings(state, robot).response + const settings = settingsResp && settingsResp.settings - const modulesCall = _robot && getRobotModules(state, _robot) - const modulesResponse = modulesCall && modulesCall.response - const actualModules = modulesResponse && modulesResponse.modules + // TODO(mc, 2018-07-23): make diagram component a container + const calToBottomFlag = settings && settings.find(s => s.id === 'calibrateToBottom') + const calibrateToBottom = !!calToBottomFlag && calToBottomFlag.value + const modulesEnabled = getModulesOn(state) const modulesReviewed = robotSelectors.getModulesReviewed(state) - const modulesFlag = getModulesOn(state) - const modulesRequired = modules[0] - - const reviewModules = modulesFlag && !modulesReviewed && !!modulesRequired + const reviewModules = modulesEnabled && !modulesReviewed return { slot, url, calibrateToBottom, reviewModules, - _robot, - modulesMissing: checkModules(modules, actualModules), + robot, deckPopulated: !!robotSelectors.getDeckPopulated(state), labware: currentLabware } } } -function mergeProps (stateProps: SP, dispatchProps: DP, ownProps: OP): Props { +function mapDispatchToProps (dispatch: Dispatch, ownProps: OP): DP { const {match: {url}} = ownProps - const {dispatch} = dispatchProps - const {_robot, modulesMissing} = stateProps - - const fetchMods = () => _robot && dispatch(fetchModules(_robot)) - const onReviewPromptClick = modulesMissing - ? fetchMods - : () => dispatch(robotActions.setModulesReviewed(true)) return { - ...stateProps, - ...ownProps, - onReviewPromptClick, - fetchModules: fetchMods, onBackClick: () => dispatch(push(url)) } } - -function checkModules ( - required: Array, - actual: ?Array -): boolean { - const requiredNames = countBy(required, 'name') - const actualNames = countBy(actual, 'name') - - return Object.keys(requiredNames) - .some(n => requiredNames[n] !== actualNames[n]) -} diff --git a/app/src/robot/types.js b/app/src/robot/types.js index 6076a56eef9..3b2a847f6cf 100644 --- a/app/src/robot/types.js +++ b/app/src/robot/types.js @@ -140,7 +140,7 @@ export type SessionModule = { // slot module is installed in slot: Slot, // name identifier of the module - name: ModuleType + name: ModuleType, } export type SessionStatus = diff --git a/components/src/CenteredTextSvg.js b/components/src/CenteredTextSvg.js index 6668e712738..403a956dba8 100644 --- a/components/src/CenteredTextSvg.js +++ b/components/src/CenteredTextSvg.js @@ -2,13 +2,14 @@ import * as React from 'react' type CenteredTextSvgProps = { - text: string, + text: React.Node, className?: string } export function CenteredTextSvg (props: CenteredTextSvgProps) { const { text, className } = props + // TODO(mc, 2018-07-23): add `fill='currentColor'` return ( + {/* TODO(mc, 2018-07-23): use props.children */} {text} )