diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js
index e47994e086..72fc657333 100644
--- a/Parse-Dashboard/app.js
+++ b/Parse-Dashboard/app.js
@@ -251,6 +251,7 @@ module.exports = function(config, options) {
Parse Dashboard
diff --git a/Parse-Dashboard/parse-dashboard-config.json b/Parse-Dashboard/parse-dashboard-config.json
index 84d5d15396..59609b09c9 100644
--- a/Parse-Dashboard/parse-dashboard-config.json
+++ b/Parse-Dashboard/parse-dashboard-config.json
@@ -10,5 +10,6 @@
"secondaryBackgroundColor": ""
}
],
- "iconsFolder": "icons"
+ "iconsFolder": "icons",
+ "enableBrowserServiceWorker": false
}
diff --git a/Parse-Dashboard/public/sw.js b/Parse-Dashboard/public/sw.js
new file mode 100644
index 0000000000..6755261373
--- /dev/null
+++ b/Parse-Dashboard/public/sw.js
@@ -0,0 +1,46 @@
+const CACHE_NAME = 'dashboard-cache-v1';
+
+self.addEventListener('install', () => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ event.waitUntil(
+ Promise.all([
+ self.clients.claim(),
+ caches.keys().then(cacheNames => {
+ return Promise.all(
+ cacheNames.map(cacheName => {
+ if (cacheName !== CACHE_NAME) {
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ })
+ ])
+ );
+});
+
+self.addEventListener('fetch', event => {
+ const req = event.request;
+ if (req.destination === 'script' || req.destination === 'style' || req.url.includes('/bundles/')) {
+ event.respondWith(
+ caches.match(req).then(cached => {
+ return (
+ cached ||
+ fetch(req).then(resp => {
+ const resClone = resp.clone();
+ caches.open(CACHE_NAME).then(cache => cache.put(req, resClone));
+ return resp;
+ })
+ );
+ })
+ );
+ }
+});
+
+self.addEventListener('message', event => {
+ if (event.data === 'unregister') {
+ self.registration.unregister();
+ }
+});
diff --git a/README.md b/README.md
index 080e3c00cd..8b84e7a23c 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Custom order in the filter popup](#custom-order-in-the-filter-popup)
- [Persistent Filters](#persistent-filters)
- [Scripts](#scripts)
+ - [Resource Cache](#resource-cache)
- [Running as Express Middleware](#running-as-express-middleware)
- [Deploying Parse Dashboard](#deploying-parse-dashboard)
- [Preparing for Deployment](#preparing-for-deployment)
@@ -510,6 +511,37 @@ Parse.Cloud.define('deleteAccount', async (req) => {
+### Resource Cache
+
+Parse Dashboard can cache its resources such as bundles in the browser, so that opening the dashboard in another tab does not reload the dashboard resources from the server but from the local browser cache. Caching only starts after login in the dashboard.
+
+| Parameter | Type | Optional | Default | Example | Description |
+|-----------------------|---------|----------|---------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|
+| `enableResourceCache` | Boolean | yes | `false` | `true` | Enables caching of dashboard resources in the browser for faster dashboard loading in additional browser tabs. |
+
+
+Example configuration:
+
+```javascript
+const dashboard = new ParseDashboard({
+ enableResourceCache: true,
+ apps: [
+ {
+ serverURL: 'http://localhost:1337/parse',
+ appId: 'myAppId',
+ masterKey: 'myMasterKey',
+ appName: 'MyApp'
+ }
+ ]
+});
+```
+
+> [!Warning]
+> This feature can make it more difficult to push dashboard updates to users. Enabling the resource cache will start a browser service worker that caches dashboard resources locally only once. As long as the service worker is running, it will prevent loading any dashboard updates from the server, even if the user reloads the browser tab. The service worker is automatically stopped, once the last dashboard browser tab is closed. On the opening of the first dashboard browser tab, a new service worker is started and the dashboard resources are loaded from the server.
+
+> [!Note]
+> For developers: during dashboard development, the resource cache should be disabled to ensure reloading the dashboard tab in the browser loads the new dashboard bundle with any changes you made in the source code. You can inspect the service worker in the developer tools of most browsers. For example in Google Chrome, go to *Developer Tools > Application tab > Service workers* to see whether the dashboard service worker is currently running and to debug it.
+
# Running as Express Middleware
Instead of starting Parse Dashboard with the CLI, you can also run it as an [express](https://github.com/expressjs/express) middleware.
diff --git a/src/dashboard/index.js b/src/dashboard/index.js
index 539e71d8c3..8336cde9ae 100644
--- a/src/dashboard/index.js
+++ b/src/dashboard/index.js
@@ -12,6 +12,7 @@ import installDevTools from 'immutable-devtools';
import React from 'react';
import ReactDOM from 'react-dom';
import Dashboard from './Dashboard';
+import registerServiceWorker from '../registerServiceWorker';
require('stylesheets/fonts.scss');
require('graphiql/graphiql.min.css');
@@ -19,3 +20,4 @@ installDevTools(Immutable);
const path = window.PARSE_DASHBOARD_PATH || '/';
ReactDOM.render(, document.getElementById('browser_mount'));
+registerServiceWorker();
diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js
new file mode 100644
index 0000000000..99d42393ec
--- /dev/null
+++ b/src/registerServiceWorker.js
@@ -0,0 +1,66 @@
+function registerServiceWorker() {
+
+ if (!window.PARSE_DASHBOARD_ENABLE_RESOURCE_CACHE) {
+ return;
+ }
+
+ if (!('serviceWorker' in navigator)) {
+ return;
+ }
+
+ const mountPath = (window.PARSE_DASHBOARD_PATH || '/').replace(/\/?$/, '/');
+ const swPath = `${mountPath}sw.js`;
+ const countKey = `pd-sw-tabs:${mountPath}`;
+
+ /**
+ * Registers the service worker at the specified path.
+ */
+ const register = () => {
+ navigator.serviceWorker.register(swPath).catch(() => {});
+ };
+
+ /**
+ * Increments the count of open dashboard tabs in localStorage.
+ */
+ const increment = () => {
+ let current = parseInt(localStorage.getItem(countKey) || '0', 10);
+ if (!navigator.serviceWorker.controller && current > 0) {
+ current = 0;
+ }
+ localStorage.setItem(countKey, String(current + 1));
+ };
+
+ /**
+ * Decrements the count of open dashboard tabs in localStorage.
+ */
+ const decrement = () => {
+ const current = parseInt(localStorage.getItem(countKey) || '0', 10);
+ const next = Math.max(0, current - 1);
+ localStorage.setItem(countKey, String(next));
+ if (next === 0) {
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage('unregister');
+ }
+ navigator.serviceWorker.getRegistration(swPath).then(reg => {
+ if (reg) {
+ reg.unregister();
+ }
+ });
+ caches.keys().then(keys => keys.forEach(k => caches.delete(k)));
+ }
+ };
+
+ increment();
+
+ window.addEventListener('load', () => {
+ register();
+ });
+ window.addEventListener('beforeunload', () => {
+ decrement();
+ });
+ window.addEventListener('pagehide', () => {
+ decrement();
+ });
+}
+
+export default registerServiceWorker;