Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/app_odd-anonymous-localization-p…
Browse files Browse the repository at this point in the history
…rovider' into oem-mode-integration
  • Loading branch information
vegano1 committed Apr 9, 2024
2 parents f6092ca + 48cd622 commit dd11ffd
Show file tree
Hide file tree
Showing 96 changed files with 664 additions and 700 deletions.
11 changes: 11 additions & 0 deletions api-client/src/robot/getRobotSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GET, request } from '../request'

import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
import type { RobotSettingsResponse } from './types'

export function getRobotSettings(
config: HostConfig
): ResponsePromise<RobotSettingsResponse> {
return request<RobotSettingsResponse>(GET, '/settings', null, config)
}
5 changes: 5 additions & 0 deletions api-client/src/robot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ export { getEstopStatus } from './getEstopStatus'
export { acknowledgeEstopDisengage } from './acknowledgeEstopDisengage'
export { getLights } from './getLights'
export { setLights } from './setLights'
export { getRobotSettings } from './getRobotSettings'

export type {
DoorStatus,
EstopPhysicalStatus,
EstopState,
EstopStatus,
Lights,
RobotSettings,
RobotSettingsField,
RobotSettingsResponse,
SetLightsData,
} from './types'
15 changes: 15 additions & 0 deletions api-client/src/robot/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,18 @@ export interface Lights {
export interface SetLightsData {
on: boolean
}

export interface RobotSettingsField {
id: string
title: string
description: string
value: boolean | null
restart_required?: boolean
}

export type RobotSettings = RobotSettingsField[]

export interface RobotSettingsResponse {
settings: RobotSettings
links?: { restart?: string }
}
82 changes: 43 additions & 39 deletions app/src/App/DesktopApp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'
import { ErrorBoundary } from 'react-error-boundary'
import { I18nextProvider } from 'react-i18next'

import {
Box,
Expand All @@ -11,6 +12,7 @@ import {
import { ApiHostProvider } from '@opentrons/react-api-client'
import NiceModal from '@ebay/nice-modal-react'

import { i18n } from '../i18n'
import { Alerts } from '../organisms/Alerts'
import { Breadcrumbs } from '../organisms/Breadcrumbs'
import { ToasterOven } from '../organisms/ToasterOven'
Expand Down Expand Up @@ -101,45 +103,47 @@ export const DesktopApp = (): JSX.Element => {

return (
<NiceModal.Provider>
<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>
<I18nextProvider i18n={i18n}>
<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>
</I18nextProvider>
</NiceModal.Provider>
)
}
Expand Down
106 changes: 59 additions & 47 deletions app/src/App/OnDeviceDisplayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ApiHostProvider } from '@opentrons/react-api-client'
import NiceModal from '@ebay/nice-modal-react'

import { SleepScreen } from '../atoms/SleepScreen'
import { OnDeviceLocalizationProvider } from '../LocalizationProvider'
import { ToasterOven } from '../organisms/ToasterOven'
import { MaintenanceRunTakeover } from '../organisms/TakeoverModal'
import { FirmwareUpdateTakeover } from '../organisms/FirmwareUpdateModal/FirmwareUpdateTakeover'
Expand Down Expand Up @@ -151,6 +152,53 @@ export const OnDeviceDisplayApp = (): JSX.Element => {
}
const dispatch = useDispatch<Dispatch>()
const isIdle = useIdle(sleepTime, options)

React.useEffect(() => {
if (isIdle) {
dispatch(updateBrightness(TURN_OFF_BACKLIGHT))
} else {
dispatch(
updateConfigValue(
'onDeviceDisplaySettings.brightness',
userSetBrightness
)
)
}
}, [dispatch, isIdle, userSetBrightness])

// TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals
return (
<ApiHostProvider hostname="127.0.0.1">
<OnDeviceLocalizationProvider>
<ErrorBoundary FallbackComponent={OnDeviceDisplayAppFallback}>
<Box width="100%" css="user-select: none;">
{isIdle ? (
<SleepScreen />
) : (
<>
<EstopTakeover />
<MaintenanceRunTakeover>
<FirmwareUpdateTakeover />
<NiceModal.Provider>
<ToasterOven>
<ProtocolReceiptToasts />
<OnDeviceDisplayAppRoutes />
</ToasterOven>
</NiceModal.Provider>
</MaintenanceRunTakeover>
</>
)}
</Box>
</ErrorBoundary>
<TopLevelRedirects />
</OnDeviceLocalizationProvider>
</ApiHostProvider>
)
}

// split to a separate function because scrollRef rerenders on every route change
// this avoids rerendering parent providers as well
export function OnDeviceDisplayAppRoutes(): JSX.Element {
const [currentNode, setCurrentNode] = React.useState<null | HTMLElement>(null)
const scrollRef = React.useCallback((node: HTMLElement | null) => {
setCurrentNode(node)
Expand All @@ -176,54 +224,18 @@ export const OnDeviceDisplayApp = (): JSX.Element => {
}
`

React.useEffect(() => {
if (isIdle) {
dispatch(updateBrightness(TURN_OFF_BACKLIGHT))
} else {
dispatch(
updateConfigValue(
'onDeviceDisplaySettings.brightness',
userSetBrightness
)
)
}
}, [dispatch, isIdle, userSetBrightness])

// TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals
return (
<ApiHostProvider hostname="127.0.0.1">
<ErrorBoundary FallbackComponent={OnDeviceDisplayAppFallback}>
<Box width="100%" css="user-select: none;">
{isIdle ? (
<SleepScreen />
) : (
<>
<EstopTakeover />
<MaintenanceRunTakeover>
<FirmwareUpdateTakeover />
<NiceModal.Provider>
<ToasterOven>
<ProtocolReceiptToasts />
<Switch>
{ON_DEVICE_DISPLAY_PATHS.map(path => (
<Route key={path} exact path={path}>
<Box css={TOUCH_SCREEN_STYLE} ref={scrollRef}>
<ModalPortalRoot />
{getPathComponent(path)}
</Box>
</Route>
))}
<Redirect exact from="/" to={'/loading'} />
</Switch>
</ToasterOven>
</NiceModal.Provider>
</MaintenanceRunTakeover>
</>
)}
</Box>
</ErrorBoundary>
<TopLevelRedirects />
</ApiHostProvider>
<Switch>
{ON_DEVICE_DISPLAY_PATHS.map(path => (
<Route key={path} exact path={path}>
<Box css={TOUCH_SCREEN_STYLE} ref={scrollRef}>
<ModalPortalRoot />
{getPathComponent(path)}
</Box>
</Route>
))}
<Redirect exact from="/" to={'/loading'} />
</Switch>
)
}

Expand Down
6 changes: 4 additions & 2 deletions app/src/App/OnDeviceDisplayAppFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { ModalHeaderBaseProps } from '../molecules/Modal/types'
export function OnDeviceDisplayAppFallback({
error,
}: FallbackProps): JSX.Element {
const { t } = useTranslation('app_settings')
const { t } = useTranslation(['app_settings', 'branded'])
const trackEvent = useTrackEvent()
const dispatch = useDispatch<Dispatch>()
const localRobot = useSelector(getLocalRobot)
Expand Down Expand Up @@ -59,7 +59,9 @@ export function OnDeviceDisplayAppFallback({
alignItems={ALIGN_CENTER}
justifyContent={JUSTIFY_CENTER}
>
<StyledText as="p">{t('error_boundary_description')}</StyledText>
<StyledText as="p">
{t('branded:error_boundary_description')}
</StyledText>
<MediumButton
width="100%"
buttonType="alert"
Expand Down
9 changes: 9 additions & 0 deletions app/src/App/__tests__/OnDeviceDisplayApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MemoryRouter } from 'react-router-dom'

import { renderWithProviders } from '../../__testing-utils__'
import { i18n } from '../../i18n'
import { OnDeviceLocalizationProvider } from '../../LocalizationProvider'
import { ConnectViaEthernet } from '../../pages/ConnectViaEthernet'
import { ConnectViaUSB } from '../../pages/ConnectViaUSB'
import { ConnectViaWifi } from '../../pages/ConnectViaWifi'
Expand All @@ -29,8 +30,10 @@ import { mockConnectedRobot } from '../../redux/discovery/__fixtures__'
import { useCurrentRunRoute, useProtocolReceiptToast } from '../hooks'
import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs'

import type { OnDeviceLocalizationProviderProps } from '../../LocalizationProvider'
import type { OnDeviceDisplaySettings } from '../../redux/config/schema-types'

vi.mock('../../LocalizationProvider')
vi.mock('../../pages/Welcome')
vi.mock('../../pages/NetworkSetupMenu')
vi.mock('../../pages/ConnectViaEthernet')
Expand Down Expand Up @@ -83,6 +86,12 @@ describe('OnDeviceDisplayApp', () => {
},
},
} as any)
// TODO(bh, 2024-03-27): implement testing of branded and anonymous i18n, but for now pass through
vi.mocked(
OnDeviceLocalizationProvider
).mockImplementation((props: OnDeviceLocalizationProviderProps) => (
<>{props.children}</>
))
})
afterEach(() => {
vi.resetAllMocks()
Expand Down
63 changes: 63 additions & 0 deletions app/src/LocalizationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react'
import { I18nextProvider } from 'react-i18next'
import reduce from 'lodash/reduce'

import { useRobotSettingsQuery } from '@opentrons/react-api-client'

import { resources } from './assets/localization'
import { i18n, i18nCb, i18nConfig } from './i18n'

import type { RobotSettingsField } from '@opentrons/api-client'

export interface OnDeviceLocalizationProviderProps {
children?: React.ReactNode
}

const BRANDED_RESOURCE = 'branded'
const ANONYMOUS_RESOURCE = 'anonymous'

// TODO(bh, 2024-03-26): anonymization limited to ODD for now, may change in future OEM phases
export function OnDeviceLocalizationProvider(
props: OnDeviceLocalizationProviderProps
): JSX.Element | null {
const { settings } = useRobotSettingsQuery().data ?? {}
const oemModeSetting = (settings ?? []).find(
(setting: RobotSettingsField) => setting?.id === 'enableOEMMode'
)
const isOEMMode = oemModeSetting?.value ?? false

// iterate through language resources, nested files, substitute anonymous file for branded file for OEM mode
const anonResources = reduce(
resources,
(acc, resource, language) => {
const anonFiles = reduce(
resource,
(acc, file, fileName) => {
if (fileName === BRANDED_RESOURCE && isOEMMode) {
return acc
} else if (fileName === ANONYMOUS_RESOURCE) {
return isOEMMode ? { ...acc, [BRANDED_RESOURCE]: file } : acc
} else {
return { ...acc, [fileName]: file }
}
},
{}
)
return { ...acc, [language]: anonFiles }
},
{}
)

const anonI18n = i18n.createInstance(
{
...i18nConfig,
resources: anonResources,
},
i18nCb
)

// block render until settings are fetched
return settings != null ? (
<I18nextProvider i18n={anonI18n}>{props.children}</I18nextProvider>
) : null
}
Loading

0 comments on commit dd11ffd

Please sign in to comment.