Skip to content

Commit

Permalink
feature(app, odd): show full error list in the run summery (#15909)
Browse files Browse the repository at this point in the history
# Overview

closes https://opentrons.atlassian.net/browse/EXEC-635 (minos a few bug
fixes ;-) )
show full error list in the run summery modal. 

## Test Plan and Hands on Testing

- run the desktop app/odd.
- enter ER mode and make sure you encountered command errors. 
- finish the run and click the view error details -> make sure you can
see the error encountered.

## Changelog

- added get calls for the run command error list
- updated FailedRunModal to show the full error list if we got it. 

## Risk assessment

low. should not affect existing code.
  • Loading branch information
TamarZanzouri authored Aug 7, 2024
1 parent 6d9db72 commit cffa307
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 57 deletions.
19 changes: 19 additions & 0 deletions api-client/src/runs/commands/getRunCommandErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { GET, request } from '../../request'

import type { ResponsePromise } from '../../request'
import type { HostConfig } from '../../types'
import type { GetCommandsParams, RunCommandErrors } from '../types'

export function getRunCommandErrors(
config: HostConfig,
runId: string,
params: GetCommandsParams
): ResponsePromise<RunCommandErrors> {
return request<RunCommandErrors>(
GET,
`/runs/${runId}/commandErrors`,
null,
config,
params
)
}
7 changes: 6 additions & 1 deletion api-client/src/runs/commands/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { RunTimeCommand } from '@opentrons/shared-data'
import type { RunTimeCommand, RunCommandError } from '@opentrons/shared-data'

export interface GetCommandsParams {
cursor: number | null // the index of the command at the center of the window
pageLength: number // the number of items to include
}

export interface RunCommandErrors {
data: RunCommandError[]
meta: GetCommandsParams & { totalLength: number }
}

