Skip to content

Commit

Permalink
feat(app): add pause-type intervention modal (#12379)
Browse files Browse the repository at this point in the history
* feat(app): add pause-type intervention modal

adds a pause-type intervention modal for protocol runs. The modal displays the command message as
well as a timer that counts up from the time the pause command started

closes RLAB-318 closes RLAB-319
  • Loading branch information
jgbowser authored May 19, 2023
1 parent 3cbbddd commit 2e822af
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 5 deletions.
1 change: 1 addition & 0 deletions app/src/assets/localization/en/protocol_command_text.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})",
"move_to_slot": "Moving to Slot {{slot_name}}",
"move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}",
"notes": "notes",
"off_deck": "off deck",
"opening_tc_lid": "Opening Thermocycler lid",
"perform_manual_step": "Perform manual step on {{robot_name}}",
Expand Down
16 changes: 14 additions & 2 deletions app/src/organisms/InterventionModal/InterventionModal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import { InterventionModal as InterventionModalComponent } from './'

import type { Story, Meta } from '@storybook/react'

const now = new Date()

const pauseCommand = {
commandType: 'waitForResume',
startedAt: now,
params: {
message:
'This is a user generated message that gives details about the pause command. This text is truncated to 220 characters. semper risus in hendrerit gravida rutrum quisque non tellus orci ac auctor augue mauris augue neque gravida in fermentum et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque',
},
}

