Skip to content

Commit 72623b7

Browse files
authored
feat(dashboards): implement analytics metrics with entity-type filtering (#174)
- Add Contributors Mentored metric (foundation-only) - Add Unique Contributors per Week metric (entityType support) - Update PR Weekly with entityType filtering - Update Issues Resolution with entityType filtering - Standardize query parameters to use 'slug' - Add chart padding configuration (min: 0, grace: 5%) LFXV2-783 LFXV2-784 LFXV2-785 LFXV2-786 Signed-off-by: Asitha de Silva <[email protected]>
1 parent 68f1ef3 commit 72623b7

File tree

7 files changed

+705
-103
lines changed

7 files changed

+705
-103
lines changed

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

Lines changed: 216 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import { finalize, switchMap } from 'rxjs';
2323

2424
import type {
2525
ActiveWeeksStreakResponse,
26+
FoundationContributorsMentoredResponse,
2627
ProgressItemWithChart,
2728
ProjectIssuesResolutionResponse,
2829
ProjectPullRequestsWeeklyResponse,
30+
UniqueContributorsWeeklyResponse,
2931
UserCodeCommitsResponse,
3032
UserPullRequestsResponse,
3133
} from '@lfx-one/shared/interfaces';
@@ -53,15 +55,21 @@ export class RecentProgressComponent {
5355
codeCommits: true,
5456
projectIssuesResolution: true,
5557
projectPullRequestsWeekly: true,
58+
contributorsMentored: true,
59+
uniqueContributorsWeekly: true,
5660
});
5761
public readonly projectSlug = computed(() => this.projectContextService.selectedFoundation()?.slug || this.projectContextService.selectedProject()?.slug);
62+
private readonly entityType = computed<'foundation' | 'project'>(() => (this.projectContextService.selectedFoundation() ? 'foundation' : 'project'));
5863
private readonly activeWeeksStreakData = this.initializeActiveWeeksStreakData();
5964
private readonly pullRequestsMergedData = this.initializePullRequestsMergedData();
6065
private readonly codeCommitsData = this.initializeCodeCommitsData();
6166
private readonly projectIssuesResolutionData = this.initializeProjectIssuesResolutionData();
6267
private readonly projectPullRequestsWeeklyData = this.initializeProjectPullRequestsWeeklyData();
68+
private readonly contributorsMentoredData = this.initializeContributorsMentoredData();
69+
private readonly uniqueContributorsWeeklyData = this.initializeUniqueContributorsWeeklyData();
6370
private readonly issuesTooltipData = this.initializeIssuesTooltipData();
6471
private readonly prVelocityTooltipData = this.initializePrVelocityTooltipData();
72+
private readonly uniqueContributorsTooltipData = this.initializeUniqueContributorsTooltipData();
6573
protected readonly isLoading = this.initializeIsLoading();
6674
protected readonly progressItems = this.initializeProgressItems();
6775
protected readonly selectedFilter = signal<string>('all');
@@ -412,6 +420,131 @@ export class RecentProgressComponent {
412420
};
413421
}
414422

