Skip to content

Commit fddfe3d

Browse files
authored
feat(dashboards): add code commits metric and refactor pending actions (#192)
LFXV2-858 LFXV2-859 - Add code commits daily metric to maintainer dashboard with Snowflake integration - Add new API endpoint /api/analytics/code-commits-daily - Add interfaces for foundation and project code commits data - Refactor pending actions component to use severity-based theming - Replace custom color classes with lfx-tag component - Add tooltips and text truncation for long action titles - Standardize button heights and card styling - Add default line chart dataset options for consistency Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent c8fc05c commit fddfe3d

File tree

14 files changed

+379
-70
lines changed

14 files changed

+379
-70
lines changed

apps/lfx-one/src/app/modules/dashboards/components/dashboard-meeting-card/dashboard-meeting-card.component.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515

1616
<!-- Meeting title with file icons -->
1717
<div class="flex items-start justify-between gap-2">
18-
<div class="flex flex-col gap-1">
19-
<h4 class="line-clamp-2 text-sm leading-tight font-medium text-gray-900" data-testid="dashboard-meeting-card-title">
18+
<div class="flex flex-col gap-1 truncate max-w-full">
19+
<h4
20+
class="line-clamp-2 text-sm leading-tight font-medium text-gray-900 truncate"
21+
data-testid="dashboard-meeting-card-title"
22+
[pTooltip]="meetingTitle()">
2023
{{ meetingTitle() }}
2124
</h4>
2225
<!-- Date/time with feature icons - directly under title -->
23-
<div class="flex items-center gap-2 flex-wrap mt-0.5">
26+
<div class="flex items-center gap-2 flex-wrap mt-0.5 h-4">
2427
<span class="text-xs text-gray-600 whitespace-nowrap" data-testid="dashboard-meeting-card-time">
2528
{{ formattedTime() }}
2629
</span>

apps/lfx-one/src/app/modules/dashboards/components/pending-actions/pending-actions.component.html

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,55 +16,31 @@ <h2 class="py-1 flex items-center gap-2">
1616
<div class="flex flex-col gap-3" data-testid="dashboard-pending-actions-list">
1717
@for (item of pendingActions(); track $index) {
1818
<div
19-
class="p-4 border rounded-lg shadow-md space-y-3"
19+
class="p-4 space-y-3 border rounded-xl shadow-md transition-all"
2020
[ngClass]="{
21-
'bg-yellow-50 hover:border-yellow-300': item.color === 'amber',
22-
'bg-blue-200 hover:border-blue-300': item.color === 'blue',
23-
'bg-green-200 hover:border-green-300': item.color === 'green',
24-
'bg-purple-200 hover:border-purple-300': item.color === 'purple',
21+
'bg-yellow-50 hover:border-yellow-300': item.severity === 'warn',
22+
'bg-blue-200 hover:border-blue-300': item.severity === 'info',
23+
'bg-green-200 hover:border-green-300': item.severity === 'success',
24+
'bg-purple-200 hover:border-purple-300': item.severity === 'secondary',
2525
}"
2626
[attr.data-testid]="'dashboard-pending-actions-item-' + item.type">
2727
<!-- Header with Type -->
2828
<div class="flex items-center gap-2 flex-wrap">
29-
<div
30-
class="flex items-center gap-2 px-2 py-1 rounded-md text-xs"
31-
[ngClass]="{
32-
'bg-yellow-200': item.color === 'amber',
33-
'bg-blue-300': item.color === 'blue',
34-
'bg-green-300': item.color === 'green',
35-
'bg-purple-300': item.color === 'purple',
36-
}">
37-
<div
38-
[ngClass]="{
39-
'text-yellow-700': item.color === 'amber',
40-
'text-blue-700': item.color === 'blue',
41-
'text-green-700': item.color === 'green',
42-
'text-purple-700': item.color === 'purple',
43-
}">
44-
<i [ngClass]="[item.icon, 'w-3']"></i>
45-
</div>
46-
<span
47-
class="text-xs font-medium"
48-
[ngClass]="{
49-
'text-amber-700': item.color === 'amber',
50-
'text-blue-700': item.color === 'blue',
51-
'text-green-700': item.color === 'green',
52-
'text-purple-700': item.color === 'purple',
53-
}">
54-
{{ item.type }}
55-
</span>
56-
</div>
29+
<lfx-tag [value]="item.type" [severity]="item.severity" [icon]="item.icon" />
5730
</div>
5831

