Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proof Of Concept] Enable plugin mode #335

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
},
}
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"cypress:stub": "start-server-and-test 'yarn start:nobrowser' http://localhost:3000 'yarn cypress run --env dhis2ApiVersion=39,networkMode=stub'"
},
"devDependencies": {
"@dhis2/cli-app-scripts": "^8.1.0",
"@dhis2/cli-style": "^10.4.1",
"@dhis2/cli-app-scripts": "10.3.11",
"@dhis2/cli-style": "^10.5.1",
"@dhis2/cypress-commands": "^9.0.2",
"@dhis2/cypress-plugins": "^9.0.2",
"@testing-library/jest-dom": "^5.14.1",
Expand All @@ -38,9 +38,10 @@
"start-server-and-test": "^1.13.1"
},
"dependencies": {
"@dhis2/app-runtime": "^3.2.1",
"@dhis2/app-runtime": "3.10.4",
"@dhis2/prop-types": "^1.6.4",
"@dhis2/ui": "^7.2.7",
"@dhis2/ui": "8.16.0",
"@krakenjs/post-robot": "^11.0.0",
"history": "^5.0.1",
"prop-types": "^15.7.2",
"query-string": "^7.0.1",
Expand Down
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
}
Loading
Loading