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';