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: [],