Skip to content

Commit

Permalink
feat(app): add Parameters tab to odd protocolDetails (#14657)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerader authored Mar 15, 2024
1 parent eb19a5e commit 5efa903
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 18 deletions.
1 change: 1 addition & 0 deletions app/src/assets/localization/en/protocol_details.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"location": "location",
"modules": "modules",
"name": "Name",
"num_choices": "{{num}} choices",
"no_available_robots_found": "No available robots found",
"no_parameters": "No parameters specified in this protocol",
"no_summary": "no summary specified for this protocol.",
Expand Down
15 changes: 8 additions & 7 deletions app/src/pages/ProtocolDetails/EmptySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ import { useTranslation } from 'react-i18next'
import { StyledText } from '../../atoms/text'

interface EmptySectionProps {
section: 'hardware' | 'labware' | 'liquids'
section: 'hardware' | 'labware' | 'liquids' | 'parameters'
}

export const EmptySection = (props: EmptySectionProps): JSX.Element => {
const { section } = props
const { t, i18n } = useTranslation('protocol_details')

let sectionText: string = t('not_in_protocol', { section: section })
if (section === 'liquids') {
sectionText = t('liquids_not_in_protocol')
} else if (section === 'parameters') {
sectionText = t('no_parameters')
}
return (
<Flex
backgroundColor={COLORS.grey35}
Expand All @@ -40,12 +46,7 @@ export const EmptySection = (props: EmptySectionProps): JSX.Element => {
aria-label="EmptySection_ot-alert"
/>
<StyledText as="h3" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
{i18n.format(
section === 'liquids'
? t('liquids_not_in_protocol')
: t('not_in_protocol', { section: section }),
'capitalize'
)}
{i18n.format(sectionText, 'capitalize')}
</StyledText>
</Flex>
)
Expand Down
160 changes: 160 additions & 0 deletions app/src/pages/ProtocolDetails/Parameters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import {
BORDERS,
COLORS,
Flex,
SPACING,
TYPOGRAPHY,
WRAP,
} from '@opentrons/components'
import { StyledText } from '../../atoms/text'
import { useToaster } from '../../organisms/ToasterOven'
import { useRunTimeParameters } from '../Protocols/hooks'
import { EmptySection } from './EmptySection'
import type { RunTimeParameter } from '@opentrons/shared-data'

const Table = styled('table')`
font-size: ${TYPOGRAPHY.fontSize22};
width: 100%;
border-spacing: 0 ${SPACING.spacing8};
margin: ${SPACING.spacing16} 0;
text-align: ${TYPOGRAPHY.textAlignLeft};
`
const TableHeader = styled('th')`
font-size: ${TYPOGRAPHY.fontSize20};
font-weight: ${TYPOGRAPHY.fontWeightSemiBold};
padding: ${SPACING.spacing4};
color: ${COLORS.grey60};
`

const TableRow = styled('tr')`
background-color: ${COLORS.grey35};
border: 1px ${COLORS.white} solid;
height: 4.75rem;
`

const TableDatum = styled('td')`
padding: ${SPACING.spacing4};
white-space: break-spaces;
text-overflow: ${WRAP};
&:first-child {
border-top-left-radius: ${BORDERS.borderRadius4};
border-bottom-left-radius: ${BORDERS.borderRadius4};
}
&:last-child {
border-top-right-radius: ${BORDERS.borderRadius4};
border-bottom-right-radius: ${BORDERS.borderRadius4};
}
`

export const Parameters = (props: { protocolId: string }): JSX.Element => {
const runTimeParameters = useRunTimeParameters(props.protocolId)
const { makeSnackbar } = useToaster()
const { t, i18n } = useTranslation('protocol_details')

const makeSnack = (): void => {
makeSnackbar(t('start_setup_customize_values'))
}

const getRange = (parameter: RunTimeParameter): string => {
const { type } = parameter
const min = 'min' in parameter ? parameter.min : 0
const max = 'max' in parameter ? parameter.max : 0
const numChoices = 'choices' in parameter ? parameter.choices.length : 0
let range: string | null = null
if (numChoices === 2 && 'choices' in parameter) {
range = `${parameter.choices[0].displayName}, ${parameter.choices[1].displayName}`
}

switch (type) {
case 'boolean': {
return t('on_off')
}
case 'float':
case 'int': {
return `${min}-${max}`
}
case 'str': {
return range ?? t('num_choices', { num: numChoices })
}
default:
// Should never hit this case
return ''
}
}

const getDefault = (parameter: RunTimeParameter): string => {
const { type, default: defaultValue } = parameter
const suffix =
'suffix' in parameter && parameter.suffix != null ? parameter.suffix : ''
switch (type) {
case 'int':
case 'float':
return `${defaultValue.toString()} ${suffix}`
case 'boolean':
return Boolean(defaultValue) ? t('on') : t('off')
case 'str':
if ('choices' in parameter && parameter.choices != null) {
const choice = parameter.choices.find(
choice => choice.value === defaultValue
)
if (choice != null) {
return choice.displayName
}
}
break
}
return ''
}

return runTimeParameters.length > 0 ? (
<Table onClick={makeSnack} data-testid="Parameters_table">
<thead>
<tr>
<TableHeader>
<StyledText paddingLeft={SPACING.spacing24}>
{i18n.format(t('name'), 'capitalize')}
</StyledText>
</TableHeader>
<TableHeader>
<StyledText paddingLeft={SPACING.spacing24}>
{i18n.format(t('default_value'), 'capitalize')}
</StyledText>
</TableHeader>
<TableHeader>
<StyledText paddingLeft={SPACING.spacing24}>
{i18n.format(t('range'), 'capitalize')}
</StyledText>
</TableHeader>
</tr>
</thead>
<tbody>
{runTimeParameters.map((parameter, index) => {
return (
<TableRow key={index}>
<TableDatum>
<Flex paddingLeft={SPACING.spacing24}>
{parameter.displayName}
</Flex>
</TableDatum>
<TableDatum>
<Flex paddingLeft={SPACING.spacing24} color={COLORS.grey60}>
{getDefault(parameter)}
</Flex>
</TableDatum>
<TableDatum>
<Flex paddingLeft={SPACING.spacing24} color={COLORS.grey60}>
{getRange(parameter)}
</Flex>
</TableDatum>
</TableRow>
)
})}
</tbody>
</Table>
) : (
<EmptySection section="parameters" />
)
}
22 changes: 15 additions & 7 deletions app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react'
import { it, describe } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { EmptySection } from '../EmptySection'
Expand All @@ -17,22 +18,29 @@ describe('EmptySection', () => {
props = {
section: 'labware',
}
const { getByText, getByLabelText } = render(props)
getByLabelText('EmptySection_ot-alert')
getByText('No labware is specified for this protocol')
render(props)
screen.getByLabelText('EmptySection_ot-alert')
screen.getByText('No labware is specified for this protocol')
})
it('should render text for liquid', () => {
props = {
section: 'liquids',
}
const { getByText } = render(props)
getByText('No liquids are specified for this protocol')
render(props)
screen.getByText('No liquids are specified for this protocol')
})
it('should render text for hardware', () => {
props = {
section: 'hardware',
}
const { getByText } = render(props)
getByText('No hardware is specified for this protocol')
render(props)
screen.getByText('No hardware is specified for this protocol')
})
it('should render text for parameters', () => {
props = {
section: 'parameters',
}
render(props)
screen.getByText('No parameters specified in this protocol')
})
})
145 changes: 145 additions & 0 deletions app/src/pages/ProtocolDetails/__tests__/Parameters.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as React from 'react'
import { when } from 'vitest-when'
import { it, describe, beforeEach, vi } from 'vitest'
import { screen } from '@testing-library/react'
import { i18n } from '../../../i18n'
import { useToaster } from '../../../organisms/ToasterOven'
import { renderWithProviders } from '../../../__testing-utils__'
import { useRunTimeParameters } from '../../Protocols/hooks'
import { Parameters } from '../Parameters'
import type { RunTimeParameter } from '@opentrons/shared-data'

vi.mock('../../../organisms/ToasterOven')
vi.mock('../../Protocols/hooks')

const mockRTPData: RunTimeParameter[] = [
{
displayName: 'Dry Run',
variableName: 'DRYRUN',
description: 'a dry run description',
type: 'boolean',
default: false,
},
{
displayName: 'Use Gripper',
variableName: 'USE_GRIPPER',
description: '',
type: 'boolean',
default: true,
},
{
displayName: 'Trash Tips',
variableName: 'TIP_TRASH',
description: 'throw tip in trash',
type: 'boolean',
default: true,
},
{
displayName: 'Deactivate Temperatures',
variableName: 'DEACTIVATE_TEMP',
description: 'deactivate temperature?',
type: 'boolean',
default: true,
},
{
displayName: 'Columns of Samples',
variableName: 'COLUMNS',
description: '',
suffix: 'mL',
type: 'int',
min: 1,
max: 14,
default: 4,
},
{
displayName: 'PCR Cycles',
variableName: 'PCR_CYCLES',
description: '',
type: 'int',
min: 1,
max: 10,
default: 6,
},
{
displayName: 'EtoH Volume',
variableName: 'ETOH_VOLUME',
description: '',
type: 'float',
min: 1.5,
max: 10.0,
default: 6.5,
},
{
displayName: 'Default Module Offsets',
variableName: 'DEFAULT_OFFSETS',
description: '',
type: 'str',
choices: [
{
displayName: 'no offsets',
value: 'none',
},
{
displayName: 'temp offset',
value: '1',
},
{
displayName: 'heater-shaker offset',
value: '2',
},
],
default: 'none',
},
{
displayName: '2 choices',
variableName: 'TWO',
description: '',
type: 'str',
choices: [
{
displayName: 'one choice',
value: '1',
},
{
displayName: 'the second',
value: '2',
},
],
default: '2',
},
]

const render = (props: React.ComponentProps<typeof Parameters>) => {
return renderWithProviders(<Parameters {...props} />, {
i18nInstance: i18n,
})
}
const MOCK_MAKE_SNACK_BAR = vi.fn()
describe('Parameters', () => {
let props: React.ComponentProps<typeof Parameters>

beforeEach(() => {
props = {
protocolId: 'mockId',
}
when(useToaster)
.calledWith()
.thenReturn({
makeSnackBar: MOCK_MAKE_SNACK_BAR,
} as any)
vi.mocked(useRunTimeParameters).mockReturnValue(mockRTPData)
})
it('renders the parameters labels and mock data', () => {
render(props)
screen.getByText('Name')
screen.getByText('Default value')
screen.getByText('Range')
screen.getByText('Dry Run')
screen.getByText('6.5')
screen.getByText('Use Gripper')
screen.getByText('Default Module Offsets')
screen.getByText('3 choices')
screen.getByText('EtoH Volume')
screen.getByText('one choice, the second')
})
})
Loading

0 comments on commit 5efa903

Please sign in to comment.