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

feat: prep for global shell [DHIS2-15635] #829

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
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
13 changes: 12 additions & 1 deletion adapter/src/components/AppWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ import { Alerts } from './Alerts.js'
import { ConnectedHeaderBar } from './ConnectedHeaderBar.js'
import { ErrorBoundary } from './ErrorBoundary.js'
import { LoadingMask } from './LoadingMask.js'
import { PluginPWAUpdateManager } from './PluginPWAUpdateManager.js'
import { styles } from './styles/AppWrapper.style.js'

const AppWrapper = ({ children, plugin, onPluginError, clearPluginError }) => {
const AppWrapper = ({
children,
plugin,
onPluginError,
clearPluginError,
reportPWAUpdateStatus,
}) => {
const { loading: localeLoading } = useCurrentUserLocale()
const { loading: latestUserLoading } = useVerifyLatestUser()

Expand All @@ -20,6 +27,9 @@ const AppWrapper = ({ children, plugin, onPluginError, clearPluginError }) => {
return (
<div className="app-shell-adapter">
<style jsx>{styles}</style>
<PluginPWAUpdateManager
reportPWAUpdateStatus={reportPWAUpdateStatus}
/>
<div className="app-shell-app">
<ErrorBoundary
plugin={true}
Expand Down Expand Up @@ -55,6 +65,7 @@ AppWrapper.propTypes = {
children: PropTypes.node,
clearPluginError: PropTypes.func,
plugin: PropTypes.bool,
reportPWAUpdateStatus: PropTypes.func,
onPluginError: PropTypes.func,
}

Expand Down
2 changes: 1 addition & 1 deletion adapter/src/components/ConnectedHeaderBar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useConfig } from '@dhis2/app-runtime'
import { usePWAUpdateState } from '@dhis2/pwa'
import { HeaderBar } from '@dhis2/ui'
import React from 'react'
import { usePWAUpdateState } from '../utils/usePWAUpdateState'
import { ConfirmUpdateModal } from './ConfirmUpdateModal'

/**
Expand Down
18 changes: 0 additions & 18 deletions adapter/src/components/OfflineInterfaceContext.js

This file was deleted.

2 changes: 1 addition & 1 deletion adapter/src/components/PWALoadingBoundary.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
useOfflineInterface,
REGISTRATION_STATE_WAITING,
REGISTRATION_STATE_FIRST_ACTIVATION,
} from '@dhis2/pwa'
import PropTypes from 'prop-types'
import { useEffect, useState } from 'react'
import { useOfflineInterface } from './OfflineInterfaceContext'

export const PWALoadingBoundary = ({ children }) => {
const [pwaReady, setPWAReady] = useState(false)
Expand Down
48 changes: 48 additions & 0 deletions adapter/src/components/PluginPWAUpdateManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { usePWAUpdateState } from '@dhis2/pwa'
import PropTypes from 'prop-types'
import React, { useEffect } from 'react'
import { ConfirmUpdateModal } from './ConfirmUpdateModal'

/**
* Analogous to the ConnectedHeaderbar, for use in plugins since they don't
* use a header bar. See the ConnectedHeaderBar for more
*/

/**
* Check for SW updates or a first activation, sending a message to the host
* app if an update is ready. When an update is applied, if there are
* multiple tabs of this app open, there's anadditional warning step because all
* clients of the service worker will reload when there's an update, which may
* cause data loss.
*/

export function PluginPWAUpdateManager({ reportPWAUpdateStatus }) {
const {
updateAvailable,
confirmReload,
confirmationRequired,
clientsCount,
onConfirmUpdate,
onCancelUpdate,
} = usePWAUpdateState()

useEffect(() => {
if (reportPWAUpdateStatus) {
reportPWAUpdateStatus({
updateAvailable,
onApplyUpdate: updateAvailable ? confirmReload : null,
})
}
}, [updateAvailable, confirmReload, reportPWAUpdateStatus])

return confirmationRequired ? (
<ConfirmUpdateModal
clientsCount={clientsCount}
onConfirm={onConfirmUpdate}
onCancel={onCancelUpdate}
/>
) : null
}
PluginPWAUpdateManager.propTypes = {
reportPWAUpdateStatus: PropTypes.func,
}
15 changes: 12 additions & 3 deletions adapter/src/components/ServerVersionProvider.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Provider } from '@dhis2/app-runtime'
import { getBaseUrlByAppName, setBaseUrlByAppName } from '@dhis2/pwa'
import {
getBaseUrlByAppName,
setBaseUrlByAppName,
useOfflineInterface,
} from '@dhis2/pwa'
import PropTypes from 'prop-types'
import React, { useEffect, useState } from 'react'
import { get } from '../utils/api.js'
import { parseDHIS2ServerVersion, parseVersion } from '../utils/parseVersion.js'
import { LoadingMask } from './LoadingMask.js'
import { LoginModal } from './LoginModal.js'
import { useOfflineInterface } from './OfflineInterfaceContext.js'

// Save this location so that it's usable after client-side navigations
const originalWindowLocation = new URL(window.location)

export const ServerVersionProvider = ({
appName,
Expand Down Expand Up @@ -148,6 +154,9 @@ export const ServerVersionProvider = ({
return <LoadingMask />
}

// Make sure the base URL is absolute to avoid errors with relative URLs after
// client-side navigation/route changes
const absoluteBaseUrl = new URL(baseUrl, originalWindowLocation).href
const serverVersion = parseDHIS2ServerVersion(systemInfo.version)
const realApiVersion = serverVersion.minor

Expand All @@ -156,7 +165,7 @@ export const ServerVersionProvider = ({
config={{
appName,
appVersion: parseVersion(appVersion),
baseUrl,
baseUrl: absoluteBaseUrl,
apiVersion: apiVersion || realApiVersion,
serverVersion,
systemInfo,
Expand Down
6 changes: 4 additions & 2 deletions adapter/src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { checkForSWUpdateAndReload } from '@dhis2/pwa'
import { checkForSWUpdateAndReload, OfflineInterfaceProvider } from '@dhis2/pwa'
import PropTypes from 'prop-types'
import React from 'react'
import { AppWrapper } from './components/AppWrapper.js'
import { ErrorBoundary } from './components/ErrorBoundary.js'
import { OfflineInterfaceProvider } from './components/OfflineInterfaceContext.js'
import { PWALoadingBoundary } from './components/PWALoadingBoundary.js'
import { ServerVersionProvider } from './components/ServerVersionProvider.js'

Expand All @@ -18,6 +17,7 @@ const AppAdapter = ({
showAlertsInPlugin,
onPluginError,
clearPluginError,
reportPWAUpdateStatus,
children,
}) => (
<ErrorBoundary
Expand All @@ -41,6 +41,7 @@ const AppAdapter = ({
plugin={plugin}
onPluginError={onPluginError}
clearPluginError={clearPluginError}
reportPWAUpdateStatus={reportPWAUpdateStatus}
>
{children}
</AppWrapper>
Expand All @@ -59,6 +60,7 @@ AppAdapter.propTypes = {
parentAlertsAdd: PropTypes.func,
plugin: PropTypes.bool,
pwaEnabled: PropTypes.bool,
reportPWAUpdateStatus: PropTypes.func,
showAlertsInPlugin: PropTypes.func,
url: PropTypes.string,
onPluginError: PropTypes.func,
Expand Down
47 changes: 38 additions & 9 deletions cli/config/plugin.webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const babelWebpackConfig = {
const cssRegex = /\.css$/
const cssModuleRegex = /\.module\.css$/

module.exports = ({ env: webpackEnv, config, paths }) => {
module.exports = ({ env: webpackEnv, config, paths, pluginifiedApp }) => {
const isProduction = webpackEnv === 'production'
const isDevelopment = !isProduction

Expand Down Expand Up @@ -94,18 +94,47 @@ module.exports = ({ env: webpackEnv, config, paths }) => {
].filter(Boolean)
}

// DHIS2
// .d2/shell/src/index.js or .../plugin.index.js
const entry = pluginifiedApp
? paths.shellAppBundleEntrypoint
: paths.shellPluginBundleEntrypoint
// plugin.html or app.html
const htmlFilename = pluginifiedApp
? paths.pluginifiedAppLaunchPath
: paths.pluginLaunchPath
// .d2/shell/public/app.html or .../plugin.html
const htmlTemplate = pluginifiedApp
? paths.shellPublicPluginifiedAppHtml
: paths.shellPublicPluginHtml
const outputFilenamePrefix = pluginifiedApp ? 'app' : 'plugin'
const precacheInjectionPoint = pluginifiedApp
? 'self.__WB_PLUGINIFIED_APP_MANIFEST'
: 'self.__WB_PLUGIN_MANIFEST'

// DHIS2
// in dev (using process.env.NODE_ENV because `isProduction` is currently
// hard-coded to 'true' as a workaround), add 'react-devtools' to the entry
// to enabled standalone React Dev Tools to work in iframes
// Intro: https://stackoverflow.com/a/71513012
// https://github.com/facebook/react/tree/main/packages/react-devtools#usage-with-react-dom
const resolvedEntry =
process.env.NODE_ENV === 'development'
? ['react-devtools', entry]
: entry

return {
mode: webpackEnv,
bail: isProduction,
entry: paths.shellPluginBundleEntrypoint,
entry: resolvedEntry,
output: {
path: paths.shellBuildOutput,
filename: isProduction
? 'static/js/plugin-[name].[contenthash:8].js'
: 'static/js/plugin.bundle.js',
? `static/js/${outputFilenamePrefix}-[name].[contenthash:8].js`
: `static/js/${outputFilenamePrefix}.bundle.js`,
chunkFilename: isProduction
? 'static/js/plugin-[name].[contenthash:8].chunk.js'
: 'static/js/plugin-[name].chunk.js',
? `static/js/${outputFilenamePrefix}-[name].[contenthash:8].chunk.js`
: `static/js/${outputFilenamePrefix}-[name].chunk.js`,
// TODO: investigate dev source maps here (devtoolModuleFilenameTemplate)
},
optimization: {
Expand Down Expand Up @@ -143,8 +172,8 @@ module.exports = ({ env: webpackEnv, config, paths }) => {
Object.assign(
{
inject: true,
filename: paths.pluginLaunchPath,
template: paths.shellPublicPluginHtml,
filename: htmlFilename,
template: htmlTemplate,
},
isProduction
? {
Expand Down Expand Up @@ -196,7 +225,7 @@ module.exports = ({ env: webpackEnv, config, paths }) => {
process.env.NODE_ENV === 'production' &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc: paths.shellBuildServiceWorker,
injectionPoint: 'self.__WB_PLUGIN_MANIFEST',
injectionPoint: precacheInjectionPoint,
// Skip compiling the SW, which happens in the app build step
compileSrc: false,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
Expand Down
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"parse-gitignore": "^1.0.1",
"postcss-loader": "^7.0.1",
"react-dev-utils": "^12.0.0",
"react-devtools": "^5.0.0",
"react-refresh": "^0.11.0",
"style-loader": "^3.3.1",
"styled-jsx": "^4.0.1",
Expand Down
4 changes: 4 additions & 0 deletions cli/src/commands/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const handler = async ({
const config = parseConfig(paths)
const shell = makeShell({ config, paths })
const plugin = makePlugin({ config, paths })
const pluginifiedApp = makePlugin({ config, paths, pluginifiedApp: true })

if (config.type === 'app') {
setAppParameters(standalone, config)
Expand Down Expand Up @@ -132,6 +133,9 @@ const handler = async ({
reporter.info('Building appShell...')
await shell.build()

reporter.info('Building pluginified app...')
await pluginifiedApp.build()

if (config.entryPoints.plugin) {
reporter.info('Building plugin...')
await plugin.build()
Expand Down
44 changes: 27 additions & 17 deletions cli/src/commands/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const handler = async ({
shell: shellSource,
proxy,
proxyPort,
pluginifyApp, // todo: workshop this flag name
plugin: shouldStartPlugin,
}) => {
const paths = makePaths(cwd)

Expand All @@ -32,6 +34,7 @@ const handler = async ({
const config = parseConfig(paths)
const shell = makeShell({ config, paths })
const plugin = makePlugin({ config, paths })
const pluginifiedApp = makePlugin({ config, paths, pluginifiedApp: true })

if (config.type !== 'app') {
reporter.error(
Expand Down Expand Up @@ -124,29 +127,36 @@ const handler = async ({

reporter.print('')
reporter.info('Starting development server...')
reporter.print(
`The app ${chalk.bold(
config.name
)} is now available on port ${newPort}`
)
reporter.print('')

const shellStartPromise = shell.start({ port: newPort })

if (config.entryPoints.plugin) {
const pluginPort = await detectPort(newPort + 1)
// only run one mode of the app;
// another script can always be run in parallel
if (pluginifyApp) {
reporter.print(
`The plugin is now available on port ${pluginPort} at /${paths.pluginLaunchPath}`
`The pluginified app is now available on port ${newPort} at /${paths.pluginifiedAppLaunchPath}`
)
reporter.print('')
await pluginifiedApp.start({
port: newPort,
})
return
}

await Promise.all([
shellStartPromise,
plugin.start({ port: pluginPort }),
])
} else {
await shellStartPromise
if (shouldStartPlugin) {
reporter.print(
`The plugin is now available on port ${newPort} at /${paths.pluginLaunchPath}`
)
reporter.print('')
await plugin.start({ port: newPort })
return
}

reporter.print(
`The app ${chalk.bold(
config.name
)} is now available on port ${newPort}`
)
reporter.print('')
await shell.start({ port: newPort })
},
{
name: 'start',
Expand Down
Loading
Loading