From ab860e60746c32ac6dd410aa58294ba751d3e18e Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 3 Jun 2024 15:39:31 +0200 Subject: [PATCH] feat: implement dashboard plugin wrapper component This takes care of offline caching for plugins which works the same in all analytics apps. --- .../DashboardPluginWrapper.js | 85 +++++++++++++++++++ src/index.js | 2 + src/modules/getPWAInstallationStatus.js | 63 ++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 src/components/DashboardPluginWrapper/DashboardPluginWrapper.js create mode 100644 src/modules/getPWAInstallationStatus.js diff --git a/src/components/DashboardPluginWrapper/DashboardPluginWrapper.js b/src/components/DashboardPluginWrapper/DashboardPluginWrapper.js new file mode 100644 index 000000000..efb85f816 --- /dev/null +++ b/src/components/DashboardPluginWrapper/DashboardPluginWrapper.js @@ -0,0 +1,85 @@ +import { useCacheableSection, CacheableSection } from '@dhis2/app-runtime' +import { CenteredContent, CircularLoader, CssVariables, Layer } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React, { useEffect } from 'react' +import { getPWAInstallationStatus } from '../../modules/getPWAInstallationStatus.js' + +const LoadingMask = () => { + return ( + + + + + + ) +} + +const CacheableSectionWrapper = ({ id, children, isParentCached }) => { + const { startRecording, isCached, remove } = useCacheableSection(id) + + useEffect(() => { + if (isParentCached && !isCached) { + startRecording({ onError: console.error }) + } else if (!isParentCached && isCached) { + // Synchronize cache state on load or prop update + // -- a back-up to imperative `removeCachedData` + remove() + } + + // NB: Adding `startRecording` to dependencies causes + // an infinite recording loop as-is (probably need to memoize it) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isParentCached]) + + return ( + }> + {children} + + ) +} + +CacheableSectionWrapper.propTypes = { + children: PropTypes.node, + id: PropTypes.string, + isParentCached: PropTypes.bool, +} + +export const DashboardPluginWrapper = ({ + onInstallationStatusChange, + children, + cacheId, + isParentCached, + ...props +}) => { + useEffect(() => { + // Get & send PWA installation status now + getPWAInstallationStatus({ + onStateChange: onInstallationStatusChange, + }).then(onInstallationStatusChange) + }, [onInstallationStatusChange]) + + return props ? ( +
+ + {children(props)} + + +
+ ) : null +} + +DashboardPluginWrapper.propTypes = { + cacheId: PropTypes.string, + children: PropTypes.func, + isParentCached: PropTypes.bool, + onInstallationStatusChange: PropTypes.func, +} diff --git a/src/index.js b/src/index.js index 0fb8fe59b..43ca5c83b 100644 --- a/src/index.js +++ b/src/index.js @@ -47,6 +47,8 @@ export { useCachedDataQuery, } from './components/CachedDataQueryProvider.js' +export { DashboardPluginWrapper } from './components/DashboardPluginWrapper/DashboardPluginWrapper.js' + // Api export { default as Analytics } from './api/analytics/Analytics.js' diff --git a/src/modules/getPWAInstallationStatus.js b/src/modules/getPWAInstallationStatus.js new file mode 100644 index 000000000..028cd10f2 --- /dev/null +++ b/src/modules/getPWAInstallationStatus.js @@ -0,0 +1,63 @@ +export const INSTALLATION_STATES = { + READY: 'READY', + INSTALLING: 'INSTALLING', +} + +function handleInstallingWorker({ installingWorker, onStateChange }) { + installingWorker.onstatechange = () => { + if (installingWorker.state === 'activated') { + // ... and update state to 'ready' + onStateChange(INSTALLATION_STATES.READY) + } + } +} + +/** + * Gets the current installation state of the PWA features, which is intended + * to be reported from this plugin to the parent app to indicate that the + * static assets are cached and ready to be accessed locally instead of over + * the network. + * + * Returns either READY, INSTALLING, or `null` for not installed/won't install + */ +export async function getPWAInstallationStatus({ onStateChange }) { + if (!navigator.serviceWorker) { + // Nothing to do here + return null + } + + const registration = await navigator.serviceWorker.getRegistration() + if (!registration) { + // This shouldn't happen since this is a PWA app, but return null + return null + } + + if (registration.active) { + return INSTALLATION_STATES.READY + } + // note that 'registration.waiting' is skipped - it implies there's an active one + if (registration.installing) { + handleInstallingWorker({ + installingWorker: registration.installing, + onStateChange, + }) + return INSTALLATION_STATES.INSTALLING + } + + // It shouldn't normally be possible to get here, but just in case, + // listen for installations + registration.onupdatefound = () => { + // update state for this plugin to 'installing' + onStateChange(INSTALLATION_STATES.INSTALLING) + + // also listen for the installing worker to become active + const installingWorker = registration.installing + if (!installingWorker) { + return + } + handleInstallingWorker({ installingWorker, onStateChange }) + } + + // and in the mean time, return null to show 'not installed' + return null +}