diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ebfe3083..9df7cc3f 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -150,7 +150,7 @@ or navigate using UI: 1. Set a unique name. 2. Provide a home page URL: your company URL or just `http://localhost`. -3. Add a callback URL for `http://localhost:3000/github/auth`. (We'll add the real redirect URL after the application is deployed.) +3. Add a callback URL for `http://localhost:3000/auth/github`. (We'll add the real redirect URL after the application is deployed.) 4. Uncheck the "Webhook -> Active" checkbox. 5. Set the scopes: - Select **Organization permissions**. diff --git a/Dockerfile b/Dockerfile index a2d02053..bd4beb1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,12 +36,17 @@ RUN echo '#!/bin/sh' > /entrypoint.sh && \ echo 'export NUXT_PUBLIC_GITHUB_ENT=${NUXT_PUBLIC_GITHUB_ENT:-$VUE_APP_GITHUB_ENT}' >> /entrypoint.sh && \ echo 'export NUXT_PUBLIC_GITHUB_TEAM=${NUXT_PUBLIC_GITHUB_TEAM:-$VUE_APP_GITHUB_TEAM}' >> /entrypoint.sh && \ echo 'export NUXT_GITHUB_TOKEN=${NUXT_GITHUB_TOKEN:-$VUE_APP_GITHUB_TOKEN}' >> /entrypoint.sh && \ - echo 'export NUXT_SESSION_PASSWORD=${NUXT_SESSION_PASSWORD:-$SESSION_SECRET}' >> /entrypoint.sh && \ + echo 'export NUXT_SESSION_PASSWORD=${NUXT_SESSION_PASSWORD:-$SESSION_SECRET$SESSION_SECRET$SESSION_SECRET$SESSION_SECRET}' >> /entrypoint.sh && \ echo 'export NUXT_OAUTH_GITHUB_CLIENT_ID=${NUXT_OAUTH_GITHUB_CLIENT_ID:-$GITHUB_CLIENT_ID}' >> /entrypoint.sh && \ echo 'export NUXT_OAUTH_GITHUB_CLIENT_SECRET=${NUXT_OAUTH_GITHUB_CLIENT_SECRET:-$GITHUB_CLIENT_SECRET}' >> /entrypoint.sh && \ + # Conditionally set NUXT_PUBLIC_USING_GITHUB_AUTH if GITHUB_CLIENT_ID is provided + echo 'if [ -n "$GITHUB_CLIENT_ID" ]; then' >> /entrypoint.sh && \ + echo 'export NUXT_PUBLIC_USING_GITHUB_AUTH=true' >> /entrypoint.sh && \ + echo 'fi' >> /entrypoint.sh && \ echo 'node /app/server/index.mjs' >> /entrypoint.sh && \ chmod +x /entrypoint.sh + USER node ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/app/components/MainComponent.vue b/app/components/MainComponent.vue index b4a6417d..d10ace68 100644 --- a/app/components/MainComponent.vue +++ b/app/components/MainComponent.vue @@ -60,11 +60,13 @@ - - @@ -148,12 +150,13 @@ export default defineNuxtComponent({ break; case 404: apiError.value = `404 Not Found - is the ${config.public.scope || ''} org:'${config.public.githubOrg || ''} ent:'${config.public.githubEnt || ''}' team:'${config.public.githubTeam}' correct? ${error.message}`; - // Update apiError with the error message - apiError.value = error.message; break; case 500: apiError.value = `500 Internal Server Error - most likely a bug in the app. Error: ${error.message}`; break; + default: + apiError.value = `${error.statusCode} Error: ${error.message}`; + break; } } } diff --git a/e2e-tests/copilot.ent.spec.ts b/e2e-tests/copilot.ent.spec.ts index dbb8fdc5..91f48dad 100644 --- a/e2e-tests/copilot.ent.spec.ts +++ b/e2e-tests/copilot.ent.spec.ts @@ -1,7 +1,5 @@ import { expect, test } from '@playwright/test' import { DashboardPage } from './pages/DashboardPage'; -import { aw } from 'vitest/dist/chunks/reporters.D7Jzd9GS.js'; -import { a } from 'vitest/dist/chunks/suite.B2jumIFP.js'; const tag = { tag: ['@ent'] } diff --git a/package-lock.json b/package-lock.json index 4ef9a655..74ebbee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "copilot-metrics-viewer", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "copilot-metrics-viewer", - "version": "2.0.0", + "version": "2.0.1", "hasInstallScript": true, "dependencies": { "@nuxt/eslint": "^0.7.4", @@ -16,7 +16,7 @@ "nuxt-auth-utils": "^0.5.7", "roboto-fontface": "^0.10.0", "undici": "^7.3.0", - "vue": "*", + "vue": "latest", "vue-chartjs": "^5.3.2", "vuetify": "^3.7.3", "webfontloader": "^1.6.28" @@ -32,7 +32,7 @@ "playwright-core": "^1.49.1", "sass-embedded": "^1.80.3", "typescript": "^5.6.3", - "vitest": "^2.1.8", + "vitest": "^2.1.9", "vue-tsc": "^2.1.6", "vuetify-nuxt-module": "^0.18.3" } @@ -3779,14 +3779,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", - "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, @@ -3795,9 +3795,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3808,13 +3808,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", - "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", + "@vitest/utils": "2.1.9", "pathe": "^1.1.2" }, "funding": { @@ -3829,13 +3829,13 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", - "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" }, @@ -3851,9 +3851,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", - "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3864,13 +3864,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, @@ -9075,9 +9075,9 @@ "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", "dev": true, "license": "MIT" }, @@ -14075,9 +14075,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", "dev": true, "license": "MIT", "dependencies": { @@ -14831,19 +14831,19 @@ } }, "node_modules/vitest": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", - "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.8", - "@vitest/mocker": "2.1.8", - "@vitest/pretty-format": "^2.1.8", - "@vitest/runner": "2.1.8", - "@vitest/snapshot": "2.1.8", - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", @@ -14855,7 +14855,7 @@ "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.8", + "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14870,8 +14870,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.8", - "@vitest/ui": "2.1.8", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, @@ -15298,13 +15298,13 @@ } }, "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", - "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", + "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, diff --git a/package.json b/package.json index dde70597..4508c473 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "copilot-metrics-viewer", - "version": "2.0.0", + "version": "2.0.1", "private": true, "type": "module", "scripts": { @@ -40,7 +40,7 @@ "playwright-core": "^1.49.1", "sass-embedded": "^1.80.3", "typescript": "^5.6.3", - "vitest": "^2.1.8", + "vitest": "^2.1.9", "vue-tsc": "^2.1.6", "vuetify-nuxt-module": "^0.18.3" } diff --git a/server/api/metrics.ts b/server/api/metrics.ts index 3de2db90..98aedda4 100644 --- a/server/api/metrics.ts +++ b/server/api/metrics.ts @@ -1,15 +1,16 @@ import type { CopilotMetrics } from "@/model/Copilot_Metrics"; import { convertToMetrics } from '@/model/MetricsToUsageConverter'; import type { MetricsApiResponse } from "@/types/metricsApiResponse"; +import type FetchError from 'ofetch'; // TODO: use for storage https://unstorage.unjs.io/drivers/azure import { readFileSync } from 'fs'; import { resolve } from 'path'; - export default defineEventHandler(async (event) => { + const logger = console; const config = useRuntimeConfig(event); let apiUrl = ''; let mockedDataPath: string; @@ -34,7 +35,6 @@ export default defineEventHandler(async (event) => { } if (config.public.isDataMocked && mockedDataPath) { - // console.log('getting mocked metrics data from:', mockedDataPath); const path = mockedDataPath; const data = readFileSync(path, 'utf8'); const dataJson = JSON.parse(data); @@ -42,23 +42,32 @@ export default defineEventHandler(async (event) => { const usageData = ensureCopilotMetrics(dataJson); // metrics is the old API format const metricsData = convertToMetrics(usageData); + + logger.info('Using mocked data'); return { metrics: metricsData, usage: usageData } as MetricsApiResponse; } if (!event.context.headers.has('Authorization')) { + logger.error('No Authentication provided'); return new Response('No Authentication provided', { status: 401 }); } - // console.log('getting metrics data from:', apiUrl); - const response = await $fetch(apiUrl, { - headers: event.context.headers - }) as unknown[]; + logger.info(`Fetching metrics data from ${apiUrl}`); - // usage is the new API format - const usageData = ensureCopilotMetrics(response as CopilotMetrics[]); - // metrics is the old API format - const metricsData = convertToMetrics(usageData); - return { metrics: metricsData, usage: usageData } as MetricsApiResponse; + try { + const response = await $fetch(apiUrl, { + headers: event.context.headers + }) as unknown[]; + + // usage is the new API format + const usageData = ensureCopilotMetrics(response as CopilotMetrics[]); + // metrics is the old API format + const metricsData = convertToMetrics(usageData); + return { metrics: metricsData, usage: usageData } as MetricsApiResponse; + } catch (error: FetchError) { + logger.error('Error fetching metrics data:', error); + return new Response('Error fetching metrics data: ' + error, { status: error.statusCode || 500 }); + } }) function ensureCopilotMetrics(data: CopilotMetrics[]): CopilotMetrics[] { diff --git a/server/api/seats.ts b/server/api/seats.ts index a83f4758..7774a101 100644 --- a/server/api/seats.ts +++ b/server/api/seats.ts @@ -1,9 +1,11 @@ import { Seat } from "@/model/Seat"; +import type FetchError from 'ofetch'; import { readFileSync } from 'fs'; import { resolve } from 'path'; export default defineEventHandler(async (event) => { + const logger = console; const config = useRuntimeConfig(event); let apiUrl = ''; let mockedDataPath: string; @@ -27,17 +29,52 @@ export default defineEventHandler(async (event) => { const data = readFileSync(path, 'utf8'); const dataJson = JSON.parse(data); const seatsData = dataJson.seats.map((item: unknown) => new Seat(item)); + + logger.info('Using mocked data'); return seatsData; } if (!event.context.headers.has('Authorization')) { + logger.error('No Authentication provided'); return new Response('No Authentication provided', { status: 401 }); } - // console.log('getting seats data from:', apiUrl); - const response = await $fetch(apiUrl, { - headers: event.context.headers - }) as { seats: unknown[] }; - const seatsData = response.seats.map((item: unknown) => new Seat(item)); + const perPage = 100; + let page = 1; + let response; + logger.info(`Fetching 1st page of seats data from ${apiUrl}`); + + try { + response = await $fetch(apiUrl, { + headers: event.context.headers, + params: { + per_page: perPage, + page: page + } + }) as { seats: unknown[], total_seats: number }; + } catch (error: FetchError) { + logger.error('Error fetching seats data:', error); + return new Response('Error fetching seats data. Error: ' + error, { status: error.statusCode || 500 }); + } + + let seatsData = response.seats.map((item: unknown) => new Seat(item)); + + // Calculate the total pages + const totalSeats = response.total_seats; + const totalPages = Math.ceil(totalSeats / perPage); + + // Fetch the remaining pages + for (page = 2; page <= totalPages; page++) { + response = await $fetch(apiUrl, { + headers: event.context.headers, + params: { + per_page: perPage, + page: page + } + }) as { seats: unknown[], total_seats: number }; + + seatsData = seatsData.concat(response.seats.map((item: unknown) => new Seat(item))); + } + return seatsData; }) \ No newline at end of file