5932
<!-- Action Text -->
6033
<div class="flex items-start justify-between gap-2">
61-
<div class="flex flex-col gap-1">
62-
<p class="text-sm leading-tight font-medium text-gray-900">
34+
<div class="flex flex-col gap-1 truncate max-w-full">
35+
<h4
36+
class="line-clamp-2 text-sm leading-tight font-medium text-gray-900 truncate"
37+
data-testid="dashboard-pending-actions-title"
38+
[pTooltip]="item.text">
6339
{{ item.text }}
64-
</p>
40+
</h4>
6541
@if (item.date) {
66-
<div class="flex items-center gap-2 flex-wrap mt-0.5">
67-
<span class="text-xs text-gray-600 whitespace-nowrap">{{ item.date }}</span>
42+
<div class="flex items-center gap-2 flex-wrap mt-0.5 h-4">
43+
<span class="text-xs text-gray-600 whitespace-nowrap" data-testid="dashboard-pending-actions-date">{{ item.date }}</span>
6844
</div>
6945
}
7046
</div>
@@ -76,7 +52,7 @@ <h2 class="py-1 flex items-center gap-2">
7652
<lfx-button
7753
size="small"
7854
class="w-full"
79-
styleClass="w-full text-sm"
55+
styleClass="w-full text-sm h-8"
8056
severity="secondary"
8157
rel="noopener noreferrer"
8258
[link]="true"
@@ -89,7 +65,7 @@ <h2 class="py-1 flex items-center gap-2">
8965
<lfx-button
9066
size="small"
9167
class="w-full text-sm"
92-
styleClass="w-full text-sm"
68+
styleClass="w-full text-sm h-8"
9369
severity="secondary"
9470
(onClick)="handleActionClick(item)"
9571
[label]="item.buttonText">

apps/lfx-one/src/app/modules/dashboards/components/pending-actions/pending-actions.component.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import { CommonModule } from '@angular/common';
55
import { Component, inject, input, output } from '@angular/core';
66
import { ButtonComponent } from '@components/button/button.component';
7+
import { TagComponent } from '@components/tag/tag.component';
78
import { HiddenActionsService } from '@shared/services/hidden-actions.service';
9+
import { TooltipModule } from 'primeng/tooltip';
810

911
import type { PendingActionItem } from '@lfx-one/shared/interfaces';
10-
1112
@Component({
1213
selector: 'lfx-pending-actions',
1314
standalone: true,
14-
imports: [CommonModule, ButtonComponent],
15+
imports: [CommonModule, ButtonComponent, TagComponent, TooltipModule],
1516
templateUrl: './pending-actions.component.html',
1617
styleUrl: './pending-actions.component.scss',
1718
})

apps/lfx-one/src/app/modules/dashboards/components/recent-progress/recent-progress.component.ts

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { catchError, of, switchMap, tap } from 'rxjs';
2323