// NOTE: this incantation allows us to omit a key from each item in a union distributively
// this means we can, for example, maintain the associated commandType and params after the Omit is applied
type DistributiveOmit<T, K extends keyof T> = T extends any ? Omit<T, K> : never
Expand Down
1 change: 1 addition & 0 deletions api-client/src/runs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { getCommand } from './commands/getCommand'
export { getCommands } from './commands/getCommands'
export { getCommandsAsPreSerializedList } from './commands/getCommandsAsPreSerializedList'
export { createRunAction } from './createRunAction'
export { getRunCommandErrors } from './commands/getRunCommandErrors'
export * from './createLabwareOffset'
export * from './createLabwareDefinition'
export * from './constants'
Expand Down
40 changes: 31 additions & 9 deletions app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
useDoorQuery,
useHost,
useInstrumentsQuery,
useRunCommandErrors,
} from '@opentrons/react-api-client'
import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data'
import {
Expand Down Expand Up @@ -112,7 +113,12 @@ import {
ProtocolDropTipModal,
} from './ProtocolDropTipModal'

import type { Run, RunError, RunStatus } from '@opentrons/api-client'
import type {
Run,
RunCommandErrors,
RunError,
RunStatus,
} from '@opentrons/api-client'
import type { IconName } from '@opentrons/components'
import type { State } from '../../../redux/types'
import type { HeaterShakerModule } from '../../../redux/modules/types'
Expand Down Expand Up @@ -166,6 +172,13 @@ export function ProtocolRunHeader({
const { closeCurrentRun, isClosingCurrentRun } = useCloseCurrentRun()
const { startedAt, stoppedAt, completedAt } = useRunTimestamps(runId)
const [showRunFailedModal, setShowRunFailedModal] = React.useState(false)
const { data: commandErrorList } = useRunCommandErrors(runId, null, {
enabled:
runStatus != null &&
// @ts-expect-error runStatus expected to possibly not be terminal
RUN_STATUSES_TERMINAL.includes(runStatus) &&
isRunCurrent,
})
const [showDropTipBanner, setShowDropTipBanner] = React.useState(true)
const isResetRunLoadingRef = React.useRef(false)
const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity })
Expand Down Expand Up @@ -258,17 +271,15 @@ export function ProtocolRunHeader({

// Side effects dependent on the current run state.
React.useEffect(() => {
// After a user-initiated stopped run, close the run current run automatically.
if (runStatus === RUN_STATUS_STOPPED && isRunCurrent && runId != null) {
trackProtocolRunEvent({
name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH,
properties: {
...robotAnalyticsData,
},
})
closeCurrentRun()
}
}, [runStatus, isRunCurrent, runId, closeCurrentRun])
}, [runStatus, isRunCurrent, runId])

const startedAtTimestamp =
startedAt != null ? formatTimestamp(startedAt) : EMPTY_TIMESTAMP
Expand Down Expand Up @@ -333,6 +344,7 @@ export function ProtocolRunHeader({
runId={runId}
setShowRunFailedModal={setShowRunFailedModal}
highestPriorityError={highestPriorityError}
commandErrorList={commandErrorList}
/>
) : null}
<Flex
Expand Down Expand Up @@ -404,6 +416,7 @@ export function ProtocolRunHeader({
handleClearClick,
isClosingCurrentRun,
setShowRunFailedModal,
commandErrorList,
highestPriorityError,
}}
isResetRunLoading={isResetRunLoadingRef.current}
Expand Down Expand Up @@ -869,6 +882,7 @@ interface TerminalRunProps {
handleClearClick: () => void
isClosingCurrentRun: boolean
setShowRunFailedModal: (showRunFailedModal: boolean) => void
commandErrorList?: RunCommandErrors
isResetRunLoading: boolean
isRunCurrent: boolean
highestPriorityError?: RunError | null
Expand All @@ -879,6 +893,7 @@ function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null {
handleClearClick,
isClosingCurrentRun,
setShowRunFailedModal,
commandErrorList,
highestPriorityError,
isResetRunLoading,
isRunCurrent,
Expand Down Expand Up @@ -914,10 +929,12 @@ function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null {
<Banner type="error" iconMarginLeft={SPACING.spacing4}>
<Flex justifyContent={JUSTIFY_SPACE_BETWEEN} width="100%">
<LegacyStyledText>
{t('error_info', {
errorType: highestPriorityError?.errorType,
errorCode: highestPriorityError?.errorCode,
})}
{highestPriorityError != null
? t('error_info', {
errorType: highestPriorityError?.errorType,
errorCode: highestPriorityError?.errorCode,
})
: 'Run completed with errors.'}
</LegacyStyledText>

<LinkButton
Expand All @@ -937,7 +954,12 @@ function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null {
!isResetRunLoading
) {
return buildSuccessBanner()
} else if (runStatus === RUN_STATUS_FAILED && !isResetRunLoading) {
} else if (
highestPriorityError != null ||
(commandErrorList?.data != null &&
commandErrorList.data.length > 0 &&
!isResetRunLoading)
) {
return buildErrorBanner()
} else {
return null
Expand Down
63 changes: 51 additions & 12 deletions app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import {
import { LegacyModal } from '../../../molecules/LegacyModal'
import { useDownloadRunLog } from '../hooks'

import type { RunError } from '@opentrons/api-client'
import type { RunError, RunCommandErrors } from '@opentrons/api-client'
import type { LegacyModalProps } from '../../../molecules/LegacyModal'
import type { RunCommandError } from '@opentrons/shared-data'

/**
* This modal is for Desktop app
Expand All @@ -43,13 +44,15 @@ interface RunFailedModalProps {
runId: string
setShowRunFailedModal: (showRunFailedModal: boolean) => void
highestPriorityError?: RunError | null
commandErrorList?: RunCommandErrors | null
}

export function RunFailedModal({
robotName,
runId,
setShowRunFailedModal,
highestPriorityError,
commandErrorList,
}: RunFailedModalProps): JSX.Element | null {
const { i18n, t } = useTranslation(['run_details', 'shared', 'branded'])
const modalProps: LegacyModalProps = {
Expand All @@ -64,7 +67,7 @@ export function RunFailedModal({
}
const { downloadRunLog } = useDownloadRunLog(robotName, runId)

if (highestPriorityError == null) return null
if (highestPriorityError == null && commandErrorList == null) return null

const handleClick = (): void => {
setShowRunFailedModal(false)
Expand All @@ -76,20 +79,56 @@ export function RunFailedModal({
downloadRunLog()
}

return (
<LegacyModal {...modalProps}>
<Flex flexDirection={DIRECTION_COLUMN}>
interface ErrorContentProps {
errors: RunCommandError[]
isSingleError: boolean
}
const ErrorContent = ({
errors,
isSingleError,
}: ErrorContentProps): JSX.Element => {
return (
<>
<LegacyStyledText as="p" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
{t('error_info', {
errorType: highestPriorityError.errorType,
errorCode: highestPriorityError.errorCode,
})}
{isSingleError
? t('error_info', {
errorType: errors[0].errorType,
errorCode: errors[0].errorCode,
})
: `${errors.length} errors`}
</LegacyStyledText>
<Flex css={ERROR_MESSAGE_STYLE}>
<LegacyStyledText as="p" textAlign={TYPOGRAPHY.textAlignLeft}>
{highestPriorityError.detail}
</LegacyStyledText>
{' '}
{errors.map((error, index) => (
<LegacyStyledText
as="p"
textAlign={TYPOGRAPHY.textAlignLeft}
key={index}
>
{' '}
{isSingleError
? error.detail
: `${error.errorCode}: ${error.detail}`}
</LegacyStyledText>
))}
</Flex>
</>
)
}

return (
<LegacyModal {...modalProps}>
<Flex flexDirection={DIRECTION_COLUMN}>
<ErrorContent
errors={
highestPriorityError
? [highestPriorityError]
: commandErrorList?.data && commandErrorList?.data.length > 0
? commandErrorList?.data
: []
}
isSingleError={!!highestPriorityError}
/>
<LegacyStyledText as="p">
{t('branded:run_failed_modal_description_desktop')}
</LegacyStyledText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
useEstopQuery,
useDoorQuery,
useInstrumentsQuery,
useRunCommandErrors,
} from '@opentrons/react-api-client'
import {
getPipetteModelSpecs,
Expand Down Expand Up @@ -183,6 +184,25 @@ const PROTOCOL_DETAILS = {
robotType: 'OT-2 Standard' as const,
}

const RUN_COMMAND_ERRORS = {
data: {
data: [
{
errorCode: '4000',
errorType: 'test',
isDefined: false,
createdAt: '9-9-9',
detail: 'blah blah',
id: '123',
},
],
meta: {
cursor: 0,
pageLength: 1,
},
},
} as any

const mockMovingHeaterShaker = {
id: 'heatershaker_id',
moduleModel: 'heaterShakerModuleV1',
Expand Down Expand Up @@ -364,6 +384,7 @@ describe('ProtocolRunHeader', () => {
...noModulesProtocol,
...MOCK_ROTOCOL_LIQUID_KEY,
} as any)
vi.mocked(useRunCommandErrors).mockReturnValue(RUN_COMMAND_ERRORS)
vi.mocked(useDeckConfigurationCompatibility).mockReturnValue([])
vi.mocked(getIsFixtureMismatch).mockReturnValue(false)
vi.mocked(useMostRecentRunId).mockReturnValue(RUN_ID)
Expand Down Expand Up @@ -480,7 +501,6 @@ describe('ProtocolRunHeader', () => {
data: { data: { ...mockIdleUnstartedRun, current: true } },
} as UseQueryResult<OpentronsApiClient.Run>)
render()
expect(mockCloseCurrentRun).toBeCalled()
expect(mockTrackProtocolRunEvent).toBeCalled()
expect(mockTrackProtocolRunEvent).toBeCalledWith({
name: ANALYTICS_PROTOCOL_RUN_ACTION.FINISH,
Expand Down Expand Up @@ -852,7 +872,6 @@ describe('ProtocolRunHeader', () => {
render()

fireEvent.click(screen.queryAllByTestId('Banner_close-button')[0])
expect(mockCloseCurrentRun).toBeCalled()
})

it('does not display the "run successful" banner if the successful run is not current', async () => {
Expand Down
Loading

0 comments on commit cffa307

Please sign in to comment.