Skip to content

Commit

Permalink
feat(app,app-shell): create desktop app error boundary (#14323)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
brenthagen authored Jan 17, 2024
1 parent 15ece39 commit cb819ee
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 36 deletions.
3 changes: 2 additions & 1 deletion app-shell/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -88,6 +88,7 @@ function startUp(): void {
registerSystemInfo(dispatch),
registerProtocolStorage(dispatch),
registerUsb(dispatch),
registerReloadUi(mainWindow),
]

ipcMain.on('dispatch', (_, action) => {
Expand Down
19 changes: 19 additions & 0 deletions app-shell/src/ui.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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
}
}
}
76 changes: 41 additions & 35 deletions app/src/App/DesktopApp.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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'

Expand Down Expand Up @@ -99,41 +101,45 @@ export const DesktopApp = (): JSX.Element => {

return (
<NiceModal.Provider>
<Navbar routes={desktopRoutes} />
<ToasterOven>
<EmergencyStopContext.Provider
value={{
isEmergencyStopModalDismissed,
setIsEmergencyStopModalDismissed,
}}
>
<Box width="100%">
<Alerts>
<Switch>
{desktopRoutes.map(({ Component, exact, path }: RouteProps) => {
return (
<Route key={path} exact={exact} path={path}>
<Breadcrumbs />
<Box
position={POSITION_RELATIVE}
width="100%"
height="100%"
backgroundColor={COLORS.grey10}
overflow={OVERFLOW_AUTO}
>
<ModalPortalRoot />
<Component />
</Box>
</Route>
)
})}
<Redirect exact from="/" to="/protocols" />
</Switch>
<RobotControlTakeover />
</Alerts>
</Box>
</EmergencyStopContext.Provider>
</ToasterOven>
<ErrorBoundary FallbackComponent={DesktopAppFallback}>
<Navbar routes={desktopRoutes} />
<ToasterOven>
<EmergencyStopContext.Provider
value={{
isEmergencyStopModalDismissed,
setIsEmergencyStopModalDismissed,
}}
>
<Box width="100%">
<Alerts>
<Switch>
{desktopRoutes.map(
({ Component, exact, path }: RouteProps) => {
return (
<Route key={path} exact={exact} path={path}>
<Breadcrumbs />
<Box
position={POSITION_RELATIVE}
width="100%"
height="100%"
backgroundColor={COLORS.grey10}
overflow={OVERFLOW_AUTO}
>
<ModalPortalRoot />
<Component />
</Box>
</Route>
)
}
)}
<Redirect exact from="/" to="/protocols" />
</Switch>
<RobotControlTakeover />
</Alerts>
</Box>
</EmergencyStopContext.Provider>
</ToasterOven>
</ErrorBoundary>
</NiceModal.Provider>
)
}
Expand Down
64 changes: 64 additions & 0 deletions app/src/App/DesktopAppFallback.tsx
Original file line number Diff line number Diff line change
@@ -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<Dispatch>()
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 (
<LegacyModal
type="warning"
title={t('error_boundary_title')}
marginLeft="0"
>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing32}>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8}>
<StyledText as="p">
{t('error_boundary_desktop_app_description')}
</StyledText>
<StyledText as="p" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
{error.message}
</StyledText>
</Flex>
<AlertPrimaryButton
alignSelf={ALIGN_FLEX_END}
onClick={handleReloadClick}
>
{t('reload_app')}
</AlertPrimaryButton>
</Flex>
</LegacyModal>
)
}
2 changes: 2 additions & 0 deletions app/src/assets/localization/en/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected] 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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions app/src/redux/analytics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
10 changes: 10 additions & 0 deletions app/src/redux/shell/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
UiInitializedAction,
UsbRequestsAction,
AppRestartAction,
ReloadUiAction,
SendLogAction,
UpdateBrightnessAction,
RobotMassStorageDeviceAdded,
Expand All @@ -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'
Expand Down Expand Up @@ -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: {
Expand Down
9 changes: 9 additions & 0 deletions app/src/redux/shell/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -110,6 +118,7 @@ export type ShellAction =
| RobotSystemAction
| UsbRequestsAction
| AppRestartAction
| ReloadUiAction
| SendLogAction
| UpdateBrightnessAction
| RobotMassStorageDeviceAdded
Expand Down

0 comments on commit cb819ee

Please sign in to comment.