2424
import type {
2525
ActiveWeeksStreakResponse,
26+
CodeCommitsDailyResponse,
2627
DashboardMetricCard,
2728
FoundationContributorsMentoredResponse,
2829
HealthMetricsDailyResponse,
@@ -59,6 +60,7 @@ export class RecentProgressComponent {
5960
contributorsMentored: true,
6061
uniqueContributorsWeekly: true,
6162
healthMetricsDaily: true,
63+
codeCommitsDaily: true,
6264
});
6365
public readonly projectSlug = computed(() => this.projectContextService.selectedFoundation()?.slug || this.projectContextService.selectedProject()?.slug);
6466
private readonly entityType = computed<'foundation' | 'project'>(() => (this.projectContextService.selectedFoundation() ? 'foundation' : 'project'));
@@ -70,6 +72,7 @@ export class RecentProgressComponent {
7072
private readonly contributorsMentoredData = this.initializeContributorsMentoredData();
7173
private readonly uniqueContributorsWeeklyData = this.initializeUniqueContributorsWeeklyData();
7274
private readonly healthMetricsDailyData = this.initializeHealthMetricsDailyData();
75+
private readonly codeCommitsDailyData = this.initializeCodeCommitsDailyData();
7376
private readonly issuesTooltipData = this.initializeIssuesTooltipData();
7477
private readonly prVelocityTooltipData = this.initializePrVelocityTooltipData();
7578
private readonly uniqueContributorsTooltipData = this.initializeUniqueContributorsTooltipData();
@@ -88,6 +91,7 @@ export class RecentProgressComponent {
8891
private readonly contributorsMentoredCard = this.initializeContributorsMentoredCard();
8992
private readonly uniqueContributorsCard = this.initializeUniqueContributorsCard();
9093
private readonly healthScoreCard = this.initializeHealthScoreCard();
94+
private readonly codeCommitsDailyCard = this.initializeCodeCommitsDailyCard();
9195

9296
// Filtered cards - materializes card values while benefiting from individual signal memoization
9397
protected readonly filteredProgressItems = this.initializeFilteredProgressItems();
@@ -140,8 +144,6 @@ export class RecentProgressComponent {
140144
borderColor: lfxColors.emerald[500],
141145
backgroundColor: hexToRgba(lfxColors.emerald[500], 0.1),
142146
fill: true,
143-
tension: 0.4,
144-
borderWidth: 2,
145147
pointRadius: 0,
146148
},
147149
],
@@ -185,8 +187,6 @@ export class RecentProgressComponent {
185187
borderColor: lfxColors.blue[500],
186188
backgroundColor: hexToRgba(lfxColors.blue[500], 0.1),
187189
fill: true,
188-
tension: 0,
189-
borderWidth: 2,
190190
pointRadius: 0,
191191
},
192192
],
@@ -230,8 +230,6 @@ export class RecentProgressComponent {
230230
borderColor: lfxColors.blue[500],
231231
backgroundColor: hexToRgba(lfxColors.blue[500], 0.1),
232232
fill: true,
233-
tension: 0.4,
234-
borderWidth: 2,
235233
pointRadius: 0,
236234
},
237235
],
@@ -371,7 +369,6 @@ export class RecentProgressComponent {
371369
data: chartData.map((row) => row.AVG_MERGED_IN_DAYS),
372370
borderColor: lfxColors.blue[500],
373371
backgroundColor: hexToRgba(lfxColors.blue[500], 0.5),
374-
borderWidth: 0,
375372
borderRadius: 2,
376373
barPercentage: 0.95,
377374
categoryPercentage: 0.95,
@@ -449,8 +446,6 @@ export class RecentProgressComponent {
449446
borderColor: lfxColors.violet[500],
450447
backgroundColor: hexToRgba(lfxColors.violet[500], 0.1),
451448
fill: true,
452-
tension: 0.4,
453-
borderWidth: 2,
454449
pointRadius: 0,
455450
},
456451
],
@@ -507,7 +502,6 @@ export class RecentProgressComponent {
507502
data: chartData.map((row) => row.UNIQUE_CONTRIBUTORS),
508503
backgroundColor: hexToRgba(lfxColors.blue[500], 0.5),
509504
borderColor: lfxColors.blue[500],
510-
borderWidth: 0,
511505
borderRadius: 2,
512506
barPercentage: 0.95,
513507
categoryPercentage: 0.95,
@@ -588,8 +582,6 @@ export class RecentProgressComponent {
588582
borderColor: lfxColors.emerald[500],
589583
backgroundColor: hexToRgba(lfxColors.emerald[500], 0.1),
590584
fill: true,
591-
tension: 0.4,
592-
borderWidth: 2,
593585
pointRadius: 0,
594586
},
595587
],
@@ -617,6 +609,55 @@ export class RecentProgressComponent {
617609
};
618610
}
619611

