Skip to content

Commit

Permalink
refactor(app,shared-data): add useRunningStepCounts hook (#15438)
Browse files Browse the repository at this point in the history
Works towards EXEC-550 and closes EXEC-510


Since we'll be showing step counts in a few more places, during ER and also on the RunningProtocol page, let's go ahead and create a hook for getting the step count and whether or not a run is deterministic (since this helps clarify certain rendering behavior).

This hook is added to the one place it's used, RunProgressMeter.
  • Loading branch information
mjhuff committed Jun 18, 2024
1 parent 75551aa commit ae062f2
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useNotifyRunQuery } from '../../resources/runs'

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

// TODO(jh, 06-17-24): This is used elsewhere in the app and should probably live in something like resources.
export function useMostRecentCompletedAnalysis(
runId: string | null
): CompletedProtocolAnalysis | null {
Expand Down
81 changes: 27 additions & 54 deletions app/src/organisms/RunProgressMeter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { css } from 'styled-components'

import {
ALIGN_CENTER,
BORDERS,
Expand All @@ -18,6 +19,7 @@ import {
TYPOGRAPHY,
useHoverTooltip,
} from '@opentrons/components'
import { useCommandQuery } from '@opentrons/react-api-client'
import {
RUN_STATUS_IDLE,
RUN_STATUS_STOPPED,
Expand All @@ -27,7 +29,6 @@ import {
RUN_STATUS_RUNNING,
RUN_STATUS_BLOCKED_BY_OPEN_DOOR,
} from '@opentrons/api-client'
import { useCommandQuery } from '@opentrons/react-api-client'

import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { getTopPortalEl } from '../../App/portal'
Expand All @@ -44,6 +45,7 @@ import {
useNotifyAllCommandsQuery,
} from '../../resources/runs'
import { getCommandTextData } from '../../molecules/Command/utils/getCommandTextData'
import { useRunningStepCounts } from '../../resources/protocols/hooks'

import type { RunStatus } from '@opentrons/api-client'

Expand Down Expand Up @@ -74,14 +76,26 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element {
})
const { data: runRecord } = useNotifyRunQuery(runId)
const runData = runRecord?.data ?? null
const analysis = useMostRecentCompletedAnalysis(runId)
const { data: allCommandsQueryData } = useNotifyAllCommandsQuery(runId, {

const { data: mostRecentCommandData } = useNotifyAllCommandsQuery(runId, {
cursor: null,
pageLength: 1,
})
// This lastRunCommand also includes "fixit" commands.
const lastRunCommand = mostRecentCommandData?.data[0] ?? null
const { data: runCommandDetails } = useCommandQuery(
runId,
lastRunCommand?.id ?? null
)

const analysis = useMostRecentCompletedAnalysis(runId)
const analysisCommands = analysis?.commands ?? []
const lastRunCommand = allCommandsQueryData?.data[0] ?? null
const runCommandsLength = allCommandsQueryData?.meta.totalLength

const {
currentStepNumber,
totalStepCount,
hasRunDiverged,
} = useRunningStepCounts(runId, mostRecentCommandData)

const downloadIsDisabled =
runStatus === RUN_STATUS_RUNNING ||
Expand All @@ -90,41 +104,10 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element {

const { downloadRunLog } = useDownloadRunLog(robotName, runId)

/**
* find the index of the analysis command within the analysis
* that has the same commandKey as the most recent
* command from the run record.
* Or in the case of a non-deterministic protocol
* source from the run rather than the analysis
* NOTE: the most recent
* command may not always be "current", for instance if
* the run has completed/failed */
const lastRunAnalysisCommandIndex =
analysisCommands.findIndex(c => c.key === lastRunCommand?.key) ?? 0
const { data: runCommandDetails } = useCommandQuery(
runId,
lastRunCommand?.id ?? null
)
let countOfTotalText = ''
if (
lastRunAnalysisCommandIndex >= 0 &&
lastRunAnalysisCommandIndex <= analysisCommands.length - 1
) {
countOfTotalText = ` ${lastRunAnalysisCommandIndex + 1}/${
analysisCommands.length
}`
} else if (
lastRunAnalysisCommandIndex === -1 &&
lastRunCommand?.key != null &&
runCommandsLength != null
) {
countOfTotalText = `${runCommandsLength}/?`
} else if (analysis == null) {
countOfTotalText = ''
}
const stepCountStr = `${currentStepNumber ?? '?'}/${totalStepCount ?? '?'}`

const runHasNotBeenStarted =
(lastRunAnalysisCommandIndex === 0 &&
(currentStepNumber === 0 &&
runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR) ||
runStatus === RUN_STATUS_IDLE

Expand All @@ -133,22 +116,15 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element {
currentStepContents = (
<StyledText as="h2">{t('not_started_yet')}</StyledText>
)
} else if (
analysis != null &&
analysisCommands[lastRunAnalysisCommandIndex] != null
) {
} else if (analysis != null && !hasRunDiverged) {
currentStepContents = (
<CommandText
commandTextData={getCommandTextData(analysis)}
command={analysisCommands[lastRunAnalysisCommandIndex]}
command={analysisCommands[(currentStepNumber as number) - 1]}
robotType={robotType}
/>
)
} else if (
analysis != null &&
lastRunAnalysisCommandIndex === -1 &&
runCommandDetails != null
) {
} else if (analysis != null && hasRunDiverged && runCommandDetails != null) {
currentStepContents = (
<CommandText
commandTextData={getCommandTextData(analysis)}
Expand Down Expand Up @@ -212,9 +188,7 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element {
}${
runStatus === RUN_STATUS_IDLE
? ':'
: ` ${countOfTotalText}${
currentStepContents != null ? ': ' : ''
}`
: ` ${stepCountStr}${currentStepContents != null ? ': ' : ''}`
}`}</StyledText>

{currentStepContents}
Expand Down Expand Up @@ -247,13 +221,12 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element {
</Tooltip>
) : null}
</Flex>
{analysis != null && lastRunAnalysisCommandIndex >= 0 ? (
{!hasRunDiverged ? (
<ProgressBar
percentComplete={
runHasNotBeenStarted
? 0
: ((lastRunAnalysisCommandIndex + 1) /
analysisCommands.length) *
: ((currentStepNumber as number) / analysisCommands.length) *
100
}
outerStyles={css`
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { renderHook } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'

import { useCommandQuery } from '@opentrons/react-api-client'

import { useLastRunProtocolCommand } from '../useLastRunProtocolCommand'

vi.mock('@opentrons/react-api-client')

const mockRunId = 'mock-run-id'
const mockCommandsData = {
data: [
{ id: 'cmd1', key: 'key1', intent: 'protocol' },
{ id: 'cmd2', key: 'key2', intent: 'protocol' },
],
meta: { totalLength: 2 },
} as any

describe('useLastRunCommandNoFixit', () => {
it('returns the last run command when it is not a fixit command', () => {
vi.mocked(useCommandQuery).mockReturnValue({ data: null } as any)

const { result } = renderHook(() =>
useLastRunProtocolCommand(mockRunId, mockCommandsData)
)

expect(result.current).toEqual({
id: 'cmd2',
key: 'key2',
intent: 'protocol',
})
})

it('returns the failed command when the last run command is a fixit command', () => {
const mockFixitCommand = {
id: 'fixit-cmd',
intent: 'fixit',
failedCommandId: 'failed-cmd-id',
}
const mockFailedCommand = { id: 'failed-cmd-id', key: 'failed-key' }
vi.mocked(useCommandQuery).mockReturnValue({
data: { data: mockFailedCommand },
} as any)

const { result } = renderHook(() =>
useLastRunProtocolCommand(mockRunId, {
data: [mockFixitCommand],
meta: { totalLength: 1 },
} as any)
)

expect(result.current).toEqual(mockFailedCommand)
})

it('returns null when there are no run commands', () => {
const { result } = renderHook(() =>
useLastRunProtocolCommand(mockRunId, null)
)

expect(result.current).toBeNull()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, it, expect, vi } from 'vitest'
import { renderHook } from '@testing-library/react'

import { useMostRecentCompletedAnalysis } from '../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { useRunningStepCounts } from '../useRunningStepCounts'
import { useLastRunProtocolCommand } from '../useLastRunProtocolCommand'

vi.mock('../useLastRunProtocolCommand')
vi.mock(
'../../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis'
)

const mockRunId = 'mock-run-id'
const mockCommandsData = {
data: [
{ id: 'cmd1', key: 'key1' },
{ id: 'cmd2', key: 'key2' },
],
meta: { totalLength: 2 },
} as any

describe('useRunningStepCounts', () => {
it('returns current step number and total step count for a deterministic run', () => {
const mockAnalysis = {
commands: [{ key: 'key1' }, { key: 'key2' }, { key: 'key3' }],
} as any
vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue(mockAnalysis)
vi.mocked(useLastRunProtocolCommand).mockReturnValue({ key: 'key2' } as any)

const { result } = renderHook(() =>
useRunningStepCounts(mockRunId, mockCommandsData)
)

expect(result.current).toEqual({
currentStepNumber: 2,
totalStepCount: 3,
hasRunDiverged: false,
})
})

it('returns current step number and null total step count for a non-deterministic run', () => {
vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue(null)
vi.mocked(useLastRunProtocolCommand).mockReturnValue(null)

const { result } = renderHook(() =>
useRunningStepCounts(mockRunId, mockCommandsData)
)

expect(result.current).toEqual({
currentStepNumber: 2,
totalStepCount: null,
hasRunDiverged: true,
})
})

it('returns null current step number and total step count when analysis and run command data are not available', () => {
vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue(null)
vi.mocked(useLastRunProtocolCommand).mockReturnValue(null)

const { result } = renderHook(() =>
useRunningStepCounts(mockRunId, undefined)
)

expect(result.current).toEqual({
currentStepNumber: null,
totalStepCount: null,
hasRunDiverged: true,
})
})
})
4 changes: 4 additions & 0 deletions app/src/resources/protocols/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { useLastRunProtocolCommand } from './useLastRunProtocolCommand'
export { useRunningStepCounts } from './useRunningStepCounts'

export type { StepCounts } from './useRunningStepCounts'
29 changes: 29 additions & 0 deletions app/src/resources/protocols/hooks/useLastRunProtocolCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import last from 'lodash/last'

import { useCommandQuery } from '@opentrons/react-api-client'

import type { CommandsData, RunCommandSummary } from '@opentrons/api-client'

// Return the last command with a "protocol" intent. If the command does not have "protocol" intent,
// return the last command with "protocol" intent.
export function useLastRunProtocolCommand(
runId: string,
commandsData: CommandsData | null
): RunCommandSummary | null {
const lastRunCommand = last(commandsData?.data) ?? null

const isProtocolIntent = lastRunCommand?.intent === 'protocol'

// Get the failed command from the fixit command.
const lastRunCommandActual = useCommandQuery(
runId,
lastRunCommand?.failedCommandId ?? null,
{
enabled: !isProtocolIntent && lastRunCommand != null,
}
)

return !isProtocolIntent && lastRunCommand != null
? lastRunCommandActual.data?.data ?? null
: lastRunCommand ?? null
}
60 changes: 60 additions & 0 deletions app/src/resources/protocols/hooks/useRunningStepCounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis'

import { useLastRunProtocolCommand } from './useLastRunProtocolCommand'

import type { CommandsData } from '@opentrons/api-client'

export interface StepCounts {
/* Excludes "fixit" commands. Returns null if the step is not found. */
currentStepNumber: number | null
/* Returns null if the run has diverged or the total command count is not found. */
totalStepCount: number | null
/* Returns whether the run has diverged from analysis. */
hasRunDiverged: boolean
}

/**
* Find the index of the analysis command within the analysis
* that has the same commandKey as the most recent
* command from the run record.
* In the case of a non-deterministic protocol,
* source from the run rather than the analysis.
* NOTE: The most recent
* command may not always be "current", for instance if
* the run has completed/failed.
* NOTE #2: "Fixit" commands are excluded from the step count.
* */
export function useRunningStepCounts(
runId: string,
commandsData: CommandsData | undefined
): StepCounts {
const analysis = useMostRecentCompletedAnalysis(runId)
const analysisCommands = analysis?.commands ?? []
const lastRunCommandNoFixit = useLastRunProtocolCommand(
runId,
commandsData ?? null
)

const lastRunAnalysisCommandIndex = analysisCommands.findIndex(
c => c.key === lastRunCommandNoFixit?.key
)

const currentStepNumberByAnalysis =
lastRunAnalysisCommandIndex === -1 ? null : lastRunAnalysisCommandIndex + 1
const currentStepNumberByRun = commandsData?.meta.totalLength ?? null

const hasRunDiverged =
lastRunCommandNoFixit?.key == null || currentStepNumberByAnalysis == null

const currentStepNumber = !hasRunDiverged
? currentStepNumberByAnalysis
: currentStepNumberByRun

const totalStepCount = !hasRunDiverged ? analysisCommands.length : null

return {
currentStepNumber,
totalStepCount,
hasRunDiverged,
}
}
Loading

0 comments on commit ae062f2

Please sign in to comment.