Skip to content

Commit

Permalink
feat(app, step-generation): visualize the step timeline and deck stat…
Browse files Browse the repository at this point in the history
…e of a protocol (#15491)

Create an experimental tool, behind a feature flag, that allows users to
inspect the timeline of
atomic commands and the resulting deck state of the robot at ever frame
of the timeline.

NOTE: This is for Flex only and also does not yet support the 96-channel

Co-authored-by: Seth Foster <[email protected]>
Co-authored-by: Max Marrone <[email protected]>
Co-authored-by: Jethary <[email protected]>
  • Loading branch information
4 people committed Jul 2, 2024
1 parent 0dc6328 commit c95b61b
Show file tree
Hide file tree
Showing 30 changed files with 1,410 additions and 33 deletions.
7 changes: 7 additions & 0 deletions app/src/App/DesktopApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { IncompatibleModuleTakeover } from '../organisms/IncompatibleModule'
import { OPENTRONS_USB } from '../redux/discovery'
import { appShellRequestor } from '../redux/shell/remote'
import { useRobot, useIsFlex } from '../organisms/Devices/hooks'
import { ProtocolTimeline } from '../pages/Protocols/ProtocolDetails/ProtocolTimeline'
import { PortalRoot as ModalPortalRoot } from './portal'
import { DesktopAppFallback } from './DesktopAppFallback'

Expand All @@ -58,6 +59,12 @@ export const DesktopApp = (): JSX.Element => {
name: 'Protocol Details',
path: '/protocols/:protocolKey',
},
{
Component: ProtocolTimeline,
exact: true,
name: 'Protocol Timeline',
path: '/protocols/:protocolKey/timeline',
},
{
Component: Labware,
name: 'Labware',
Expand Down
18 changes: 17 additions & 1 deletion app/src/App/__tests__/DesktopApp.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import { MemoryRouter } from 'react-router-dom'
import { screen } from '@testing-library/react'
import { when } from 'vitest-when'
import { vi, describe, beforeEach, afterEach, expect, it } from 'vitest'

import { renderWithProviders } from '../../__testing-utils__'
Expand All @@ -14,7 +15,9 @@ import { ProtocolRunDetails } from '../../pages/Devices/ProtocolRunDetails'
import { RobotSettings } from '../../pages/Devices/RobotSettings'
import { GeneralSettings } from '../../pages/AppSettings/GeneralSettings'
import { AlertsModal } from '../../organisms/Alerts/AlertsModal'
import { useFeatureFlag } from '../../redux/config'
import { useIsFlex } from '../../organisms/Devices/hooks'
import { ProtocolTimeline } from '../../pages/Protocols/ProtocolDetails/ProtocolTimeline'
import { useSoftwareUpdatePoll } from '../hooks'
import { DesktopApp } from '../DesktopApp'

Expand All @@ -27,8 +30,10 @@ vi.mock('../../pages/Devices/DevicesLanding')
vi.mock('../../pages/Protocols/ProtocolsLanding')
vi.mock('../../pages/Devices/ProtocolRunDetails')
vi.mock('../../pages/Devices/RobotSettings')
vi.mock('../hooks')
vi.mock('../../organisms/Alerts/AlertsModal')
vi.mock('../../pages/Protocols/ProtocolDetails/ProtocolTimeline')
vi.mock('../../redux/config')
vi.mock('../hooks')

const render = (path = '/') => {
return renderWithProviders(
Expand All @@ -41,6 +46,9 @@ const render = (path = '/') => {

describe('DesktopApp', () => {
beforeEach(() => {
when(vi.mocked(useFeatureFlag))
.calledWith('protocolTimeline')
.thenReturn(true)
vi.mocked(CalibrationDashboard).mockReturnValue(
<div>Mock CalibrationDashboard</div>
)
Expand All @@ -52,6 +60,9 @@ describe('DesktopApp', () => {
vi.mocked(ProtocolRunDetails).mockReturnValue(
<div>Mock ProtocolRunDetails</div>
)
vi.mocked(ProtocolTimeline).mockReturnValue(
<div>Mock ProtocolTimeline</div>
)
vi.mocked(RobotSettings).mockReturnValue(<div>Mock RobotSettings</div>)
vi.mocked(GeneralSettings).mockReturnValue(<div>Mock AppSettings</div>)
vi.mocked(Breadcrumbs).mockReturnValue(<div>Mock Breadcrumbs</div>)
Expand Down Expand Up @@ -96,6 +107,11 @@ describe('DesktopApp', () => {
screen.getByText('Mock ProtocolsLanding')
})

it('renders a ProtocolsTimeline component from /protocolTimeline', () => {
render(`/protocols/95e67900-bc9f-4fbf-92c6-cc4d7226a51b/timeline`)
screen.getByText('Mock ProtocolTimeline')
})

it('renders a ProtocolRunDetails component from /devices/:robotName/protocol-runs/:runId/:protocolRunDetailsTab', () => {
render(
'/devices/otie/protocol-runs/95e67900-bc9f-4fbf-92c6-cc4d7226a51b/setup'
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/localization/en/app_settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"__dev_internal__forceHttpPolling": "Poll all network requests instead of using MQTT",
"__dev_internal__protocolStats": "Protocol Stats",
"__dev_internal__protocolTimeline": "Protocol Timeline",
"__dev_internal__enableRunNotes": "Display Notes During a Protocol Run",
"__dev_internal__enableQuickTransfer": "Enable Quick Transfer",
"__dev_internal__enableCsvFile": "Enable CSV File",
Expand Down
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 @@ -22,6 +22,7 @@
"extension_mount": "extension mount",
"file_required": "File required",
"go_to_labware_definition": "Go to labware definition",
"go_to_timeline": "Go to timeline",
"gripper_pick_up_count_description": "individual move labware commands that use the gripper.",
"gripper_pick_up_count": "Grip Count",
"hardware": "hardware",
Expand Down
2 changes: 2 additions & 0 deletions app/src/assets/localization/en/protocol_list.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"csv_file_required": "CSV file required for analysis. Add the CSV during run setup.",
"delete_protocol_message": " and its run history will be permanently deleted.",
"delete_this_protocol": "Delete this protocol?",
"edit": "edit",
"go_to_timeline": "Go to timeline",
"last_updated_at": "Updated {{date}}",
"left_mount": "left mount",
"loading_data": "Loading data...",
Expand Down
2 changes: 1 addition & 1 deletion app/src/molecules/Command/CommandText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ export function CommandText(props: Props): JSX.Element | null {
const { seconds, message } = command.params
return (
<LegacyStyledText as={as} {...styleProps}>
{t('wait_for_duration', { seconds, message })}
{t('wait_for_duration', { seconds, message: message ?? '' })}
</LegacyStyledText>
)
}
Expand Down
6 changes: 5 additions & 1 deletion app/src/molecules/Command/utils/getCommandTextData.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { LegacyGoodRunData } from '@opentrons/api-client'
import type {
CompletedProtocolAnalysis,
ProtocolAnalysisOutput,
RunTimeCommand,
} from '@opentrons/shared-data'
import type { CommandTextData } from '../types'

export function getCommandTextData(
protocolData: CompletedProtocolAnalysis | LegacyGoodRunData,
protocolData:
| CompletedProtocolAnalysis
| LegacyGoodRunData
| ProtocolAnalysisOutput,
protocolCommands?: RunTimeCommand[]
): CommandTextData {
const { pipettes, labware, modules, liquids } = protocolData
Expand Down
10 changes: 5 additions & 5 deletions app/src/molecules/InterventionModal/ModalContentMixed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
Icon,
Flex,
Box,
StyledText,
LegacyStyledText,
DIRECTION_COLUMN,
SPACING,
COLORS,
Expand Down Expand Up @@ -86,21 +86,21 @@ export function ModalContentMixed(props: ModalContentMixedProps): JSX.Element {
`}
>
{props.headline != null ? (
<StyledText
<LegacyStyledText
oddStyle="level3HeaderBold"
desktopStyle="headingSmallBold"
>
{props.headline}
</StyledText>
</LegacyStyledText>
) : null}
{props.subText != null ? (
<StyledText
<LegacyStyledText
oddStyle="level4HeaderRegular"
desktopStyle="bodyDefaultRegular"
color={COLORS.grey60}
>
{props.subText}
</StyledText>
</LegacyStyledText>
) : null}
</Flex>
</Flex>
Expand Down
110 changes: 110 additions & 0 deletions app/src/organisms/ProtocolDetails/AnnotatedSteps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react'
import { css } from 'styled-components'
import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data'
import {
ALIGN_CENTER,
BORDERS,
COLORS,
DIRECTION_COLUMN,
Flex,
SPACING,
LegacyStyledText,
TYPOGRAPHY,
OVERFLOW_AUTO,
} from '@opentrons/components'
import { CommandIcon, CommandText } from '../../molecules/Command'

import type {
CompletedProtocolAnalysis,
ProtocolAnalysisOutput,
RunTimeCommand,
} from '@opentrons/shared-data'

interface AnnotatedStepsProps {
analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput
currentCommandIndex?: number
}

export function AnnotatedSteps(props: AnnotatedStepsProps): JSX.Element {
const { analysis, currentCommandIndex } = props
const HIDE_SCROLLBAR = css`
::-webkit-scrollbar {
display: none;
}
`

return (
<Flex
css={HIDE_SCROLLBAR}
flexDirection={DIRECTION_COLUMN}
maxHeight="82vh"
flex="1 1 0"
overflowY={OVERFLOW_AUTO}
>
<Flex
flexDirection={DIRECTION_COLUMN}
marginY={SPACING.spacing16}
gridGap={SPACING.spacing4}
>
{analysis.commands.map((c, i) => (
<IndividualCommand
key={i}
stepNumber={(i + 1).toString()}
command={c}
isHighlighted={i === currentCommandIndex}
analysis={analysis}
/>
))}
</Flex>
</Flex>
)
}

interface IndividualCommandProps {
command: RunTimeCommand
analysis: ProtocolAnalysisOutput | CompletedProtocolAnalysis
stepNumber: string
isHighlighted: boolean
}
function IndividualCommand(props: IndividualCommandProps): JSX.Element {
const { command, analysis, stepNumber, isHighlighted } = props
const backgroundColor = isHighlighted ? COLORS.blue30 : COLORS.grey20
const iconColor = isHighlighted ? COLORS.blue60 : COLORS.grey50
return (
<Flex alignItems={ALIGN_CENTER} gridGap={SPACING.spacing8}>
<LegacyStyledText
minWidth={SPACING.spacing16}
fontSize={TYPOGRAPHY.fontSizeCaption}
>
{stepNumber}
</LegacyStyledText>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
width="100%"
backgroundColor={backgroundColor}
color={COLORS.black90}
borderRadius={BORDERS.borderRadius4}
padding={SPACING.spacing8}
css={css`
transition: background-color 500ms ease-out,
border-color 500ms ease-out;
`}
>
<Flex
key={command.id}
alignItems={ALIGN_CENTER}
gridGap={SPACING.spacing8}
>
<CommandIcon command={command} color={iconColor} />
<CommandText
command={command}
robotType={analysis?.robotType ?? FLEX_ROBOT_TYPE}
color={COLORS.black90}
commandTextData={analysis}
/>
</Flex>
</Flex>
</Flex>
)
}
21 changes: 20 additions & 1 deletion app/src/organisms/ProtocolDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { ProtocolLabwareDetails } from './ProtocolLabwareDetails'
import { ProtocolLiquidsDetails } from './ProtocolLiquidsDetails'
import { RobotConfigurationDetails } from './RobotConfigurationDetails'
import { ProtocolParameters } from './ProtocolParameters'
import { AnnotatedSteps } from './AnnotatedSteps'

import type { JsonConfig, PythonConfig } from '@opentrons/shared-data'
import type { StoredProtocolData } from '../../redux/protocol-storage'
Expand Down Expand Up @@ -204,10 +205,11 @@ export function ProtocolDetails(
const { protocolKey, srcFileNames, mostRecentAnalysis, modified } = props
const { t, i18n } = useTranslation(['protocol_details', 'shared'])
const enableProtocolStats = useFeatureFlag('protocolStats')
const enableProtocolTimeline = useFeatureFlag('protocolTimeline')
const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? []
const hasRunTimeParameters = runTimeParameters.length > 0
const [currentTab, setCurrentTab] = React.useState<
'robot_config' | 'labware' | 'liquids' | 'stats' | 'parameters'
'robot_config' | 'labware' | 'liquids' | 'stats' | 'parameters' | 'timeline'
>(hasRunTimeParameters ? 'parameters' : 'robot_config')
const [
showChooseRobotToRunProtocolSlideout,
Expand Down Expand Up @@ -342,6 +344,10 @@ export function ProtocolDetails(
stats: enableProtocolStats ? (
<ProtocolStats analysis={mostRecentAnalysis} />
) : null,
timeline:
enableProtocolTimeline && mostRecentAnalysis != null ? (
<AnnotatedSteps analysis={mostRecentAnalysis} />
) : null,
parameters: <ProtocolParameters runTimeParameters={runTimeParameters} />,
}

Expand Down Expand Up @@ -682,6 +688,19 @@ export function ProtocolDetails(
</LegacyStyledText>
</RoundTab>
)}
{enableProtocolTimeline && mostRecentAnalysis != null && (
<RoundTab
data-testid="ProtocolDetails_stats"
isCurrent={currentTab === 'timeline'}
onClick={() => {
setCurrentTab('timeline')
}}
>
<LegacyStyledText>
{i18n.format(t('timeline'), 'capitalize')}
</LegacyStyledText>
</RoundTab>
)}
</Flex>
<Box
backgroundColor={COLORS.white}
Expand Down
Loading

0 comments on commit c95b61b

Please sign in to comment.