@@ -23,9 +23,11 @@ import { finalize, switchMap } from 'rxjs';
2323
2424import 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