Skip to content

Commit

Permalink
Add loading spinner component and integrate into dashboard cards
Browse files Browse the repository at this point in the history
  • Loading branch information
austenstone committed Nov 12, 2024
1 parent 713cf28 commit f4bdfc8
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 67 deletions.
17 changes: 9 additions & 8 deletions backend/src/services/copilot.seats.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ class SeatsService {
},
include: [{
model: Assignee,
as: 'assignee'
as: 'assignee',
attributes: ['login', 'id']
}],
where: {
createdAt: {
id: {
[Op.in]: Sequelize.literal(`(
SELECT MAX(createdAt)
FROM Seats
GROUP BY assignee_id
SELECT MAX(id)
FROM Seats
GROUP BY assignee_id
)`)
}
},
Expand Down Expand Up @@ -65,7 +66,7 @@ class SeatsService {
site_admin: seat.assignee.site_admin,
}
});

const assigningTeam = seat.assigning_team ? await AssigningTeam.findOrCreate({
where: { id: seat.assigning_team.id },
defaults: {
Expand All @@ -83,7 +84,7 @@ class SeatsService {
parent: seat.assigning_team.parent,
}
}) : null;

await Seat.create({
created_at: seat.created_at,
updated_at: seat.updated_at,
Expand All @@ -96,7 +97,7 @@ class SeatsService {
});
}
}

