-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Opentrons ai client modules (#16673)
# Overview This PR adds the Modules section to the create protocol flow. ![image](https://github.com/user-attachments/assets/38c69931-0de2-44d7-892a-b1c75ceb5133) ## Test Plan and Hands on Testing - On the landing page click Create a new protocol button, you will be redirected to the new page - fill up the required information in the Application and Instruments sections and click Confirm - You now can add a module and an adapter (modules are not required) - The Prompt Preview component is updated with the data entered. ## Changelog - Add Modules section to create protocol flow ## Review requests - Verify new section. ## Risk assessment - low
- Loading branch information
1 parent
a065ef4
commit ac00351
Showing
27 changed files
with
748 additions
and
4 deletions.
There are no files selected for viewing
Binary file added
BIN
+142 KB
opentrons-ai-client/src/assets/images/modules/MagneticBlock_GEN1_HERO.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.6 KB
opentrons-ai-client/src/assets/images/modules/heater_shaker_module_transparent.png
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.
Binary file added
BIN
+32 KB
opentrons-ai-client/src/assets/images/modules/magdeck_tempdeck_combined.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+1.93 KB
opentrons-ai-client/src/assets/images/modules/module_pipette_collision_warning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+12.8 KB
opentrons-ai-client/src/assets/images/modules/temp_deck_gen_2_transparent.png
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
.../ControlledEmptySelectorButtonGroup/__tests__/ControlledEmptySelectorButtonGroup.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
40 changes: 40 additions & 0 deletions
40
opentrons-ai-client/src/molecules/ControlledEmptySelectorButtonGroup/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
}} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
) | ||
} |
93 changes: 93 additions & 0 deletions
93
opentrons-ai-client/src/molecules/ModuleListItemGroup/__tests__/ModuleListItemGroup.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
Oops, something went wrong.