From 555016c510e7652a498da37bedc9b1ff455c5fe9 Mon Sep 17 00:00:00 2001 From: Nick Wong Date: Mon, 30 Oct 2023 22:21:27 -0400 Subject: [PATCH 1/2] Add docker image for dashboard --- .../workflows/dashboard-release-docker.yml | 62 +++++++ dashboard/.dockerignore | 7 + dashboard/Dockerfile | 9 +- dashboard/public/mockServiceWorker.js | 160 +++++++++--------- 4 files changed, 155 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/dashboard-release-docker.yml create mode 100644 dashboard/.dockerignore diff --git a/.github/workflows/dashboard-release-docker.yml b/.github/workflows/dashboard-release-docker.yml new file mode 100644 index 00000000..7c1ffcce --- /dev/null +++ b/.github/workflows/dashboard-release-docker.yml @@ -0,0 +1,62 @@ +name: Dashboard - Release Docker + +on: + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: "${{ github.repository }}-dashboard" + +jobs: + dashboard-release-docker: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./dashboard + + steps: + - name: checkout repository + uses: actions/checkout@v3 + - name: set up QEMU + uses: docker/setup-qemu-action@v2 + - name: set up docker buildx + uses: docker/setup-buildx-action@v2 + - name: log in to the container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: string_tag + uses: ASzc/change-string-case-action@v5 + with: + string: ${{ env.REGISTRY}}/${{ env.IMAGE_NAME }} + + # https://stackoverflow.com/questions/73402042/github-action-expressions-split-string + - name: set version string + env: + VERSION: ${{ steps.release_asset.outputs.version }} + id: substring + run: echo "version=${VERSION:1}" >> $GITHUB_OUTPUT + + - name: set docker version label + env: + LABEL: ${{ !github.event.release.prerelease && 'latest-stable' || 'latest-beta' }} + id: release_type + run: echo "label=${LABEL}" >> $GITHUB_OUTPUT + + - name: build and push + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64,linux/arm64 + file: ./Dockerfile + build-args: | + lodestone_version=${{ steps.release_asset.outputs.version }} + push: true + tags: | + ${{ steps.string_tag.outputs.lowercase }}:latest + ${{ steps.string_tag.outputs.lowercase }}:${{ steps.release_type.outputs.label }} + ${{ steps.string_tag.outputs.lowercase }}:${{ steps.substring.outputs.version }} diff --git a/dashboard/.dockerignore b/dashboard/.dockerignore new file mode 100644 index 00000000..c5500558 --- /dev/null +++ b/dashboard/.dockerignore @@ -0,0 +1,7 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.next +.git diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile index 6313df4d..cc6172e2 100644 --- a/dashboard/Dockerfile +++ b/dashboard/Dockerfile @@ -1,10 +1,13 @@ -FROM node as build +FROM node:21 as build WORKDIR /app -COPY . ./ +ENV NODE_ENV production + +COPY package*.json /app/ +RUN npm ci --legacy-peer-deps -RUN npm install +COPY . ./ RUN npm run build FROM nginx diff --git a/dashboard/public/mockServiceWorker.js b/dashboard/public/mockServiceWorker.js index b24dd148..ab63a849 100644 --- a/dashboard/public/mockServiceWorker.js +++ b/dashboard/public/mockServiceWorker.js @@ -8,111 +8,111 @@ * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995'; -const activeClientIds = new Set(); +const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995' +const activeClientIds = new Set() self.addEventListener('install', function () { - self.skipWaiting(); -}); + self.skipWaiting() +}) self.addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()); -}); + event.waitUntil(self.clients.claim()) +}) self.addEventListener('message', async function (event) { - const clientId = event.source.id; + const clientId = event.source.id if (!clientId || !self.clients) { - return; + return } - const client = await self.clients.get(clientId); + const client = await self.clients.get(clientId) if (!client) { - return; + return } const allClients = await self.clients.matchAll({ type: 'window', - }); + }) switch (event.data) { case 'KEEPALIVE_REQUEST': { sendToClient(client, { type: 'KEEPALIVE_RESPONSE', - }); - break; + }) + break } case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', payload: INTEGRITY_CHECKSUM, - }); - break; + }) + break } case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId); + activeClientIds.add(clientId) sendToClient(client, { type: 'MOCKING_ENABLED', payload: true, - }); - break; + }) + break } case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId); - break; + activeClientIds.delete(clientId) + break } case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId); + activeClientIds.delete(clientId) const remainingClients = allClients.filter((client) => { - return client.id !== clientId; - }); + return client.id !== clientId + }) // Unregister itself when there are no more clients if (remainingClients.length === 0) { - self.registration.unregister(); + self.registration.unregister() } - break; + break } } -}); +}) self.addEventListener('fetch', function (event) { - const { request } = event; - const accept = request.headers.get('accept') || ''; + const { request } = event + const accept = request.headers.get('accept') || '' // Bypass server-sent events. if (accept.includes('text/event-stream')) { - return; + return } // Bypass navigation requests. if (request.mode === 'navigate') { - return; + return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { - return; + return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been deleted (still remains active until the next reload). if (activeClientIds.size === 0) { - return; + return } // Generate unique request ID. - const requestId = Math.random().toString(16).slice(2); + const requestId = Math.random().toString(16).slice(2) event.respondWith( handleRequest(event, requestId).catch((error) => { @@ -120,9 +120,9 @@ self.addEventListener('fetch', function (event) { console.warn( '[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, - request.url - ); - return; + request.url, + ) + return } // At this point, any exception indicates an issue with the original request/response. @@ -131,22 +131,22 @@ self.addEventListener('fetch', function (event) { [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, request.method, request.url, - `${error.name}: ${error.message}` - ); - }) - ); -}); + `${error.name}: ${error.message}`, + ) + }), + ) +}) async function handleRequest(event, requestId) { - const client = await resolveMainClient(event); - const response = await getResponse(event, client, requestId); + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - (async function () { - const clonedResponse = response.clone(); + ;(async function () { + const clonedResponse = response.clone() sendToClient(client, { type: 'RESPONSE', payload: { @@ -160,11 +160,11 @@ async function handleRequest(event, requestId) { headers: Object.fromEntries(clonedResponse.headers.entries()), redirected: clonedResponse.redirected, }, - }); - })(); + }) + })() } - return response; + return response } // Resolve the main client for the given event. @@ -172,49 +172,49 @@ async function handleRequest(event, requestId) { // that registered the worker. It's with the latter the worker should // communicate with during the response resolving phase. async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId); + const client = await self.clients.get(event.clientId) if (client.frameType === 'top-level') { - return client; + return client } const allClients = await self.clients.matchAll({ type: 'window', - }); + }) return allClients .filter((client) => { // Get only those clients that are currently visible. - return client.visibilityState === 'visible'; + return client.visibilityState === 'visible' }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. - return activeClientIds.has(client.id); - }); + return activeClientIds.has(client.id) + }) } async function getResponse(event, client, requestId) { - const { request } = event; - const clonedRequest = request.clone(); + const { request } = event + const clonedRequest = request.clone() function passthrough() { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const headers = Object.fromEntries(clonedRequest.headers.entries()); + const headers = Object.fromEntries(clonedRequest.headers.entries()) // Remove MSW-specific request headers so the bypassed requests // comply with the server's CORS preflight check. // Operate with the headers as an object because request "Headers" // are immutable. - delete headers['x-msw-bypass']; + delete headers['x-msw-bypass'] - return fetch(clonedRequest, { headers }); + return fetch(clonedRequest, { headers }) } // Bypass mocking when the client is not active. if (!client) { - return passthrough(); + return passthrough() } // Bypass initial page load requests (i.e. static assets). @@ -222,13 +222,13 @@ async function getResponse(event, client, requestId) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return passthrough(); + return passthrough() } // Bypass requests with the explicit bypass header. // Such requests can be issued by "ctx.fetch()". if (request.headers.get('x-msw-bypass') === 'true') { - return passthrough(); + return passthrough() } // Notify the client that a request has been intercepted. @@ -251,53 +251,53 @@ async function getResponse(event, client, requestId) { bodyUsed: request.bodyUsed, keepalive: request.keepalive, }, - }); + }) switch (clientMessage.type) { case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data); + return respondWithMock(clientMessage.data) } case 'MOCK_NOT_FOUND': { - return passthrough(); + return passthrough() } case 'NETWORK_ERROR': { - const { name, message } = clientMessage.data; - const networkError = new Error(message); - networkError.name = name; + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name // Rejecting a "respondWith" promise emulates a network error. - throw networkError; + throw networkError } } - return passthrough(); + return passthrough() } function sendToClient(client, message) { return new Promise((resolve, reject) => { - const channel = new MessageChannel(); + const channel = new MessageChannel() channel.port1.onmessage = (event) => { if (event.data && event.data.error) { - return reject(event.data.error); + return reject(event.data.error) } - resolve(event.data); - }; + resolve(event.data) + } - client.postMessage(message, [channel.port2]); - }); + client.postMessage(message, [channel.port2]) + }) } function sleep(timeMs) { return new Promise((resolve) => { - setTimeout(resolve, timeMs); - }); + setTimeout(resolve, timeMs) + }) } async function respondWithMock(response) { - await sleep(response.delay); - return new Response(response.body, response); + await sleep(response.delay) + return new Response(response.body, response) } From 33240364ed780d836f049d9638f3127fe9ac0f87 Mon Sep 17 00:00:00 2001 From: Nick Wong Date: Mon, 30 Oct 2023 22:28:10 -0400 Subject: [PATCH 2/2] Revert accidental formatting update --- dashboard/public/mockServiceWorker.js | 160 +++++++++++++------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/dashboard/public/mockServiceWorker.js b/dashboard/public/mockServiceWorker.js index ab63a849..b24dd148 100644 --- a/dashboard/public/mockServiceWorker.js +++ b/dashboard/public/mockServiceWorker.js @@ -8,111 +8,111 @@ * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995' -const activeClientIds = new Set() +const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995'; +const activeClientIds = new Set(); self.addEventListener('install', function () { - self.skipWaiting() -}) + self.skipWaiting(); +}); self.addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) + event.waitUntil(self.clients.claim()); +}); self.addEventListener('message', async function (event) { - const clientId = event.source.id + const clientId = event.source.id; if (!clientId || !self.clients) { - return + return; } - const client = await self.clients.get(clientId) + const client = await self.clients.get(clientId); if (!client) { - return + return; } const allClients = await self.clients.matchAll({ type: 'window', - }) + }); switch (event.data) { case 'KEEPALIVE_REQUEST': { sendToClient(client, { type: 'KEEPALIVE_RESPONSE', - }) - break + }); + break; } case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', payload: INTEGRITY_CHECKSUM, - }) - break + }); + break; } case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) + activeClientIds.add(clientId); sendToClient(client, { type: 'MOCKING_ENABLED', payload: true, - }) - break + }); + break; } case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId) - break + activeClientIds.delete(clientId); + break; } case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) + activeClientIds.delete(clientId); const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) + return client.id !== clientId; + }); // Unregister itself when there are no more clients if (remainingClients.length === 0) { - self.registration.unregister() + self.registration.unregister(); } - break + break; } } -}) +}); self.addEventListener('fetch', function (event) { - const { request } = event - const accept = request.headers.get('accept') || '' + const { request } = event; + const accept = request.headers.get('accept') || ''; // Bypass server-sent events. if (accept.includes('text/event-stream')) { - return + return; } // Bypass navigation requests. if (request.mode === 'navigate') { - return + return; } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { - return + return; } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been deleted (still remains active until the next reload). if (activeClientIds.size === 0) { - return + return; } // Generate unique request ID. - const requestId = Math.random().toString(16).slice(2) + const requestId = Math.random().toString(16).slice(2); event.respondWith( handleRequest(event, requestId).catch((error) => { @@ -120,9 +120,9 @@ self.addEventListener('fetch', function (event) { console.warn( '[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, - request.url, - ) - return + request.url + ); + return; } // At this point, any exception indicates an issue with the original request/response. @@ -131,22 +131,22 @@ self.addEventListener('fetch', function (event) { [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, request.method, request.url, - `${error.name}: ${error.message}`, - ) - }), - ) -}) + `${error.name}: ${error.message}` + ); + }) + ); +}); async function handleRequest(event, requestId) { - const client = await resolveMainClient(event) - const response = await getResponse(event, client, requestId) + const client = await resolveMainClient(event); + const response = await getResponse(event, client, requestId); // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - ;(async function () { - const clonedResponse = response.clone() + (async function () { + const clonedResponse = response.clone(); sendToClient(client, { type: 'RESPONSE', payload: { @@ -160,11 +160,11 @@ async function handleRequest(event, requestId) { headers: Object.fromEntries(clonedResponse.headers.entries()), redirected: clonedResponse.redirected, }, - }) - })() + }); + })(); } - return response + return response; } // Resolve the main client for the given event. @@ -172,49 +172,49 @@ async function handleRequest(event, requestId) { // that registered the worker. It's with the latter the worker should // communicate with during the response resolving phase. async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) + const client = await self.clients.get(event.clientId); if (client.frameType === 'top-level') { - return client + return client; } const allClients = await self.clients.matchAll({ type: 'window', - }) + }); return allClients .filter((client) => { // Get only those clients that are currently visible. - return client.visibilityState === 'visible' + return client.visibilityState === 'visible'; }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) + return activeClientIds.has(client.id); + }); } async function getResponse(event, client, requestId) { - const { request } = event - const clonedRequest = request.clone() + const { request } = event; + const clonedRequest = request.clone(); function passthrough() { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const headers = Object.fromEntries(clonedRequest.headers.entries()) + const headers = Object.fromEntries(clonedRequest.headers.entries()); // Remove MSW-specific request headers so the bypassed requests // comply with the server's CORS preflight check. // Operate with the headers as an object because request "Headers" // are immutable. - delete headers['x-msw-bypass'] + delete headers['x-msw-bypass']; - return fetch(clonedRequest, { headers }) + return fetch(clonedRequest, { headers }); } // Bypass mocking when the client is not active. if (!client) { - return passthrough() + return passthrough(); } // Bypass initial page load requests (i.e. static assets). @@ -222,13 +222,13 @@ async function getResponse(event, client, requestId) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return passthrough() + return passthrough(); } // Bypass requests with the explicit bypass header. // Such requests can be issued by "ctx.fetch()". if (request.headers.get('x-msw-bypass') === 'true') { - return passthrough() + return passthrough(); } // Notify the client that a request has been intercepted. @@ -251,53 +251,53 @@ async function getResponse(event, client, requestId) { bodyUsed: request.bodyUsed, keepalive: request.keepalive, }, - }) + }); switch (clientMessage.type) { case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) + return respondWithMock(clientMessage.data); } case 'MOCK_NOT_FOUND': { - return passthrough() + return passthrough(); } case 'NETWORK_ERROR': { - const { name, message } = clientMessage.data - const networkError = new Error(message) - networkError.name = name + const { name, message } = clientMessage.data; + const networkError = new Error(message); + networkError.name = name; // Rejecting a "respondWith" promise emulates a network error. - throw networkError + throw networkError; } } - return passthrough() + return passthrough(); } function sendToClient(client, message) { return new Promise((resolve, reject) => { - const channel = new MessageChannel() + const channel = new MessageChannel(); channel.port1.onmessage = (event) => { if (event.data && event.data.error) { - return reject(event.data.error) + return reject(event.data.error); } - resolve(event.data) - } + resolve(event.data); + }; - client.postMessage(message, [channel.port2]) - }) + client.postMessage(message, [channel.port2]); + }); } function sleep(timeMs) { return new Promise((resolve) => { - setTimeout(resolve, timeMs) - }) + setTimeout(resolve, timeMs); + }); } async function respondWithMock(response) { - await sleep(response.delay) - return new Response(response.body, response) + await sleep(response.delay); + return new Response(response.body, response); }