612+
private transformCodeCommitsDaily(data: CodeCommitsDailyResponse, metric: DashboardMetricCard): DashboardMetricCard {
613+
// Total commits from the API
614+
const totalCommits = data.totalCommits || 0;
615+
616+
// Determine trend based on commit count
617+
const trend = totalCommits > 0 ? 'up' : 'down';
618+
619+
return {
620+
...metric,
621+
loading: this.loadingState().codeCommitsDaily,
622+
value: totalCommits.toLocaleString(),
623+
trend,
624+
chartData: {
625+
labels: data.data.map((row) => row.ACTIVITY_DATE),
626+
datasets: [
627+
{
628+
label: 'Daily Commits',
629+
data: data.data.map((row) => row.DAILY_COMMIT_COUNT),
630+
borderColor: lfxColors.blue[500],
631+
backgroundColor: hexToRgba(lfxColors.blue[500], 0.1),
632+
fill: true,
633+
pointRadius: 0,
634+
},
635+
],
636+
},
637+
chartOptions: {
638+
...BASE_LINE_CHART_OPTIONS,
639+
plugins: {
640+
...BASE_LINE_CHART_OPTIONS.plugins,
641+
tooltip: {
642+
...(BASE_LINE_CHART_OPTIONS.plugins?.tooltip ?? {}),
643+
callbacks: {
644+
title: (context: TooltipItem<'line'>[]) => {
645+
const dateStr = context[0]?.label || '';
646+
if (!dateStr) return '';
647+
const date = parseLocalDateString(dateStr);
648+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
649+
},
650+
label: (context: TooltipItem<'line'>) => {
651+
const count = context.parsed.y;
652+
return `Commits: ${count.toLocaleString()}`;
653+
},
654+
},
655+
},
656+
},
657+
},
658+
};
659+
}
660+
620661
private initializeActiveWeeksStreakData() {
621662
return toSignal(
622663
toObservable(this.personaService.currentPersona).pipe(
@@ -854,6 +895,35 @@ export class RecentProgressComponent {
854895
);
855896
}
856897

898+
private initializeCodeCommitsDailyData() {
899+
return toSignal(
900+
toObservable(this.projectSlug).pipe(
901+
switchMap((projectSlug) => {
902+
if (!projectSlug) {
903+
this.loadingState.update((state) => ({ ...state, codeCommitsDaily: false }));
904+
return [{ data: [], totalCommits: 0, totalDays: 0 }];
905+
}
906+
this.loadingState.update((state) => ({ ...state, codeCommitsDaily: true }));
907+
const entityType = this.entityType();
908+
return this.analyticsService.getCodeCommitsDaily(projectSlug, entityType).pipe(
909+
tap(() => this.loadingState.update((state) => ({ ...state, codeCommitsDaily: false }))),
910+
catchError(() => {
911+
this.loadingState.update((state) => ({ ...state, codeCommitsDaily: false }));
912+
return of({ data: [], totalCommits: 0, totalDays: 0 });
913+
})
914+
);
915+
})
916+
),
917+
{
918+
initialValue: {
919+
data: [],
920+
totalCommits: 0,
921+
totalDays: 0,
922+
},
923+
}
924+
);
925+
}
926+
857927
private initializeIsLoading() {
858928
return computed<boolean>(() => {
859929
const state = this.loadingState();
@@ -866,7 +936,8 @@ export class RecentProgressComponent {
866936
state.projectPullRequestsWeekly ||
867937
state.contributorsMentored ||
868938
state.uniqueContributorsWeekly ||
869-
state.healthMetricsDaily
939+
state.healthMetricsDaily ||
940+
state.codeCommitsDaily
870941
);
871942
}
872943

@@ -979,6 +1050,10 @@ export class RecentProgressComponent {
9791050
return computed(() => this.transformHealthMetricsDaily(this.healthMetricsDailyData(), this.getMetricConfig('Health Score')));
9801051
}
9811052

1053+
private initializeCodeCommitsDailyCard() {
1054+
return computed(() => this.transformCodeCommitsDaily(this.codeCommitsDailyData(), this.getMetricConfig('Code Commits')));
1055+
}
1056+
9821057
private initializeFilteredProgressItems() {
9831058
return computed<DashboardMetricCard[]>(() => {
9841059
const persona = this.personaService.currentPersona();
@@ -989,6 +1064,7 @@ export class RecentProgressComponent {
9891064
const allCards = [
9901065
{ card: this.issuesTrendCard(), category: 'code' },
9911066
{ card: this.prVelocityCard(), category: 'code' },
1067+
{ card: this.codeCommitsDailyCard(), category: 'code' },
9921068
{ card: this.contributorsMentoredCard(), category: 'projectHealth' },
9931069
{ card: this.uniqueContributorsCard(), category: 'projectHealth' },
9941070
{ card: this.healthScoreCard(), category: 'projectHealth' },

apps/lfx-one/src/app/shared/services/analytics.service.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { inject, Injectable } from '@angular/core';
66
import {
77
ActiveWeeksStreakResponse,
88
CertifiedEmployeesResponse,
9+
CodeCommitsDailyResponse,
910
FoundationCompanyBusFactorResponse,
1011
FoundationContributorsMentoredResponse,
1112
FoundationHealthScoreDistributionResponse,
@@ -534,4 +535,28 @@ export class AnalyticsService {
534535
})
535536
);
536537
}
538+
539+
/**
540+
* Get code commits daily data from Snowflake
541+
* @param slug - Foundation or project slug
542+
* @param entityType - Query scope: 'foundation' (foundation-level data) or 'project' (single project data)
543+
* @returns Observable of code commits daily response with commit metrics
544+
*/
545+
public getCodeCommitsDaily(slug: string, entityType: 'foundation' | 'project'): Observable<CodeCommitsDailyResponse> {
546+
const params = { slug, entityType };
547+
return this.http
548+
.get<CodeCommitsDailyResponse>('/api/analytics/code-commits-daily', {
549+
params,
550+
})
551+
.pipe(
552+
catchError((error) => {
553+
console.error('Failed to fetch code commits daily:', error);
554+
return of({
555+
data: [],
556+
totalCommits: 0,
557+
totalDays: 0,
558+
});
559+
})
560+
);
561+
}
537562
}

0 commit comments

Comments
 (0)