diff --git a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.html b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.html index a417960..2a63d03 100644 --- a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.html +++ b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.html @@ -47,6 +47,22 @@

{{ seat.assignee.login }}

> + + + +
+

Loading activity data... This may take a moment.

+
+
+ +
+
@@ -55,62 +71,3 @@

{{ seat.assignee.login }}

- - diff --git a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.scss b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.scss index 3aec728..cbdcc32 100644 --- a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.scss +++ b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.scss @@ -56,3 +56,62 @@ font-size: 18px; color: #666; } + + + +.avatar { + border-radius: 50%; + margin-right: 8px; + vertical-align: middle; +} + +.chart-controls { + display: flex; + justify-content: flex-end; + margin-bottom: 15px; +} + +.loading-indicator { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 400px; + color: #666; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(0, 122, 204, 0.2); + border-top-color: #007ACC; + border-radius: 50%; + animation: spin 1s ease-in-out infinite; + margin: 20px 0; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 300px; + font-size: 18px; + color: #666; +} + +.additional-stats { + margin-top: 15px; + padding-top: 10px; + border-top: 1px solid #eee; +} + +.additional-stats h3 { + margin-bottom: 8px; + color: #333; + font-size: 16px; +} \ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts index c0384e2..6dcfcfb 100644 --- a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts +++ b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts @@ -72,7 +72,40 @@ export class CopilotSeatComponent implements OnInit { ] } } - _chartOptions?: Highcharts.Options; + chart2Options: Highcharts.Options = { + title: { + text: "Seat Activity by Editor" + }, + xAxis: { + type: "datetime" + }, + legend: { + enabled: false + }, + series: [ + { + name: "Seat Activity", + type: "gantt", + data: [] + } + ], + plotOptions: { + gantt: { + borderWidth: 0, + borderColor: undefined, + dataLabels: { + enabled: true + } + } + }, + tooltip: {}, + yAxis: { + categories: [ + "vscode", + "copilot-summarization-pr" + ] + } + } id?: number | string; seat?: Seat; seatActivity?: Seat[]; @@ -91,29 +124,20 @@ export class CopilotSeatComponent implements OnInit { ) { } ngOnInit() { - // Extract the seat ID from the URL route parameters const id = this.activatedRoute.snapshot.paramMap.get('id'); - if (!id) return; // Exit if no ID is found + if (!id) return; this.id = id; - // Load the initial data with default timerange this.loadData(); } - /** - * Loads seat activity data based on the selected time range - */ loadData() { if (!this.id) return; - - // Show loading indicator - SET TO TRUE at the start of data loading this.loading = true; - this.cdr.detectChanges(); let params: { since?: string; until?: string } = {}; const until = dayjs().toISOString(); - // Set the since date based on the selected time range switch (this.selectedTimeRange) { case '7days': params = { since: dayjs().subtract(7, 'day').toISOString(), until }; @@ -122,24 +146,17 @@ export class CopilotSeatComponent implements OnInit { params = { since: dayjs().subtract(30, 'day').toISOString(), until }; break; case 'all': - // Instead of empty params, use a far past date (e.g., 5 years ago) - // This ensures we don't send 'undefined' to the API - params = { + params = { since: dayjs().subtract(5, 'year').toISOString(), - until + until }; break; } - // Create observables for all the data we need to fetch - const seatActivity$ = this.copilotSeatService.getSeat(this.id, params); - - // First get the seat data to access the assignee login - seatActivity$.pipe( + this.copilotSeatService.getSeat(this.id, params).pipe( map(seatData => { - // Store the complete seat activity data this.seatActivity = seatData; - + if (seatData.length > 0) { this.seat = seatData[seatData.length - 1]; return this.seat?.assignee?.login; @@ -151,81 +168,58 @@ export class CopilotSeatComponent implements OnInit { return of(null); }) ).subscribe(login => { - // Now that we have the login, we can make the subsequent requests if (!login) { - // Complete the process with default empty values if no login is available this.loading = false; this.cdr.detectChanges(); return; } - - // We now have the login, use it for survey queries - const surveyParams = { + + this.surveyService.getAllSurveys({ since: params.since, until: params.until, userId: login - }; - - const surveys$ = this.surveyService.getAllSurveys(surveyParams).pipe( + }).pipe( catchError(error => { - console.error('Error loading survey data:', error); - return of([] as Survey[]); - }) - ); - - // Update forkJoin to only include surveys - forkJoin({ - surveys: surveys$ - }).subscribe({ - next: (results) => { - // Process survey data - ensure surveys is an array - const surveysArray = Array.isArray(results.surveys) ? results.surveys : [results.surveys]; - this.surveyCount = surveysArray.length; - - if (this.surveyCount > 0) { - const totalTimeSavings = surveysArray.reduce((sum: number, survey: Survey) => - sum + (survey.percentTimeSaved|| 0), 0); - const avgSavings = totalTimeSavings / this.surveyCount; - this.avgTimeSavings = avgSavings.toFixed(1) + '%'; - } - - // Transform the activity data into Highcharts Gantt chart format - // Use the full seatActivity array, not just the current seat - this._chartOptions = this.highchartsService.transformSeatActivityToGantt(this.seatActivity || []); - - // Merge the transformed options with default chart options - this.chartOptions = { - ...this.chartOptions, - ...this._chartOptions - }; - - // Calculate total time spent based on Gantt data durations - this.timeSpent = " ~ " + Math.floor(dayjs.duration({ - milliseconds: (this.chartOptions.series as Highcharts.SeriesGanttOptions[])?.reduce((total, series) => { - return total += series.data?.reduce((dataTotal, data) => dataTotal += (data.end || 0) - (data.start || 0), 0) || 0; - }, 0) - }).asHours()).toString() + " hrs"; // Formatted as hours - - // Hide loading indicator - SET TO FALSE after successful data load - this.loading = false; - - // Trigger chart update and refresh the component view - this.updateFlag = true; - this.cdr.detectChanges(); - }, - error: (error) => { - console.error('Error loading data:', error); - // Hide loading indicator - SET TO FALSE even if there's an error this.loading = false; this.cdr.detectChanges(); + console.error('Error loading survey data:', error); + return of([] as Survey[]); + }), + ).subscribe(surveys => { + const surveysArray = Array.isArray(surveys) ? surveys : [surveys]; + this.surveyCount = surveysArray.length; + + if (this.surveyCount > 0) { + const totalTimeSavings = surveysArray.reduce((sum: number, survey: Survey) => + sum + (survey.percentTimeSaved || 0), 0); + const avgSavings = totalTimeSavings / this.surveyCount; + this.avgTimeSavings = avgSavings.toFixed(1) + '%'; } + + this.chartOptions = { + ...this.chartOptions, + ...this.highchartsService.transformSeatActivityToGantt(this.seatActivity || []) + }; + + this.chart2Options = { + ...this.chart2Options, + ...this.highchartsService.transformSeatActivityToScatter(this.seatActivity || []) + }; + + this.timeSpent = " ~ " + Math.floor(dayjs.duration({ + milliseconds: (this.chartOptions.series as Highcharts.SeriesGanttOptions[])?.reduce((total, series) => { + return total += series.data?.reduce((dataTotal, data) => dataTotal += (data.end || 0) - (data.start || 0), 0) || 0; + }, 0) + }).asHours()).toString() + " hrs"; // Formatted as hours + + this.loading = false; + + this.updateFlag = true; + this.cdr.detectChanges(); }); }); } - /** - * Handles time range selection change - */ onTimeRangeChange() { this.loadData(); } diff --git a/frontend/src/app/services/highcharts.service.ts b/frontend/src/app/services/highcharts.service.ts index e97665b..b3e63c6 100644 --- a/frontend/src/app/services/highcharts.service.ts +++ b/frontend/src/app/services/highcharts.service.ts @@ -591,7 +591,7 @@ export class HighchartsService { // Skip if totalActive is undefined or 0 or there is a data quality issue making daily suggestions per average user > 250 const currentMetrics = metrics.find(m => m.date.startsWith(date.slice(0, 10))); if (!currentMetrics || (currentMetrics.copilot_ide_code_completions?.total_engaged_users ?? 0) < 1 || ((currentMetrics.copilot_ide_code_completions?.total_code_suggestions ?? 0) / (currentMetrics.copilot_ide_code_completions?.total_engaged_users ?? 1)) > 250) return; - + if (currentMetrics?.copilot_ide_code_completions) { // Suggestions per user (dailyActiveIdeCompletionsSeries.data).push({ @@ -604,13 +604,13 @@ export class HighchartsService { (dailyActiveIdeAcceptsSeries.data).push({ x: new Date(date).getTime(), y: (currentMetrics.copilot_ide_code_completions.total_code_acceptances / - currentMetrics.copilot_ide_code_completions.total_engaged_users), + currentMetrics.copilot_ide_code_completions.total_engaged_users), raw: date }); // NEW: acceptance-rate (%) const sugg = currentMetrics.copilot_ide_code_completions.total_code_suggestions; - const acc = currentMetrics.copilot_ide_code_completions.total_code_acceptances; + const acc = currentMetrics.copilot_ide_code_completions.total_code_acceptances; if (sugg > 0) { (dailyActiveIdeAcceptanceRateSeries.data).push({ x: new Date(date).getTime(), @@ -754,6 +754,56 @@ export class HighchartsService { }; } + transformSeatActivityToScatter(seatActivity: Seat[]): Highcharts.Options { + console.log(seatActivity); + return { + chart: { + type: 'scatter', + }, + xAxis: { + title: { + text: 'Activity' + }, + labels: { + format: '{value} m' + }, + startOnTick: true, + endOnTick: true, + showLastLabel: true + }, + yAxis: { + title: { + text: 'Time Saved' + }, + labels: { + format: '{value} kg' + } + }, + series: [{ + name: 'Seat Activity', + type: 'scatter' as const, + data: seatActivity.map(seat => ({ + x: new Date(seat.last_activity_at || seat.created_at).getTime(), + y: Math.random() * 100, // Placeholder for time saved, adjust as needed + name: seat.assignee?.login || `Seat ${seat.assignee?.id}`, + color: this.getEditorColor(seat.last_activity_editor?.split('/')[0] || 'unknown'), + raw: seat + })), + tooltip: { + headerFormat: '{point.name}
', + pointFormatter: function (this: CustomHighchartsPoint) { + const parts = [ + `Editor: ${this.raw?.last_activity_editor || 'unknown'}
`, + `Activity: ${new Date(this.x).toLocaleString()}
`, + `Time Saved: ${this.y} minutes` + ]; + return parts.join(''); + } + } + }] + } + } + transformSeatActivityToGantt(seatActivity: Seat[]): Highcharts.Options { const editorGroups = seatActivity.reduce((acc, seat) => { const editor = seat.last_activity_editor?.split('/')[0] || 'unknown';