export default {
title: 'App/Organisms/InterventionModal',
component: InterventionModalComponent,
Expand All @@ -12,7 +23,8 @@ const Template: Story<
React.ComponentProps<typeof InterventionModalComponent>
> = args => <InterventionModalComponent {...args} />

export const BasicExample = Template.bind({})
BasicExample.args = {
export const PauseIntervention = Template.bind({})
PauseIntervention.args = {
robotName: 'Otie',
command: pauseCommand,
}
73 changes: 73 additions & 0 deletions app/src/organisms/InterventionModal/PauseInterventionContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'

import {
ALIGN_CENTER,
COLORS,
DIRECTION_COLUMN,
Flex,
Icon,
SPACING,
useInterval,
} from '@opentrons/components'

import { EMPTY_TIMESTAMP } from '../Devices/constants'
import { formatInterval } from '../RunTimeControl/utils'
import { StyledText } from '../../atoms/text'

import type { WaitForResumeRunTimeCommand } from '@opentrons/shared-data'

export interface PauseContentProps {
command: WaitForResumeRunTimeCommand
}

export function PauseInterventionContent({
command,
}: PauseContentProps): JSX.Element {
const { t, i18n } = useTranslation('protocol_command_text')

return (
<Flex flexDirection={DIRECTION_COLUMN} gridGap="0.75rem">
<PauseHeader startedAt={command.startedAt} />
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
<StyledText as="h6" color={COLORS.errorDisabled}>
{i18n.format(t('notes'), 'upperCase')}:
</StyledText>
<StyledText as="p">
{command.params?.message != null && command.params.message !== ''
? command.params.message.length > 220
? `${command.params.message.substring(0, 217)}...`
: command.params.message
: t('wait_for_resume')}
</StyledText>
</Flex>
</Flex>
)
}

interface PauseHeaderProps {
startedAt: string | null
}

function PauseHeader({ startedAt }: PauseHeaderProps): JSX.Element {
const { t, i18n } = useTranslation('run_details')
const [now, setNow] = React.useState(Date())
useInterval(() => setNow(Date()), 500, true)

const runTime =
startedAt != null ? formatInterval(startedAt, now) : EMPTY_TIMESTAMP

return (
<Flex alignItems={ALIGN_CENTER} gridGap="0.75rem">
<Icon
name="pause-circle"
size={SPACING.spacing32}
flex="0 0 auto"
color={COLORS.darkGreyEnabled}
/>
<StyledText as="h1">
{i18n.format(t('paused_for'), 'capitalize')} {runTime}
</StyledText>
</Flex>
)
}
40 changes: 40 additions & 0 deletions app/src/organisms/InterventionModal/__fixtures__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const fullCommandMessage =
'This is a user generated message that gives details about the pause command. This text is truncated to 220 characters. semper risus in hendrerit gravida rutrum quisque non tellus orci ac auctor augue mauris augue neque gravida in fermentum et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque'

export const truncatedCommandMessage =
'This is a user generated message that gives details about the pause command. This text is truncated to 220 characters. semper risus in hendrerit gravida rutrum quisque non tellus orci ac auctor augue mauris augue nequ...'

export const shortCommandText =
"this won't get truncated because it isn't more than 220 characters."

export const mockPauseCommandWithStartTime = {
commandType: 'waitForResume',
startedAt: new Date(),
params: {
message: fullCommandMessage,
},
} as any

export const mockPauseCommandWithoutStartTime = {
commandType: 'waitForResume',
startedAt: null,
params: {
message: fullCommandMessage,
},
} as any

export const mockPauseCommandWithShortMessage = {
commandType: 'waitForResume',
startedAt: null,
params: {
message: shortCommandText,
},
} as any

export const mockPauseCommandWithNoMessage = {
commandType: 'waitForResume',
startedAt: null,
params: {
message: null,
},
} as any
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import * as React from 'react'
import { renderWithProviders } from '@opentrons/components'
import { i18n } from '../../../i18n'
import { InterventionModal } from '..'
import {
mockPauseCommandWithNoMessage,
mockPauseCommandWithoutStartTime,
mockPauseCommandWithShortMessage,
mockPauseCommandWithStartTime,
shortCommandText,
truncatedCommandMessage,
} from '../__fixtures__'

const ROBOT_NAME = 'Otie'

Expand All @@ -14,7 +22,7 @@ const render = (props: React.ComponentProps<typeof InterventionModal>) => {
describe('InterventionModal', () => {
let props: React.ComponentProps<typeof InterventionModal>
beforeEach(() => {
props = { robotName: ROBOT_NAME }
props = { robotName: ROBOT_NAME, command: mockPauseCommandWithStartTime }
})

it('renders an InterventionModal with the robot name in the header, learn more link, and confirm button', () => {
Expand All @@ -23,4 +31,28 @@ describe('InterventionModal', () => {
expect(getByText('Learn more about user interventions')).toBeTruthy()
expect(getByRole('button', { name: 'Confirm and resume' })).toBeTruthy()
})

it('renders a pause intervention modal given a pause-type command', () => {
const { getByText } = render(props)
expect(getByText(truncatedCommandMessage)).toBeTruthy()
expect(getByText(/Paused for [0-9]{2}:[0-9]{2}:[0-9]{2}/)).toBeTruthy()
})

it('renders a pause intervention modal with an empty timestamp when no start time given', () => {
props = { ...props, command: mockPauseCommandWithoutStartTime }
const { getByText } = render(props)
expect(getByText('Paused for --:--:--')).toBeTruthy()
})

it('does not truncate command text when shorter than 220 characters', () => {
props = { ...props, command: mockPauseCommandWithShortMessage }
const { getByText } = render(props)
expect(getByText(shortCommandText)).toBeTruthy()
})

it('displays a default message if pause step does not have a message', () => {
props = { ...props, command: mockPauseCommandWithNoMessage }
const { getByText } = render(props)
expect(getByText('Pausing protocol')).toBeTruthy()
})
})
19 changes: 17 additions & 2 deletions app/src/organisms/InterventionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
} from '@opentrons/components'

import { StyledText } from '../../atoms/text'
import { PauseInterventionContent } from './PauseInterventionContent'

import type { WaitForResumeRunTimeCommand } from '@opentrons/shared-data'

const BASE_STYLE = {
position: POSITION_ABSOLUTE,
Expand All @@ -43,7 +46,7 @@ const MODAL_STYLE = {
position: POSITION_RELATIVE,
overflowY: OVERFLOW_AUTO,
maxHeight: '100%',
width: '100%',
maxWidth: '47rem',
margin: SPACING.spacing24,
border: `6px ${String(BORDERS.styleSolid)} ${String(COLORS.blueEnabled)}`,
borderRadius: BORDERS.radiusSoftCorners,
Expand Down Expand Up @@ -80,15 +83,27 @@ const FOOTER_STYLE = {
justifyContent: JUSTIFY_SPACE_BETWEEN,
} as const

export type InterventionCommandType = WaitForResumeRunTimeCommand

export interface InterventionModalProps {
robotName: string
command: InterventionCommandType
}

export function InterventionModal({
robotName,
command,
}: InterventionModalProps): JSX.Element {
const { t } = useTranslation(['protocol_command_text', 'protocol_info'])

let modalContent: JSX.Element

switch (command.commandType) {
case 'pause': // legacy pause command
case 'waitForResume':
modalContent = <PauseInterventionContent command={command} />
}

return (
<Flex
position={POSITION_FIXED}
Expand All @@ -113,7 +128,7 @@ export function InterventionModal({
</StyledText>
</Box>
<Box {...CONTENT_STYLE}>
Content Goes Here
{modalContent}
<Box {...FOOTER_STYLE}>
<StyledText>
<Link css={TYPOGRAPHY.darkLinkLabelSemiBold} href="" external>
Expand Down

0 comments on commit 2e822af

Please sign in to comment.