Skip to content

Commit

Permalink
Add plugin infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
tokland committed Jun 12, 2024
1 parent 219d3f4 commit 3770335
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 0 deletions.
1 change: 1 addition & 0 deletions d2.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ module.exports = {
minDHIS2Version: '2.37',
entryPoints: {
app: './src/app/index.js',
plugin: './src/PluginWrapper.js',
},
}
129 changes: 129 additions & 0 deletions src/PluginWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useCacheableSection, CacheableSection } from '@dhis2/app-runtime'
import { CenteredContent, CircularLoader, Layer } from '@dhis2/ui'
import postRobot from '@krakenjs/post-robot'
import { debounce } from 'lodash/fp'
import PropTypes from 'prop-types'
import React, { useEffect, useLayoutEffect, useState } from 'react'
import { Plugin } from './plugin/Plugin.js'
import { getPWAInstallationStatus } from './util/getPWAInstallationStatus.js'

const LoadingMask = () => {
return (
<Layer>
<CenteredContent>
<CircularLoader />
</CenteredContent>
</Layer>
)
}

const CacheableSectionWrapper = ({
id,
children,
cacheNow,
isParentCached,
}) => {
const { startRecording, isCached, remove } = useCacheableSection(id)

useEffect(() => {
if (cacheNow) {
startRecording({ onError: console.error })
}

// 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
}, [cacheNow])

useEffect(() => {
const listener = postRobot.on(
'removeCachedData',
// todo: check domain too; differs based on deployment env though
{ window: window.parent },
() => remove()
)

return () => listener.cancel()
}, [remove])

useEffect(() => {
// Synchronize cache state on load or prop update
// -- a back-up to imperative `removeCachedData`
if (!isParentCached && isCached) {
remove()
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isParentCached])

return (
<CacheableSection id={id} loadingMask={LoadingMask}>
{children}
</CacheableSection>
)
}
CacheableSectionWrapper.propTypes = {
cacheNow: PropTypes.bool,
children: PropTypes.node,
id: PropTypes.string,
isParentCached: PropTypes.bool,
}

const sendInstallationStatus = (installationStatus) => {
postRobot.send(window.parent, 'installationStatus', { installationStatus })
}

const PluginWrapper = () => {
const [propsFromParent, setPropsFromParent] = useState()
const [renderId, setRenderId] = useState(null)

const receivePropsFromParent = (event) => setPropsFromParent(event.data)

useEffect(() => {
postRobot
.send(window.parent, 'getProps')
.then(receivePropsFromParent)
.catch((err) => console.error(err))

// Get & send PWA installation status now, and also prepare to send
// future updates (installing/ready)
getPWAInstallationStatus({
onStateChange: sendInstallationStatus,
}).then(sendInstallationStatus)

// Allow parent to update props
const listener = postRobot.on(
'newProps',
{ window: window.parent /* Todo: check domain */ },
receivePropsFromParent
)

return () => listener.cancel()
}, [])

useLayoutEffect(() => {
const updateRenderId = debounce(300, () =>
setRenderId((renderId) =>
typeof renderId === 'number' ? renderId + 1 : 1
)
)

window.addEventListener('resize', updateRenderId)

return () => window.removeEventListener('resize', updateRenderId)
}, [])

return propsFromParent ? (
<div
style={{
display: 'flex',
height: '100%',
overflow: 'hidden',
}}
>
<Plugin id={renderId} {...propsFromParent} />
</div>
) : null
}

export default PluginWrapper
6 changes: 6 additions & 0 deletions src/plugin/Plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react'
import { App } from '../app/app'

export const Plugin = (props) => {
return <App />
}
64 changes: 64 additions & 0 deletions src/util/getPWAInstallationStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 }) {
console.log('debug:getPWAInstallationStatus')
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 3770335

Please sign in to comment.