Skip to content

feat(app): Flex Stacker module card statuses #18058

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

Merged
merged 14 commits into from
Apr 17, 2025
2 changes: 1 addition & 1 deletion api-client/src/modules/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export interface AbsorbanceReaderData {
}
export interface FlexStackerData {
latchState: 'opened' | 'closed' | 'unknown'
platformState: 'extended' | 'retracted' | 'unknown'
platformState: 'extended' | 'retracted' | 'unknown' | 'missing'
hopperDoorState: 'opened' | 'closed' | 'unknown'
axisStateX: 'extended' | 'retracted' | 'unknown'
axisStateZ: 'extended' | 'retracted' | 'unknown'
Expand Down
5 changes: 4 additions & 1 deletion app/src/assets/localization/en/device_details.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@
"firmware_update_occurring": "Firmware update in progress...",
"firmware_updated_successfully": "Firmware updated successfully",
"fixture": "Fixture",
"flex_stacker_door_status": "Door status: {{status}}",
"flex_stacker_door_status": "Door",
"flex_stacker_shuttle_status": "Labware shuttle",
"flex_stacker_extended": "extended",
"flex_stacker_retracted": "in stacker",
"have_not_run": "No recent runs",
"have_not_run_description": "After you run some protocols, they will appear here.",
"heater": "Heater",
Expand Down
115 changes: 86 additions & 29 deletions app/src/organisms/ModuleCard/FlexStackerModuleData.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { useTranslation } from 'react-i18next'
import { StyledText, COLORS } from '@opentrons/components'
import {
StyledText,
COLORS,
TYPOGRAPHY,
SPACING,
Flex,
WRAP,
DIRECTION_COLUMN,
} from '@opentrons/components'
import { StatusLabel } from '/app/atoms/StatusLabel'

import type { FlexStackerModule } from '/app/redux/modules/types'
Expand All @@ -14,45 +22,94 @@
const { moduleData } = props
const { t, i18n } = useTranslation(['device_details', 'shared'])

const StatusLabelProps = {
status: 'Idle',
const getShuttleStatusText = (): string => {
switch (moduleData.platformState) {
case 'extended':
return t('flex_stacker_extended')
case 'retracted':
return t('flex_stacker_retracted')
default:
return t('shared:unknown')
}
}

Check warning on line 34 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L25-L34

Added lines #L25 - L34 were not covered by tests

const shuttleDisplayStatus = i18n.format(getShuttleStatusText(), 'capitalize')

Check warning on line 36 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L36

Added line #L36 was not covered by tests

const doorDisplayStatus = i18n.format(
moduleData.hopperDoorState === 'closed'
? t('shared:closed')
: t('shared:open'),
'capitalize'
)

Check warning on line 43 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L38-L43

Added lines #L38 - L43 were not covered by tests

const ShuttleStatusLabelProps = {
status: shuttleDisplayStatus,

Check warning on line 46 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L45-L46

Added lines #L45 - L46 were not covered by tests
backgroundColor: COLORS.grey30,
iconColor: COLORS.grey60,
textColor: COLORS.grey60,
pulse: false,
}
switch (moduleData.status) {
case 'storing':
case 'dispensing': {
StatusLabelProps.status = moduleData.status
StatusLabelProps.backgroundColor = COLORS.blue30
StatusLabelProps.iconColor = COLORS.blue60
StatusLabelProps.textColor = COLORS.blue60

switch (moduleData.platformState) {
case 'extended':
case 'retracted': {
ShuttleStatusLabelProps.backgroundColor = COLORS.blue30
ShuttleStatusLabelProps.iconColor = COLORS.blue60
ShuttleStatusLabelProps.textColor = COLORS.blue60

Check warning on line 58 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L53-L58

Added lines #L53 - L58 were not covered by tests
break
}
case 'error': {
StatusLabelProps.status = 'Error'
StatusLabelProps.backgroundColor = COLORS.yellow30
StatusLabelProps.iconColor = COLORS.yellow60
StatusLabelProps.textColor = COLORS.yellow60
case 'missing': {
ShuttleStatusLabelProps.backgroundColor = COLORS.red30
ShuttleStatusLabelProps.iconColor = COLORS.red60
ShuttleStatusLabelProps.textColor = COLORS.red60

Check warning on line 64 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L61-L64

Added lines #L61 - L64 were not covered by tests
break
}
}
const lidDisplayStatus =
moduleData.hopperDoorState === 'closed'
? i18n.format(t('shared:closed'), 'capitalize')
: i18n.format(t('shared:open'), 'capitalize')

const DoorStatusLabelProps = {
status: doorDisplayStatus,
backgroundColor: COLORS.grey30,
iconColor: COLORS.grey60,
textColor: COLORS.grey60,
pulse: false,
}

Check warning on line 75 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L69-L75

Added lines #L69 - L75 were not covered by tests

if (moduleData.hopperDoorState === 'opened') {
DoorStatusLabelProps.backgroundColor = COLORS.blue30
DoorStatusLabelProps.iconColor = COLORS.blue60
DoorStatusLabelProps.textColor = COLORS.blue60
}

Check warning on line 81 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L77-L81

Added lines #L77 - L81 were not covered by tests

return (
<>
<StatusLabel {...StatusLabelProps} />
<StyledText
desktopStyle="bodyDefaultRegular"
data-testid="stacker_module_data"
<Flex
flexWrap={WRAP}
flexDirection={DIRECTION_COLUMN}
gridGap={`${SPACING.spacing2} ${SPACING.spacing32}`}

Check warning on line 87 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L84-L87

Added lines #L84 - L87 were not covered by tests
>
<Flex
flexDirection={DIRECTION_COLUMN}
data-testid="stacker_door_data"
paddingTop={SPACING.spacing8}

Check warning on line 92 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L89-L92

Added lines #L89 - L92 were not covered by tests
>
<StyledText desktopStyle="bodyDefaultRegular" color={COLORS.grey60}>
{t('flex_stacker_door_status')}
</StyledText>
<StatusLabel {...DoorStatusLabelProps} />
</Flex>
<Flex
flexDirection={DIRECTION_COLUMN}
data-testid="stacker_shuttle_data"
paddingTop={SPACING.spacing8}

Check warning on line 102 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L94-L102

Added lines #L94 - L102 were not covered by tests
>
{t('flex_stacker_door_status', {
status: lidDisplayStatus,
})}
</StyledText>
</>
<StyledText
desktopStyle="bodyDefaultRegular"
color={COLORS.grey60}
fontWeight={TYPOGRAPHY.fontWeightRegular}

Check warning on line 107 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L104-L107

Added lines #L104 - L107 were not covered by tests
>
{t('flex_stacker_shuttle_status')}
</StyledText>
<StatusLabel {...ShuttleStatusLabelProps} />
</Flex>
</Flex>

Check warning on line 113 in app/src/organisms/ModuleCard/FlexStackerModuleData.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/ModuleCard/FlexStackerModuleData.tsx#L109-L113

Added lines #L109 - L113 were not covered by tests
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { renderWithProviders } from '/app/__testing-utils__'
import { i18n } from '/app/i18n'
import { FlexStackerModuleData } from '../FlexStackerModuleData'

import type { ComponentProps } from 'react'
import type { FlexStackerModule } from '/app/redux/modules/types'

const render = (props: ComponentProps<typeof FlexStackerModuleData>) => {
return renderWithProviders(<FlexStackerModuleData {...props} />, {
i18nInstance: i18n,
})[0]
}

describe('FlexStackerModuleData', () => {
let props: ComponentProps<typeof FlexStackerModuleData>

beforeEach(() => {
props = {
moduleData: {
platformState: 'extended',
hopperDoorState: 'closed',
} as FlexStackerModule['data'],
}
})

afterEach(() => {
vi.resetAllMocks()
})

it('renders both door and shuttle statuses', () => {
render(props)
screen.getByTestId('stacker_door_data')
screen.getByTestId('stacker_shuttle_data')

const doorLabel = screen.getByText('Closed')
expect(doorLabel)
expect(doorLabel).toHaveStyle('backgroundColor: COLORS.grey30')

const shuttleLabel = screen.getByText('Extended')
expect(shuttleLabel).toHaveStyle('backgroundColor: COLORS.blue30')
})

it('applies correct styles for door when opened', () => {
props.moduleData.hopperDoorState = 'opened'
render(props)
const doorLabel = screen.getByText('Open')
expect(doorLabel)
expect(doorLabel).toHaveStyle('backgroundColor: COLORS.grey30')
})

it('applies correct styles for shuttle when retracted', () => {
props.moduleData.platformState = 'retracted'
render(props)
const shuttleLabel = screen.getByText('In stacker')
expect(shuttleLabel)
expect(shuttleLabel).toHaveStyle('backgroundColor: COLORS.blue30')
})

it('applies correct styles for shuttle when missing', () => {
props.moduleData.platformState = 'missing'
render(props)
const shuttleLabel = screen.getByText('Unknown')
expect(shuttleLabel)
expect(shuttleLabel).toHaveStyle('backgroundColor: COLORS.red30')
})
})
40 changes: 40 additions & 0 deletions app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { MagneticModuleData } from '../MagneticModuleData'
import { TemperatureModuleData } from '../TemperatureModuleData'
import { ThermocyclerModuleData } from '../ThermocyclerModuleData'
import { HeaterShakerModuleData } from '../HeaterShakerModuleData'
import { FlexStackerModuleData } from '../FlexStackerModuleData'
import { ModuleOverflowMenu } from '../ModuleOverflowMenu'
import { FirmwareUpdateFailedModal } from '../FirmwareUpdateFailedModal'
import { ErrorInfo } from '../ErrorInfo'
Expand All @@ -34,13 +35,15 @@ import type {
HeaterShakerModule,
MagneticModule,
ThermocyclerModule,
FlexStackerModule,
} from '/app/redux/modules/types'

vi.mock('../ErrorInfo')
vi.mock('../MagneticModuleData')
vi.mock('../TemperatureModuleData')
vi.mock('../ThermocyclerModuleData')
vi.mock('../HeaterShakerModuleData')
vi.mock('../FlexStackerModuleData')
vi.mock('/app/redux/config')
vi.mock('../ModuleOverflowMenu')
vi.mock('/app/organisms/RunTimeControl')
Expand Down Expand Up @@ -169,6 +172,27 @@ const mockHotThermo = {
},
} as ThermocyclerModule

const mockFlexStacker = {
id: 'flex_stacker_id',
serialNumber: 'fs123',
hardwareRevision: 'flex_stacker_v1.0',
moduleModel: 'flexStackerModuleV1',
moduleType: 'flexStackerModuleType',
firmwareVersion: 'v2.0.0',
hasAvailableUpdate: false,
usbPort: {
path: '/dev/ot_module_flex_stacker',
hub: false,
port: 1,
portGroup: 'unknown',
},
data: {
platformState: 'extended',
hopperDoorState: 'closed',
status: 'idle',
},
} as FlexStackerModule

const mockMakeSnackbar = vi.fn()
const mockMakeToast = vi.fn()
const mockEatToast = vi.fn()
Expand Down Expand Up @@ -209,6 +233,9 @@ describe('ModuleCard', () => {
vi.mocked(HeaterShakerModuleData).mockReturnValue(
<div>Mock Heater Shaker Module Data</div>
)
vi.mocked(FlexStackerModuleData).mockReturnValue(
<div>Mock Flex Stacker Module Data</div>
)
vi.mocked(ModuleOverflowMenu).mockReturnValue(
<div>mock module overflow menu</div>
)
Expand Down Expand Up @@ -276,6 +303,19 @@ describe('ModuleCard', () => {
screen.getByAltText('heaterShakerModuleV1')
})

it('renders information for a heater shaker module with mocked status', () => {
vi.mocked(getIsHeaterShakerAttached).mockReturnValue(true)
render({
...props,
module: mockFlexStacker,
})

screen.getByText('Flex Stacker Module GEN1')
screen.getByText('Mock Flex Stacker Module Data')
screen.getByText('usb-1')
screen.getByAltText('flexStackerModuleV1')
})

it('renders kebab icon, opens and closes overflow menu on click', () => {
render({
...props,
Expand Down
2 changes: 1 addition & 1 deletion app/src/redux/modules/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export interface AbsorbanceReaderData {
}
export interface FlexStackerData {
latchState: 'opened' | 'closed' | 'unknown'
platformState: 'extended' | 'retracted' | 'unknown'
platformState: 'extended' | 'retracted' | 'unknown' | 'missing'
hopperDoorState: 'opened' | 'closed' | 'unknown'
axisStateX: 'extended' | 'retracted' | 'unknown'
axisStateZ: 'extended' | 'retracted' | 'unknown'
Expand Down
4 changes: 2 additions & 2 deletions components/src/icons/ModuleIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type ModuleIconName =
| 'ot-magnet-v2'
| 'ot-thermocycler'
| 'ot-absorbance'
| 'stacked'
| 'ot-flex-stacker'

export const MODULE_ICON_NAME_BY_TYPE: {
[type in ModuleType]: ModuleIconName
Expand All @@ -30,7 +30,7 @@ export const MODULE_ICON_NAME_BY_TYPE: {
[MAGNETIC_MODULE_TYPE]: 'ot-magnet-v2',
[THERMOCYCLER_MODULE_TYPE]: 'ot-thermocycler',
[ABSORBANCE_READER_TYPE]: 'ot-absorbance',
[FLEX_STACKER_MODULE_TYPE]: 'stacked',
[FLEX_STACKER_MODULE_TYPE]: 'ot-flex-stacker',
}

interface ModuleIconProps extends StyleProps {
Expand Down
5 changes: 5 additions & 0 deletions components/src/icons/icon-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@ export const ICON_DATA_BY_NAME: Record<
path: 'M23 0h1v24H0V0h1v23h22z',
viewBox: '0 0 24 24',
},
'ot-flex-stacker': {
path:
'M14.3 8.75789H1.7C1.3134 8.75789 1 9.05951 1 9.43158V10.7789C1 11.151 1.3134 11.4526 1.7 11.4526H14.3C14.6866 11.4526 15 11.151 15 10.7789V9.43158C15 9.05951 14.6866 8.75789 14.3 8.75789ZM14.3 12.8H1.7C1.3134 12.8 1 13.1016 1 13.4737V14.8211C1 15.1931 1.3134 15.4947 1.7 15.4947H14.3C14.6866 15.4947 15 15.1931 15 14.8211V13.4737C15 13.1016 14.6866 12.8 14.3 12.8ZM8.0875 7.32632L11.2375 3.78947C11.325 3.70526 11.2375 3.62105 11.15 3.62105H4.7625C4.675 3.62105 4.5875 3.78947 4.675 3.78947L7.825 7.32632H8.0875ZM9.05 0H6.8625V5.55789H9.05V0Z',
viewBox: '0 0 16 16',
},
'ot-heater-shaker': {
path:
'M10.0673 2.95C10.0673 1.87304 9.0488 1 7.79243 1C6.53607 1 5.51758 1.87304 5.51758 2.95C5.51758 5.10687 5.63384 6.80816 5.82919 8.10699C4.73271 8.75798 4 9.93802 4 11.2857C4 13.3371 5.69759 15 7.79167 15C9.88575 15 11.5833 13.3371 11.5833 11.2857C11.5833 9.85314 10.7554 8.60999 9.54271 7.99036C9.86372 6.608 10.0673 4.88684 10.0673 2.95ZM7.79243 2.3C8.21122 2.3 8.55072 2.59101 8.55072 2.95V4.9H7.03415V2.95C7.03415 2.59101 7.37364 2.3 7.79243 2.3ZM2.27929 3.3654C2.29023 3.31226 2.25753 3.25967 2.20484 3.24674L1.30047 3.02476C1.24538 3.01124 1.19004 3.04619 1.17925 3.10188C0.910517 4.48945 0.967614 5.92042 1.34594 7.28202C1.3629 7.34304 1.43201 7.37221 1.48796 7.34255L2.30932 6.90723C2.35045 6.88543 2.37103 6.83814 2.35952 6.79304C2.07343 5.67126 2.04602 4.49918 2.27929 3.3654ZM4.27705 3.36031C4.28442 3.30912 4.25203 3.26047 4.20184 3.24804L3.30306 3.02541C3.24734 3.01161 3.19142 3.04745 3.18134 3.10396C2.93318 4.49526 2.99334 5.92402 3.35747 7.2894C3.37399 7.35133 3.44409 7.38105 3.50049 7.35059L4.32474 6.90549C4.36372 6.88444 4.38416 6.84025 4.37535 6.79683C4.14583 5.66539 4.11259 4.50294 4.27705 3.36031ZM11.4461 6.90575C11.4069 6.88457 11.3865 6.84002 11.3956 6.7964C11.6325 5.66616 11.6626 4.50237 11.4846 3.36147C11.4765 3.30982 11.5091 3.26036 11.5599 3.2479L12.4675 3.02514C12.5231 3.01148 12.5789 3.04731 12.589 3.10374C12.8372 4.49511 12.777 5.92395 12.4129 7.2894C12.3964 7.35133 12.3263 7.38105 12.2699 7.35059L11.4461 6.90575ZM13.3998 6.79315C13.3884 6.83821 13.4089 6.88539 13.45 6.90716L14.2719 7.34278C14.3277 7.37236 14.3967 7.34342 14.4138 7.28262C14.7976 5.92195 14.8581 4.49027 14.5902 3.10195C14.5794 3.04624 14.5241 3.01125 14.4689 3.02478L13.5639 3.24691C13.5115 3.25978 13.4788 3.3119 13.4893 3.36483C13.7145 4.49999 13.6839 5.67116 13.3998 6.79315Z',
Expand Down
Loading