-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement dashboard plugin wrapper component
This takes care of offline caching for plugins which works the same in all analytics apps.
- Loading branch information
Showing
3 changed files
with
150 additions
and
0 deletions.
There are no files selected for viewing
85 changes: 85 additions & 0 deletions
85
src/components/DashboardPluginWrapper/DashboardPluginWrapper.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Layer> | ||
<CenteredContent> | ||
<CircularLoader /> | ||
</CenteredContent> | ||
</Layer> | ||
) | ||
} | ||
|
||
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 ( | ||
<CacheableSection id={id} loadingMask={<LoadingMask />}> | ||
{children} | ||
</CacheableSection> | ||
) | ||
} | ||
|
||
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 ? ( | ||
<div | ||
style={{ | ||
display: 'flex', | ||
height: '100%', | ||
overflow: 'hidden', | ||
}} | ||
> | ||
<CacheableSectionWrapper | ||
id={cacheId} | ||
isParentCached={isParentCached} | ||
> | ||
{children(props)} | ||
</CacheableSectionWrapper> | ||
<CssVariables colors spacers elevations /> | ||
</div> | ||
) : null | ||
} | ||
|
||
DashboardPluginWrapper.propTypes = { | ||
cacheId: PropTypes.string, | ||
children: PropTypes.func, | ||
isParentCached: PropTypes.bool, | ||
onInstallationStatusChange: PropTypes.func, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |