Skip to content

Commit

Permalink
feat: opentrons ai client Create New Protocol - Application Section (#…
Browse files Browse the repository at this point in the history
…16578)

# Overview

This PR adds the first part of the Create New Protocol flow for the
Opentrons AI Client
It also refactors the renderWithProviders test util to use Jotai
provider


![image](https://github.com/user-attachments/assets/6b478303-be53-4790-9d21-90952024096d)

## Test Plan and Hands on Testing

- On the landing page click Create a new protocol button, you will be
redirected to the new page
- You can select a Scientific application and describe it. You can also
select other and a new input will be displayed.
- The Prompt Preview component is updated with the data entered.
- 
## Changelog

 - Add Create New Protocol page
 - Add first section Application functionality
 - Header is now sticky
 - Updated renderWithProviders to use Jotai provider

## Review requests

- Verify new page.

## Risk assessment

- low
  • Loading branch information
fbelginetw authored Oct 28, 2024
1 parent 6ae579c commit cebe51a
Show file tree
Hide file tree
Showing 25 changed files with 896 additions and 169 deletions.
40 changes: 25 additions & 15 deletions opentrons-ai-client/src/OpentronsAI.test.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { screen } from '@testing-library/react'
import { describe, it, vi, beforeEach } from 'vitest'
import * as auth0 from '@auth0/auth0-react'

import { renderWithProviders } from './__testing-utils__'
import { i18n } from './i18n'
import { Loading } from './molecules/Loading'

import { OpentronsAI } from './OpentronsAI'
import { Landing } from './pages/Landing'
import { useGetAccessToken } from './resources/hooks'
import { Header } from './molecules/Header'
import { Footer } from './molecules/Footer'
import { HeaderWithMeter } from './molecules/HeaderWithMeter'
import { headerWithMeterAtom } from './resources/atoms'

vi.mock('@auth0/auth0-react')

vi.mock('./pages/Landing')
vi.mock('./molecules/Header')
vi.mock('./molecules/HeaderWithMeter')
vi.mock('./molecules/Footer')
vi.mock('./molecules/Loading')
vi.mock('./resources/hooks/useGetAccessToken')
Expand All @@ -27,9 +28,14 @@ vi.mock('./resources/hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

const initialValues: Array<[any, any]> = [
[headerWithMeterAtom, { displayHeaderWithMeter: false, progress: 0 }],
]

const render = (): ReturnType<typeof renderWithProviders> => {
return renderWithProviders(<OpentronsAI />, {
i18nInstance: i18n,
initialValues,
})
}

Expand All @@ -41,7 +47,14 @@ describe('OpentronsAI', () => {
vi.mocked(Landing).mockReturnValue(<div>mock Landing page</div>)
vi.mocked(Loading).mockReturnValue(<div>mock Loading</div>)
vi.mocked(Header).mockReturnValue(<div>mock Header component</div>)
vi.mocked(HeaderWithMeter).mockReturnValue(
<div>mock Header With Meter component</div>
)
vi.mocked(Footer).mockReturnValue(<div>mock Footer component</div>)
;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({
isAuthenticated: true,
isLoading: false,
})
})

it('should render loading screen when isLoading is true', () => {
Expand All @@ -54,28 +67,25 @@ describe('OpentronsAI', () => {
})

it('should render text', () => {
;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({
isAuthenticated: true,
isLoading: false,
})
render()
screen.getByText('mock Landing page')
})

it('should render Header component', () => {
;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({
isAuthenticated: true,
isLoading: false,
})
it('should render the default Header component if displayHeaderWithMeter is false', () => {
render()

screen.getByText('mock Header component')
})

it('should render Header with meter component if displayHeaderWithMeter is true', () => {
initialValues[0][1].displayHeaderWithMeter = true

render()

screen.getByText('mock Header With Meter component')
})

it('should render Footer component', () => {
;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({
isAuthenticated: true,
isLoading: false,
})
render()
screen.getByText('mock Footer component')
})
Expand Down
44 changes: 32 additions & 12 deletions opentrons-ai-client/src/OpentronsAI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,24 @@ import { useAuth0 } from '@auth0/auth0-react'
import { useAtom } from 'jotai'
import { useEffect } from 'react'
import { Loading } from './molecules/Loading'
import { mixpanelAtom, tokenAtom } from './resources/atoms'
import { headerWithMeterAtom, mixpanelAtom, tokenAtom } from './resources/atoms'
import { useGetAccessToken } from './resources/hooks'
import { initializeMixpanel } from './analytics/mixpanel'
import { useTrackEvent } from './resources/hooks/useTrackEvent'
import { Header } from './molecules/Header'
import { CLIENT_MAX_WIDTH } from './resources/constants'
import { Footer } from './molecules/Footer'
import { HeaderWithMeter } from './molecules/HeaderWithMeter'
import styled from 'styled-components'

export function OpentronsAI(): JSX.Element | null {
const { isAuthenticated, isLoading, loginWithRedirect } = useAuth0()
const [, setToken] = useAtom(tokenAtom)
const [mixpanel] = useAtom(mixpanelAtom)
const [{ displayHeaderWithMeter, progress }] = useAtom(headerWithMeterAtom)
const [mixpanelState, setMixpanelState] = useAtom(mixpanelAtom)
const { getAccessToken } = useGetAccessToken()
const trackEvent = useTrackEvent()

initializeMixpanel(mixpanel)

const fetchAccessToken = async (): Promise<void> => {
try {
const accessToken = await getAccessToken()
Expand All @@ -37,6 +38,11 @@ export function OpentronsAI(): JSX.Element | null {
}
}

if (mixpanelState?.isInitialized === false) {
setMixpanelState({ ...mixpanelState, isInitialized: true })
initializeMixpanel(mixpanelState)
}

useEffect(() => {
if (!isAuthenticated && !isLoading) {
void loginWithRedirect()
Expand All @@ -61,30 +67,44 @@ export function OpentronsAI(): JSX.Element | null {
}

return (
<div
<Flex
id="opentrons-ai"
style={{ width: '100%', height: '100vh', overflow: OVERFLOW_AUTO }}
width={'100%'}
height={'100vh'}
flexDirection={DIRECTION_COLUMN}
>
<StickyHeader>
{displayHeaderWithMeter ? (
<HeaderWithMeter progressPercentage={progress} />
) : (
<Header />
)}
</StickyHeader>

<Flex
height="100%"
flex={1}
flexDirection={DIRECTION_COLUMN}
backgroundColor={COLORS.grey10}
overflow={OVERFLOW_AUTO}
>
<Header />

<Flex
width="100%"
height="100%"
maxWidth={CLIENT_MAX_WIDTH}
alignSelf={ALIGN_CENTER}
flex={1}
>
<HashRouter>
<OpentronsAIRoutes />
</HashRouter>
</Flex>

<Footer />
</Flex>
</div>
</Flex>
)
}

const StickyHeader = styled.div`
position: sticky;
top: 0;
z-index: 100;
`
4 changes: 2 additions & 2 deletions opentrons-ai-client/src/OpentronsAIRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Route, Navigate, Routes } from 'react-router-dom'
import { Landing } from './pages/Landing'

import type { RouteProps } from './resources/types'
import { CreateProtocol } from './pages/CreateProtocol'

const opentronsAIRoutes: RouteProps[] = [
// replace Landing with the correct component
{
Component: Landing,
Component: CreateProtocol,
name: 'Create A New Protocol',
navLinkTo: '/new-protocol',
path: '/new-protocol',
Expand Down
56 changes: 37 additions & 19 deletions opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,49 @@
import type * as React from 'react'
import { QueryClient, QueryClientProvider } from 'react-query'
import { I18nextProvider } from 'react-i18next'
import { Provider } from 'react-redux'
import { vi } from 'vitest'
import { render } from '@testing-library/react'
import { createStore } from 'redux'

import type { PreloadedState, Store } from 'redux'
import type { RenderOptions, RenderResult } from '@testing-library/react'
import { useHydrateAtoms } from 'jotai/utils'
import { Provider } from 'jotai'

export interface RenderWithProvidersOptions<State> extends RenderOptions {
initialState?: State
interface HydrateAtomsProps {
initialValues: Array<[any, any]>
children: React.ReactNode
}

interface TestProviderProps {
initialValues: Array<[any, any]>
children: React.ReactNode
}

const HydrateAtoms = ({
initialValues,
children,
}: HydrateAtomsProps): React.ReactNode => {
useHydrateAtoms(initialValues)
return children
}

export const TestProvider = ({
initialValues,
children,
}: TestProviderProps): React.ReactNode => (
<Provider>
<HydrateAtoms initialValues={initialValues}>{children}</HydrateAtoms>
</Provider>
)

export interface RenderWithProvidersOptions extends RenderOptions {
initialValues?: Array<[any, any]>
i18nInstance: React.ComponentProps<typeof I18nextProvider>['i18n']
}

export function renderWithProviders<State>(
export function renderWithProviders(
Component: React.ReactElement,
options?: RenderWithProvidersOptions<State>
): [RenderResult, Store<State>] {
const { initialState = {}, i18nInstance = null } = options ?? {}

const store: Store<State> = createStore(
vi.fn(),
initialState as PreloadedState<State>
)
store.dispatch = vi.fn()
store.getState = vi.fn(() => initialState) as () => State
options?: RenderWithProvidersOptions
): RenderResult {
const { i18nInstance = null, initialValues = [] } = options ?? {}

const queryClient = new QueryClient()

Expand All @@ -36,7 +54,7 @@ export function renderWithProviders<State>(
> = ({ children }) => {
const BaseWrapper = (
<QueryClientProvider client={queryClient}>
<Provider store={store}>{children}</Provider>
<TestProvider initialValues={initialValues}>{children}</TestProvider>
</QueryClientProvider>
)
if (i18nInstance != null) {
Expand All @@ -48,5 +66,5 @@ export function renderWithProviders<State>(
}
}

return [render(Component, { wrapper: ProviderWrapper }), store]
return render(Component, { wrapper: ProviderWrapper, ...options })
}
7 changes: 2 additions & 5 deletions opentrons-ai-client/src/analytics/mixpanel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import mixpanel from 'mixpanel-browser'
import { getHasOptedIn } from './selectors'

export const getIsProduction = (): boolean =>
global.location.host === 'designer.opentrons.com' // UPDATE THIS TO CORRECT URL
import type { Mixpanel } from '../resources/types'

export type AnalyticsEvent =
| {
Expand All @@ -20,7 +18,7 @@ const MIXPANEL_OPTS = {
opt_out_tracking_by_default: true,
}

export function initializeMixpanel(state: any): void {
export function initializeMixpanel(state: Mixpanel): void {
const optedIn = getHasOptedIn(state) ?? false
if (MIXPANEL_ID != null) {
console.debug('Initializing Mixpanel', { optedIn })
Expand Down Expand Up @@ -53,7 +51,6 @@ export function setMixpanelTracking(optedIn: boolean): void {
// Register "super properties" which are included with all events
mixpanel.register({
appVersion: 'test', // TODO update this?
// NOTE(IL, 2020): Since PD may be in the same Mixpanel project as other OT web apps, this 'appName' property is intended to distinguish it
appName: 'opentronsAIClient',
})
} else {
Expand Down
4 changes: 3 additions & 1 deletion opentrons-ai-client/src/analytics/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const getHasOptedIn = (state: any): boolean | null =>
import type { Mixpanel } from '../resources/types'

export const getHasOptedIn = (state: Mixpanel): boolean | null =>
state.analytics.hasOptedIn
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"application_title": "Application",
"application_scientific_dropdown_title": "What's your scientific application?",
"application_scientific_dropdown_placeholder": "Select an option",
"basic_aliquoting": "Basic aliquoting",
"pcr": "PCR",
"other": "Other",
"application_other_title": "Other application",
"application_other_caption": "Example: “cherrypicking” or “serial dilution”",
"application_describe_title": "Describe what you are trying to do",
"application_describe_caption": "Example: “The protocol performs automated liquid handling for Pierce BCA Protein Assay Kit to determine protein concentrations in various sample types, such as cell lysates and eluates of purification process.",
"section_confirm_button": "Confirm",
"instruments_title": "Instruments",
"modules_title": "Modules",
"labware_liquids_title": "Labware & Liquids",
"steps_title": "Steps"
}
2 changes: 2 additions & 0 deletions opentrons-ai-client/src/assets/localization/en/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import shared from './shared.json'
import protocol_generator from './protocol_generator.json'
import create_protocol from './create_protocol.json'

export const en = {
shared,
protocol_generator,
create_protocol,
}
Loading

0 comments on commit cebe51a

Please sign in to comment.