Skip to content

Commit

Permalink
refactor(app): add alert to SystemInfoCard if u2e driver outdated (#5638
Browse files Browse the repository at this point in the history
)

Closes #5491
  • Loading branch information
mcous authored May 12, 2020
1 parent 32b191b commit 116f0c4
Show file tree
Hide file tree
Showing 22 changed files with 653 additions and 117 deletions.
72 changes: 72 additions & 0 deletions app/src/analytics/__tests__/hooks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// @flow
import * as React from 'react'
import { Provider } from 'react-redux'
import { mount } from 'enzyme'
import noop from 'lodash/noop'

import * as Cfg from '../../config'
import * as Mixpanel from '../mixpanel'
import { useTrackEvent } from '../hooks'

import type { State } from '../../types'
import type { Config } from '../../config/types'
import type { AnalyticsEvent, AnalyticsConfig } from '../types'

jest.mock('../../config')
jest.mock('../mixpanel')

const getConfig: JestMockFn<[State], $Shape<Config>> = Cfg.getConfig
const trackEvent: JestMockFn<[AnalyticsEvent, AnalyticsConfig], void> =
Mixpanel.trackEvent

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

const MOCK_ANALYTICS_CONFIG = {
appId: 'abc',
optedIn: true,
seenOptIn: true,
}

describe('analytics hooks', () => {
beforeEach(() => {
getConfig.mockImplementation(state => {
expect(state).toBe(MOCK_STATE)
return { analytics: MOCK_ANALYTICS_CONFIG }
})
})

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

describe('useTrackEvent', () => {
let trackEventResult

const TestTrackEvent = () => {
trackEventResult = useTrackEvent()
return <></>
}

const render = () => {
return mount(<TestTrackEvent />, {
wrappingComponent: Provider,
wrappingComponentProps: {
store: {
subscribe: noop,
dispatch: noop,
getState: () => MOCK_STATE,
},
},
})
}

it('should return a trackEvent function with config bound from state', () => {
const event = { name: 'someEvent', properties: { foo: 'bar' } }

render()
trackEventResult(event)

expect(trackEvent).toHaveBeenCalledWith(event, MOCK_ANALYTICS_CONFIG)
})
})
})
2 changes: 1 addition & 1 deletion app/src/analytics/epics.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const optIntoAnalyticsEpic: Epic = (_, state$) =>
ignoreElements()
)

