Skip to content

Commit

Permalink
feat: implement dashboard plugin wrapper component
Browse files Browse the repository at this point in the history
This takes care of offline caching for plugins which works the same in
all analytics apps.
  • Loading branch information
edoardo committed Jun 3, 2024
1 parent fdf1b9f commit ab860e6
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 0 deletions.
85 changes: 85 additions & 0 deletions src/components/DashboardPluginWrapper/DashboardPluginWrapper.js
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,
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
63 changes: 63 additions & 0 deletions src/modules/getPWAInstallationStatus.js
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
}

0 comments on commit ab860e6

Please sign in to comment.