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

Opentrons ai client modules #16673

Merged
merged 21 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
132238f
feat: add ControlledRadioButtonGroup
fbelginetw Oct 28, 2024
a5a79a9
refactor: add minHeight to main content on Landing page
fbelginetw Oct 28, 2024
d592e2c
feat: [wip] add instruments section and it's main components
fbelginetw Oct 28, 2024
ba4b0fe
refactor: update logic of which options to display
fbelginetw Oct 29, 2024
b77c4f5
refactor: add text to translation file
fbelginetw Oct 29, 2024
04db6a7
refactor: adjust instruments section to new design and add tests
fbelginetw Oct 30, 2024
f1177a1
refactor: move helpers and add tests
fbelginetw Oct 30, 2024
97114b9
refactor: missing translation
fbelginetw Oct 30, 2024
eb02355
feat: add exit button and confirm modal
fbelginetw Oct 30, 2024
a797963
refactor: fix performance issues with RadioButton
fbelginetw Oct 30, 2024
fa88a39
Merge branch 'edge' into opentrons-ai-client-instruments
fbelginetw Oct 30, 2024
fd44580
feat initial commit
fbelginetw Oct 31, 2024
419cd69
refactor: add translation
fbelginetw Oct 31, 2024
8266938
refactor: add adapter functionality
fbelginetw Nov 1, 2024
f49a7f2
refactor: display the adapter name in the prompt review
fbelginetw Nov 4, 2024
b655e78
fix: lint
fbelginetw Nov 4, 2024
de20b8d
Merge branch 'edge' into opentrons-ai-client-modules
fbelginetw Nov 4, 2024
656651f
refactor: rollback radiobutton changes
fbelginetw Nov 4, 2024
a1fc3b3
Merge branch 'edge' into opentrons-ai-client-modules
fbelginetw Nov 4, 2024
dbf8065
Merge branch 'edge' into opentrons-ai-client-modules
fbelginetw Nov 5, 2024
4e7508a
Merge branch 'edge' into opentrons-ai-client-modules
fbelginetw Nov 6, 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
"flex_gripper": "Flex Gripper",
"flex_gripper_no_label": "No, do not use the Flex Gripper",
"modules_title": "Modules",
"no_modules_added_yet": "No modules added yet",
"modules_remove_label": "remove",
"modules_adapter_label": "Adapter",
"heater_shaker_module_v1": "Heater-Shaker Module GEN1",
"temperature_module_v2": "Temperature Module GEN2",
"thermocycler_module_v2": "Thermocycler Module GEN2",
"magnetic_module_v1": "Magnetic Block GEN1",
"labware_liquids_title": "Labware & Liquids",
"steps_title": "Steps"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { ControlledEmptySelectorButtonGroup } from '../index'
import { describe, it, expect } from 'vitest'
import { fireEvent, screen } from '@testing-library/react'
import { FormProvider, useForm } from 'react-hook-form'
import { MODULES_FIELD_NAME } from '../../../organisms/ModulesSection'
import type { DisplayModules } from '../../../organisms/ModulesSection'

const modulesMock: DisplayModules[] = [
{
type: 'heaterShakerModuleType',
model: 'heaterShakerModuleV1',
name: 'Heater-Shaker Module GEN1',
},
{
type: 'temperatureModuleType',
model: 'temperatureModuleV2',
name: 'Temperature Module GEN2',
},
]

const TestFormProviderComponent = () => {
const methods = useForm({})

const selectedValue = methods.watch(MODULES_FIELD_NAME) ?? []

return (
<FormProvider {...methods}>
<ControlledEmptySelectorButtonGroup modules={modulesMock} />

{'selected values: ' + selectedValue.map((m: DisplayModules) => m.name)}
</FormProvider>
)
}

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