async getAssigneesActivity(daysInactive: number): Promise<AssigneeDailyActivity> {
const assignees = await Assignee.findAll({
attributes: ['login', 'id'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,27 @@
<mat-card-header *ngIf="title">
<mat-card-title>{{title}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<!-- Bar items container -->
<mat-card-content>
<ng-container *ngIf="sections; else loading">
<div class="bar-container">
<!-- Repeat for each bar item -->
<div class="bar-item" *ngFor="let section of sections; let i = index">
<!-- Header with icon and title -->
<div class="bar-header">
<mat-icon>{{section.icon}}</mat-icon>
<span class="bar-title">{{section.name}}</span>
</div>

<!-- Progress bar -->
<mat-progress-bar
mode="determinate"
[value]="section.percentage">
</mat-progress-bar>

<!-- Footer with value and percentage -->
<div class="bar-footer">
<span class="value">{{section.value | number:'1.0-0'}}</span>
<span class="percentage">{{section.percentage | number:'1.0-0'}}%</span>
</div>
</div>
</div>
</ng-container>
<ng-template #loading>
<app-loading-spinner></app-loading-spinner>
</ng-template>
</mat-card-content>
</mat-card>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { CopilotMetrics } from '../../../../../services/metrics.service.interfaces';
import { HighchartsService } from '../../../../../services/highcharts.service';
import { LoadingSpinnerComponent } from '../../../../../shared/loading-spinner/loading-spinner.component';

export interface DashboardCardBarsInput {
value: number;
Expand All @@ -22,7 +23,8 @@ export interface DashboardCardBarsInput {
MatIconModule,
CommonModule,
MatProgressBarModule,
MatIconModule
MatIconModule,
LoadingSpinnerComponent
],
templateUrl: './dashboard-card-bars.component.html',
styleUrls: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
<mat-card-title>{{title}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<highcharts-chart [Highcharts]="Highcharts" [options]="chartOptions" style="width: 100%; display: block;"
[(update)]="updateFlag">
</highcharts-chart>
<ng-container *ngIf="_chartOptions !== undefined; else loading">
<highcharts-chart [Highcharts]="Highcharts" [options]="chartOptions" style="width: 100%; display: block;"
[(update)]="updateFlag">
</highcharts-chart>
</ng-container>
<ng-template #loading>
<app-loading-spinner></app-loading-spinner>
</ng-template>
</mat-card-content>
</mat-card>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HighchartsChartModule } from 'highcharts-angular';
import { CommonModule } from '@angular/common';
import { HighchartsService } from '../../../../../services/highcharts.service';
import { CopilotMetrics } from '../../../../../services/metrics.service.interfaces';
import { LoadingSpinnerComponent } from '../../../../../shared/loading-spinner/loading-spinner.component';

@Component({
selector: 'app-dashboard-card-drilldown-bar-chart',
Expand All @@ -15,7 +16,8 @@ import { CopilotMetrics } from '../../../../../services/metrics.service.interfac
SunburstChartComponent,
MatCardModule,
CommonModule,
HighchartsChartModule
HighchartsChartModule,
LoadingSpinnerComponent
],
templateUrl: './dashboard-card-drilldown-bar-chart.component.html',
styleUrls: [
Expand Down Expand Up @@ -51,6 +53,7 @@ export class DashboardCardDrilldownBarChartComponent implements OnChanges {
}]
}
};
_chartOptions?: Highcharts.Options;
updateFlag = false;

constructor(
Expand All @@ -59,9 +62,11 @@ export class DashboardCardDrilldownBarChartComponent implements OnChanges {

ngOnChanges(changes: SimpleChanges) {
if (changes['data'] && this.data) {
const result = this.highchartsService.transformCopilotMetricsToBarChatDrilldown(this.data);
this.chartOptions.series = result.series;
this.chartOptions.drilldown = result.drilldown;
this._chartOptions = this.highchartsService.transformCopilotMetricsToBarChatDrilldown(this.data);
this.chartOptions = {
...this.chartOptions,
...this._chartOptions
};
this.updateFlag = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
<mat-card appearance="outlined" class="dashboard-card">
<mat-card-header *ngIf="title">
<mat-card-title>{{title}}</mat-card-title>
<mat-card-subtitle *ngIf="subtitle">{{subtitle}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="value-container">
<h1 class="primary-value">{{value | number:'1.0-0'}}</h1>
<div *ngIf="change" class="trend-indicator">
<mat-icon *ngIf="icon === undefined" [class]="change > 0 ? 'positive' : 'negative'">{{change > 0 ? 'trending_up' :
'trending_down'}}</mat-icon>
<mat-icon *ngIf="icon?.length">{{icon}}</mat-icon>
<span>{{change | number:'1.0-0'}}{{changeSuffix}}{{changeDescription ? changeDescription : ''}}</span>
<ng-container *ngIf="value !== undefined; else loading">
<div class="value-container">
<h1 class="primary-value">{{value | number:'1.0-0'}}</h1>
<div *ngIf="change" class="trend-indicator">
<mat-icon *ngIf="icon === undefined" [class]="change > 0 ? 'positive' : 'negative'">{{change > 0 ?
'trending_up' :
'trending_down'}}</mat-icon>
<mat-icon *ngIf="icon?.length">{{icon}}</mat-icon>
<span>{{change | number:'1.0-0'}}{{changeSuffix}}{{changeDescription ? changeDescription : ''}}</span>
</div>
</div>
</div>
</ng-container>
<ng-template #loading>
<app-loading-spinner></app-loading-spinner>
</ng-template>
</mat-card-content>
</mat-card>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
.primary-value {
font-size: 48px;
margin: 0;
color: var(--sys-on-surface);
}

.trend-indicator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { LoadingSpinnerComponent } from '../../../../../shared/loading-spinner/loading-spinner.component';

@Component({
selector: 'app-dashboard-card-value',
standalone: true,
imports: [
MatCardModule,
MatIconModule,
CommonModule
CommonModule,
LoadingSpinnerComponent
],
templateUrl: './dashboard-card-value.component.html',
styleUrls: [
Expand All @@ -25,5 +27,6 @@ export class DashboardCardValueComponent {
@Input() changeSuffix?: string = '%';
@Input() changeDescription?: string;
@Input() icon?: string;
@Input() subtitle?: string;
Math = Math;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ <h1>Dashboard</h1>
<span class="spacer"></span>
</div> -->
<div class="cards-grid">
<app-dashboard-card-value routerLink="/copilot/seats" title="Copilot Seats" [value]="totalSeats" [change]="seatPercentage" changeSuffix=""
icon="" changeDescription="% have Copilot"></app-dashboard-card-value>
<app-dashboard-card-value title="Active Users" [value]="activeToday" [change]="activeWeeklyChangePercent"
changeSuffix="" changeDescription="% from last week"></app-dashboard-card-value>
<app-dashboard-card-value routerLink="/copilot/surveys" title="Surveys Complete" icon="" [value]="totalSurveys" [change]="totalSurveysThisWeek"
changeSuffix="" changeDescription=" this week"></app-dashboard-card-value>
<app-dashboard-card-bars title="Engagement"
[data]="metricsData ? metricsData[metricsData.length - 1] : undefined"
<app-dashboard-card-value routerLink="/copilot/seats" title="Seats" [value]="totalSeats"
[change]="seatPercentage" changeSuffix="" icon="" changeDescription="% have Copilot" subtitle="Total Copilot Seats"></app-dashboard-card-value>
<app-dashboard-card-value title="Active Users" [value]="activeCurrentWeekAverage"
[change]="activeWeeklyChangePercent" changeSuffix=""
changeDescription="% since last" subtitle="Average activity for last 7 days"></app-dashboard-card-value>
<app-dashboard-card-value routerLink="/copilot/surveys" title="Surveys Complete" icon="" [value]="totalSurveys"
[change]="totalSurveysThisWeek" changeSuffix="" changeDescription=" this week"></app-dashboard-card-value>
<app-dashboard-card-bars title="Engagement" [data]="metricsData ? metricsData[metricsData.length - 1] : undefined"
[totalSeats]="totalSeats"></app-dashboard-card-bars>
<app-dashboard-card-drilldown-bar-chart title="Engagement Breakdown"
[data]="metricsData"></app-dashboard-card-drilldown-bar-chart>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ import { forkJoin } from 'rxjs';
styleUrl: './dashboard.component.scss'
})
export class CopilotDashboardComponent implements OnInit {
totalMembers = 0;
totalSeats = 0;
totalSurveys = 0;
totalSurveysThisWeek = 0;
totalMembers?: number;
totalSeats?: number;
totalSurveys?: number;
totalSurveysThisWeek?: number;
metricsData?: CopilotMetrics[];
seatPercentage = 0;
activeToday = 0;
activeWeeklyChangePercent = 0;
seatPercentage?: number;
activeToday?: number;
activeWeeklyChangePercent?: number;
activeCurrentWeekAverage?: number;
activeLastWeekAverage?: number;

constructor(
private metricsService: MetricsService,
Expand Down Expand Up @@ -71,12 +73,25 @@ export class CopilotDashboardComponent implements OnInit {
this.metricsData = data;
this.activeToday = data[data.length - 1].total_active_users;

const lastWeekIndex = data.length - 8; // 7 days ago
const lastWeekUsers = lastWeekIndex >= 0 ? data[lastWeekIndex].total_active_users : 0;
// Get last 7 days (current week) 📅
const currentWeekData = data.slice(-7);
this.activeCurrentWeekAverage = currentWeekData.reduce((sum, day) =>
sum + day.total_active_users, 0) / currentWeekData.length;

// Get previous 7 days (last week) 📊
const lastWeekData = data.slice(-14, -7);
this.activeLastWeekAverage = lastWeekData.length > 0
? lastWeekData.reduce((sum, day) => sum + day.total_active_users, 0) / lastWeekData.length
: 0;

console.log('currentWeekAverage', this.activeCurrentWeekAverage);
console.log('lastWeekAverage', this.activeLastWeekAverage);

const percentChange = lastWeekUsers === 0
? 100 // If last week was 0, treat as 100% increase
: ((this.activeToday - lastWeekUsers) / lastWeekUsers) * 100;
// Calculate percent change between weeks 📈
const percentChange = this.activeLastWeekAverage === 0
? 100
: ((this.activeCurrentWeekAverage - this.activeLastWeekAverage) / this.activeLastWeekAverage) * 100;

this.activeWeeklyChangePercent = Math.round(percentChange * 10) / 10;
});
}
Expand Down
32 changes: 19 additions & 13 deletions frontend/src/app/services/highcharts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,29 +91,27 @@ export class HighchartsService {
}

transformCopilotMetricsToBarChatDrilldown(data: any[]) {
data.map(dateData => {
const date = new Date(dateData.date);
dateData.date = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return data;
})
const engagedUsersSeries = {
name: 'Users',
type: 'column' as 'column' | 'spline',
data: data.map(dateData => ({
type: 'column',
name: dateData.date,
y: dateData.total_engaged_users,
drilldown: `date_${dateData.date}`,
}))
data: data.map(dateData => {
const date = new Date(dateData.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return {
type: 'column',
name: date,
y: dateData.total_engaged_users,
date: new Date(dateData.date),
drilldown: `date_${dateData.date}`,
}
})
} as any;

const drilldownSeries: any[] = [];

data.forEach(dateData => {
// First level drilldown - main categories
const dateSeriesId = `date_${dateData.date}`;
drilldownSeries.push({
name: dateData.date,
name: new Date(dateData.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', weekday: 'long' }),
id: dateSeriesId,
data: [
{
Expand Down Expand Up @@ -239,6 +237,14 @@ export class HighchartsService {
series: [engagedUsersSeries],
drilldown: {
series: drilldownSeries
},
tooltip: {
headerFormat: '<span>{series.name}</span><br>',
pointFormatter: function () {
const point: any = this;

Check failure

Code scanning / ESLint

Disallow aliasing `this` Error

Unexpected aliasing of 'this' to local variable.

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
const formatted = point.date ? point.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', weekday: 'short' }) : point.name;
return `<span style="color:${point.color}">${formatted}</span>: <b>${point.y}</b> users<br/>`;
}
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LoadingSpinnerComponent } from './loading-spinner.component';

describe('LoadingSpinnerComponent', () => {
let component: LoadingSpinnerComponent;
let fixture: ComponentFixture<LoadingSpinnerComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoadingSpinnerComponent]
})
.compileComponents();

fixture = TestBed.createComponent(LoadingSpinnerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Loading

0 comments on commit f4bdfc8

Please sign in to comment.