Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(app): add alert to SystemInfoCard if u2e driver outdated #5638

Merged
merged 5 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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