diff --git a/app/src/components/RobotSettings/ControlsCard.js b/app/src/components/RobotSettings/ControlsCard.js index 5ca9343801d..d6f46fa71ee 100644 --- a/app/src/components/RobotSettings/ControlsCard.js +++ b/app/src/components/RobotSettings/ControlsCard.js @@ -3,7 +3,13 @@ import * as React from 'react' import {connect} from 'react-redux' -import {Card} from '@opentrons/components' +import { + fetchRobotLights, + setRobotLights, + makeGetRobotLights +} from '../../http-api-client' + +import {RefreshCard} from '@opentrons/components' import {LabeledToggle, LabeledButton} from '../controls' import type {State, Dispatch} from '../../types' @@ -11,20 +17,33 @@ import type {Robot} from '../../robot' type OP = Robot -type SP = {} +type SP = { + lightsOn: boolean, +} -type DP = {} +type DP = { + dispatch: Dispatch +} -type Props = OP & SP & DP +type Props = OP & SP & { + fetchLights: () => mixed, + toggleLights: () => mixed +} const TITLE = 'Robot Controls' -export default connect(makeMakeStateToProps, mapDispatchToProps)(ControlsCard) +export default connect(makeMakeStateToProps, null, mergeProps)(ControlsCard) function ControlsCard (props: Props) { + const {name, lightsOn, fetchLights, toggleLights} = props + return ( - - {}}> + +

Control lights on deck.

Return robot to starting position.

-
+ ) } function makeMakeStateToProps (): (state: State, ownProps: OP) => SP { - return (state, ownProps) => ({}) + const getRobotLights = makeGetRobotLights() + + return (state, ownProps) => { + const lights = getRobotLights(state, ownProps) + const lightsOn = !!(lights && lights.response && lights.response.on) + + return {lightsOn} + } } -function mapDispatchToProps (dispatch: Dispatch, ownProps: OP): DP { - return {} +function mergeProps (stateProps: SP, dispatchProps: DP, ownProps: OP): Props { + const {lightsOn} = stateProps + const {dispatch} = dispatchProps + + return { + ...ownProps, + ...stateProps, + fetchLights: () => dispatch(fetchRobotLights(ownProps)), + toggleLights: () => dispatch(setRobotLights(ownProps, !lightsOn)) + } } diff --git a/app/src/http-api-client/__tests__/robot.test.js b/app/src/http-api-client/__tests__/robot.test.js index 287f95aef57..7321dfafb81 100644 --- a/app/src/http-api-client/__tests__/robot.test.js +++ b/app/src/http-api-client/__tests__/robot.test.js @@ -6,9 +6,12 @@ import client from '../client' import { moveRobotTo, home, + fetchRobotLights, + setRobotLights, reducer, makeGetRobotMove, - makeGetRobotHome + makeGetRobotHome, + makeGetRobotLights } from '..' jest.mock('../client') @@ -139,6 +142,86 @@ describe('robot/*', () => { }) }) + describe('fetchRobotLights action creator', () => { + const path = 'lights' + const response = {on: true} + + test('calls GET /robot/lights', () => { + client.__setMockResponse(response) + + return store.dispatch(fetchRobotLights(robot)) + .then(() => expect(client) + .toHaveBeenCalledWith(robot, 'GET', 'robot/lights')) + }) + + test('dispatches ROBOT_REQUEST and ROBOT_SUCCESS', () => { + const expectedActions = [ + {type: 'api:ROBOT_REQUEST', payload: {robot, path}}, + {type: 'api:ROBOT_SUCCESS', payload: {robot, response, path}} + ] + + client.__setMockResponse(response) + + return store.dispatch(fetchRobotLights(robot)) + .then(() => expect(store.getActions()).toEqual(expectedActions)) + }) + + test('dispatches ROBOT_REQUEST and ROBOT_FAILURE', () => { + const error = {name: 'ResponseError', status: '400'} + const expectedActions = [ + {type: 'api:ROBOT_REQUEST', payload: {robot, path}}, + {type: 'api:ROBOT_FAILURE', payload: {robot, error, path}} + ] + + client.__setMockError(error) + + return store.dispatch(fetchRobotLights(robot)) + .then(() => expect(store.getActions()).toEqual(expectedActions)) + }) + }) + + describe('setRobotLights action creator', () => { + const path = 'lights' + const response = {on: false} + + test('calls POST /robot/home to home robot', () => { + const expectedBody = {on: false} + + client.__setMockResponse(response) + + return store.dispatch(setRobotLights(robot, false)) + .then(() => expect(client) + .toHaveBeenCalledWith(robot, 'POST', 'robot/lights', expectedBody)) + }) + + test('dispatches ROBOT_REQUEST and ROBOT_SUCCESS', () => { + const request = {on: true} + const expectedActions = [ + {type: 'api:ROBOT_REQUEST', payload: {robot, request, path}}, + {type: 'api:ROBOT_SUCCESS', payload: {robot, response, path}} + ] + + client.__setMockResponse(response) + + return store.dispatch(setRobotLights(robot, true)) + .then(() => expect(store.getActions()).toEqual(expectedActions)) + }) + + test('dispatches ROBOT_REQUEST and ROBOT_FAILURE', () => { + const request = {on: false} + const error = {name: 'ResponseError', status: '400'} + const expectedActions = [ + {type: 'api:ROBOT_REQUEST', payload: {robot, request, path}}, + {type: 'api:ROBOT_FAILURE', payload: {robot, error, path}} + ] + + client.__setMockError(error) + + return store.dispatch(setRobotLights(robot, false)) + .then(() => expect(store.getActions()).toEqual(expectedActions)) + }) + }) + const REDUCER_REQUEST_RESPONSE_TESTS = [ { path: 'move', @@ -149,6 +232,11 @@ describe('robot/*', () => { path: 'home', request: {target: 'pipette', mount: 'left'}, response: {message: 'we did it!'} + }, + { + path: 'lights', + request: null, + response: {on: true} } ] @@ -239,7 +327,8 @@ describe('robot/*', () => { beforeEach(() => { state.api.robot[NAME] = { home: {inProgress: true}, - move: {inProgress: true} + move: {inProgress: true}, + lights: {inProgress: true} } }) @@ -256,5 +345,12 @@ describe('robot/*', () => { expect(getHome(state, robot)).toEqual(state.api.robot[NAME].home) expect(getHome(state, {name: 'foo'})).toEqual({inProgress: false}) }) + + test('makeGetRobotLights', () => { + const getLights = makeGetRobotLights() + + expect(getLights(state, robot)).toEqual(state.api.robot[NAME].lights) + expect(getLights(state, {name: 'foo'})).toEqual({inProgress: false}) + }) }) }) diff --git a/app/src/http-api-client/index.js b/app/src/http-api-client/index.js index 7471fcb08f3..df919f5408f 100644 --- a/app/src/http-api-client/index.js +++ b/app/src/http-api-client/index.js @@ -46,7 +46,8 @@ export type { export type { RobotMove, - RobotHome + RobotHome, + RobotLights } from './robot' export type { @@ -112,8 +113,11 @@ export { home, moveRobotTo, clearRobotMoveResponse, + fetchRobotLights, + setRobotLights, makeGetRobotMove, - makeGetRobotHome + makeGetRobotHome, + makeGetRobotLights } from './robot' export { diff --git a/app/src/http-api-client/robot.js b/app/src/http-api-client/robot.js index c3a30b28c99..1c73f4f5089 100644 --- a/app/src/http-api-client/robot.js +++ b/app/src/http-api-client/robot.js @@ -49,11 +49,19 @@ type RobotHomeResponse = { message: string, } -type RequestPath = 'move' | 'home' +type RobotLightsRequest = ?{ + on: boolean +} + +type RobotLightsResponse = { + on: boolean +} -type RobotRequest = RobotMoveRequest | RobotHomeRequest +type RequestPath = 'move' | 'home' | 'lights' -type RobotResponse = RobotMoveResponse | RobotHomeResponse +type RobotRequest = RobotMoveRequest | RobotHomeRequest | RobotLightsRequest + +type RobotResponse = RobotMoveResponse | RobotHomeResponse | RobotLightsResponse type RobotRequestAction = {| type: 'api:ROBOT_REQUEST', @@ -99,9 +107,12 @@ export type RobotMove = ApiCall export type RobotHome = ApiCall +export type RobotLights = ApiCall + type RobotByNameState = { move?: RobotMove, home?: RobotHome, + lights?: RobotLights, } type RobotState = { @@ -110,6 +121,7 @@ type RobotState = { const MOVE: RequestPath = 'move' const HOME: RequestPath = 'home' +const LIGHTS: RequestPath = 'lights' export function moveRobotTo ( robot: RobotService, @@ -163,6 +175,37 @@ export function home (robot: RobotService, mount?: Mount): ThunkPromiseAction { } } +export function fetchRobotLights (robot: RobotService): ThunkPromiseAction { + return (dispatch) => { + dispatch(robotRequest(robot, LIGHTS)) + + return client(robot, 'GET', 'robot/lights') + .then( + (response) => robotSuccess(robot, LIGHTS, response), + (error) => robotFailure(robot, LIGHTS, error) + ) + .then(dispatch) + } +} + +export function setRobotLights ( + robot: RobotService, + on: boolean +): ThunkPromiseAction { + const body = {on} + + return (dispatch) => { + dispatch(robotRequest(robot, LIGHTS, body)) + + return client(robot, 'POST', 'robot/lights', body) + .then( + (response) => robotSuccess(robot, LIGHTS, response), + (error) => robotFailure(robot, LIGHTS, error) + ) + .then(dispatch) + } +} + export function robotReducer (state: ?RobotState, action: Action): RobotState { if (!state) return {} @@ -235,6 +278,33 @@ export function robotReducer (state: ?RobotState, action: Action): RobotState { return state } +export const makeGetRobotMove = () => { + const selector: Selector = createSelector( + selectRobotState, + (state) => state.move || {inProgress: false} + ) + + return selector +} + +export const makeGetRobotHome = () => { + const selector: Selector = createSelector( + selectRobotState, + (state) => state.home || {inProgress: false} + ) + + return selector +} + +export const makeGetRobotLights = () => { + const selector: Selector = createSelector( + selectRobotState, + (state) => state.lights || {inProgress: false} + ) + + return selector +} + function robotRequest ( robot: RobotService, path: RequestPath, @@ -262,24 +332,6 @@ function robotFailure ( return {type: 'api:ROBOT_FAILURE', payload: {robot, path, error}} } -export const makeGetRobotMove = () => { - const selector: Selector = createSelector( - selectRobotState, - (state) => state.move || {inProgress: false} - ) - - return selector -} - -export const makeGetRobotHome = () => { - const selector: Selector = createSelector( - selectRobotState, - (state) => state.home || {inProgress: false} - ) - - return selector -} - function selectRobotState (state: State, props: BaseRobot): RobotByNameState { return state.api.robot[props.name] || {} } diff --git a/components/src/structure/RefreshCard.js b/components/src/structure/RefreshCard.js index 548ce75ab56..4caade13590 100644 --- a/components/src/structure/RefreshCard.js +++ b/components/src/structure/RefreshCard.js @@ -10,7 +10,7 @@ type Props = React.ElementProps & { /** a change in the watch prop will trigger a refresh */ watch?: string, /** refreshing flag */ - refreshing: boolean, + refreshing?: boolean, /** refresh function */ refresh: () => mixed, } @@ -26,13 +26,15 @@ export default class RefreshCard extends React.Component { return ( - + {refreshing != null && ( + + )} {children} )