diff --git a/adapter/src/components/LoginModal.js b/adapter/src/components/LoginModal.js
index cb5c2d136..1b7fdbced 100644
--- a/adapter/src/components/LoginModal.js
+++ b/adapter/src/components/LoginModal.js
@@ -1,3 +1,4 @@
+import { setBaseUrlByAppName } from '@dhis2/pwa'
import {
Modal,
ModalTitle,
@@ -6,16 +7,16 @@ import {
Button,
InputField,
} from '@dhis2/ui'
+import PropTypes from 'prop-types'
import React, { useState } from 'react'
import i18n from '../locales/index.js'
import { post } from '../utils/api.js'
+// Check if base URL is set statically as an env var (typical in production)
const staticUrl = process.env.REACT_APP_DHIS2_BASE_URL
-export const LoginModal = () => {
- const [server, setServer] = useState(
- staticUrl || window.localStorage.DHIS2_BASE_URL || ''
- )
+export const LoginModal = ({ appName, baseUrl }) => {
+ const [server, setServer] = useState(baseUrl || '')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [isDirty, setIsDirty] = useState(false)
@@ -27,7 +28,10 @@ export const LoginModal = () => {
setIsDirty(true)
if (isValid(server) && isValid(username) && isValid(password)) {
if (!staticUrl) {
+ // keep the localStorage value here -- it's still used in some
+ // obscure cases, like in the cypress network shim
window.localStorage.DHIS2_BASE_URL = server
+ await setBaseUrlByAppName({ appName, baseUrl: server })
}
try {
await post(
@@ -99,3 +103,7 @@ export const LoginModal = () => {
)
}
+LoginModal.propTypes = {
+ appName: PropTypes.string,
+ baseUrl: PropTypes.string,
+}
diff --git a/adapter/src/components/ServerVersionProvider.js b/adapter/src/components/ServerVersionProvider.js
index 6a76f86cc..71a6006fd 100644
--- a/adapter/src/components/ServerVersionProvider.js
+++ b/adapter/src/components/ServerVersionProvider.js
@@ -1,4 +1,5 @@
import { Provider } from '@dhis2/app-runtime'
+import { getBaseUrlByAppName, setBaseUrlByAppName } from '@dhis2/pwa'
import PropTypes from 'prop-types'
import React, { useEffect, useState } from 'react'
import { get } from '../utils/api.js'
@@ -10,45 +11,134 @@ import { useOfflineInterface } from './OfflineInterfaceContext.js'
export const ServerVersionProvider = ({
appName,
appVersion,
- url,
+ url, // url from env vars
apiVersion,
pwaEnabled,
children,
}) => {
const offlineInterface = useOfflineInterface()
- const [{ loading, error, systemInfo }, setState] = useState({
+ const [systemInfoState, setSystemInfoState] = useState({
loading: true,
+ error: undefined,
+ systemInfo: undefined,
})
+ const [baseUrlState, setBaseUrlState] = useState({
+ loading: !url,
+ error: undefined,
+ baseUrl: url,
+ })
+ const [offlineInterfaceLoading, setOfflineInterfaceLoading] = useState(true)
+ const { systemInfo } = systemInfoState
+ const { baseUrl } = baseUrlState
useEffect(() => {
- if (!url) {
- setState({ loading: false, error: new Error('No url specified') })
+ // if URL prop is not set, set state to error to show login modal.
+ // Submitting valid login form with server and credentials reloads page,
+ // ostensibly with a filled url prop (now persisted locally)
+ if (!baseUrl) {
+ // Use a function as the argument to avoid needing baseUrlState as
+ // a dependency for useEffect
+ setBaseUrlState((state) =>
+ state.loading
+ ? state
+ : { loading: true, error: undefined, systemInfo: undefined }
+ )
+ // try getting URL from IndexedDB
+ getBaseUrlByAppName(appName)
+ .then((baseUrlFromDB) => {
+ if (baseUrlFromDB) {
+ // Set baseUrl in state if found in DB
+ setBaseUrlState({
+ loading: false,
+ error: undefined,
+ baseUrl: baseUrlFromDB,
+ })
+ return
+ }
+ // If no URL found in DB, try localStorage
+ // (previous adapter versions stored the base URL there)
+ const baseUrlFromLocalStorage =
+ window.localStorage.DHIS2_BASE_URL
+ if (baseUrlFromLocalStorage) {
+ setBaseUrlState({
+ loading: false,
+ error: undefined,
+ baseUrl: baseUrlFromLocalStorage,
+ })
+ // Also set it in IndexedDB for SW to access
+ return setBaseUrlByAppName({
+ appName,
+ baseUrl: baseUrlFromLocalStorage,
+ })
+ }
+ // If no base URL found in either, set error to show login modal
+ setBaseUrlState({
+ loading: false,
+ error: new Error('No url specified'),
+ baseUrl: undefined,
+ })
+ })
+ .catch((err) => {
+ console.error(err)
+ setBaseUrlState({
+ loading: false,
+ error: err,
+ baseUrl: undefined,
+ })
+ })
+
return
}
- setState((state) => (state.loading ? state : { loading: true }))
- const request = get(`${url}/api/system/info`)
+ // If url IS set, try querying API to test authentication and get
+ // server version. If it fails, set error to show login modal
+
+ setSystemInfoState((state) =>
+ state.loading
+ ? state
+ : { loading: true, error: undefined, systemInfo: undefined }
+ )
+ const request = get(`${baseUrl}/api/system/info`)
request
.then((systemInfo) => {
- setState({ loading: false, systemInfo })
+ setSystemInfoState({
+ loading: false,
+ error: undefined,
+ systemInfo: systemInfo,
+ })
})
.catch((e) => {
// Todo: If this is a network error, the app cannot load -- handle that gracefully here
// if (e === 'Network error') { ... }
- setState({ loading: false, error: e })
+ setSystemInfoState({
+ loading: false,
+ error: e,
+ systemInfo: undefined,
+ })
})
return () => {
request.abort()
}
- }, [url])
+ }, [appName, baseUrl])
- if (loading) {
- return
+ useEffect(() => {
+ offlineInterface.ready.then(() => {
+ setOfflineInterfaceLoading(false)
+ })
+ }, [offlineInterface])
+
+ // This needs to come before 'loading' case to show modal at correct times
+ if (systemInfoState.error || baseUrlState.error) {
+ return
}
- if (error) {
- return
+ if (
+ systemInfoState.loading ||
+ baseUrlState.loading ||
+ offlineInterfaceLoading
+ ) {
+ return
}
const serverVersion = parseDHIS2ServerVersion(systemInfo.version)
@@ -59,7 +149,7 @@ export const ServerVersionProvider = ({
config={{
appName,
appVersion: parseVersion(appVersion),
- baseUrl: url,
+ baseUrl,
apiVersion: apiVersion || realApiVersion,
serverVersion,
systemInfo,
diff --git a/cli/src/lib/pwa/compileServiceWorker.js b/cli/src/lib/pwa/compileServiceWorker.js
index 148c173e4..0a2be598f 100644
--- a/cli/src/lib/pwa/compileServiceWorker.js
+++ b/cli/src/lib/pwa/compileServiceWorker.js
@@ -1,6 +1,7 @@
const path = require('path')
const { reporter } = require('@dhis2/cli-helpers-engine')
const webpack = require('webpack')
+const getEnv = require('../shell/env')
const getPWAEnvVars = require('./getPWAEnvVars')
/**
@@ -34,13 +35,7 @@ function compileServiceWorker({ config, paths, mode }) {
// TODO: This could be cleaner if the production SW is built in the same
// way instead of using the CRA webpack config, so both can more easily
// share environment variables.
- const prefixedPWAEnvVars = Object.entries(getPWAEnvVars(config)).reduce(
- (output, [key, value]) => ({
- ...output,
- [`REACT_APP_DHIS2_APP_${key.toUpperCase()}`]: value,
- }),
- {}
- )
+ const env = getEnv({ name: config.title, ...getPWAEnvVars(config) })
const webpackConfig = {
mode, // "production" or "development"
@@ -54,7 +49,7 @@ function compileServiceWorker({ config, paths, mode }) {
new webpack.DefinePlugin({
'process.env': JSON.stringify({
...process.env,
- ...prefixedPWAEnvVars,
+ ...env,
}),
}),
],
diff --git a/examples/pwa-app/d2.config.js b/examples/pwa-app/d2.config.js
index a088cc478..00c40e76a 100644
--- a/examples/pwa-app/d2.config.js
+++ b/examples/pwa-app/d2.config.js
@@ -11,7 +11,8 @@ const config = {
entryPoints: {
app: './src/App.js',
- plugin: './src/components/VisualizationsList.js',
+ // Uncomment this to test plugin builds:
+ // plugin: './src/components/VisualizationsList.js',
},
}
diff --git a/examples/pwa-app/src/App.js b/examples/pwa-app/src/App.js
index 006f193d6..78c19aaf6 100644
--- a/examples/pwa-app/src/App.js
+++ b/examples/pwa-app/src/App.js
@@ -1,9 +1,11 @@
import React from 'react'
import classes from './App.module.css'
+import RequestTester from './components/RequestTester.js'
import SectionWrapper from './components/SectionWrapper.js'
const MyApp = () => (
+
)
diff --git a/examples/pwa-app/src/components/RequestTester.js b/examples/pwa-app/src/components/RequestTester.js
new file mode 100644
index 000000000..17bad2c8f
--- /dev/null
+++ b/examples/pwa-app/src/components/RequestTester.js
@@ -0,0 +1,53 @@
+import { useDataEngine, useDhis2ConnectionStatus } from '@dhis2/app-runtime'
+import { Box, Button, ButtonStrip, Help } from '@dhis2/ui'
+import React from 'react'
+
+const query = {
+ me: {
+ resource: 'me',
+ params: {
+ fields: ['id', 'name'],
+ },
+ },
+}
+
+export default function RequestTester() {
+ const engine = useDataEngine()
+ const { isConnected, lastConnected } = useDhis2ConnectionStatus()
+
+ const internalRequest = () => {
+ console.log('Request tester: internal request')
+ engine.query(query)
+ }
+ const externalRequest = () => {
+ console.log('Request tester: external request')
+ fetch('https://random.dog/woof.json')
+ }
+
+ return (
+
+
+ Connection to DHIS2 server:{' '}
+ {isConnected ? (
+ Connected
+ ) : (
+ NOT CONNECTED
+ )}
+
+
+ Last connected: {lastConnected?.toLocaleTimeString() || 'null'}
+
+
Based on useDhis2ConnectionStatus()
+
+
+
+
+
+
+
+ )
+}
diff --git a/pwa/src/index.js b/pwa/src/index.js
index 67aa47475..51fa2e435 100644
--- a/pwa/src/index.js
+++ b/pwa/src/index.js
@@ -1,4 +1,4 @@
-export { setUpServiceWorker } from './service-worker/service-worker.js'
+export { setUpServiceWorker } from './service-worker/set-up-service-worker.js'
export { OfflineInterface } from './offline-interface/offline-interface.js'
export {
checkForUpdates,
@@ -9,3 +9,4 @@ export {
REGISTRATION_STATE_ACTIVE,
REGISTRATION_STATE_FIRST_ACTIVATION,
} from './lib/registration.js'
+export { getBaseUrlByAppName, setBaseUrlByAppName } from './lib/base-url-db.js'
diff --git a/pwa/src/lib/base-url-db.js b/pwa/src/lib/base-url-db.js
new file mode 100644
index 000000000..6903658c9
--- /dev/null
+++ b/pwa/src/lib/base-url-db.js
@@ -0,0 +1,48 @@
+import { openDB /* deleteDB */ } from 'idb'
+
+export const BASE_URL_DB = 'dhis2-base-url-db'
+export const BASE_URL_STORE = 'dhis2-base-url-store'
+
+const DB_VERSION = 1
+
+/**
+ * Opens indexed DB and object store for baser urls by app name. Should be used any
+ * time the DB is accessed to make sure object stores are set up correctly and
+ * avoid DB-access race condition on first installation.
+ *
+ * @returns {Promise} dbPromise. Usage: `const db = await dbPromise`
+ */
+function openBaseUrlsDB() {
+ return openDB(BASE_URL_DB, DB_VERSION, {
+ upgrade(db, oldVersion /* newVersion, transaction */) {
+ // DB versioning trick that can iteratively apply upgrades
+ // https://developers.google.com/web/ilt/pwa/working-with-indexeddb#using_database_versioning
+ switch (oldVersion) {
+ case 0: {
+ db.createObjectStore(BASE_URL_STORE, {
+ keyPath: 'appName',
+ })
+ }
+ // falls through (this comment satisfies eslint)
+ default: {
+ console.debug('[sections-db] Done upgrading DB')
+ }
+ }
+ },
+ })
+}
+
+/** Deletes the DB (probably not needed) */
+// function deleteBaseUrlsDB() {
+// return deleteDB(BASE_URL_DB)
+// }
+
+export async function setBaseUrlByAppName({ appName, baseUrl }) {
+ const db = await openBaseUrlsDB()
+ return db.put(BASE_URL_STORE, { appName, baseUrl })
+}
+
+export async function getBaseUrlByAppName(appName) {
+ const db = await openBaseUrlsDB()
+ return db.get(BASE_URL_STORE, appName).then((entry) => entry?.baseUrl)
+}
diff --git a/pwa/src/lib/constants.js b/pwa/src/lib/constants.js
index 730c4dee4..fdfce73ab 100644
--- a/pwa/src/lib/constants.js
+++ b/pwa/src/lib/constants.js
@@ -9,4 +9,7 @@ export const swMsgs = Object.freeze({
confirmRecordingCompletion: 'CONFIRM_RECORDING_COMPLETION',
completeRecording: 'COMPLETE_RECORDING',
recordingCompleted: 'RECORDING_COMPLETED',
+ dhis2ConnectionStatusUpdate: 'DHIS2_CONNECTION_STATUS_UPDATE',
+ getImmediateDhis2ConnectionStatusUpdate:
+ 'GET_IMMEDIATE_DHIS2_CONNECTION_STATUS_UPDATE',
})
diff --git a/pwa/src/offline-interface/offline-interface.js b/pwa/src/offline-interface/offline-interface.js
index 5cf0d2beb..3a3483dd3 100644
--- a/pwa/src/offline-interface/offline-interface.js
+++ b/pwa/src/offline-interface/offline-interface.js
@@ -68,6 +68,10 @@ export class OfflineInterface {
// Helper property that consumers can check
this.pwaEnabled = PWA_ENABLED
+ // The latest value from the service worker. The `this.ready` promise
+ // will resolve when this gets a boolean value from the SW
+ this.latestIsConnected = null
+
if (this.pwaEnabled) {
register()
} else {
@@ -101,6 +105,30 @@ export class OfflineInterface {
this.offlineEvents.emit(type, payload)
}
navigator.serviceWorker.addEventListener('message', handleSWMessage)
+
+ // When this promise resolves, it indicates that a connection status
+ // value has been received from the service worker and is available
+ // as a property on this offlineInterface.
+ // Expected to be used by ServerVersionProvider in the app adapter
+ // to delay rendering the app-runtime Provider until ready.
+ this.ready = new Promise((resolve) => {
+ // Listen to status updates and store the latest value here so the
+ // connection status hook can initialize to this value
+ this.offlineEvents.on(
+ swMsgs.dhis2ConnectionStatusUpdate,
+ ({ isConnected }) => {
+ // If this is the first time receiving an update from the
+ // SW, resolve the this.ready promise
+ const shouldResolveReady = this.latestIsConnected === null
+ this.latestIsConnected = isConnected
+ if (shouldResolveReady) {
+ resolve()
+ }
+ }
+ )
+ })
+ // Prompt the SW to send back connection status without its usual delay
+ swMessage(swMsgs.getImmediateDhis2ConnectionStatusUpdate)
}
/** Basically `checkForUpdates` from registration.js exposed here */
@@ -172,6 +200,17 @@ export class OfflineInterface {
})
}
+ /**
+ * @param {Object} params
+ * @param {Function} params.onUpdate - Called on status updates with argument { isConnected: bool }
+ * @returns {Function} - An unsubscribe function
+ */
+ subscribeToDhis2ConnectionStatus({ onUpdate }) {
+ this.offlineEvents.on(swMsgs.dhis2ConnectionStatusUpdate, onUpdate)
+ return () =>
+ this.offlineEvents.off(swMsgs.dhis2ConnectionStatusUpdate, onUpdate)
+ }
+
/**
* Starts a recording session for a cacheable section. Returns a promise
* that resolves if the SW message is successfully sent or rejects if
diff --git a/pwa/src/service-worker/dhis2-connection-status.js b/pwa/src/service-worker/dhis2-connection-status.js
new file mode 100644
index 000000000..c2fb05d50
--- /dev/null
+++ b/pwa/src/service-worker/dhis2-connection-status.js
@@ -0,0 +1,73 @@
+import throttle from 'lodash/throttle'
+import { getBaseUrlByAppName } from '../lib/base-url-db.js'
+import { swMsgs } from '../lib/constants.js'
+import { getAllClientsInScope } from './utils.js'
+
+/**
+ * Tracks connection to the DHIS2 server based on fetch successes or failures.
+ * Starts as null because it can't be determined until a request is sent
+ */
+export function initDhis2ConnectionStatus() {
+ // base url is only set as an env var in production.
+ // in dev/standalone env, this may be undefined,
+ // and the base URL can be accessed from IDB later.
+ // note: if this SW is part of a global shell,
+ // URL would need to be found on a per-client basis
+ const dhis2BaseUrl = process.env.REACT_APP_DHIS2_BASE_URL
+ if (dhis2BaseUrl) {
+ try {
+ self.dhis2BaseUrl = new URL(dhis2BaseUrl).href
+ } catch {
+ // the base URL is relative; construct an absolute one
+ self.dhis2BaseUrl = new URL(dhis2BaseUrl, self.location.href).href
+ }
+ }
+}
+
+// Throttle this a bit to reduce SW/client messaging
+const BROADCAST_INTERVAL_MS = 1000
+export const broadcastDhis2ConnectionStatus = throttle(async (isConnected) => {
+ const clients = await getAllClientsInScope()
+ clients.forEach((client) =>
+ client.postMessage({
+ type: swMsgs.dhis2ConnectionStatusUpdate,
+ payload: { isConnected },
+ })
+ )
+}, BROADCAST_INTERVAL_MS)
+
+async function isRequestToDhis2Server(request) {
+ // If dhis2BaseUrl isn't set, try getting it from IDB
+ if (!self.dhis2BaseUrl) {
+ const baseUrl = await getBaseUrlByAppName(
+ process.env.REACT_APP_DHIS2_APP_NAME
+ )
+ if (!baseUrl) {
+ // No base URL is set; as a best effort, go ahead and update status
+ // based on this request, even though it might not be to the DHIS2 server
+ return true
+ } else {
+ self.dhis2BaseUrl = baseUrl
+ }
+ }
+
+ return request.url.startsWith(self.dhis2BaseUrl)
+}
+
+/**
+ * A plugin to hook into lifecycle events in workbox strategies
+ * https://developer.chrome.com/docs/workbox/using-plugins/
+ */
+export const dhis2ConnectionStatusPlugin = {
+ fetchDidFail: async ({ request }) => {
+ if (await isRequestToDhis2Server(request)) {
+ broadcastDhis2ConnectionStatus(false)
+ }
+ },
+ fetchDidSucceed: async ({ request, response }) => {
+ if (await isRequestToDhis2Server(request)) {
+ broadcastDhis2ConnectionStatus(true)
+ }
+ return response
+ },
+}
diff --git a/pwa/src/service-worker/recording-mode.js b/pwa/src/service-worker/recording-mode.js
index 5f6152dd4..9633677ec 100644
--- a/pwa/src/service-worker/recording-mode.js
+++ b/pwa/src/service-worker/recording-mode.js
@@ -1,3 +1,4 @@
+import { Strategy } from 'workbox-strategies'
import { swMsgs } from '../lib/constants.js'
import { openSectionsDB, SECTIONS_STORE } from '../lib/sections-db.js'
@@ -8,6 +9,14 @@ const CACHEABLE_SECTION_URL_FILTER_PATTERNS = JSON.parse(
'[]'
).map((pattern) => new RegExp(pattern))
+/**
+ * Tracks recording states for multiple clients to handle multiple windows
+ * recording simultaneously
+ */
+export function initClientRecordingStates() {
+ self.clientRecordingStates = {}
+}
+
// Triggered on 'START_RECORDING' message
export function startRecording(event) {
console.debug('[SW] Starting recording')
@@ -77,19 +86,25 @@ export function shouldRequestBeRecorded({ url, event }) {
}
/** Request handler during recording mode */
-export function handleRecordedRequest({ request, event }) {
- const recordingState = self.clientRecordingStates[event.clientId]
+export class RecordingMode extends Strategy {
+ _handle(request, handler) {
+ const { event } = handler
+ const recordingState = self.clientRecordingStates[event.clientId]
- clearTimeout(recordingState.recordingTimeout)
- recordingState.pendingRequests.add(request)
+ clearTimeout(recordingState.recordingTimeout)
+ recordingState.pendingRequests.add(request)
- fetch(request)
- .then((response) => {
- return handleRecordedResponse(request, response, event.clientId)
- })
- .catch((error) => {
- stopRecording(error, event.clientId)
- })
+ return handler
+ .fetch(request)
+ .then((response) => {
+ return handleRecordedResponse(request, response, event.clientId)
+ })
+ .catch((error) => {
+ stopRecording(error, event.clientId)
+ // trigger 'fetchDidFail' callback
+ throw error
+ })
+ }
}
/** Response handler during recording mode */
diff --git a/pwa/src/service-worker/service-worker.js b/pwa/src/service-worker/set-up-service-worker.js
similarity index 84%
rename from pwa/src/service-worker/service-worker.js
rename to pwa/src/service-worker/set-up-service-worker.js
index 2252bda09..8a053d18f 100644
--- a/pwa/src/service-worker/service-worker.js
+++ b/pwa/src/service-worker/set-up-service-worker.js
@@ -2,15 +2,22 @@ import { precacheAndRoute, matchPrecache, precache } from 'workbox-precaching'
import { registerRoute, setDefaultHandler } from 'workbox-routing'
import {
NetworkFirst,
+ NetworkOnly,
StaleWhileRevalidate,
Strategy,
} from 'workbox-strategies'
import { swMsgs } from '../lib/constants.js'
+import {
+ broadcastDhis2ConnectionStatus,
+ dhis2ConnectionStatusPlugin,
+ initDhis2ConnectionStatus,
+} from './dhis2-connection-status'
import {
startRecording,
completeRecording,
- handleRecordedRequest,
shouldRequestBeRecorded,
+ initClientRecordingStates,
+ RecordingMode,
} from './recording-mode.js'
import {
urlMeetsAppShellCachingCriteria,
@@ -38,9 +45,8 @@ export function setUpServiceWorker() {
// Globals (Note: global state resets each time SW goes idle)
- // Tracks recording states for multiple clients to handle multiple windows
- // recording simultaneously
- self.clientRecordingStates = {}
+ initClientRecordingStates()
+ initDhis2ConnectionStatus()
// Local constants
@@ -138,9 +144,20 @@ export function setUpServiceWorker() {
precacheAndRoute(sharedBuildManifest)
}
+ // Handling pings: only use the network, and don't update the connection
+ // status (let the runtime do that)
+ // Two endpoints: /api(/version)/system/ping and /api/ping
+ registerRoute(
+ ({ url }) => /\/api(\/\d+)?(\/system)?\/ping/.test(url.pathname),
+ new NetworkOnly()
+ )
+
// Request handler during recording mode: ALL requests are cached
// Handling routing: https://developers.google.com/web/tools/workbox/modules/workbox-routing#matching_and_handling_in_routes
- registerRoute(shouldRequestBeRecorded, handleRecordedRequest)
+ registerRoute(
+ shouldRequestBeRecorded,
+ new RecordingMode({ plugins: [dhis2ConnectionStatusPlugin] })
+ )
// If not recording, fall through to default caching strategies for app
// shell:
@@ -151,7 +168,10 @@ export function setUpServiceWorker() {
PRODUCTION_ENV &&
urlMeetsAppShellCachingCriteria(url) &&
/\.(jpg|gif|png|bmp|tiff|ico|woff)$/.test(url.pathname),
- new StaleWhileRevalidate({ cacheName: 'other-assets' })
+ new StaleWhileRevalidate({
+ cacheName: 'other-assets',
+ plugins: [dhis2ConnectionStatusPlugin],
+ })
)
// Network-first caching by default
@@ -161,7 +181,10 @@ export function setUpServiceWorker() {
({ url }) =>
urlMeetsAppShellCachingCriteria(url) ||
(!PRODUCTION_ENV && fileExtensionRegexp.test(url.pathname)),
- new NetworkFirst({ cacheName: 'app-shell' })
+ new NetworkFirst({
+ cacheName: 'app-shell',
+ plugins: [dhis2ConnectionStatusPlugin],
+ })
)
// Strategy for all other requests: try cache if network fails,
@@ -182,7 +205,9 @@ export function setUpServiceWorker() {
}
}
// Use fallback strategy as default
- setDefaultHandler(new NetworkAndTryCache())
+ setDefaultHandler(
+ new NetworkAndTryCache({ plugins: [dhis2ConnectionStatusPlugin] })
+ )
// Service Worker event handlers
@@ -206,6 +231,15 @@ export function setUpServiceWorker() {
self.skipWaiting()
}
+ // Immediately trigger this throttled function -- this allows the app
+ // to get the value ASAP upon startup, which it otherwise usually
+ // has to wait for
+ if (
+ event.data.type === swMsgs.getImmediateDhis2ConnectionStatusUpdate
+ ) {
+ broadcastDhis2ConnectionStatus.flush()
+ }
+
if (event.data.type === swMsgs.startRecording) {
startRecording(event)
}
diff --git a/pwa/src/service-worker/utils.js b/pwa/src/service-worker/utils.js
index d8c69657e..5f6f499ae 100644
--- a/pwa/src/service-worker/utils.js
+++ b/pwa/src/service-worker/utils.js
@@ -107,18 +107,11 @@ export async function removeUnusedCaches() {
)
}
-/**
- * Can be used to access information about this service worker's clients.
- * Sends back information on a message with 'CLIENTS_INFO' type; the payload
- * currently contains the number of current clients, including uncontrolled.
- * @returns {Object} { clientsCounts: number }
- */
-export async function getClientsInfo(event) {
- const clientId = event.source.id
-
+/** Get all clients including uncontrolled, but only those within SW scope */
+export function getAllClientsInScope() {
// Include uncontrolled clients: necessary to know if there are multiple
// tabs open upon first SW installation
- const filteredClientsList = await self.clients
+ return self.clients
.matchAll({
includeUncontrolled: true,
})
@@ -129,12 +122,24 @@ export async function getClientsInfo(event) {
client.url.startsWith(self.registration.scope)
)
)
+}
+
+/**
+ * Can be used to access information about this service worker's clients.
+ * Sends back information on a message with 'CLIENTS_INFO' type; the payload
+ * currently contains the number of current clients, including uncontrolled.
+ * @returns {Object} { clientsCounts: number }
+ */
+export async function getClientsInfo(event) {
+ const clientId = event.source.id
+
+ const clientsList = await getAllClientsInScope()
self.clients.get(clientId).then((client) => {
client.postMessage({
type: swMsgs.clientsInfo,
payload: {
- clientsCount: filteredClientsList.length,
+ clientsCount: clientsList.length,
},
})
})
diff --git a/shell/package.json b/shell/package.json
index cdd899c05..3a9471883 100644
--- a/shell/package.json
+++ b/shell/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@dhis2/app-adapter": "10.2.3",
- "@dhis2/app-runtime": "^3.6.1",
+ "@dhis2/app-runtime": "^3.9.0",
"@dhis2/d2-i18n": "^1.1.1",
"@dhis2/pwa": "10.2.3",
"@dhis2/ui": "^8.6.2",
diff --git a/shell/src/App.js b/shell/src/App.js
index a606488e2..1e5115c9c 100644
--- a/shell/src/App.js
+++ b/shell/src/App.js
@@ -7,9 +7,7 @@ const D2App = React.lazy(() =>
) // Automatic bundle splitting!
const appConfig = {
- url:
- process.env.REACT_APP_DHIS2_BASE_URL ||
- window.localStorage.DHIS2_BASE_URL,
+ url: process.env.REACT_APP_DHIS2_BASE_URL,
appName: process.env.REACT_APP_DHIS2_APP_NAME || '',
appVersion: process.env.REACT_APP_DHIS2_APP_VERSION || '',
apiVersion: parseInt(process.env.REACT_APP_DHIS2_API_VERSION),
diff --git a/yarn.lock b/yarn.lock
index 7e5763d52..7185a7e6c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2110,37 +2110,37 @@
classnames "^2.3.1"
prop-types "^15.7.2"
-"@dhis2/app-runtime@^3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.6.1.tgz#d39c492239cc81faf2b3ec92fe39c11440640953"
- integrity sha512-I+hTHXTqqDSbOCRFd/60AvCGkyYmwMtUD1EkyURuB/NaK37xQQZzrz2/JD3esBYGl2Ner3nLoOgbQwXEMvBeZw==
- dependencies:
- "@dhis2/app-service-alerts" "3.6.1"
- "@dhis2/app-service-config" "3.6.1"
- "@dhis2/app-service-data" "3.6.1"
- "@dhis2/app-service-offline" "3.6.1"
-
-"@dhis2/app-service-alerts@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-alerts/-/app-service-alerts-3.6.1.tgz#e3081d05a70b12f8da171e44afc45bdd04265be5"
- integrity sha512-hv7cSvSEwlxsSzRoqQ83ymzaMl6lXcFJ3gpsG1qDebNVAjRZAWXZV9GKWGXtdP7GRCOoOypXrzv8WgUlHgMhRQ==
-
-"@dhis2/app-service-config@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-config/-/app-service-config-3.6.1.tgz#0fb2132e8515e9bdf0af83eb7a138583ff885fe4"
- integrity sha512-n3Awr5I1qhJ8UHQTmdaBRiwStU8XbSRVEDfE7TnA0kFCVWoDZIH4YJxnmcTFJLUFxYtaEN4nN9GgrGBGoVcfnQ==
-
-"@dhis2/app-service-data@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-data/-/app-service-data-3.6.1.tgz#74b0d4d2b4935aa416d52863eacea9c15a1c4d80"
- integrity sha512-BX1VOvkwaGmi9NB+r4nnjUQ34tXMlK44c+Tc2jI7EYP6jCSRsbGXeBagP5nSkBdm7XXb2IWlvY2n3/fZFed0kg==
+"@dhis2/app-runtime@^3.9.0":
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.9.0.tgz#c7e295fd0a68fac976a930bc77105206ded0b61a"
+ integrity sha512-n0S4pbyvK7FnBQFMONGrhR9YYavBQI+mQLHfCX/vtvOyeoioBUNIinuQlGysuLMEkSVaK5OjV40rvTMzdxF2kQ==
+ dependencies:
+ "@dhis2/app-service-alerts" "3.9.0"
+ "@dhis2/app-service-config" "3.9.0"
+ "@dhis2/app-service-data" "3.9.0"
+ "@dhis2/app-service-offline" "3.9.0"
+
+"@dhis2/app-service-alerts@3.9.0":
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-alerts/-/app-service-alerts-3.9.0.tgz#48d3805676e75ee58104fea4f76cfa779335444e"
+ integrity sha512-z2eZxm/pxrmFbisbK7/qJKtif2CNWJjaaAH5rfrs5OIajlHy3rO37vSaTQHWv+xWvZFQrs2Op2InxzG0qh5ncA==
+
+"@dhis2/app-service-config@3.9.0":
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-config/-/app-service-config-3.9.0.tgz#8dc59d8de246f54057c0c685d5f94b4cbade6f73"
+ integrity sha512-OuRn2mJGrQQ8QIC+oIVYYpclB4LErRK2wtsuy/cXLfRbeUti1qWIh110rgd1hnTx+BgRCs5s3NWdIQxS4hYGIQ==
+
+"@dhis2/app-service-data@3.9.0":
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-data/-/app-service-data-3.9.0.tgz#37f528b5f7f589cbab8dcc7f997c1668bc6566a9"
+ integrity sha512-/FJgJhL6YGtIVNX5oaNmavkGmimrVHQsS8ueeUO4FvTjYXGlnnN3IuxypQcy/x4yiUyigbPgFJRnbC1J2af2fg==
dependencies:
react-query "^3.13.11"
-"@dhis2/app-service-offline@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-offline/-/app-service-offline-3.6.1.tgz#4c010888d5b7255920304b8da1581af2144540da"
- integrity sha512-nj2FwFiU/XbMsbr+I4HG2v/tmXJW2VBESyhqZ57nzBKhFfVBJgB1bdLBq3gCvw1tRZC2UhqSplilx/vFXg6c8g==
+"@dhis2/app-service-offline@3.9.0":
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-offline/-/app-service-offline-3.9.0.tgz#fe4f4a91a1da77554965f6a5fe6f6951d4c467f4"
+ integrity sha512-0q5zl0vw+a47Ab2qgu6hsZY5ybnH/ea43Vkk4aXYdgcf57xB8ck9DkIcNbc2e1+k9FhvimipxsgTZSbEA/8hJA==
dependencies:
lodash "^4.17.21"