diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 96ce24a194..b3f9cadcdf 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -5,6 +5,7 @@ import { Switch, Route } from 'react-router-dom' import Breadcrumbs from './breadcrumbs/Breadcrumbs' import Navbar from './components/Navbar' +import { NetworkStatusMessage } from './components/network-status' import PrivateRoute from './components/PrivateRoute' import Sidebar from './components/Sidebar' import Dashboard from './dashboard/Dashboard' @@ -23,6 +24,7 @@ const HospitalRun = () => { return (
+
diff --git a/src/__tests__/components/network-status/NetworkStatusMessage.test.tsx b/src/__tests__/components/network-status/NetworkStatusMessage.test.tsx new file mode 100644 index 0000000000..cbd144510c --- /dev/null +++ b/src/__tests__/components/network-status/NetworkStatusMessage.test.tsx @@ -0,0 +1,51 @@ +import { render, shallow } from 'enzyme' +import React from 'react' + +import { useTranslation } from '../../../__mocks__/react-i18next' +import { NetworkStatusMessage } from '../../../components/network-status' +import { useNetworkStatus } from '../../../components/network-status/useNetworkStatus' + +jest.mock('../../../components/network-status/useNetworkStatus') +const useNetworkStatusMock = (useNetworkStatus as unknown) as jest.MockInstance< + ReturnType, + any +> + +const englishTranslationsMock = { + 'networkStatus.offline': 'you are working in offline mode', + 'networkStatus.online': 'you are back online', +} + +const useTranslationReturnValue = useTranslation() as any +useTranslationReturnValue.t = (key: keyof typeof englishTranslationsMock) => + englishTranslationsMock[key] +const { t } = useTranslationReturnValue + +describe('NetworkStatusMessage', () => { + it('returns null if the app has always been online', () => { + useNetworkStatusMock.mockReturnValue({ + isOnline: true, + wasOffline: false, + }) + const wrapper = shallow() + expect(wrapper.equals(null as any)).toBe(true) + }) + it(`shows the message "${t('networkStatus.offline')}" if the app goes offline`, () => { + useNetworkStatusMock.mockReturnValue({ + isOnline: false, + wasOffline: false, + }) + const wrapper = render() + expect(wrapper.text()).toContain(t('networkStatus.offline')) + }) + it(`shows the message "${t( + 'networkStatus.online', + )}" if the app goes back online after it was offline`, () => { + useNetworkStatusMock.mockReturnValue({ + isOnline: true, + wasOffline: true, + }) + const wrapper = render() + expect(wrapper.text()).toContain(t('networkStatus.online')) + }) +}) diff --git a/src/components/network-status/NetworkStatusMessage.tsx b/src/components/network-status/NetworkStatusMessage.tsx new file mode 100644 index 0000000000..c8269d22d6 --- /dev/null +++ b/src/components/network-status/NetworkStatusMessage.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { useNetworkStatus } from './useNetworkStatus' + +const ONLINE_COLOR = 'rgba(0, 255, 0, 0.55)' +const OFFLINE_COLOR = 'rgba(255, 0, 0, 0.65)' +const OPACITY_TRANSITION_TIME = 4000 +const BASE_STYLE = { + height: '50px', + pointerEvents: 'none' as 'none', + transition: `opacity ${OPACITY_TRANSITION_TIME}ms ease-in`, +} + +export const NetworkStatusMessage = () => { + const { t } = useTranslation() + const { isOnline, wasOffline } = useNetworkStatus() + const [shouldRender, setShouldRender] = useState(true) + const [opacity, setOpacity] = useState(1) + + if (isOnline && !wasOffline) { + return null + } + + if (!isOnline && opacity !== 1) { + setShouldRender(true) + setOpacity(1) + } + + if (isOnline && wasOffline && opacity !== 0) { + setOpacity(0) + setTimeout(() => { + setShouldRender(false) + }, OPACITY_TRANSITION_TIME) + } + + if (!shouldRender) { + return null + } + + const style = { + ...BASE_STYLE, + backgroundColor: isOnline ? ONLINE_COLOR : OFFLINE_COLOR, + opacity, + } + + return ( +
+ {isOnline ? t('networkStatus.online') : t('networkStatus.offline')} +
+ ) +} diff --git a/src/components/network-status/index.ts b/src/components/network-status/index.ts new file mode 100644 index 0000000000..e69b8f6c18 --- /dev/null +++ b/src/components/network-status/index.ts @@ -0,0 +1,2 @@ +export { NetworkStatusMessage } from './NetworkStatusMessage' +export { useNetworkStatus } from './useNetworkStatus' diff --git a/src/components/network-status/types.ts b/src/components/network-status/types.ts new file mode 100644 index 0000000000..1ab39c4229 --- /dev/null +++ b/src/components/network-status/types.ts @@ -0,0 +1,4 @@ +export interface NetworkStatus { + isOnline: boolean + wasOffline: boolean +} diff --git a/src/components/network-status/useNetworkStatus.ts b/src/components/network-status/useNetworkStatus.ts new file mode 100644 index 0000000000..698a7471e5 --- /dev/null +++ b/src/components/network-status/useNetworkStatus.ts @@ -0,0 +1,28 @@ +import { useState, useEffect } from 'react' + +import { NetworkStatus } from './types' + +export const useNetworkStatus = (): NetworkStatus => { + const isOnline = navigator.onLine + const [networkStatus, setNetworkStatus] = useState({ + isOnline, + wasOffline: !isOnline, + }) + const handleOnline = () => { + setNetworkStatus((prevState) => ({ ...prevState, isOnline: true })) + } + const handleOffline = () => { + setNetworkStatus((prevState) => ({ ...prevState, isOnline: false, wasOffline: true })) + } + useEffect(() => { + window.addEventListener('online', handleOnline) + window.addEventListener('offline', handleOffline) + + return () => { + window.removeEventListener('online', handleOnline) + window.removeEventListener('offline', handleOffline) + } + }, []) + + return networkStatus +} diff --git a/src/locales/enUs/translations/index.ts b/src/locales/enUs/translations/index.ts index ae9f29343f..55e5285113 100644 --- a/src/locales/enUs/translations/index.ts +++ b/src/locales/enUs/translations/index.ts @@ -2,6 +2,7 @@ import actions from './actions' import dashboard from './dashboard' import incidents from './incidents' import labs from './labs' +import networkStatus from './network-status' import patient from './patient' import patients from './patients' import scheduling from './scheduling' @@ -12,6 +13,7 @@ import states from './states' export default { ...actions, ...dashboard, + ...networkStatus, ...patient, ...patients, ...scheduling, diff --git a/src/locales/enUs/translations/network-status/index.ts b/src/locales/enUs/translations/network-status/index.ts new file mode 100644 index 0000000000..925bfa9e69 --- /dev/null +++ b/src/locales/enUs/translations/network-status/index.ts @@ -0,0 +1,6 @@ +export default { + networkStatus: { + offline: 'you are working in offline mode', + online: 'you are back online', + }, +}