423+
private transformContributorsMentored(data: FoundationContributorsMentoredResponse): ProgressItemWithChart {
424+
// Reverse the data to show oldest week on the left
425+
const chartData = [...data.data].reverse();
426+
427+
return {
428+
label: 'Contributors Mentored',
429+
icon: 'fa-light fa-user-graduate',
430+
value: data.totalMentored.toString(),
431+
trend: data.avgWeeklyNew > 0 ? 'up' : undefined,
432+
subtitle: 'Total contributors mentored',
433+
chartType: 'line',
434+
category: 'projectHealth',
435+
isConnected: true,
436+
chartData: {
437+
labels: chartData.map((row) => row.WEEK_START_DATE),
438+
datasets: [
439+
{
440+
label: 'Total Contributors Mentored',
441+
data: chartData.map((row) => row.MENTORED_CONTRIBUTOR_COUNT),
442+
borderColor: '#8b5cf6',
443+
backgroundColor: 'rgba(139, 92, 246, 0.1)',
444+
fill: true,
445+
tension: 0.4,
446+
borderWidth: 2,
447+
pointRadius: 0,
448+
},
449+
],
450+
},
451+
chartOptions: PROGRESS_LINE_CHART_OPTIONS,
452+
};
453+
}
454+
455+
private transformUniqueContributorsWeekly(
456+
data: UniqueContributorsWeeklyResponse,
457+
tooltipData: { total: string; avgNew: string; avgReturning: string } | null
458+
): ProgressItemWithChart {
459+
// Reverse the data to show oldest week on the left
460+
const chartData = [...data.data].reverse();
461+
462+
// Round average to whole number for display
463+
const avgUniqueContributors = Math.round(data.avgUniqueContributors || 0);
464+
465+
const tooltipText = tooltipData
466+
? `<div class="flex flex-col">
467+
<div>Total unique contributors: ${tooltipData.total}</div>
468+
<div>Avg new per week: ${tooltipData.avgNew}</div>
469+
<div>Avg returning per week: ${tooltipData.avgReturning}</div>
470+
</div>`
471+
: undefined;
472+
473+
return {
474+
label: 'Unique Contributors per Week',
475+
icon: 'fa-light fa-users',
476+
value: avgUniqueContributors.toString(),
477+
trend: avgUniqueContributors > 0 ? 'up' : 'down',
478+
subtitle: 'Active contributors',
479+
tooltipText,
480+
isConnected: true,
481+
chartType: 'bar',
482+
category: 'code',
483+
chartData: {
484+
labels: chartData.map((row) => row.WEEK_START_DATE),
485+
datasets: [
486+
{
487+
label: 'Unique Contributors',
488+
data: chartData.map((row) => row.UNIQUE_CONTRIBUTORS),
489+
backgroundColor: 'rgba(0, 148, 255, 0.5)',
490+
borderColor: '#0094FF',
491+
borderWidth: 0,
492+
borderRadius: 2,
493+
barPercentage: 0.95,
494+
categoryPercentage: 0.95,
495+
},
496+
],
497+
},
498+
chartOptions: {
499+
...PROGRESS_BAR_CHART_WITH_FOOTER_OPTIONS,
500+
plugins: {
501+
...PROGRESS_BAR_CHART_WITH_FOOTER_OPTIONS.plugins,
502+
tooltip: {
503+
...(PROGRESS_BAR_CHART_WITH_FOOTER_OPTIONS.plugins?.tooltip ?? {}),
504+
callbacks: {
505+
title: (context: TooltipItem<'bar'>[]) => {
506+
try {
507+
const dateStr = context[0]?.label || '';
508+
if (!dateStr) return '';
509+
const date = parseLocalDateString(dateStr);
510+
const formattedDate = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
511+
return `Week of ${formattedDate}`;
512+
} catch (e) {
513+
console.error('Error in title callback:', e);
514+
return context[0]?.label || '';
515+
}
516+
},
517+
label: (context: TooltipItem<'bar'>) => {
518+
try {
519+
const dataIndex = context.dataIndex;
520+
const weekData = chartData[dataIndex];
521+
return `Unique contributors: ${weekData.UNIQUE_CONTRIBUTORS}`;
522+
} catch (e) {
523+
console.error('Error in label callback:', e);
524+
return '';
525+
}
526+
},
527+
footer: (context: TooltipItem<'bar'>[]) => {
528+
try {
529+
const dataIndex = context[0].dataIndex;
530+
const weekData = chartData[dataIndex];
531+
return [
532+
`New: ${weekData.NEW_CONTRIBUTORS}`,
533+
`Returning: ${weekData.RETURNING_CONTRIBUTORS}`,
534+
`Total active: ${weekData.TOTAL_ACTIVE_CONTRIBUTORS}`,
535+
];
536+
} catch (e) {
537+
console.error('Error in footer callback:', e);
538+
return [];
539+
}
540+
},
541+
},
542+
},
543+
},
544+
},
545+
};
546+
}
547+
415548
private initializeActiveWeeksStreakData() {
416549
return toSignal(
417550
this.analyticsService.getActiveWeeksStreak().pipe(finalize(() => this.loadingState.update((state) => ({ ...state, activeWeeksStreak: false })))),
@@ -457,8 +590,9 @@ export class RecentProgressComponent {
457590
return [{ data: [], totalOpenedIssues: 0, totalClosedIssues: 0, resolutionRatePct: 0, medianDaysToClose: 0, totalDays: 0 }];
458591
}
459592
this.loadingState.update((state) => ({ ...state, projectIssuesResolution: true }));
593+
const entityType = this.entityType();
460594
return this.analyticsService
461-
.getProjectIssuesResolution(projectSlug)
595+
.getProjectIssuesResolution(projectSlug, entityType)
462596
.pipe(finalize(() => this.loadingState.update((state) => ({ ...state, projectIssuesResolution: false }))));
463597
})
464598
),
@@ -484,8 +618,9 @@ export class RecentProgressComponent {
484618
return [{ data: [], totalMergedPRs: 0, avgMergeTime: 0, totalWeeks: 0 }];
485619
}
486620
this.loadingState.update((state) => ({ ...state, projectPullRequestsWeekly: true }));
621+
const entityType = this.entityType();
487622
return this.analyticsService
488-
.getProjectPullRequestsWeekly(projectSlug)
623+
.getProjectPullRequestsWeekly(projectSlug, entityType)
489624
.pipe(finalize(() => this.loadingState.update((state) => ({ ...state, projectPullRequestsWeekly: false }))));
490625
})
491626
),
@@ -500,6 +635,57 @@ export class RecentProgressComponent {
500635
);
501636
}
502637

