Skip to content

Commit

Permalink
feat(app): add Jupyter Notebook button to robot's Advanced Settings (#…
Browse files Browse the repository at this point in the history
…6474)

Closes #6102
  • Loading branch information
mcous authored Sep 9, 2020
1 parent 4e67e31 commit d615d2d
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 46 deletions.
5 changes: 4 additions & 1 deletion app/src/components/RobotSettings/AdvancedSettingsCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { downloadLogs } from '../../shell/robot-logs/actions'
import { getRobotLogsDownloading } from '../../shell/robot-logs/selectors'
import { Portal } from '../portal'
import {
BORDER_SOLID_LIGHT,
AlertModal,
Card,
LabeledButton,
Expand All @@ -23,6 +24,7 @@ import {
} from '@opentrons/components'

import { UploadRobotUpdate } from './UploadRobotUpdate'
import { OpenJupyterControl } from './OpenJupyterControl'

import type { State, Dispatch } from '../../types'
import type { ViewableRobot } from '../../discovery/types'
Expand Down Expand Up @@ -54,7 +56,7 @@ export function AdvancedSettingsCard(
props: AdvancedSettingsCardProps
): React.Node {
const { robot, resetUrl } = props
const { name, health, status } = robot
const { name, ip, health, status } = robot
const settings = useSelector<State, RobotSettings>(state =>
getRobotSettings(state, name)
)
Expand Down Expand Up @@ -100,6 +102,7 @@ export function AdvancedSettingsCard(
>
<p>Restore robot to factory configuration</p>
</LabeledButton>
<OpenJupyterControl robotIp={ip} borderBottom={BORDER_SOLID_LIGHT} />
{settings.map(({ id, title, description, value }) => (
<LabeledToggle
key={id}
Expand Down
60 changes: 60 additions & 0 deletions app/src/components/RobotSettings/OpenJupyterControl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @flow
import * as React from 'react'

import { SecondaryBtn, Link } from '@opentrons/components'
import { useTrackEvent } from '../../analytics'
import { TitledControl } from '../TitledControl'

import type { StyleProps } from '@opentrons/components'

// TODO(mc, 2020-09-09): i18n
const OPEN = 'Open'
const JUPYTER_NOTEBOOK = 'Jupyter Notebook'
const OPEN_JUPYTER_DESCRIPTION = (
<>
Open the{' '}
<Link external href="https://jupyter.org/">
Jupyter Notebook
</Link>{' '}
running on this OT-2 in your web browser. (Experimental feature! See{' '}
<Link
external
href="https://docs.opentrons.com/v2/new_advanced_running.html#jupyter-notebook"
>
documentation
</Link>{' '}
for more details.)
</>
)

const EVENT_JUPYTER_OPEN = { name: 'jupyterOpen', properties: {} }

export type OpenJupyterControlProps = {|
robotIp: string,
...StyleProps,
|}

export function OpenJupyterControl(props: OpenJupyterControlProps): React.Node {
const { robotIp, ...styleProps } = props
const href = `http://${robotIp}:48888`
const trackEvent = useTrackEvent()

return (
<TitledControl
{...styleProps}
title={JUPYTER_NOTEBOOK}
description={OPEN_JUPYTER_DESCRIPTION}
control={
<SecondaryBtn
onClick={() => trackEvent(EVENT_JUPYTER_OPEN)}
as={Link}
href={href}
width="9rem"
external
>
{OPEN}
</SecondaryBtn>
}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @flow
import * as React from 'react'
import { BORDER_SOLID_LIGHT } from '@opentrons/components'
import { mountWithStore } from '@opentrons/components/__utils__'

import * as RobotSettings from '../../../robot-settings'
import { mockConnectableRobot } from '../../../discovery/__fixtures__'
import { AdvancedSettingsCard } from '../AdvancedSettingsCard'
import { OpenJupyterControl } from '../OpenJupyterControl'

import type { State } from '../../../types'

jest.mock('react-router-dom', () => ({ Link: 'a' }))
jest.mock('../../../analytics')
jest.mock('../../../robot-settings/selectors')
jest.mock('../../../shell/robot-logs/selectors')

const getRobotSettings: JestMockFn<
[State, string],
$Call<typeof RobotSettings.getRobotSettings, State, string>
> = RobotSettings.getRobotSettings

// TODO(mc, 2020-09-09): flesh out these tests
describe('RobotSettings > AdvancedSettingsCard', () => {
const render = (robot = mockConnectableRobot) => {
const resetUrl = `/robots/${robot.name}/reset`
return mountWithStore(
<AdvancedSettingsCard robot={robot} resetUrl={resetUrl} />
)
}

beforeEach(() => {
getRobotSettings.mockReturnValue([])
})

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

it('should render an OpenJupyterControl', () => {
const { wrapper } = render()
const openJupyter = wrapper.find(OpenJupyterControl)

expect(openJupyter.prop('robotIp')).toBe(mockConnectableRobot.ip)
expect(openJupyter.prop('borderBottom')).toBe(BORDER_SOLID_LIGHT)
})
})
77 changes: 32 additions & 45 deletions app/src/components/RobotSettings/__tests__/ControlsCard.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @flow
import * as React from 'react'
import { Provider } from 'react-redux'
import { mount } from 'enzyme'
import { mountWithStore } from '@opentrons/components/__utils__'

import * as RobotControls from '../../../robot-controls'
import * as RobotAdmin from '../../../robot-admin'
Expand All @@ -15,7 +14,7 @@ import { LabeledToggle, LabeledButton } from '@opentrons/components'
import { CONNECTABLE, UNREACHABLE } from '../../../discovery'
import { DeckCalibrationWarning } from '../DeckCalibrationWarning'

import type { State } from '../../../types'
import type { State, Action } from '../../../types'
import type { ViewableRobot } from '../../../discovery/types'

jest.mock('../../../robot-controls/selectors')
Expand Down Expand Up @@ -60,9 +59,15 @@ const getFeatureFlags: JestMockFn<
$Call<typeof Config.getFeatureFlags, State>
> = Config.getFeatureFlags

const MOCK_STATE: State = ({ mockState: true }: any)

describe('ControlsCard', () => {
let mockStore
let render
const render = (robot: ViewableRobot = mockRobot) => {
return mountWithStore<_, State, Action>(
<ControlsCard robot={robot} calibrateDeckUrl="/deck/calibrate" />,
{ initialState: MOCK_STATE }
)
}

const getDeckCalButton = wrapper =>
wrapper
Expand Down Expand Up @@ -90,26 +95,8 @@ describe('ControlsCard', () => {

beforeEach(() => {
jest.useFakeTimers()
mockStore = {
subscribe: () => {},
getState: () => ({
mockState: true,
}),
dispatch: jest.fn(),
}

getDeckCalibrationStatus.mockReturnValue(Calibration.DECK_CAL_STATUS_OK)
getFeatureFlags.mockReturnValue({})

render = (robot: ViewableRobot = mockRobot) => {
return mount(
<ControlsCard robot={robot} calibrateDeckUrl="/deck/calibrate" />,
{
wrappingComponent: Provider,
wrappingComponentProps: { store: mockStore },
}
)
}
})

afterEach(() => {
Expand All @@ -119,27 +106,27 @@ describe('ControlsCard', () => {
})

it('calls fetchLights on mount', () => {
render()
const { store } = render()

expect(mockStore.dispatch).toHaveBeenCalledWith(
expect(store.dispatch).toHaveBeenCalledWith(
RobotControls.fetchLights(mockRobot.name)
)
})

it('calls fetchCalibrationStatus on mount and on a 10s interval', () => {
render()
const { store } = render()

expect(mockStore.dispatch).toHaveBeenCalledWith(
expect(store.dispatch).toHaveBeenCalledWith(
Calibration.fetchCalibrationStatus(mockRobot.name)
)
mockStore.dispatch.mockReset()
store.dispatch.mockReset()
jest.advanceTimersByTime(20000)
expect(mockStore.dispatch).toHaveBeenCalledTimes(2)
expect(mockStore.dispatch).toHaveBeenNthCalledWith(
expect(store.dispatch).toHaveBeenCalledTimes(2)
expect(store.dispatch).toHaveBeenNthCalledWith(
1,
Calibration.fetchCalibrationStatus(mockRobot.name)
)
expect(mockStore.dispatch).toHaveBeenNthCalledWith(
expect(store.dispatch).toHaveBeenNthCalledWith(
2,
Calibration.fetchCalibrationStatus(mockRobot.name)
)
Expand All @@ -148,39 +135,39 @@ describe('ControlsCard', () => {
it('calls updateLights with toggle on button click', () => {
mockGetLightsOn.mockReturnValue(true)

const wrapper = render()
const { wrapper, store } = render()

getLightsButton(wrapper).invoke('onClick')()

expect(mockStore.dispatch).toHaveBeenCalledWith(
expect(store.dispatch).toHaveBeenCalledWith(
RobotControls.updateLights(mockRobot.name, false)
)
})

it('calls restartRobot on button click', () => {
const wrapper = render()
const { wrapper, store } = render()

getRestartButton(wrapper).invoke('onClick')()

expect(mockStore.dispatch).toHaveBeenCalledWith(
expect(store.dispatch).toHaveBeenCalledWith(
RobotAdmin.restartRobot(mockRobot.name)
)
})

it('calls home on button click', () => {
const wrapper = render()
const { wrapper, store } = render()

getHomeButton(wrapper).invoke('onClick')()

expect(mockStore.dispatch).toHaveBeenCalledWith(
expect(store.dispatch).toHaveBeenCalledWith(
RobotControls.home(mockRobot.name, RobotControls.ROBOT)
)
})

it('DC, check cal, home, and restart buttons enabled if connected and not running', () => {
mockGetIsRunning.mockReturnValue(false)

const wrapper = render()
const { wrapper } = render()

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(false)
expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
Expand All @@ -191,7 +178,7 @@ describe('ControlsCard', () => {
})

it('DC, check cal, home, and restart buttons disabled if not connectable', () => {
const wrapper = render(mockUnconnectableRobot)
const { wrapper } = render(mockUnconnectableRobot)

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(true)
expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
Expand All @@ -208,7 +195,7 @@ describe('ControlsCard', () => {
status: CONNECTABLE,
}: any)

const wrapper = render(mockRobotNotConnected)
const { wrapper } = render(mockRobotNotConnected)

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(true)
expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
Expand All @@ -221,7 +208,7 @@ describe('ControlsCard', () => {
it('DC, check cal, home, and restart buttons disabled if protocol running', () => {
mockGetIsRunning.mockReturnValue(true)

const wrapper = render()
const { wrapper } = render()

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(true)
expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
Expand All @@ -234,18 +221,18 @@ describe('ControlsCard', () => {
it('does not render check cal button if GET /calibration/status has not responded', () => {
getDeckCalibrationStatus.mockReturnValue(null)

const wrapper = render()
const { wrapper } = render()
expect(wrapper.exists(CheckCalibrationControl)).toBe(false)
})

it('disables check cal button if deck calibration is bad', () => {
getDeckCalibrationStatus.mockImplementation((state, rName) => {
expect(state).toEqual({ mockState: true })
expect(state).toEqual(MOCK_STATE)
expect(rName).toEqual(mockRobot.name)
return Calibration.DECK_CAL_STATUS_BAD_CALIBRATION
})

const wrapper = render()
const { wrapper } = render()

expect(getCheckCalibrationControl(wrapper).prop('disabledReason')).toBe(
'Bad deck calibration detected. Please perform a full deck calibration.'
Expand Down Expand Up @@ -273,7 +260,7 @@ describe('ControlsCard', () => {
})

it('DeckCalibrationWarning component renders if deck calibration is bad', () => {
const wrapper = render()
const { wrapper } = render()

// check that the deck calibration warning component is not null
// TODO(lc, 2020-06-18): Mock out the new transform status such that
Expand Down
Loading

0 comments on commit d615d2d

Please sign in to comment.