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