638+
private initializeContributorsMentoredData() {
639+
return toSignal(
640+
toObservable(this.projectSlug).pipe(
641+
switchMap((projectSlug) => {
642+
if (!projectSlug) {
643+
this.loadingState.update((state) => ({ ...state, contributorsMentored: false }));
644+
return [{ data: [], totalMentored: 0, avgWeeklyNew: 0, totalWeeks: 0 }];
645+
}
646+
this.loadingState.update((state) => ({ ...state, contributorsMentored: true }));
647+
return this.analyticsService
648+
.getContributorsMentored(projectSlug)
649+
.pipe(finalize(() => this.loadingState.update((state) => ({ ...state, contributorsMentored: false }))));
650+
})
651+
),
652+
{
653+
initialValue: {
654+
data: [],
655+
totalMentored: 0,
656+
avgWeeklyNew: 0,
657+
totalWeeks: 0,
658+
},
659+
}
660+
);
661+
}
662+
663+
private initializeUniqueContributorsWeeklyData() {
664+
return toSignal(
665+
toObservable(this.projectSlug).pipe(
666+
switchMap((projectSlug) => {
667+
if (!projectSlug) {
668+
this.loadingState.update((state) => ({ ...state, uniqueContributorsWeekly: false }));
669+
return [{ data: [], totalUniqueContributors: 0, avgUniqueContributors: 0, totalWeeks: 0 }];
670+
}
671+
this.loadingState.update((state) => ({ ...state, uniqueContributorsWeekly: true }));
672+
const entityType = this.entityType();
673+
return this.analyticsService
674+
.getUniqueContributorsWeekly(projectSlug, entityType)
675+
.pipe(finalize(() => this.loadingState.update((state) => ({ ...state, uniqueContributorsWeekly: false }))));
676+
})
677+
),
678+
{
679+
initialValue: {
680+
data: [],
681+
totalUniqueContributors: 0,
682+
avgUniqueContributors: 0,
683+
totalWeeks: 0,
684+
},
685+
}
686+
);
687+
}
688+
503689
private initializeIsLoading() {
504690
return computed<boolean>(() => {
505691
const state = this.loadingState();
@@ -515,8 +701,11 @@ export class RecentProgressComponent {
515701
const codeCommitsDataValue = this.codeCommitsData();
516702
const issuesResolutionData = this.projectIssuesResolutionData();
517703
const prWeeklyData = this.projectPullRequestsWeeklyData();
704+
const contributorsMentoredData = this.contributorsMentoredData();
705+
const uniqueContributorsData = this.uniqueContributorsWeeklyData();
518706
const issuesTooltip = this.issuesTooltipData();
519707
const prVelocityTooltip = this.prVelocityTooltipData();
708+
const uniqueContributorsTooltip = this.uniqueContributorsTooltipData();
520709

521710
const baseMetrics = persona === 'maintainer' ? MAINTAINER_PROGRESS_METRICS : CORE_DEVELOPER_PROGRESS_METRICS;
522711

@@ -536,6 +725,12 @@ export class RecentProgressComponent {
536725
if (metric.label === 'PR Review & Merge Velocity') {
537726
return this.transformProjectPullRequestsWeekly(prWeeklyData, prVelocityTooltip);
538727
}
728+
if (metric.label === 'Contributors Mentored') {
729+
return this.transformContributorsMentored(contributorsMentoredData);
730+
}
731+
if (metric.label === 'Unique Contributors per Week') {
732+
return this.transformUniqueContributorsWeekly(uniqueContributorsData, uniqueContributorsTooltip);
733+
}
539734
return metric;
540735
});
541736
});
@@ -578,4 +773,23 @@ export class RecentProgressComponent {
578773
};
579774
});
580775
}
776+
777+
private initializeUniqueContributorsTooltipData() {
778+
return computed(() => {
779+
const contributorsData = this.uniqueContributorsWeeklyData();
780+
if (!contributorsData || contributorsData.data.length === 0) {
781+
return null;
782+
}
783+
const chartData = [...contributorsData.data].reverse();
784+
const totalUnique = contributorsData.totalUniqueContributors || 0;
785+
const avgNew = chartData.length > 0 ? Math.round(chartData.reduce((sum, row) => sum + row.NEW_CONTRIBUTORS, 0) / chartData.length) : 0;
786+
const avgReturning = chartData.length > 0 ? Math.round(chartData.reduce((sum, row) => sum + row.RETURNING_CONTRIBUTORS, 0) / chartData.length) : 0;
787+
788+
return {
789+
total: totalUnique.toLocaleString(),
790+
avgNew: avgNew.toString(),
791+
avgReturning: avgReturning.toString(),
792+
};
793+
});
794+
}
581795
}

0 commit comments

Comments
 (0)