Skip to content

Commit

Permalink
feat(app): enable module firmware update button when update available (
Browse files Browse the repository at this point in the history
…#4923)

Building on previous work to get the newest module fw on the robot per each release, this adds
functionality to the app allowing the user to update an attached module of their connected robot, if that module has an available firmware update.

re #4576, Closes #4575
  • Loading branch information
b-cooper authored Feb 7, 2020
1 parent ae06796 commit 1edc587
Show file tree
Hide file tree
Showing 24 changed files with 797 additions and 19 deletions.
4 changes: 2 additions & 2 deletions app/src/components/InstrumentSettings/ModulesCardContents.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export function ModulesCardContents(props: Props) {

return (
<>
{modules.map((mod, index) => (
<ModuleItem key={index} module={mod} canControl={canControl} />
{modules.map(mod => (
<ModuleItem key={mod.serial} module={mod} canControl={canControl} />
))}
</>
)
Expand Down
98 changes: 91 additions & 7 deletions app/src/components/ModuleItem/ModuleUpdate.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,104 @@
// @flow
import * as React from 'react'
import last from 'lodash/last'
import { useSelector, useDispatch } from 'react-redux'
import {
OutlineButton,
HoverTooltip,
Icon,
AlertModal,
} from '@opentrons/components'
import { Portal } from '../portal'
import { getConnectedRobotName } from '../../robot/selectors'
import {
useDispatchApiRequest,
getRequestById,
PENDING,
FAILURE,
dismissRequest,
} from '../../robot-api'
import { updateModule } from '../../modules'
import type { UpdateModuleAction } from '../../modules/types'
import type { State, Dispatch } from '../../types'
import type { RequestState } from '../../robot-api/types'
import styles from './styles.css'

import { OutlineButton } from '@opentrons/components'
const FW_IS_UP_TO_DATE = 'Module Firmware is up to date'
const CONNECT_TO_UPDATE = 'Connect to Robot to update Module'
const OK_TEXT = 'Ok'

import styles from './styles.css'
const UPDATE = 'update'
const UPDATE_TO_DATE = 'up to date'

const FAILED_UPDATE_HEADER = 'Failed to update Module Firmware'
const FAILED_UPDATE_BODY =
'An error occurred while attempting to update your robot.'

type Props = {|
availableUpdate?: ?string,
hasAvailableUpdate: boolean,
canControl: boolean,
moduleId: string,
|}

export function ModuleUpdate(props: Props) {
const { availableUpdate } = props
const buttonText = availableUpdate ? 'update' : 'updated'
const { hasAvailableUpdate, moduleId, canControl } = props
const dispatch = useDispatch<Dispatch>()
const robotName = useSelector(getConnectedRobotName)
const [
dispatchApiRequest,
requestIds,
] = useDispatchApiRequest<UpdateModuleAction>()

const handleClick = () => {
canControl &&
robotName &&
dispatchApiRequest(updateModule(robotName, moduleId))
}
const latestRequestId = last(requestIds)
const latestRequest = useSelector<State, RequestState | null>(state =>
getRequestById(state, latestRequestId)
)
const isPending = latestRequest?.status === PENDING

const buttonText = hasAvailableUpdate ? UPDATE : UPDATE_TO_DATE
let tooltipText = null
if (!canControl) tooltipText = CONNECT_TO_UPDATE
if (!hasAvailableUpdate) tooltipText = FW_IS_UP_TO_DATE

const handleCloseErrorModal = () => {
dispatch(dismissRequest(latestRequestId))
}
return (
<div className={styles.module_update}>
<OutlineButton disabled={!availableUpdate}>{buttonText}</OutlineButton>
<div className={styles.module_update_wrapper}>
<HoverTooltip tooltipComponent={tooltipText}>
{hoverTooltipHandlers => (
<div {...hoverTooltipHandlers}>
<OutlineButton
className={styles.module_update_button}
onClick={handleClick}
disabled={!canControl || !hasAvailableUpdate || isPending}
>
{isPending ? (
<Icon name="ot-spinner" height="1em" spin />
) : (
buttonText
)}
</OutlineButton>
</div>
)}
</HoverTooltip>
{latestRequest && latestRequest.status === FAILURE && (
<Portal>
<AlertModal
alertOverlay
heading={FAILED_UPDATE_HEADER}
buttons={[{ children: OK_TEXT, onClick: handleCloseErrorModal }]}
>
<p>{FAILED_UPDATE_BODY}</p>
<p>{latestRequest.error.message}</p>
</AlertModal>
</Portal>
)}
</div>
)
}
108 changes: 108 additions & 0 deletions app/src/components/ModuleItem/__tests__/ModuleUpdate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// @flow
import * as React from 'react'
import { Provider } from 'react-redux'
import { shallow, mount } from 'enzyme'
import configureMockStore from 'redux-mock-store'

import { Button } from '@opentrons/components'
import { ModuleUpdate } from '../ModuleUpdate'

const mockStore = configureMockStore([])

describe('ModuleUpdate', () => {
let store
beforeEach(() => {
store = mockStore({
mockState: true,
robot: {
connection: { connectedTo: 'BITTER-MOCK' },
},
robotApi: {},
})
})

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

test('component renders', () => {
const treeAvailableCanControl = shallow(
<Provider store={store}>
<ModuleUpdate
canControl={true}
moduleId="FAKEMODULE1234"
hasAvailableUpdate={true}
/>
</Provider>
)
const treeAvailableNoControl = shallow(
<Provider store={store}>
<ModuleUpdate
canControl={false}
moduleId="FAKEMODULE1234"
hasAvailableUpdate={true}
/>
</Provider>
)
const treeNotAvailableCanControl = shallow(
<Provider store={store}>
<ModuleUpdate
canControl={true}
moduleId="FAKEMODULE1234"
hasAvailableUpdate={false}
/>
</Provider>
)
const treeNotAvailableNoControl = shallow(
<Provider store={store}>
<ModuleUpdate
canControl={false}
moduleId="FAKEMODULE1234"
hasAvailableUpdate={false}
/>
</Provider>
)
expect(treeAvailableCanControl).toMatchSnapshot()
expect(treeNotAvailableCanControl).toMatchSnapshot()
expect(treeAvailableNoControl).toMatchSnapshot()
expect(treeNotAvailableNoControl).toMatchSnapshot()
})

test('displays a Warning for invalid files', () => {
const SPECS = [
{
canControl: true,
hasAvailableUpdate: true,
expectDisabled: false,
},
{
canControl: true,
hasAvailableUpdate: false,
expectDisabled: true,
},
{
canControl: false,
hasAvailableUpdate: true,
expectDisabled: true,
},
{
canControl: false,
hasAvailableUpdate: false,
expectDisabled: true,
},
]

SPECS.forEach(({ canControl, hasAvailableUpdate, expectDisabled }) => {
const wrapper = mount(
<Provider store={store}>
<ModuleUpdate
moduleId="FAKEMODULE1234"
canControl={canControl}
hasAvailableUpdate={hasAvailableUpdate}
/>
</Provider>
)
expect(wrapper.find(Button).prop('disabled')).toEqual(expectDisabled)
})
})
})
Loading

0 comments on commit 1edc587

Please sign in to comment.