export const analyticsEpic = combineEpics(
export const analyticsEpic: Epic = combineEpics(
sendAnalyticsEventEpic,
optIntoAnalyticsEpic
)
18 changes: 18 additions & 0 deletions app/src/analytics/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @flow
import { useSelector } from 'react-redux'

import { getConfig } from '../config'
import { trackEvent } from './mixpanel'

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

/**
* React hook to send an analytics tracking event directly from a component
*
* @returns {AnalyticsEvent => void} track event function
*/
export function useTrackEvent(): AnalyticsEvent => void {
const config = useSelector((state: State) => getConfig(state).analytics)
return event => trackEvent(event, config)
}
1 change: 1 addition & 0 deletions app/src/analytics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { initializeMixpanel } from './mixpanel'

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

export * from './hooks'
export * from './selectors'
export { analyticsEpic } from './epics'

Expand Down
67 changes: 49 additions & 18 deletions app/src/components/Alerts/U2EDriverOutdatedAlert.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
// @flow
import * as React from 'react'
import { Link } from 'react-router-dom'
import { useHistory, Link as InternalLink } from 'react-router-dom'
import styled from 'styled-components'

import { AlertModal, CheckboxField, useToggle } from '@opentrons/components'
import {
AlertModal,
CheckboxField,
Link,
useToggle,
} from '@opentrons/components'
import { useFeatureFlag } from '../../config'
import { U2E_DRIVER_UPDATE_URL } from '../../system-info'
import { useTrackEvent } from '../../analytics'
import {
U2E_DRIVER_UPDATE_URL,
U2E_DRIVER_OUTDATED_MESSAGE,
U2E_DRIVER_DESCRIPTION,
U2E_DRIVER_OUTDATED_CTA,
EVENT_U2E_DRIVER_ALERT_DISMISSED,
EVENT_U2E_DRIVER_LINK_CLICKED,
} from '../../system-info'
import type { AlertProps } from './types'

// TODO(mc, 2020-05-07): i18n
const DRIVER_OUT_OF_DATE = 'Realtek USB-to-Ethernet Driver Out of Date'
const DRIVER_OUT_OF_DATE = 'Realtek USB-to-Ethernet Driver Update Available'
const VIEW_ADAPTER_INFO = 'view adapter info'
const GET_UPDATE = 'get update'
const DONT_REMIND_ME_AGAIN = "Don't remind me again"

const DRIVER_UPDATE_DESCRIPTION =
"It looks like your computer's Realtek USB-to-Ethernet adapter driver may be out of date. The OT-2 uses this adapter for its USB connection to your computer."
const DRIVER_UPDATE_CTA =
"Please update your computer's driver to ensure you can connect to your OT-2."

const ADAPTER_INFO_URL = '/menu/network-and-system'

const LinkButton = styled(Link)`
width: auto;
padding-left: 1rem;
padding-right: 1rem;
`

const IgnoreCheckbox = styled(CheckboxField)`
position: absolute;
left: 1rem;
bottom: 1.5rem;
`

export function U2EDriverOutdatedAlert(props: AlertProps) {
const history = useHistory()
const trackEvent = useTrackEvent()
const [rememberDismiss, toggleRememberDismiss] = useToggle()
const dismissAlert = () => props.dismissAlert(rememberDismiss)
const { dismissAlert } = props

// TODO(mc, 2020-05-07): remove this feature flag
const enabled = useFeatureFlag('enableSystemInfo')
Expand All @@ -43,23 +59,38 @@ export function U2EDriverOutdatedAlert(props: AlertProps) {
heading={DRIVER_OUT_OF_DATE}
buttons={[
{
Component: Link,
Component: LinkButton,
as: InternalLink,
to: ADAPTER_INFO_URL,
children: VIEW_ADAPTER_INFO,
onClick: dismissAlert,
onClick: () => {
dismissAlert(rememberDismiss)
trackEvent({
name: EVENT_U2E_DRIVER_ALERT_DISMISSED,
properties: { rememberDismiss },
})
},
},
{
Component: 'a',
Component: LinkButton,
href: U2E_DRIVER_UPDATE_URL,
target: '_blank',
rel: 'noopener noreferrer',
external: true,
children: GET_UPDATE,
onClick: dismissAlert,
onClick: () => {
history.push(ADAPTER_INFO_URL)
dismissAlert(rememberDismiss)
trackEvent({
name: EVENT_U2E_DRIVER_LINK_CLICKED,
properties: { source: 'modal' },
})
},
},
]}
>
<p>{DRIVER_UPDATE_DESCRIPTION}</p>
<p>{DRIVER_UPDATE_CTA}</p>
<p>
{U2E_DRIVER_OUTDATED_MESSAGE} {U2E_DRIVER_DESCRIPTION}
</p>
<p>{U2E_DRIVER_OUTDATED_CTA}</p>
<IgnoreCheckbox
label={DONT_REMIND_ME_AGAIN}
value={rememberDismiss}
Expand Down
30 changes: 28 additions & 2 deletions app/src/components/Alerts/__tests__/U2EDriverOutdatedAlert.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import { act } from 'react-dom/test-utils'
import { mount } from 'enzyme'

import { AlertModal } from '@opentrons/components'
import * as Analytics from '../../../analytics'
import { U2EDriverOutdatedAlert } from '../U2EDriverOutdatedAlert'

// TODO(mc, 2020-05-07): create a tested Link wrapper that's safe to mock
jest.mock('../../../analytics')

jest.mock('react-router-dom', () => ({
// TODO(mc, 2020-05-11): code smell; figure out a better way to test usage
// of the useHistory hook
useHistory: () => ({ push: jest.fn() }),
// TODO(mc, 2020-05-07): create a tested Link wrapper that's safe to mock
Link: () => <></>,
}))

Expand All @@ -19,12 +25,20 @@ jest.mock('../../../config/hooks', () => ({
const EXPECTED_DOWNLOAD_URL =
'https://www.realtek.com/component/zoo/category/network-interface-controllers-10-100-1000m-gigabit-ethernet-usb-3-0-software'

const useTrackEvent: JestMockFn<[], $Call<typeof Analytics.useTrackEvent>> =
Analytics.useTrackEvent

describe('U2EDriverOutdatedAlert', () => {
const dismissAlert = jest.fn()
const trackEvent = jest.fn()
const render = () => {
return mount(<U2EDriverOutdatedAlert dismissAlert={dismissAlert} />)
}

beforeEach(() => {
useTrackEvent.mockReturnValue(trackEvent)
})

afterEach(() => {
jest.resetAllMocks()
})
Expand All @@ -34,7 +48,7 @@ describe('U2EDriverOutdatedAlert', () => {
const alertModal = wrapper.find(AlertModal)

expect(alertModal.prop('heading')).toBe(
'Realtek USB-to-Ethernet Driver Out of Date'
'Realtek USB-to-Ethernet Driver Update Available'
)
})

Expand All @@ -46,6 +60,10 @@ describe('U2EDriverOutdatedAlert', () => {

expect(link.prop('children')).toContain('view adapter info')
expect(dismissAlert).toHaveBeenCalledWith(false)
expect(trackEvent).toHaveBeenCalledWith({
name: 'u2eDriverAlertDismissed',
properties: { rememberDismiss: false },
})
})

it('should have a link to the Realtek website', () => {
Expand All @@ -56,6 +74,10 @@ describe('U2EDriverOutdatedAlert', () => {

expect(link.prop('children')).toContain('get update')
expect(dismissAlert).toHaveBeenCalledWith(false)
expect(trackEvent).toHaveBeenCalledWith({
name: 'u2eDriverLinkClicked',
properties: { source: 'modal' },
})
})

it('should be able to perma-ignore the alert', () => {
Expand All @@ -69,5 +91,9 @@ describe('U2EDriverOutdatedAlert', () => {
wrapper.find('Link[to="/menu/network-and-system"]').invoke('onClick')()

expect(dismissAlert).toHaveBeenCalledWith(true)
expect(trackEvent).toHaveBeenCalledWith({
name: 'u2eDriverAlertDismissed',
properties: { rememberDismiss: true },
})
})
})
45 changes: 40 additions & 5 deletions app/src/components/SystemInfoCard/U2EAdapterInfo.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,54 @@
// @flow
import * as React from 'react'
import { useSelector } from 'react-redux'
import { css } from 'styled-components'

import { ControlSection } from '@opentrons/components'
import { getU2EAdapterDevice } from '../../system-info'
import {
Text,
FONT_SIZE_BODY_1,
FONT_WEIGHT_SEMIBOLD,
} from '@opentrons/components'

import * as SystemInfo from '../../system-info'
import { U2EDriverWarning } from './U2EDriverWarning'
import { U2EDeviceDetails } from './U2EDeviceDetails'

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

const U2E_ADAPTER_INFORMATION = 'USB-to-Ethernet Adapter Information'

export const U2EAdapterInfo = () => {
const device = useSelector(getU2EAdapterDevice)
const device = useSelector(SystemInfo.getU2EAdapterDevice)
const driverOutdated = useSelector((state: State) => {
const status = SystemInfo.getU2EWindowsDriverStatus(state)
return status === SystemInfo.OUTDATED
})

return (
<ControlSection title={U2E_ADAPTER_INFORMATION}>
<div
css={css`
font-size: ${FONT_SIZE_BODY_1};
padding: 1rem;
`}
>
<Text
as="h3"
fontSize={FONT_SIZE_BODY_1}
fontWeight={FONT_WEIGHT_SEMIBOLD}
css={css`
margin-bottom: 0.5rem;
`}
>
{U2E_ADAPTER_INFORMATION}
</Text>
{driverOutdated && (
<U2EDriverWarning
css={css`
margin-bottom: 1rem;
`}
/>
)}
<U2EDeviceDetails device={device} />
</ControlSection>
</div>
)
}
Loading

0 comments on commit 116f0c4

Please sign in to comment.