diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts
index defed3d1e19..77549b05da5 100644
--- a/api-client/src/runs/types.ts
+++ b/api-client/src/runs/types.ts
@@ -113,6 +113,9 @@ export interface CommandData {
export interface RunError {
id: string
errorType: string
+ errorInfo: { [key: string]: string }
+ wrappedErrors: RunError[]
+ errorCode: string
createdAt: string
detail: string
}
diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json
index ac6eb0e8df3..22b15981555 100644
--- a/app/src/assets/localization/en/run_details.json
+++ b/app/src/assets/localization/en/run_details.json
@@ -35,6 +35,7 @@
"end_step_time": "End",
"end": "End",
"error_type": "Error: {{errorType}}",
+ "error_info": "Error {{errorCode}}: {{errorType}}",
"failed_step": "Failed step",
"ignore_stored_data": "Ignore stored data",
"labware_offset_data": "labware offset data",
diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx
index 37a30cb32a0..637bf5e0433 100644
--- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx
+++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx
@@ -1,6 +1,7 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom'
+import isEmpty from 'lodash/isEmpty'
import { css } from 'styled-components'
import {
@@ -20,13 +21,7 @@ import { SmallButton } from '../../../atoms/buttons'
import { Modal } from '../../../molecules/Modal'
import type { ModalHeaderBaseProps } from '../../../molecules/Modal/types'
-
-interface RunError {
- id: string
- errorType: string
- createdAt: string
- detail: string
-}
+import type { RunError } from '@opentrons/api-client'
interface RunFailedModalProps {
runId: string
@@ -44,11 +39,13 @@ export function RunFailedModal({
const { stopRun } = useStopRunMutation()
const [isCanceling, setIsCanceling] = React.useState(false)
- if (errors == null) return null
+ if (errors == null || errors.length === 0) return null
const modalHeader: ModalHeaderBaseProps = {
title: t('run_failed_modal_title'),
}
+ const highestPriorityError = getHighestPriorityError(errors)
+
const handleClose = (): void => {
setIsCanceling(true)
setShowRunFailedModal(false)
@@ -76,8 +73,9 @@ export function RunFailedModal({
alignItems={ALIGN_FLEX_START}
>
- {t('error_type', {
- errorType: errors[0].errorType,
+ {t('error_info', {
+ errorType: highestPriorityError.errorType,
+ errorCode: highestPriorityError.errorCode,
})}
-
- {errors?.map(error => (
-
- {error.detail}
+
+
+ {highestPriorityError.detail}
+
+ {!isEmpty(highestPriorityError.errorInfo) && (
+
+ {JSON.stringify(highestPriorityError.errorInfo)}
- ))}
+ )}
@@ -135,3 +132,63 @@ const SCROLL_BAR_STYLE = css`
border-radius: 11px;
}
`
+
+const _getHighestPriorityError = (error: RunError): RunError => {
+ if (error.wrappedErrors.length === 0) {
+ return error
+ }
+
+ let highestPriorityError = error
+
+ error.wrappedErrors.forEach(wrappedError => {
+ const e = _getHighestPriorityError(wrappedError)
+ const isHigherPriority = _getIsHigherPriority(
+ e.errorCode,
+ highestPriorityError.errorCode
+ )
+ if (isHigherPriority) {
+ highestPriorityError = e
+ }
+ })
+ return highestPriorityError
+}
+
+/**
+ * returns true if the first error code is higher priority than the second, false otherwise
+ */
+const _getIsHigherPriority = (
+ errorCode1: string,
+ errorCode2: string
+): boolean => {
+ const errorNumber1 = Number(errorCode1)
+ const errorNumber2 = Number(errorCode2)
+
+ const isSameCategory =
+ Math.floor(errorNumber1 / 1000) === Math.floor(errorNumber2 / 1000)
+ const isCode1GenericError = errorNumber1 % 1000 === 0
+
+ let isHigherPriority = null
+
+ if (
+ (isSameCategory && !isCode1GenericError) ||
+ (!isSameCategory && errorNumber1 < errorNumber2)
+ ) {
+ isHigherPriority = true
+ } else {
+ isHigherPriority = false
+ }
+
+ return isHigherPriority
+}
+
+export const getHighestPriorityError = (errors: RunError[]): RunError => {
+ const highestFirstWrappedError = _getHighestPriorityError(errors[0])
+ return [highestFirstWrappedError, ...errors.slice(1)].reduce((acc, val) => {
+ const e = _getHighestPriorityError(val)
+ const isHigherPriority = _getIsHigherPriority(e.errorCode, acc.errorCode)
+ if (isHigherPriority) {
+ return e
+ }
+ return acc
+ })
+}
diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx
index bb0f98546e4..b8bf79aad9e 100644
--- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx
+++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx
@@ -16,12 +16,53 @@ const mockPush = jest.fn()
const mockErrors = [
{
id: 'd0245210-dfb9-4f1c-8ad0-3416b603a7ba',
- errorType: 'ExceptionInProtocolError',
+ errorType: 'generalError',
createdAt: '2023-04-09T21:41:51.333171+00:00',
- detail:
- 'ProtocolEngineError [line 16]: ModuleNotAttachedError: No available',
+ detail: 'Error with code 4000 (lowest priority)',
+ errorInfo: {},
+ errorCode: '4000',
+ wrappedErrors: [
+ {
+ id: 'd0245210-dfb9-4f1c-8ad0-3416b603a7ba',
+ errorType: 'roboticsInteractionError',
+ createdAt: '2023-04-09T21:41:51.333171+00:00',
+ detail: 'Error with code 3000 (second lowest priortiy)',
+ errorInfo: {},
+ errorCode: '3000',
+ wrappedErrors: [],
+ },
+ {
+ id: 'd0245210-dfb9-4f1c-8ad0-3416b603a7ba',
+ errorType: 'roboticsControlError',
+ createdAt: '2023-04-09T21:41:51.333171+00:00',
+ detail: 'Error with code 2000 (second highest priority)',
+ errorInfo: {},
+ errorCode: '2000',
+ wrappedErrors: [
+ {
+ id: 'd0245210-dfb9-4f1c-8ad0-3416b603a7ba',
+ errorType: 'hardwareCommunicationError',
+ createdAt: '2023-04-09T21:41:51.333171+00:00',
+ detail: 'Error with code 1000 (highest priority)',
+ errorInfo: {},
+ errorCode: '1000',
+ wrappedErrors: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 'd0245210-dfb9-4f1c-8ad0-3416b603a7ba',
+ errorType: 'roboticsInteractionError',
+ createdAt: '2023-04-09T21:41:51.333171+00:00',
+ detail: 'Error with code 2001 (second highest priortiy)',
+ errorInfo: {},
+ errorCode: '2001',
+ wrappedErrors: [],
},
]
+
let mockStopRun: jest.Mock
jest.mock('react-router-dom', () => {
@@ -60,12 +101,11 @@ describe('RunFailedModal', () => {
mockUseStopRunMutation.mockReturnValue({ stopRun: mockStopRun } as any)
})
- it('should render text and button', () => {
+ it('should render the highest priority error', () => {
const [{ getByText }] = render(props)
getByText('Run failed')
- getByText(
- 'ProtocolEngineError [line 16]: ModuleNotAttachedError: No available'
- )
+ getByText('Error 1000: hardwareCommunicationError')
+ getByText('Error with code 1000 (highest priority)')
getByText(
'Download the run logs from the Opentrons App and send it to support@opentrons.com for assistance.'
)
@@ -79,7 +119,4 @@ describe('RunFailedModal', () => {
expect(mockStopRun).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/dashboard')
})
- // ToDo (kj:04/12/2023) I made this test todo since we need the system update to align with the design.
- // This test will be added when we can get error code and other information
- it.todo('should render error code and message')
})
diff --git a/app/src/organisms/RunTimeControl/__fixtures__/index.ts b/app/src/organisms/RunTimeControl/__fixtures__/index.ts
index a748a6c985d..1a18a9a6bcf 100644
--- a/app/src/organisms/RunTimeControl/__fixtures__/index.ts
+++ b/app/src/organisms/RunTimeControl/__fixtures__/index.ts
@@ -125,6 +125,9 @@ export const mockFailedRun: RunData = {
errorType: 'RuntimeError',
createdAt: 'noon forty-five',
detail: 'this run failed',
+ errorInfo: {},
+ wrappedErrors: [],
+ errorCode: '4000',
},
],
pipettes: [],