From cb819ee36870e2e4023c792346511347ca9171bf Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Wed, 17 Jan 2024 14:24:51 -0500 Subject: [PATCH] feat(app,app-shell): create desktop app error boundary (#14323) this error boundary will capture a runtime error anywhere in the desktop app and show a general error modal with error message instead of an app whitescreen. on clicking the "Reload app" button, the app navigates to the root page and initiates an electron browser window reload via a new RELOAD_UI action that is registered in the app-shell ui module. --- app-shell/src/main.ts | 3 +- app-shell/src/ui.ts | 19 +++++ app/src/App/DesktopApp.tsx | 76 ++++++++++--------- app/src/App/DesktopAppFallback.tsx | 64 ++++++++++++++++ .../assets/localization/en/app_settings.json | 2 + app/src/redux/analytics/constants.ts | 1 + app/src/redux/shell/actions.ts | 10 +++ app/src/redux/shell/types.ts | 9 +++ 8 files changed, 148 insertions(+), 36 deletions(-) create mode 100644 app/src/App/DesktopAppFallback.tsx diff --git a/app-shell/src/main.ts b/app-shell/src/main.ts index b1ef492b949..db68d33a753 100644 --- a/app-shell/src/main.ts +++ b/app-shell/src/main.ts @@ -2,7 +2,7 @@ import { app, ipcMain } from 'electron' import contextMenu from 'electron-context-menu' -import { createUi } from './ui' +import { createUi, registerReloadUi } from './ui' import { initializeMenu } from './menu' import { createLogger } from './log' import { registerProtocolAnalysis } from './protocol-analysis' @@ -88,6 +88,7 @@ function startUp(): void { registerSystemInfo(dispatch), registerProtocolStorage(dispatch), registerUsb(dispatch), + registerReloadUi(mainWindow), ] ipcMain.on('dispatch', (_, action) => { diff --git a/app-shell/src/ui.ts b/app-shell/src/ui.ts index 6bdd1240edf..e36a0ac5625 100644 --- a/app-shell/src/ui.ts +++ b/app-shell/src/ui.ts @@ -1,9 +1,14 @@ // sets up the main window ui import { app, shell, BrowserWindow } from 'electron' import path from 'path' + +import { RELOAD_UI } from '@opentrons/app/src/redux/shell/actions' + import { getConfig } from './config' import { createLogger } from './log' +import type { Action } from './types' + const config = getConfig('ui') const log = createLogger('ui') @@ -61,3 +66,17 @@ export function createUi(): BrowserWindow { return mainWindow } + +export function registerReloadUi( + browserWindow: BrowserWindow +): (action: Action) => unknown { + return function handleAction(action: Action) { + switch (action.type) { + case RELOAD_UI: + log.info(`reloading UI: ${action.payload.message}`) + browserWindow.webContents.reload() + + break + } + } +} diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index 86e81abcfd9..f42ef7e0e80 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom' +import { ErrorBoundary } from 'react-error-boundary' import { Box, @@ -29,6 +30,7 @@ import { OPENTRONS_USB } from '../redux/discovery' import { appShellRequestor } from '../redux/shell/remote' import { useRobot, useIsFlex } from '../organisms/Devices/hooks' import { PortalRoot as ModalPortalRoot } from './portal' +import { DesktopAppFallback } from './DesktopAppFallback' import type { RouteProps, DesktopRouteParams } from './types' @@ -99,41 +101,45 @@ export const DesktopApp = (): JSX.Element => { return ( - - - - - - - {desktopRoutes.map(({ Component, exact, path }: RouteProps) => { - return ( - - - - - - - - ) - })} - - - - - - - + + + + + + + + {desktopRoutes.map( + ({ Component, exact, path }: RouteProps) => { + return ( + + + + + + + + ) + } + )} + + + + + + + + ) } diff --git a/app/src/App/DesktopAppFallback.tsx b/app/src/App/DesktopAppFallback.tsx new file mode 100644 index 00000000000..f1e1ce1e990 --- /dev/null +++ b/app/src/App/DesktopAppFallback.tsx @@ -0,0 +1,64 @@ +import * as React from 'react' +import { useDispatch } from 'react-redux' +import { useHistory } from 'react-router-dom' +import { useTranslation } from 'react-i18next' + +import { useTrackEvent, ANALYTICS_DESKTOP_APP_ERROR } from '../redux/analytics' + +import type { FallbackProps } from 'react-error-boundary' + +import { + AlertPrimaryButton, + ALIGN_FLEX_END, + DIRECTION_COLUMN, + Flex, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' + +import { StyledText } from '../atoms/text' +import { LegacyModal } from '../molecules/LegacyModal' +import { reloadUi } from '../redux/shell' + +import type { Dispatch } from '../redux/types' + +export function DesktopAppFallback({ error }: FallbackProps): JSX.Element { + const { t } = useTranslation('app_settings') + const trackEvent = useTrackEvent() + const dispatch = useDispatch() + const history = useHistory() + const handleReloadClick = (): void => { + trackEvent({ + name: ANALYTICS_DESKTOP_APP_ERROR, + properties: { errorMessage: error.message }, + }) + // route to the root page and initiate an electron browser window reload via app-shell + history.push('/') + dispatch(reloadUi(error.message)) + } + + return ( + + + + + {t('error_boundary_desktop_app_description')} + + + {error.message} + + + + {t('reload_app')} + + + + ) +} diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 747956b0b27..e94a64cedfb 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -34,6 +34,7 @@ "enable_dev_tools": "Developer Tools", "enable_dev_tools_description": "Enabling this setting opens Developer Tools on app launch, enables additional logging and gives access to feature flags.", "error_boundary_description": "You need to restart the touchscreen. Then download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", + "error_boundary_desktop_app_description": "You need to reload the app. Contact support with the following error message:", "error_boundary_title": "An unknown error has occurred", "feature_flags": "Feature Flags", "general": "General", @@ -71,6 +72,7 @@ "prompt": "Always show the prompt to choose calibration block or trash bin", "receive_alert": "Receive an alert when an Opentrons software update is available.", "release_notes": "Release notes", + "reload_app": "Reload app", "remind_later": "Remind me later", "reset_to_default": "Reset to default", "restart_touchscreen": "Restart touchscreen", diff --git a/app/src/redux/analytics/constants.ts b/app/src/redux/analytics/constants.ts index da66e4fe84a..108601a9de7 100644 --- a/app/src/redux/analytics/constants.ts +++ b/app/src/redux/analytics/constants.ts @@ -42,3 +42,4 @@ export const ANALYTICS_OPEN_LABWARE_CREATOR_FROM_BOTTOM_OF_LABWARE_LIBRARY_LIST 'openLabwareCreatorFromBottomOfLabwareLibraryList' export const ANALYTICS_SENT_TO_FLEX = 'sendToFlex' // This would be changed export const ANALYTICS_ODD_APP_ERROR = 'oddError' +export const ANALYTICS_DESKTOP_APP_ERROR = 'desktopAppError' diff --git a/app/src/redux/shell/actions.ts b/app/src/redux/shell/actions.ts index 4c20d59bd73..3374b4729cc 100644 --- a/app/src/redux/shell/actions.ts +++ b/app/src/redux/shell/actions.ts @@ -2,6 +2,7 @@ import type { UiInitializedAction, UsbRequestsAction, AppRestartAction, + ReloadUiAction, SendLogAction, UpdateBrightnessAction, RobotMassStorageDeviceAdded, @@ -15,6 +16,7 @@ export const USB_HTTP_REQUESTS_START: 'shell:USB_HTTP_REQUESTS_START' = export const USB_HTTP_REQUESTS_STOP: 'shell:USB_HTTP_REQUESTS_STOP' = 'shell:USB_HTTP_REQUESTS_STOP' export const APP_RESTART: 'shell:APP_RESTART' = 'shell:APP_RESTART' +export const RELOAD_UI: 'shell:RELOAD_UI' = 'shell:RELOAD_UI' export const SEND_LOG: 'shell:SEND_LOG' = 'shell:SEND_LOG' export const UPDATE_BRIGHTNESS: 'shell:UPDATE_BRIGHTNESS' = 'shell:UPDATE_BRIGHTNESS' @@ -48,6 +50,14 @@ export const appRestart = (message: string): AppRestartAction => ({ meta: { shell: true }, }) +export const reloadUi = (message: string): ReloadUiAction => ({ + type: RELOAD_UI, + payload: { + message: message, + }, + meta: { shell: true }, +}) + export const sendLog = (message: string): SendLogAction => ({ type: SEND_LOG, payload: { diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index cc6524f3544..e5aff7672ac 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -63,6 +63,14 @@ export interface AppRestartAction { meta: { shell: true } } +export interface ReloadUiAction { + type: 'shell:RELOAD_UI' + payload: { + message: string + } + meta: { shell: true } +} + export interface SendLogAction { type: 'shell:SEND_LOG' payload: { @@ -110,6 +118,7 @@ export type ShellAction = | RobotSystemAction | UsbRequestsAction | AppRestartAction + | ReloadUiAction | SendLogAction | UpdateBrightnessAction | RobotMassStorageDeviceAdded