diff --git a/backend/package.json b/backend/package.json index 609b0d2..865ee0a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "start": "node --enable-source-maps dist/index.js | bunyan -o short -l info", "test": "jest", "build": "tsc", - "dev": "tsx watch src/index.ts | bunyan -o short -l debug", + "dev": "tsx watch src/index.ts | bunyan -o short -l info", "lint": "eslint src/**/*.ts", "db:start": "docker-compose -f ../compose.yml up -d db", "dotenv": "cp -n .env.example .env || true" diff --git a/backend/src/controllers/seats.controller.ts b/backend/src/controllers/seats.controller.ts index e1c8862..039207b 100644 --- a/backend/src/controllers/seats.controller.ts +++ b/backend/src/controllers/seats.controller.ts @@ -32,7 +32,11 @@ class SeatsController { return; } try { - const activityDays = await SeatsService.getMembersActivity(org, _daysInactive, precision as 'hour' | 'day' | 'minute'); + const activityDays = await SeatsService.getMembersActivity({ + org, + daysInactive: _daysInactive, + precision: precision as 'hour' | 'day' + }); res.status(200).json(activityDays); } catch (error) { res.status(500).json(error); @@ -40,9 +44,8 @@ class SeatsController { } async getActivityTotals(req: Request, res: Response): Promise { - const org = req.query.org?.toString() try { - const totals = await SeatsService.getMembersActivityTotals(org); + const totals = await SeatsService.getMembersActivityTotals(req.query); res.status(200).json(totals); } catch (error) { res.status(500).json(error); diff --git a/backend/src/controllers/survey.controller.ts b/backend/src/controllers/survey.controller.ts index 1379553..c5418eb 100644 --- a/backend/src/controllers/survey.controller.ts +++ b/backend/src/controllers/survey.controller.ts @@ -58,7 +58,6 @@ class SurveyController { }) res.status(201).json(survey); } catch (error) { - console.log(error); res.status(500).json(error); return; } @@ -83,14 +82,6 @@ class SurveyController { } : {} } as WhereOptions }); - console.log('test', JSON.stringify({ - reason: { - [Op.and]: [ - { [Op.ne]: '' }, - { [Op.gte]: minReasonLength } - ] - } - })); res.status(200).json(surveys); } catch (error) { res.status(500).json(error); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index b3bfe46..da7c4cc 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -49,6 +49,8 @@ router.post('/setup/existing-app', setupController.addExistingApp); router.post('/setup/db', setupController.setupDB); router.get('/setup/status', setupController.setupStatus); +router.get('/status', setupController.getStatus); + router.get('/predictive-modeling/targets', targetValuesController.getTargetValues); router.post('/predictive-modeling/targets', targetValuesController.updateTargetValues); diff --git a/backend/src/services/copilot.seats.service.ts b/backend/src/services/copilot.seats.service.ts index 9347671..d1e074b 100644 --- a/backend/src/services/copilot.seats.service.ts +++ b/backend/src/services/copilot.seats.service.ts @@ -1,6 +1,6 @@ import { Endpoints } from '@octokit/types'; import { Seat } from "../models/copilot.seats.model.js"; -import { QueryTypes, Sequelize } from 'sequelize'; +import { Op, QueryTypes, Sequelize } from 'sequelize'; import { components } from "@octokit/openapi-types"; import { Member, Team } from '../models/teams.model.js'; import app from '../index.js'; @@ -142,7 +142,14 @@ class SeatsService { } } - async getMembersActivity(org?: string, daysInactive = 30, precision = 'day' as 'hour' | 'day' | 'minute'): Promise { + async getMembersActivity(params: { + org?: string; + daysInactive?: number; + precision?: 'hour' | 'day' | 'minute'; + since?: string; + until?: string; + } = {}): Promise { + const { org, daysInactive = 30, precision = 'day', since, until } = params; if (!app.database.sequelize) throw new Error('No database connection available'); // const assignees = await app.database.sequelize.query( // `SELECT @@ -165,6 +172,11 @@ class SeatsService { // mapToModel: true // 🎯 Maps results to the Model // } // ); + + const dateFilter = { + ...(since && { [Op.gte]: new Date(since as string) }), + ...(until && { [Op.lte]: new Date(until as string) }) + }; const assignees = await Member.findAll({ attributes: ['login', 'id'], include: [ @@ -175,6 +187,7 @@ class SeatsService { order: [['last_activity_at', 'ASC']], where: { ...(org ? { org } : {}), + ...Object.getOwnPropertySymbols(dateFilter).length ? { createdAt: dateFilter } : {} } } ], @@ -230,17 +243,24 @@ class SeatsService { return sortedActivityDays; } - async getMembersActivityTotals(org?: string) { - const assignees2 = await app.database.sequelize?.query(` - SELECT \`Member\`.\`login\`, \`Member\`.\`id\`, \`activity\`.\`id\` AS \`activity.id\`, \`activity\`.\`last_activity_at\` AS \`activity.last_activity_at\` - FROM \`Members\` AS \`Member\` - INNER JOIN \`Seats\` AS \`activity\` ON \`Member\`.\`id\` = \`activity\`.\`assignee_id\` - `, { - replacements: { org }, - type: QueryTypes.SELECT - }); - console.log(assignees2); - + async getMembersActivityTotals(params: { + org?: string; + since?: string; + until?: string; + }) { + // const assignees2 = await app.database.sequelize?.query(` + // SELECT \`Member\`.\`login\`, \`Member\`.\`id\`, \`activity\`.\`id\` AS \`activity.id\`, \`activity\`.\`last_activity_at\` AS \`activity.last_activity_at\` + // FROM \`Members\` AS \`Member\` + // INNER JOIN \`Seats\` AS \`activity\` ON \`Member\`.\`id\` = \`activity\`.\`assignee_id\` + // `, { + // replacements: { org }, + // type: QueryTypes.SELECT + // }); + const { org, since, until } = params; + const dateFilter = { + ...(since && { [Op.gte]: new Date(since as string) }), + ...(until && { [Op.lte]: new Date(until as string) }) + }; const assignees = await Member.findAll({ attributes: ['login', 'id'], include: [{ @@ -250,6 +270,7 @@ class SeatsService { order: [['last_activity_at', 'ASC']], where: { ...(org ? { org } : {}), + ...Object.getOwnPropertySymbols(dateFilter).length ? { createdAt: dateFilter } : {} } }] }); diff --git a/backend/src/services/status.service.ts b/backend/src/services/status.service.ts index e65a1bc..000c9cf 100644 --- a/backend/src/services/status.service.ts +++ b/backend/src/services/status.service.ts @@ -1,21 +1,20 @@ +import app from "../index.js"; import { Seat } from "../models/copilot.seats.model.js"; import { Survey } from "../models/survey.model.js"; import { Member } from "../models/teams.model.js"; +import { Endpoints } from "@octokit/types"; export interface StatusType { - github?: { - isGood: boolean + github?: boolean; + seatsHistory?: { + oldestCreatedAt: string; + daysSinceOldestCreatedAt?: number; }; - pollingHistory?: { - isGood: boolean; - message: string; - value?: any; - progress?: string; - }; - repos?: { - value: number; - }; - surveys?: StatusType; + installations: { + installation: Endpoints["GET /app/installations"]["response"]["data"][0] + repos: Endpoints["GET /app/installations"]["response"]["data"]; + }[]; + surveyCount: number; } class StatusService { @@ -36,26 +35,32 @@ class StatusService { where: { assignee_id: assignee.id }, - order: [['createdAt', 'DESC']], + order: [['createdAt', 'ASC']], }); const oldestSeat = seats.find(seat => seat.createdAt); const daysSince = oldestSeat ? Math.floor((new Date().getTime() - oldestSeat.createdAt.getTime()) / (1000 * 3600 * 24)) : undefined; - status.pollingHistory = { - isGood: true, - message: `${oldestSeat?.createdAt}`, - value: daysSince + status.seatsHistory = { + oldestCreatedAt: oldestSeat?.createdAt.toISOString() || 'No data', + daysSinceOldestCreatedAt: daysSince } } + + status.installations = []; + for (const installation of app.github.installations) { + const repos = await installation.octokit.request(installation.installation.repositories_url); + status.installations.push({ + installation: installation.installation, + repos: repos.data.repositories + }); + } + const surveys = await Survey.findAll({ order: [['updatedAt', 'DESC']] }); if (surveys) { - // status.surveys = { - // message: `${surveys.length} surveys created`, - // value: surveys.length - // } + status.surveyCount = surveys.length; } return status; diff --git a/frontend/src/app/database/database.component.ts b/frontend/src/app/database/database.component.ts index 41d5576..1876369 100644 --- a/frontend/src/app/database/database.component.ts +++ b/frontend/src/app/database/database.component.ts @@ -64,7 +64,7 @@ export class DatabaseComponent implements AfterViewInit { }), finalize(async () => { await this.router.navigate(['/copilot'], { - queryParams: { celebrate: true } + // queryParams: { celebrate: true } }) }) ).subscribe(() => this.checkStatus()); diff --git a/frontend/src/app/guards/setup.guard.ts b/frontend/src/app/guards/setup.guard.ts index 37b387c..2f2fcfc 100644 --- a/frontend/src/app/guards/setup.guard.ts +++ b/frontend/src/app/guards/setup.guard.ts @@ -2,20 +2,24 @@ import { Injectable, isDevMode } from '@angular/core'; import { CanActivate, GuardResult, MaybeAsync, Router } from '@angular/router'; import { of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; -import { InstallationsService } from '../services/api/installations.service'; +import { InstallationsService, statusResponse } from '../services/api/installations.service'; @Injectable({ providedIn: 'root' }) export class SetupStatusGuard implements CanActivate { + responseCache?: statusResponse; + constructor( private installationsService: InstallationsService, private router: Router ) {} canActivate(): MaybeAsync { + if (this.responseCache?.isSetup === true) return of(true); return this.installationsService.refreshStatus().pipe( map((response) => { + this.responseCache = response; if (!response.dbConnected) { this.router.navigate(['/setup/db']); return false; diff --git a/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.html b/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.html index f23919f..fe30b06 100644 --- a/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.html +++ b/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.html @@ -5,14 +5,14 @@

Dashboard

-->
- + [change]="totalSurveysThisWeek" changeSuffix="" changeDescription=" this week"> --> @@ -39,37 +39,12 @@

Dashboard

- + @for (status of statuses; track $index) { + + + + } - - - Engagement - - - - - - - - Engagement Breakdown - - - - - - - - - - Most Active Users - - - - -
diff --git a/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.scss b/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.scss index 9561b9f..eae32ce 100644 --- a/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.scss +++ b/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.scss @@ -1,6 +1,7 @@ @use '@angular/material' as mat; .cards-grid { + margin-top:24px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; @@ -28,9 +29,9 @@ overflow: hidden; } - #status { - grid-column: span 3; - } + // #status { + // grid-column: span 3; + // } } /* Add media query for smaller screens */ diff --git a/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.ts b/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.ts index aac35d1..5e4d1fa 100644 --- a/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.ts +++ b/frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.ts @@ -1,19 +1,16 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { AppModule } from '../../../app.module'; -import { DashboardCardBarsComponent } from "./dashboard-card/dashboard-card-bars/dashboard-card-bars.component"; import { DashboardCardValueComponent } from './dashboard-card/dashboard-card-value/dashboard-card-value.component'; -import { DashboardCardDrilldownBarChartComponent } from './dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component'; import { MetricsService } from '../../../services/api/metrics.service'; import { CopilotMetrics } from '../../../services/api/metrics.service.interfaces'; import { ActivityResponse, Seat, SeatService } from '../../../services/api/seat.service'; import { MembersService } from '../../../services/api/members.service'; import { CopilotSurveyService, Survey } from '../../../services/api/copilot-survey.service'; -import { forkJoin, takeUntil } from 'rxjs'; +import { forkJoin, Subject, Subscription, takeUntil } from 'rxjs'; import { AdoptionChartComponent } from '../copilot-value/adoption-chart/adoption-chart.component'; import { DailyActivityChartComponent } from '../copilot-value/daily-activity-chart/daily-activity-chart.component'; import { TimeSavedChartComponent } from '../copilot-value/time-saved-chart/time-saved-chart.component'; import { LoadingSpinnerComponent } from '../../../shared/loading-spinner/loading-spinner.component'; -import { ActiveUsersChartComponent } from './dashboard-card/active-users-chart/active-users-chart.component'; import { InstallationsService } from '../../../services/api/installations.service'; import { StatusComponent } from './status/status.component'; @@ -23,32 +20,20 @@ import { StatusComponent } from './status/status.component'; imports: [ AppModule, DashboardCardValueComponent, - DashboardCardBarsComponent, - DashboardCardDrilldownBarChartComponent, AdoptionChartComponent, DailyActivityChartComponent, TimeSavedChartComponent, LoadingSpinnerComponent, - ActiveUsersChartComponent, StatusComponent ], templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.scss' }) -export class CopilotDashboardComponent implements OnInit { - allSeats?: Seat[]; - totalMembers?: number; - totalSeats?: number; - surveysData?: Survey[]; - totalSurveys?: number; - totalSurveysThisWeek?: number; +export class CopilotDashboardComponent implements OnInit, OnDestroy { + subscriptions = [] as Subscription[]; metricsData?: CopilotMetrics[]; activityData?: ActivityResponse; - seatPercentage?: number; - activeToday?: number; - activeWeeklyChangePercent?: number; - activeCurrentWeekAverage?: number; - activeLastWeekAverage?: number; + surveysData?: Survey[]; chartOptions: Highcharts.Options = { chart: { marginTop: 0, @@ -86,8 +71,15 @@ export class CopilotDashboardComponent implements OnInit { } } } + private readonly _destroy$ = new Subject(); activityTotals?: Record; + status?: any; + statuses = [] as { + title: string, + message: string, + status: 'success' | 'error' | 'warning' + }[]; statusChecks = [ // First column: Telemetry { title: 'API Connectivity', statusMessage: 'Unknown' }, @@ -106,7 +98,7 @@ export class CopilotDashboardComponent implements OnInit { { title: 'Estimates/Daily-User Ratio', statusMessage: 'Unknown' }, { title: 'Target Levels Acquired', statusMessage: '0 Levels Acquired' } ]; - + constructor( private metricsService: MetricsService, private membersService: MembersService, @@ -122,77 +114,63 @@ export class CopilotDashboardComponent implements OnInit { const formattedSince = since.toISOString().split('T')[0]; this.installationsService.currentInstallation.pipe( - takeUntil(this.installationsService.destroy$) + takeUntil(this._destroy$.asObservable()) ).subscribe(installation => { - this.activityTotals = undefined; - this.allSeats = undefined; - this.totalMembers = undefined; - this.totalSeats = undefined; - this.seatPercentage = undefined; - this.activeToday = undefined; - this.activeWeeklyChangePercent = undefined; - this.activeCurrentWeekAverage = undefined; - this.activeLastWeekAverage = undefined; - this.totalSurveys = undefined; - this.totalSurveysThisWeek = undefined; + this.subscriptions.forEach(s => s.unsubscribe()); this.metricsData = undefined; this.activityData = undefined; - this.surveysData = undefined; - - this.surveyService.getAllSurveys().subscribe(data => { - this.surveysData = data; - this.totalSurveys = data.length; - this.totalSurveysThisWeek = data.reduce((acc, survey) => { - const surveyDate = new Date(survey.createdAt!); - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - return surveyDate > oneWeekAgo ? acc + 1 : acc; - }, 0); - this.cdr.detectChanges(); - }); - - forkJoin({ - members: this.membersService.getAllMembers(), - seats: this.seatService.getAllSeats(installation?.account?.login) - }).subscribe(result => { - this.allSeats = result.seats; - this.totalMembers = result.members.length; - this.totalSeats = result.seats.length; - this.seatPercentage = (this.totalSeats / this.totalMembers) * 100; - }); + this.statuses = []; - this.seatService.getActivity(installation?.account?.login, 30).subscribe((activity) => { - this.activityData = activity; - this.cdr.detectChanges(); - }) + this.subscriptions.push( + this.installationsService.getStatus2().subscribe(status => { + this.status = status; + this.statuses[0] = { + title: 'GitHub App', + message: status.installations.reduce((acc: number, i: any) => acc += i.repos.length, 0) + ' repositories', + status: status.installations.length > 0 ? 'success' : 'error' + }; + this.statuses[1] = { + title: 'Polling History', + message: status.seatsHistory.daysSinceOldestCreatedAt + ' days', + status: status.seatsHistory.daysSinceOldestCreatedAt > 30 ? 'success' : status.seatsHistory.daysSinceOldestCreatedAt > 5 ? 'warning' : 'error' + }; + }) + ); - this.seatService.getActivityTotals(installation?.account?.login).subscribe(totals => { - Object.keys(totals).forEach((key, index) => index > 10 ? delete totals[key] : null); - this.activityTotals = totals; - this.cdr.detectChanges(); - }); + this.subscriptions.push( + this.surveyService.getAllSurveys().subscribe(data => { + this.surveysData = data; + this.cdr.detectChanges(); + this.statuses[2] = { + title: 'Estimates Collected', + message: this.surveysData.length + ' estimates', + status: this.surveysData.length > 0 ? 'success' : 'warning' + } + }) + ) - this.metricsService.getMetrics({ - org: installation?.account?.login, - since: formattedSince, - }).subscribe(data => { - this.metricsData = data; - this.activeToday = data[data.length - 1]?.total_active_users || 0; - const currentWeekData = data.slice(-7); - this.activeCurrentWeekAverage = currentWeekData.reduce((sum, day) => - sum + day.total_active_users, 0) / currentWeekData.length; - const lastWeekData = data.slice(-14, -7); - this.activeLastWeekAverage = lastWeekData.length > 0 - ? lastWeekData.reduce((sum, day) => sum + day.total_active_users, 0) / lastWeekData.length - : 0; + this.subscriptions.push( + this.seatService.getActivity(installation?.account?.login, 30).subscribe((activity) => { + this.activityData = activity; + this.cdr.detectChanges(); + }) + ) - const percentChange = this.activeLastWeekAverage === 0 - ? 100 - : ((this.activeCurrentWeekAverage - this.activeLastWeekAverage) / this.activeLastWeekAverage) * 100; - - this.activeWeeklyChangePercent = Math.round(percentChange * 10) / 10; - this.cdr.detectChanges(); - }); + this.subscriptions.push( + this.metricsService.getMetrics({ + org: installation?.account?.login, + since: formattedSince, + }).subscribe(data => { + this.metricsData = data; + this.cdr.detectChanges(); + }) + ) }); } + + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + this._destroy$.next(); + this._destroy$.complete(); + } } diff --git a/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.html b/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.html index 5661b3e..be5a5fa 100644 --- a/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.html +++ b/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.html @@ -1,14 +1,22 @@ -@for (s of status; track $index) { -
-
- {{s.title}} - {{s.statusMessage}} -
- -
- -
+
+
+ {{title}} + {{message}}
-} \ No newline at end of file + +
+ +
+
diff --git a/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.scss b/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.scss index 9091767..cfac0ba 100644 --- a/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.scss +++ b/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.scss @@ -1,12 +1,7 @@ -:host { - display: grid; - grid-template-columns: 1fr 1fr 1fr; -} - .status { display: flex; + flex-direction: row; padding: 17.6px 20px 16px 20px; - border: var(--mdc-outlined-card-outline-width) solid var(--mdc-outlined-card-outline-color, var(--mat-app-outline-variant)); .status-icon { display: flex; justify-content: center; @@ -16,12 +11,15 @@ .error { background-color: var(--error) !important; + color:white; } .success { background-color: var(--success) !important; + color:white; } .warning { background-color: var(--warning) !important; + color:white; } \ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.ts b/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.ts index 964033e..5b11150 100644 --- a/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.ts +++ b/frontend/src/app/main/copilot/copilot-dashboard/status/status.component.ts @@ -16,12 +16,7 @@ import { MatIconModule } from '@angular/material/icon'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class StatusComponent { - @Input() status?: any[]; - - constructor() { - } - - ngOnInit() { - console.log(this.status); - } + @Input() title?: string; + @Input() message?: string; + @Input() status?: 'success' | 'error' | 'warning' = 'error'; } diff --git a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html index 09b111b..3ee83da 100644 --- a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html +++ b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html @@ -3,7 +3,7 @@

Metrics

- +

@@ -14,8 +14,40 @@

Metrics

IDE Completions - + + + + + + + + Most Active Users + + + + + + + + + Engagement Breakdown + + + + + - \ No newline at end of file + + + + \ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts index 42c0b86..c372546 100644 --- a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts +++ b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts @@ -1,11 +1,18 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { DateRangeSelectComponent } from "../../../shared/date-range-select/date-range-select.component"; import { MetricsService } from '../../../services/api/metrics.service'; import { CopilotMetrics } from '../../../services/api/metrics.service.interfaces'; import { CopilotMetricsPieChartComponent } from './copilot-metrics-pie-chart/copilot-metrics-pie-chart.component'; import { MatCardModule } from '@angular/material/card'; import { Installation, InstallationsService } from '../../../services/api/installations.service'; -import { takeUntil } from 'rxjs'; +import { forkJoin, Subject, Subscription, takeUntil } from 'rxjs'; +import { DashboardCardBarsComponent } from '../copilot-dashboard/dashboard-card/dashboard-card-bars/dashboard-card-bars.component'; +import { DashboardCardDrilldownBarChartComponent } from '../copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component'; +import { ActiveUsersChartComponent } from '../copilot-dashboard/dashboard-card/active-users-chart/active-users-chart.component'; +import { SeatService } from '../../../services/api/seat.service'; +import { MembersService } from '../../../services/api/members.service'; +import { CommonModule } from '@angular/common'; +import { LoadingSpinnerComponent } from '../../../shared/loading-spinner/loading-spinner.component'; @Component({ selector: 'app-metrics', @@ -13,7 +20,12 @@ import { takeUntil } from 'rxjs'; imports: [ DateRangeSelectComponent, CopilotMetricsPieChartComponent, - MatCardModule + MatCardModule, + DashboardCardBarsComponent, + DashboardCardDrilldownBarChartComponent, + ActiveUsersChartComponent, + CommonModule, + LoadingSpinnerComponent, ], templateUrl: './copilot-metrics.component.html', styleUrls: [ @@ -25,39 +37,83 @@ export class CopilotMetricsComponent implements OnInit { metrics?: CopilotMetrics[]; metricsTotals?: CopilotMetrics; installation?: Installation = undefined; + activityTotals?: Record; + totalSeats?: number; + subscriptions: Subscription[] = []; + private readonly _destroy$ = new Subject(); + range?: { start: Date, end: Date }; constructor( private metricsService: MetricsService, - private installationsService: InstallationsService + private installationsService: InstallationsService, + private seatService: SeatService, + private cdr: ChangeDetectorRef, + private membersService: MembersService ) { } ngOnInit() { this.installationsService.currentInstallation.pipe( - takeUntil(this.installationsService.destroy$) + takeUntil(this._destroy$.asObservable()) ).subscribe(installation => { this.installation = installation; + if (this.range) { + this.dateRangeChange(this.range); + } }); } - dateRangeChange(event: {start: Date, end: Date}) { + ngOnDestroy() { + this.reset(); + this._destroy$.next(); + this._destroy$.complete(); + } + + reset() { + this.subscriptions.forEach(s => s.unsubscribe()); + this.metrics = undefined; + this.metricsTotals = undefined; + this.activityTotals = undefined; + this.totalSeats = undefined; + } + + dateRangeChange(event: { start: Date, end: Date }) { const utcStart = Date.UTC(event.start.getFullYear(), event.start.getMonth(), event.start.getDate()); const utcEnd = Date.UTC(event.end.getFullYear(), event.end.getMonth(), event.end.getDate()); const startModified = new Date(utcStart - 1); const endModified = new Date(utcEnd + 1); - this.metricsService.getMetrics({ - org: this.installation?.account?.login, - since: startModified.toISOString(), - until: endModified.toISOString() - }).subscribe((metrics) => { - this.metrics = metrics; - }); - this.metricsService.getMetricsTotals({ - org: this.installation?.account?.login, - since: startModified.toISOString(), - until: endModified.toISOString() - }).subscribe((metricsTotals) => { - this.metricsTotals = metricsTotals; - }) + this.reset(); + + this.subscriptions.push( + this.seatService.getActivityTotals({ + org: this.installation?.account?.login, + since: startModified.toISOString(), + until: endModified.toISOString() + }).subscribe(totals => { + Object.keys(totals).forEach((key, index) => index > 10 ? delete totals[key] : null); + this.activityTotals = totals; + this.cdr.detectChanges(); + }) + ) + + this.subscriptions.push( + this.metricsService.getMetrics({ + org: this.installation?.account?.login, + since: startModified.toISOString(), + until: endModified.toISOString() + }).subscribe((metrics) => { + this.metrics = metrics; + }) + ) + + this.subscriptions.push( + this.metricsService.getMetricsTotals({ + org: this.installation?.account?.login, + since: startModified.toISOString(), + until: endModified.toISOString() + }).subscribe((metricsTotals) => { + this.metricsTotals = metricsTotals; + }) + ) } } 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 fddd1c1..cf8d695 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 @@ -1,9 +1,11 @@
diff --git a/frontend/src/app/main/copilot/copilot-seats/copilot-seats.component.ts b/frontend/src/app/main/copilot/copilot-seats/copilot-seats.component.ts index 08f3419..612e616 100644 --- a/frontend/src/app/main/copilot/copilot-seats/copilot-seats.component.ts +++ b/frontend/src/app/main/copilot/copilot-seats/copilot-seats.component.ts @@ -28,7 +28,7 @@ export class CopilotSeatsComponent implements OnInit { columnDef: 'login', header: 'User', cell: (element: Seat) => `${element.assignee.login}`, - link: (element: Seat) => `https://github.com/${element.assignee.login}` + // link: (element: Seat) => `https://github.com/${element.assignee.login}` }, { columnDef: 'last_activity_at', diff --git a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts index b190388..a7877c8 100644 --- a/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts +++ b/frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.ts @@ -98,7 +98,6 @@ export class NewCopilotSurveyComponent implements OnInit { kudos: survey.kudos ? survey.kudos + 1 : 1 }).subscribe(() => { survey.kudos = (survey.kudos || 0) + 1; - console.log(`Kudos added to survey with id ${survey.id}. Total kudos: ${survey.kudos}`); }); } } diff --git a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts index 65d930c..5f80c3f 100644 --- a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts @@ -27,7 +27,7 @@ export class TimeSavedChartComponent implements OnInit, OnChanges { text: 'Time Saved (hrs per week)' }, min: 0, - max: 12, + // max: 12, labels: { format: '{value}hrs' }, @@ -61,7 +61,7 @@ export class TimeSavedChartComponent implements OnInit, OnChanges { headerFormat: '{point.x:%b %d, %Y}
', pointFormat: [ '{series.name}: ', - '{point.y:.1f}%' + '{point.y:.1f}hrs' ].join(''), style: { fontSize: '14px' diff --git a/frontend/src/app/main/copilot/copilot-value/value.component.ts b/frontend/src/app/main/copilot/copilot-value/value.component.ts index 9b1f4d0..4b28712 100644 --- a/frontend/src/app/main/copilot/copilot-value/value.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/value.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { AppModule } from '../../../app.module'; import { AdoptionChartComponent } from "./adoption-chart/adoption-chart.component"; import { ActivityResponse, SeatService } from '../../../services/api/seat.service'; @@ -7,7 +7,7 @@ import { TimeSavedChartComponent } from './time-saved-chart/time-saved-chart.com import { CopilotMetrics } from '../../../services/api/metrics.service.interfaces'; import { MetricsService } from '../../../services/api/metrics.service'; import { FormControl } from '@angular/forms'; -import { combineLatest, startWith, takeUntil } from 'rxjs'; +import { combineLatest, startWith, Subscription, takeUntil } from 'rxjs'; import { CopilotSurveyService, Survey } from '../../../services/api/copilot-survey.service'; import * as Highcharts from 'highcharts'; import HC_exporting from 'highcharts/modules/exporting'; @@ -71,39 +71,56 @@ export class CopilotValueComponent implements OnInit { } } }; + subscriptions = [] as Subscription[]; constructor( private seatService: SeatService, private metricsService: MetricsService, private copilotSurveyService: CopilotSurveyService, - private installationsService: InstallationsService + private installationsService: InstallationsService, + private cdr: ChangeDetectorRef ) { } ngOnInit() { this.installationsService.currentInstallation.pipe( takeUntil(this.installationsService.destroy$) ).subscribe(installation => { - combineLatest([ - this.daysInactive.valueChanges.pipe(startWith(this.daysInactive.value || 30)), - this.adoptionFidelity.valueChanges.pipe(startWith(this.adoptionFidelity.value || 'day')) - ]).subscribe(([days, fidelity]) => { - this.seatService.getActivity(installation?.account?.login, days || 30, fidelity || 'day').subscribe(data => { - this.activityData = data; - }); - }); - this.metricsService.getMetrics({ - org: installation?.account?.login, - }).subscribe(data => { - this.metricsData = data; - }); - this.copilotSurveyService.getAllSurveys({ - org: installation?.account?.login - }).subscribe(data => { - this.surveysData = data; - }); + this.subscriptions.forEach(s => s.unsubscribe()); + this.subscriptions = []; + + this.subscriptions.push( + combineLatest([ + this.daysInactive.valueChanges.pipe(startWith(this.daysInactive.value || 30)), + this.adoptionFidelity.valueChanges.pipe(startWith(this.adoptionFidelity.value || 'day')) + ]).subscribe(([days, fidelity]) => { + this.seatService.getActivity(installation?.account?.login, days || 30, fidelity || 'day').subscribe(data => { + this.activityData = data; + this.cdr.detectChanges(); + }); + }) + ) + this.subscriptions.push( + this.metricsService.getMetrics({ + org: installation?.account?.login, + }).subscribe(data => { + this.metricsData = data; + }) + ) + + this.subscriptions.push( + this.copilotSurveyService.getAllSurveys({ + org: installation?.account?.login + }).subscribe(data => { + this.surveysData = data; + }) + ) }); } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + chartChanged(chart: Highcharts.Chart, include = true) { if (chart && !this.charts.includes(chart)) { const _chart = chart; diff --git a/frontend/src/app/main/settings/settings.component.html b/frontend/src/app/main/settings/settings.component.html index 16e460f..86b3e20 100644 --- a/frontend/src/app/main/settings/settings.component.html +++ b/frontend/src/app/main/settings/settings.component.html @@ -155,4 +155,7 @@

Advanced

- \ No newline at end of file + + diff --git a/frontend/src/app/main/settings/settings.component.ts b/frontend/src/app/main/settings/settings.component.ts index 743b558..38de62f 100644 --- a/frontend/src/app/main/settings/settings.component.ts +++ b/frontend/src/app/main/settings/settings.component.ts @@ -109,7 +109,6 @@ export class SettingsComponent implements OnInit { percentCoding: this.form.controls.percentCoding.value, percentTimeSaved: this.form.controls.percentTimeSaved.value }; - console.log('Saving settings', settings) // Now you can store the settings object in the database this.settingsService.createSettings(settings).subscribe(() => { diff --git a/frontend/src/app/services/api/installations.service.ts b/frontend/src/app/services/api/installations.service.ts index f95be51..c416600 100644 --- a/frontend/src/app/services/api/installations.service.ts +++ b/frontend/src/app/services/api/installations.service.ts @@ -1,4 +1,4 @@ -import { Injectable, OnDestroy } from '@angular/core'; +import { Injectable, OnDestroy, OnInit } from '@angular/core'; import { BehaviorSubject, of, Subject, tap } from 'rxjs'; import { serverUrl } from '../server.service'; import { Endpoints } from '@octokit/types'; @@ -38,7 +38,7 @@ export class InstallationsService implements OnDestroy { } } - public ngOnDestroy(): void { + ngOnDestroy(): void { this._destroy$.next(); this._destroy$.complete(); } @@ -72,5 +72,8 @@ export class InstallationsService implements OnDestroy { localStorage.setItem('installation', id.toString()); } + getStatus2() { + return this.http.get(`${serverUrl}/api/status`); + } } diff --git a/frontend/src/app/services/api/seat.service.ts b/frontend/src/app/services/api/seat.service.ts index b90fcac..ef67156 100644 --- a/frontend/src/app/services/api/seat.service.ts +++ b/frontend/src/app/services/api/seat.service.ts @@ -45,9 +45,14 @@ export class SeatService { ); }; - getActivityTotals(org?: string) { + getActivityTotals(queryParams?: { + org?: string | undefined; + since?: string; + until?: string; + }) { + if (!queryParams?.org) delete queryParams?.org; return this.http.get>(`${this.apiUrl}/activity/totals`, { - params: org ? { org } : undefined + params: queryParams }); } } \ No newline at end of file diff --git a/frontend/src/app/services/highcharts.service.ts b/frontend/src/app/services/highcharts.service.ts index dc25554..6c135e5 100644 --- a/frontend/src/app/services/highcharts.service.ts +++ b/frontend/src/app/services/highcharts.service.ts @@ -479,22 +479,22 @@ export class HighchartsService { const surveyAverages = Object.keys(activity).reduce((acc, activityDate) => { const dateKey = activityDate.split('T')[0]; acc[dateKey] = { - sum: 0, - count: 0 + sum: 0, + count: 0 }; - - const dateSurveys = surveys.filter(survey => - new Date(survey.createdAt!).toISOString().split('T')[0] === dateKey + + const dateSurveys = surveys.filter(survey => + new Date(survey.createdAt!).toISOString().split('T')[0] === dateKey ); - + if (dateSurveys.length > 0) { - acc[dateKey].sum = dateSurveys.reduce((sum, survey) => - sum + survey.percentTimeSaved, 0); - acc[dateKey].count = dateSurveys.length; + const avgPercentTimeSaved = dateSurveys.reduce((sum, survey) => sum + survey.percentTimeSaved, 0) + acc[dateKey].sum = avgPercentTimeSaved * 0.01 * 0.3 * 40; // TODO pull settings + acc[dateKey].count = dateSurveys.length; } - + return acc; - }, {} as Record); + }, {} as Record); // Generate series with 7-day rolling average @@ -524,48 +524,51 @@ export class HighchartsService { .sort((a, b) => a.x - b.x); return { - series: [{ - name: 'Time Saved', - type: 'spline' as const, - data: seriesData, - lineWidth: 2, - marker: { - enabled: true, - radius: 4, - symbol: 'circle' - }, - states: { - hover: { - lineWidth: 3 + series: [ + { + name: 'Time Saved', + type: 'spline' as const, + data: seriesData, + lineWidth: 2, + marker: { + enabled: true, + radius: 4, + symbol: 'circle' + }, + states: { + hover: { + lineWidth: 3 + } } - } - }, { - type: 'scatter' as const, - name: 'Survey', - data: surveys.map(survey => ({ - x: new Date(survey.createdAt!).getTime(), - y: survey.percentTimeSaved, - raw: survey - })), - marker: { - enabled: true, - radius: 4, - symbol: 'triangle', }, - tooltip: { - headerFormat: '{point.x:%b %d, %Y}
', - pointFormatter: function () { - return [ - `User: `, - '' + this.raw?.userId + '', - `
Time saved: `, - '' + Math.round(this.y || 0) + '%', - `
PR: `, - '#' + this.raw?.prNumber + '', - ].join(''); - } as Highcharts.FormatterCallbackFunction - } - }] + // { + // type: 'scatter' as const, + // name: 'Survey', + // data: surveys.map(survey => ({ + // x: new Date(survey.createdAt!).getTime(), + // y: survey.percentTimeSaved, + // raw: survey + // })), + // marker: { + // enabled: true, + // radius: 4, + // symbol: 'triangle', + // }, + // tooltip: { + // headerFormat: '{point.x:%b %d, %Y}
', + // pointFormatter: function () { + // return [ + // `User: `, + // '' + this.raw?.userId + '', + // `
Time saved: `, + // '' + Math.round(this.y || 0) + '%', + // `
PR: `, + // '#' + this.raw?.prNumber + '', + // ].join(''); + // } as Highcharts.FormatterCallbackFunction + // } + // } + ] }; } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 9cc8f96..dbc66c1 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -95,6 +95,6 @@ highcharts-chart { :root { --error: #93000a; /* Error */ - --success: #28a745; /* Green */ - --warning: #FFAB00; /* Orange/Yellow */ + --success: #388e3c; /* Green */ + --warning: #f57c00; /* Orange/Yellow */ } \ No newline at end of file