describe('ControlledEmptySelectorButtonGroup', () => {
it('should render ControlledEmptySelectorButtonGroup component', () => {
render()

screen.getByText('Heater-Shaker Module GEN1')
screen.getByText('Temperature Module GEN2')
})

it('should add the value when the button is clicked', async () => {
render()

const button1 = screen.getByText('Heater-Shaker Module GEN1')

expect(
screen.queryByText(
'selected values: Heater-Shaker Module GEN1,Temperature Module GEN2'
)
).not.toBeInTheDocument()

fireEvent.click(button1)

const button2 = screen.getByText('Temperature Module GEN2')

fireEvent.click(button2)

expect(
await screen.findByText(
'selected values: Heater-Shaker Module GEN1,Temperature Module GEN2'
)
).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Flex, WRAP, SPACING, EmptySelectorButton } from '@opentrons/components'
import { Controller, useFormContext } from 'react-hook-form'
import type { DisplayModules } from '../../organisms/ModulesSection'
import { MODULES_FIELD_NAME } from '../../organisms/ModulesSection'

export function ControlledEmptySelectorButtonGroup({
modules,
}: {
modules: DisplayModules[]
}): JSX.Element | null {
const { watch } = useFormContext()
const modulesWatch: DisplayModules[] = watch(MODULES_FIELD_NAME) ?? []

return (
<Controller
defaultValue={[]}
name={MODULES_FIELD_NAME}
render={({ field }) => {
return (
<Flex flexWrap={WRAP} gap={SPACING.spacing8}>
{modules.map(module => (
<EmptySelectorButton
key={module.type}
iconName="plus"
onClick={() => {
if (modulesWatch.some(m => m.type === module.type)) {
return
}
field.onChange([...modulesWatch, module])
}}
text={module.name}
textAlignment="left"
/>
))}
</Flex>
)
}}
/>
)
}
80 changes: 80 additions & 0 deletions opentrons-ai-client/src/molecules/ModelDiagram/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { css } from 'styled-components'
import {
MAGNETIC_MODULE_TYPE,
TEMPERATURE_MODULE_TYPE,
THERMOCYCLER_MODULE_TYPE,
MAGNETIC_MODULE_V1,
MAGNETIC_MODULE_V2,
TEMPERATURE_MODULE_V1,
TEMPERATURE_MODULE_V2,
THERMOCYCLER_MODULE_V1,
HEATERSHAKER_MODULE_TYPE,
HEATERSHAKER_MODULE_V1,
THERMOCYCLER_MODULE_V2,
MAGNETIC_BLOCK_TYPE,
MAGNETIC_BLOCK_V1,
ABSORBANCE_READER_TYPE,
ABSORBANCE_READER_V1,
} from '@opentrons/shared-data'

import magdeck_gen1 from '../../assets/images/modules/magdeck_gen1.png'
import magdeck_gen2 from '../../assets/images/modules/magdeck_gen2.png'
import tempdeck_gen1 from '../../assets/images/modules/tempdeck_gen1.png'
import temp_deck_gen_2_transparent from '../../assets/images/modules/temp_deck_gen_2_transparent.png'
import thermocycler from '../../assets/images/modules/thermocycler.png'
import thermocycler_gen2 from '../../assets/images/modules/thermocycler_gen2.png'
import heater_shaker_module_transparent from '../../assets/images/modules/heater_shaker_module_transparent.png'
import mag_block from '../../assets/images/modules/MagneticBlock_GEN1_HERO.png'
import type { ModuleType, ModuleModel } from '@opentrons/shared-data'

interface Props {
type: ModuleType
model: ModuleModel
}

type ModuleImg = {
[type in ModuleType]: {
[model in ModuleModel]?: string
}
}

const MODULE_IMG_BY_TYPE: ModuleImg = {
[MAGNETIC_MODULE_TYPE]: {
[MAGNETIC_MODULE_V1]: magdeck_gen1,
[MAGNETIC_MODULE_V2]: magdeck_gen2,
},
[TEMPERATURE_MODULE_TYPE]: {
[TEMPERATURE_MODULE_V1]: tempdeck_gen1,
[TEMPERATURE_MODULE_V2]: temp_deck_gen_2_transparent,
},
[THERMOCYCLER_MODULE_TYPE]: {
[THERMOCYCLER_MODULE_V1]: thermocycler,
[THERMOCYCLER_MODULE_V2]: thermocycler_gen2,
},
[HEATERSHAKER_MODULE_TYPE]: {
[HEATERSHAKER_MODULE_V1]: heater_shaker_module_transparent,
},
[MAGNETIC_BLOCK_TYPE]: {
[MAGNETIC_BLOCK_V1]: mag_block,
},
[ABSORBANCE_READER_TYPE]: {
// TODO (AA): update absorbance reader image
[ABSORBANCE_READER_V1]: heater_shaker_module_transparent,
},
}

const IMAGE_MAX_WIDTH = '96px'
export function ModuleDiagram(props: Props): JSX.Element {
const model = MODULE_IMG_BY_TYPE[props.type][props.model]
return (
<img
css={css`
max-width: ${IMAGE_MAX_WIDTH};
width: 100%;
height: auto;
`}
src={model}
alt={props.type}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { ModuleListItemGroup } from '../index'
import { describe, it, expect } from 'vitest'
import { fireEvent, screen } from '@testing-library/react'
import { FormProvider, useForm } from 'react-hook-form'
import type { DisplayModules } from '../../../organisms/ModulesSection'

const modulesMock: DisplayModules[] = [
{
type: 'heaterShakerModuleType',
model: 'heaterShakerModuleV1',
name: 'Heater-Shaker Module GEN1',
},
{
type: 'temperatureModuleType',
model: 'temperatureModuleV2',
name: 'Temperature Module GEN2',
},
]

const TestFormProviderComponent = () => {
const methods = useForm({
defaultValues: {
modules: modulesMock,
},
})

return (
<FormProvider {...methods}>
<ModuleListItemGroup />
</FormProvider>
)
}

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

describe('ModuleListItemGroup', () => {
it('should render ModuleListItemGroup component', () => {
render()

expect(screen.getAllByText('Adapter').length).toBe(2)
expect(screen.getAllByText('remove').length).toBe(2)

screen.getByAltText('heaterShakerModuleType')
screen.getByText('Heater-Shaker Module GEN1')

screen.getByAltText('temperatureModuleType')
screen.getByText('Temperature Module GEN2')
})

it('should remove the list item if remove is clicked', async () => {
render()

const removeListItemButton = screen.getAllByText('remove')[0]

fireEvent.click(removeListItemButton)

expect(
screen.queryByText('Heater-Shaker Module GEN1')
).not.toBeInTheDocument()
})

it('should render the dropdown if adapters are available', () => {
render()

expect(screen.getAllByText('Choose an adapter').length).toBe(2)
})

it('should be able to select an adapter', () => {
render()

const dropdownButton = screen.getAllByText('Choose an adapter')[1]

fireEvent.click(dropdownButton)

const adapterOption = screen.getByText(
'Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap'
)

fireEvent.click(adapterOption)

expect(
screen.getByText(
'Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap'
)
).toBeInTheDocument()
})
})
Loading
Loading