8686
8787 <div v-show =" !apiError" >
8888 <v-progress-linear v-show =" !metricsReady" indeterminate color =" indigo" />
89- <v-window v-show =" (metricsReady && metrics.length) || (seatsReady && tab === 'seat analysis')" v-model =" tab" >
89+ <v-window v-show =" (metricsReady && metrics.length) || (seatsReady && tab === 'seat analysis') || (userMetricsReady && tab === 'user metrics') " v-model =" tab" >
9090 <v-window-item v-for =" item in tabItems" :key =" item" :value =" item" >
9191 <v-card flat >
9292 <MetricsViewer v-if =" item === getDisplayTabName(itemName)" :metrics =" metrics" :date-range-description =" dateRangeDescription" />
@@ -103,14 +103,30 @@ v-if="item === 'copilot chat'" :metrics="metrics"
103103 <AgentActivityViewer v-if =" item === 'agent activity'" :report-data =" reportData" :date-range-description =" dateRangeDescription" />
104104 <PullRequestViewer v-if =" item === 'pull requests'" :report-data =" reportData" :date-range-description =" dateRangeDescription" />
105105 <AgentModeViewer v-if =" item === 'github.com'" :original-metrics =" originalMetrics" :date-range =" dateRange" :date-range-description =" dateRangeDescription" />
106- <SeatsAnalysisViewer v-if =" item === 'seat analysis'" :seats =" seats" />
106+ <SeatsAnalysisViewer
107+ v-if =" item === 'seat analysis'"
108+ :seats =" seats"
109+ :total-seats-count =" seatsTotalCount"
110+ :current-page =" seatsCurrentPage"
111+ :total-pages =" seatsTotalPages"
112+ :per-page =" seatsPerPage"
113+ :seats-history =" seatsHistory"
114+ @page-change =" handleSeatsPageChange"
115+ />
116+ <UserMetricsViewer
117+ v-if =" item === 'user metrics'"
118+ :user-metrics =" userMetrics"
119+ :date-range-description =" dateRangeDescription"
120+ :user-metrics-history =" userMetricsHistory"
121+ :query-params =" seatsQueryParams"
122+ />
107123 <ApiResponse
108124v-if =" item === 'api response'" :metrics =" metrics" :original-metrics =" originalMetrics"
109125 :seats =" seats" />
110126 </v-card >
111127 </v-window-item >
112128 <v-alert
113- v-show =" (metricsReady && metrics.length == 0 && tab !== 'seat analysis') || (seatsReady && seats.length == 0 && tab === 'seat analysis')"
129+ v-show =" (metricsReady && metrics.length == 0 && tab !== 'seat analysis' && tab !== 'user metrics' ) || (seatsReady && seats.length == 0 && tab === 'seat analysis') || (userMetricsReady && userMetrics.length == 0 && tab === 'user metrics ')"
114130 density =" compact" text =" No data available to display" title =" No data" type =" warning" />
115131 </v-window >
116132
@@ -123,7 +139,10 @@ import type { Metrics } from '@/model/Metrics';
123139import type { CopilotMetrics } from ' @/model/Copilot_Metrics' ;
124140import type { MetricsApiResponse } from ' @/types/metricsApiResponse' ;
125141import type { Seat } from " @/model/Seat" ;
126- import type { ReportDayTotals } from " ../../server/services/github-copilot-usage-api" ;
142+ import type { SeatsApiResponse } from ' ../server/api/seats' ;
143+ import type { ReportDayTotals , UserTotals } from " ../../server/services/github-copilot-usage-api" ;
144+ import type { SeatHistoryEntry } from " ../../server/storage/seats-storage" ;
145+ import type { UserMetricsHistoryEntry } from " ../../server/storage/user-metrics-storage" ;
127146import type { H3Error } from ' h3'
128147
129148// Components
@@ -137,6 +156,7 @@ import AgentModeViewer from './AgentModeViewer.vue'
137156import AgentActivityViewer from ' ./AgentActivityViewer.vue'
138157import PullRequestViewer from ' ./PullRequestViewer.vue'
139158import DateRangeSelector from ' ./DateRangeSelector.vue'
159+ import UserMetricsViewer from ' ./UserMetricsViewer.vue'
140160import { Options } from ' @/model/Options' ;
141161import { useRoute } from ' vue-router' ;
142162
@@ -152,7 +172,8 @@ export default defineNuxtComponent({
152172 AgentModeViewer ,
153173 AgentActivityViewer ,
154174 PullRequestViewer ,
155- DateRangeSelector
175+ DateRangeSelector ,
176+ UserMetricsViewer
156177 },
157178 methods: {
158179 logout() {
@@ -255,12 +276,45 @@ export default defineNuxtComponent({
255276 break ;
256277 }
257278 }
279+ },
280+ /** Handle page navigation from SeatsAnalysisViewer paginator */
281+ async handleSeatsPageChange(page : number ) {
282+ const { data : seatsData, error : seatsError, execute } = this .seatsFetch ;
283+ // Update setup ref so the reactive query re-evaluates with the new page
284+ (this as any ).seatsCurrentPage = page ;
285+ await execute ();
286+ if (! seatsError .value ) {
287+ const resp = seatsData .value as SeatsApiResponse | null ;
288+ if (resp ) {
289+ this .seats = resp .seats || [];
290+ this .seatsTotalCount = resp .total_seats || 0 ;
291+ this .seatsCurrentPage = resp .page || page ;
292+ this .seatsTotalPages = resp .total_pages || 1 ;
293+ }
294+ }
295+ },
296+ /** Fetch seats and user-metrics history (DB / historical mode) */
297+ async fetchHistory() {
298+ const options = Options .fromRoute (this .route );
299+ const params = options .toParams ();
300+ const qs = new URLSearchParams (params ).toString ();
301+
302+ try {
303+ const [seatsH, userH] = await Promise .all ([
304+ $fetch <SeatHistoryEntry []>(` /api/seats-history${qs ? ' ?' + qs : ' ' } ` ).catch (() => []),
305+ $fetch <UserMetricsHistoryEntry []>(` /api/user-metrics-history${qs ? ' ?' + qs : ' ' } ` ).catch (() => []),
306+ ]);
307+ this .seatsHistory = seatsH ;
308+ this .userMetricsHistory = userH ;
309+ } catch {
310+ // history is non-critical — swallow errors
311+ }
258312 }
259313 },
260314
261315 data() {
262316 return {
263- tabItems: [' languages' , ' editors' , ' copilot chat' , ' agent activity' , ' pull requests' , ' github.com' , ' seat analysis' , ' api response' ],
317+ tabItems: [' languages' , ' editors' , ' copilot chat' , ' agent activity' , ' pull requests' , ' github.com' , ' seat analysis' , ' user metrics ' , ' api response' ],
264318 tab: null ,
265319 dateRangeDescription: ' Over the last 28 days' ,
266320 isLoading: false ,
@@ -270,6 +324,14 @@ export default defineNuxtComponent({
270324 reportData: [] as ReportDayTotals [],
271325 seatsReady: false ,
272326 seats: [] as Seat [],
327+ seatsTotalCount: 0 ,
328+ seatsCurrentPage: 1 ,
329+ seatsTotalPages: 1 ,
330+ seatsPerPage: 300 ,
331+ seatsHistory: [] as SeatHistoryEntry [],
332+ userMetricsReady: false ,
333+ userMetrics: [] as UserTotals [],
334+ userMetricsHistory: [] as UserMetricsHistoryEntry [],
273335 apiError: undefined as string | undefined ,
274336 showMigrationBanner: true ,
275337 config: null as ReturnType <typeof useRuntimeConfig > | null ,
@@ -295,16 +357,39 @@ export default defineNuxtComponent({
295357 await this .fetchMetrics ();
296358
297359 const { data : seatsData, error : seatsError, execute : executeSeats } = this .seatsFetch ;
360+ const { data : userMetricsData, error : userMetricsError, execute : executeUserMetrics } = this .userMetricsFetch ;
298361
299362 if (! this .signInRequired ) {
300363 await executeSeats ();
301364
302365 if (seatsError .value ) {
303366 this .processError (seatsError .value as H3Error );
304367 } else {
305- this .seats = (seatsData .value as Seat []) || [];
368+ const resp = seatsData .value as SeatsApiResponse | null ;
369+ if (resp ) {
370+ this .seats = resp .seats || [];
371+ this .seatsTotalCount = resp .total_seats || 0 ;
372+ this .seatsCurrentPage = resp .page || 1 ;
373+ this .seatsTotalPages = resp .total_pages || 1 ;
374+ }
306375 this .seatsReady = true ;
307376 }
377+
378+ await executeUserMetrics ();
379+
380+ if (userMetricsError .value ) {
381+ // User metrics errors are non-fatal — log but don't block the UI
382+ console .warn (' User metrics fetch failed:' , userMetricsError .value );
383+ } else {
384+ this .userMetrics = (userMetricsData .value as UserTotals []) || [];
385+ this .userMetricsReady = true ;
386+ }
387+
388+ // Fetch history data if historical mode is enabled
389+ const config = useRuntimeConfig ();
390+ if (config .public .enableHistoricalMode ) {
391+ await this .fetchHistory ();
392+ }
308393 }
309394
310395 } catch (error ) {
@@ -322,6 +407,7 @@ export default defineNuxtComponent({
322407 const dateRange = ref ({ since: undefined as string | undefined , until: undefined as string | undefined });
323408 const isLoading = ref (false );
324409 const route = ref (useRoute ());
410+ const seatsCurrentPage = ref (1 );
325411
326412 const signInRequired = computed (() => {
327413 return config .public .usingGithubAuth && ! loggedIn .value ;
@@ -330,6 +416,24 @@ export default defineNuxtComponent({
330416 const seatsFetch = useFetch (' /api/seats' , {
331417 server: true ,
332418 immediate: ! signInRequired .value ,
419+ query: computed (() => {
420+ const options = Options .fromRoute (route .value );
421+ return { ... options .toParams (), page: String (seatsCurrentPage .value ), per_page: ' 300' };
422+ })
423+ });
424+
425+ /** Scope + org/ent params forwarded to history endpoints and user-trend API */
426+ const seatsQueryParams = computed (() => {
427+ const options = Options .fromRoute (route .value );
428+ const p = options .toParams ();
429+ // Only forward identity params, not pagination
430+ const { since : _s, until : _u, ... rest } = p ;
431+ return rest ;
432+ });
433+
434+ const userMetricsFetch = useFetch (' /api/user-metrics' , {
435+ server: true ,
436+ immediate: false ,
333437 query: computed (() => {
334438 const options = Options .fromRoute (route .value );
335439 return options .toParams ();
@@ -344,9 +448,12 @@ export default defineNuxtComponent({
344448 signInRequired ,
345449 user ,
346450 seatsFetch ,
451+ userMetricsFetch ,
347452 dateRange ,
348453 isLoading ,
349454 route ,
455+ seatsCurrentPage ,
456+ seatsQueryParams ,
350457 };
351458 },
352459})
0 commit comments