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

feat: opentrons ai client Create New Protocol - Application Section #16578

Merged
merged 35 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c7123ad
feat: initial commit add landing page and replace App
fbelginetw Oct 17, 2024
e6071a8
refactor: cleanup code and add basic tests
fbelginetw Oct 18, 2024
8d5fd11
feat: add useIsMobile hook
fbelginetw Oct 18, 2024
64d1996
feat: add tests and button onClicks
fbelginetw Oct 18, 2024
5b2022d
feat: add mixpanel analytics
fbelginetw Oct 18, 2024
567b39f
Merge branch 'edge' into opentrons-ai-client-landing-page
fbelginetw Oct 21, 2024
2514383
refactor: add and fix tests
fbelginetw Oct 21, 2024
fde8b2c
Merge branch 'edge' into opentrons-ai-client-landing-page
fbelginetw Oct 21, 2024
ddb7ebc
Merge branch 'edge' into opentrons-ai-client-landing-page
fbelginetw Oct 21, 2024
77ab903
feat: add create protocol page
fbelginetw Oct 22, 2024
4173ea2
refactor: separate translation file and adjust layout
fbelginetw Oct 22, 2024
002550e
refactor: create controlled dropdown and input field
fbelginetw Oct 22, 2024
16cd6af
refactor: simplify accordion to avoid issues with dropdowns
fbelginetw Oct 22, 2024
73ec6c2
feat: add application section, update translation files
fbelginetw Oct 22, 2024
4562b25
refactor: add new section and adjust layout
fbelginetw Oct 22, 2024
94cea0f
Merge branch 'edge' into opentrons-ai-client-landing-page
fbelginetw Oct 22, 2024
7ed7f2b
refactor: add header and adjust for wide screens
fbelginetw Oct 22, 2024
b4bc268
Merge branch 'edge' into opentrons-ai-client-landing-page
fbelginetw Oct 23, 2024
76bd334
refactor: add Footer, update analytics capture for logout, and add tests
fbelginetw Oct 23, 2024
a010d83
refactor: mock useTrackEvent to stop warnings
fbelginetw Oct 23, 2024
760e940
refactor: alphabetical order
fbelginetw Oct 23, 2024
fe51246
Merge branch 'opentrons-ai-client-landing-page' into opentrons-ai-cli…
fbelginetw Oct 23, 2024
2016a5b
feat: add atom to control header with meter
fbelginetw Oct 23, 2024
4031568
Merge branch 'edge' into opentrons-ai-client-application
fbelginetw Oct 23, 2024
db352a4
fix: pr comments from lading page
fbelginetw Oct 23, 2024
a660cd9
feat: add isInitialized to mixpanel to avoid calling it twice
fbelginetw Oct 23, 2024
8bc408a
feat: add atom to control current section
fbelginetw Oct 24, 2024
8eb5b9f
fix: remove unused imports
fbelginetw Oct 24, 2024
9b85412
feat: add logic for focus/current steps, progress bar and isCompleted…
fbelginetw Oct 24, 2024
493125e
refactor: protocol section container and create protocol tests
fbelginetw Oct 24, 2024
e4e0eb6
refactor: add more section tests
fbelginetw Oct 24, 2024
2bdde5e
refactor: sticky header
fbelginetw Oct 24, 2024
5d0df2b
refactor: remame createProtocolTestUtils
fbelginetw Oct 24, 2024
1736018
refactor: update renderWithProviders to use jotai
fbelginetw Oct 25, 2024
73faf1c
refactor: remove todo
fbelginetw Oct 25, 2024
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
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
Loading