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 landing page #16552

Merged
merged 16 commits into from
Oct 23, 2024
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
52 changes: 6 additions & 46 deletions opentrons-ai-client/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { fireEvent, screen } from '@testing-library/react'
import { screen } from '@testing-library/react'
import { describe, it, vi, beforeEach, expect } from 'vitest'
import * as auth0 from '@auth0/auth0-react'

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

import { App } from './App'
import { OpentronsAI } from './OpentronsAI'

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

const mockLogout = vi.fn()

vi.mock('./molecules/SidePanel')
vi.mock('./organisms/MainContentContainer')
vi.mock('./molecules/Loading')
vi.mock('./OpentronsAI')

const render = (): ReturnType<typeof renderWithProviders> => {
return renderWithProviders(<App />, {
Expand All @@ -26,42 +17,11 @@ const render = (): ReturnType<typeof renderWithProviders> => {

describe('App', () => {
beforeEach(() => {
vi.mocked(SidePanel).mockReturnValue(<div>mock SidePanel</div>)
vi.mocked(MainContentContainer).mockReturnValue(
<div>mock MainContentContainer</div>
)
vi.mocked(Loading).mockReturnValue(<div>mock Loading</div>)
})

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

it('should render text', () => {
;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({
isAuthenticated: true,
isLoading: false,
})
render()
screen.getByText('mock SidePanel')
screen.getByText('mock MainContentContainer')
screen.getByText('Logout')
vi.mocked(OpentronsAI).mockReturnValue(<div>mock OpentronsAI</div>)
})

it('should call a mock function when clicking logout button', () => {
;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({
isAuthenticated: true,
isLoading: false,
logout: mockLogout,
})
it('should render OpentronsAI', () => {
render()
const logoutButton = screen.getByText('Logout')
fireEvent.click(logoutButton)
expect(mockLogout).toHaveBeenCalled()
expect(screen.getByText('mock OpentronsAI')).toBeInTheDocument()
})
})
81 changes: 2 additions & 79 deletions opentrons-ai-client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,5 @@
import { useEffect } from 'react'
import { useAuth0 } from '@auth0/auth0-react'
import { useTranslation } from 'react-i18next'
import { useForm, FormProvider } from 'react-hook-form'
import { useAtom } from 'jotai'
import {
COLORS,
Flex,
Link as LinkButton,
POSITION_ABSOLUTE,
POSITION_RELATIVE,
TYPOGRAPHY,
} from '@opentrons/components'

import { tokenAtom } from './resources/atoms'
import { useGetAccessToken } from './resources/hooks'
import { SidePanel } from './molecules/SidePanel'
import { Loading } from './molecules/Loading'
import { MainContentContainer } from './organisms/MainContentContainer'

export interface InputType {
userPrompt: string
}
import { OpentronsAI } from './OpentronsAI'

export function App(): JSX.Element | null {
const { t } = useTranslation('protocol_generator')
const { isAuthenticated, logout, isLoading, loginWithRedirect } = useAuth0()
const [, setToken] = useAtom(tokenAtom)
const { getAccessToken } = useGetAccessToken()

const fetchAccessToken = async (): Promise<void> => {
try {
const accessToken = await getAccessToken()
setToken(accessToken)
} catch (error) {
console.error('Error fetching access token:', error)
}
}
const methods = useForm<InputType>({
defaultValues: {
userPrompt: '',
},
})

useEffect(() => {
if (!isAuthenticated && !isLoading) {
void loginWithRedirect()
}
if (isAuthenticated) {
void fetchAccessToken()
}
}, [isAuthenticated, isLoading, loginWithRedirect])

if (isLoading) {
return <Loading />
}

if (!isAuthenticated) {
return null
}

return (
<Flex
position={POSITION_RELATIVE}
minHeight="100vh"
backgroundColor={COLORS.grey10}
>
<Flex position={POSITION_ABSOLUTE} top="1rem" right="1rem">
<LinkButton
onClick={() => logout()}
textDecoration={TYPOGRAPHY.textDecorationUnderline}
>
{t('logout')}
</LinkButton>
</Flex>
<FormProvider {...methods}>
<SidePanel />
<MainContentContainer />
</FormProvider>
</Flex>
)
return <OpentronsAI />
}
82 changes: 82 additions & 0 deletions opentrons-ai-client/src/OpentronsAI.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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'

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

vi.mock('./pages/Landing')
vi.mock('./molecules/Header')
vi.mock('./molecules/Footer')
vi.mock('./molecules/Loading')
vi.mock('./resources/hooks/useGetAccessToken')
vi.mock('./analytics/mixpanel')

const mockUseTrackEvent = vi.fn()

vi.mock('./resources/hooks/useTrackEvent', () => ({
useTrackEvent: () => mockUseTrackEvent,
}))

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

describe('OpentronsAI', () => {
beforeEach(() => {
vi.mocked(useGetAccessToken).mockReturnValue({
getAccessToken: vi.fn().mockResolvedValue('mock access token'),
})
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(Footer).mockReturnValue(<div>mock Footer component</div>)
})

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

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,
})
render()
screen.getByText('mock Header component')
})

it('should render Footer component', () => {
;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({
isAuthenticated: true,
isLoading: false,
})
render()
screen.getByText('mock Footer component')
})
})
90 changes: 90 additions & 0 deletions opentrons-ai-client/src/OpentronsAI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { HashRouter } from 'react-router-dom'
import {
DIRECTION_COLUMN,
Flex,
OVERFLOW_AUTO,
COLORS,
ALIGN_CENTER,
} from '@opentrons/components'
import { OpentronsAIRoutes } from './OpentronsAIRoutes'
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 { 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'

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

initializeMixpanel(mixpanel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be done outside of the component so we don't initialize mixpanel multiple times right? otherwise we'll call this on every render. maybe we can have a single initialize function that gets called at the root of the project or something


const fetchAccessToken = async (): Promise<void> => {
try {
const accessToken = await getAccessToken()
setToken(accessToken)
} catch (error) {
console.error('Error fetching access token:', error)
}
}

useEffect(() => {
if (!isAuthenticated && !isLoading) {
void loginWithRedirect()
}
if (isAuthenticated) {
void fetchAccessToken()
}
}, [isAuthenticated, isLoading, loginWithRedirect])

useEffect(() => {
if (isAuthenticated) {
trackEvent({ name: 'user-login', properties: {} })
}
}, [isAuthenticated])

if (isLoading) {
return <Loading />
}

if (!isAuthenticated) {
return null
}

return (
<div
id="opentrons-ai"
style={{ width: '100%', height: '100vh', overflow: OVERFLOW_AUTO }}
>
<Flex
height="100%"
flexDirection={DIRECTION_COLUMN}
backgroundColor={COLORS.grey10}
>
<Header />

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

<Footer />
</Flex>
</div>
)
}
39 changes: 39 additions & 0 deletions opentrons-ai-client/src/OpentronsAIRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Route, Navigate, Routes } from 'react-router-dom'
import { Landing } from './pages/Landing'

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

const opentronsAIRoutes: RouteProps[] = [
// replace Landing with the correct component
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this meant to be done later?

{
Component: Landing,
name: 'Create A New Protocol',
navLinkTo: '/new-protocol',
path: '/new-protocol',
},
{
Component: Landing,
name: 'Update An Existing Protocol',
navLinkTo: '/update-protocol',
path: '/update-protocol',
},
]

export function OpentronsAIRoutes(): JSX.Element {
const landingPage: RouteProps = {
Component: Landing,
name: 'Landing',
navLinkTo: '/',
path: '/',
}
const allRoutes: RouteProps[] = [...opentronsAIRoutes, landingPage]

return (
<Routes>
{allRoutes.map(({ Component, path }: RouteProps) => (
<Route key={path} path={path} element={<Component />} />
))}
<Route path="*" element={<Navigate to={landingPage.path} />} />
</Routes>
)
}
Loading
Loading