Skip to content

Commit d986bbb

Browse files
authored
feat(analytics): add foundation health metrics integration (#181)
* feat(analytics): add foundation health metrics integration Implements comprehensive foundation health analytics system with real-time Snowflake data: Backend Implementation (LFXV2-816): - 5 new Snowflake analytics endpoints for foundation metrics - Total projects with cumulative monthly aggregation - Total members with cumulative monthly data - Software value with top projects breakdown - Maintainers with daily trend analysis - Health score distribution across projects - Server-side controllers with proper error handling - Analytics routes configuration and validation Frontend Integration (LFXV2-817): - Refactored Foundation Health component with Angular signals - Reactive data loading with proper state management - 5 new analytics service methods with RxJS operators - Loading states per metric with fallback handling - Dynamic data visualization with sparklines and charts - Error handling with empty state fallbacks Shared Types (LFXV2-818): - Comprehensive TypeScript interfaces for Snowflake responses - Foundation health metric configurations - Monthly aggregation data structures - Top projects and health score distribution types - Primary foundation health metrics constants Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix(analytics): correct number formatting in software value display Changed from toFixed().toLocaleString() to toLocaleString() with decimal options. toFixed() returns a string, so calling toLocaleString() on it had no effect. Now using toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) which properly formats numbers with thousand separators and one decimal place. LFXV2-817 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]>
1 parent cc3ae19 commit d986bbb

File tree

13 files changed

+1557
-244
lines changed

13 files changed

+1557
-244
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"cSpell.words": [
3+
"ADESILVA",
34
"animateonscroll",
45
"aswf",
56
"AUTHELIA",
@@ -16,6 +17,7 @@
1617
"fullcalendar",
1718
"iconfield",
1819
"inputicon",
20+
"JEVANS",
1921
"Linkify",
2022
"litellm",
2123
"networkidle",

apps/lfx-one/src/app/modules/dashboards/components/foundation-health/foundation-health.component.html

Lines changed: 109 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -40,115 +40,134 @@ <h2>{{ title() }}</h2>
4040
</div>
4141
</div>
4242

43-
<!-- Carousel Container -->
44-
<div class="overflow-hidden">
45-
<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="foundation-health-carousel">
46-
@for (card of metricCards(); track card.title) {
47-
<div class="p-4 bg-white rounded-lg border border-slate-200 flex-shrink-0 w-[calc(100vw-3rem)] md:w-80" [attr.data-testid]="card.testId">
48-
<div class="flex flex-col h-full justify-between">
49-
<!-- Card Header -->
50-
<div class="flex items-center gap-2">
51-
<i [class]="card.icon + ' w-4 h-4 text-muted-foreground'"></i>
52-
<h5 class="text-sm font-medium">{{ card.title }}</h5>
53-
</div>
43+
@if (!hasFoundationSelected()) {
44+
<!-- No Foundation Selected State -->
45+
<div class="flex items-center justify-center p-12">
46+
<div class="text-center space-y-3">
47+
<i class="fa-light fa-circle-exclamation text-4xl text-gray-400"></i>
48+
<p class="text-sm text-gray-600 font-medium">Please select a foundation project to view foundation health data</p>
49+
<p class="text-xs text-gray-500">Use the project selector in the sidebar to choose a foundation</p>
50+
</div>
51+
</div>
52+
} @else if (isLoading() && !metricCards()) {
53+
<!-- Loading State -->
54+
<div class="flex items-center justify-center p-12">
55+
<div class="text-center space-y-3">
56+
<i class="fa-light fa-spinner-third fa-spin text-4xl text-[#0094FF]"></i>
57+
<p class="text-sm text-gray-500">Loading foundation health data...</p>
58+
</div>
59+
</div>
60+
} @else {
61+
<!-- Carousel Container -->
62+
<div class="overflow-hidden">
63+
<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="foundation-health-carousel">
64+
@for (card of metricCards(); track card.title) {
65+
<div class="p-4 bg-white rounded-lg border border-slate-200 flex-shrink-0 w-[calc(100vw-3rem)] md:w-80" [attr.data-testid]="card.testId">
66+
<div class="flex flex-col h-full justify-between">
67+
<!-- Card Header -->
68+
<div class="flex items-center gap-2">
69+
<i [class]="card.icon + ' w-4 h-4 text-muted-foreground'"></i>
70+
<h5 class="text-sm font-medium">{{ card.title }}</h5>
71+
</div>
5472

55-
<!-- Custom Content -->
56-
@switch (card.customContentType) {
57-
<!-- Sparkline Chart -->
58-
@case ('sparkline') {
59-
@if (card.chartData) {
60-
<div class="mt-3 w-full h-16">
61-
<lfx-chart [type]="'line'" [data]="card.chartData" [options]="sparklineOptions" height="100%"></lfx-chart>
62-
</div>
73+
<!-- Custom Content -->
74+
@switch (card.customContentType) {
75+
<!-- Sparkline Chart -->
76+
@case ('sparkline') {
77+
@if (card.chartData) {
78+
<div class="mt-3 w-full h-16">
79+
<lfx-chart [type]="'line'" [data]="card.chartData" [options]="card.chartOptions || sparklineOptions" height="100%"></lfx-chart>
80+
</div>
81+
}
6382
}
64-
}
6583

66-
<!-- Bar Chart -->
67-
@case ('bar-chart') {
68-
@if (card.chartData) {
69-
<div class="mt-3 flex justify-center">
70-
<div class="w-[200px] h-[60px]">
71-
<lfx-chart [type]="'bar'" [data]="card.chartData" [options]="barChartOptions" height="100%"></lfx-chart>
84+
<!-- Bar Chart -->
85+
@case ('bar-chart') {
86+
@if (card.chartData) {
87+
<div class="mt-3 flex justify-center">
88+
<div class="w-[200px] h-[60px]">
89+
<lfx-chart [type]="'bar'" [data]="card.chartData" [options]="barChartOptions" height="100%"></lfx-chart>
90+
</div>
7291
</div>
73-
</div>
92+
}
7493
}
75-
}
7694

77-
<!-- Top Projects List -->
78-
@case ('top-projects') {
79-
@if (card.topProjects) {
80-
<div class="space-y-0.5 p-0 pr-5 pl-[60px] m-0 mb-[5px]">
81-
<div class="text-xs font-medium text-muted-foreground">Top 3 Projects by Value</div>
82-
@for (project of card.topProjects; track project.name) {
83-
<div class="flex items-center justify-between text-sm">
84-
<span class="text-muted-foreground">{{ project.name }}</span>
85-
<span class="font-medium">{{ project.formattedValue }}</span>
86-
</div>
87-
}
88-
</div>
95+
<!-- Top Projects List -->
96+
@case ('top-projects') {
97+
@if (card.topProjects) {
98+
<div class="space-y-0.5 p-0 pl-[60px] m-0 mb-[5px]">
99+
<div class="text-xs font-medium text-muted-foreground">Top 3 Projects by Value</div>
100+
@for (project of card.topProjects; track project.name) {
101+
<div class="flex items-center justify-between text-sm">
102+
<span class="text-muted-foreground">{{ project.name }}</span>
103+
<span class="font-medium">{{ project.formattedValue }}</span>
104+
</div>
105+
}
106+
</div>
107+
}
89108
}
90-
}
91109

92-
<!-- Company Bus Factor (Inline) -->
93-
@case ('bus-factor') {
94-
@if (card.busFactor) {
95-
<div class="flex-1 flex flex-col justify-end pb-2">
96-
<div class="flex gap-0">
97-
<!-- Top Companies Section -->
98-
<div [style.width.%]="card.busFactor.topCompaniesPercentage" class="flex flex-col gap-1">
99-
<div class="text-xs font-medium text-slate-700">
100-
{{ card.busFactor.topCompaniesCount }} Companies ({{ card.busFactor.topCompaniesPercentage }}%)
110+
<!-- Company Bus Factor (Inline) -->
111+
@case ('bus-factor') {
112+
@if (card.busFactor) {
113+
<div class="flex-1 flex flex-col justify-end pb-2">
114+
<div class="flex gap-0">
115+
<!-- Top Companies Section -->
116+
<div [style.width.%]="card.busFactor.topCompaniesPercentage" class="flex flex-col gap-1">
117+
<div class="text-xs font-medium text-slate-700">
118+
{{ card.busFactor.topCompaniesCount }} Companies ({{ card.busFactor.topCompaniesPercentage }}%)
119+
</div>
120+
<div class="h-4 rounded-l-sm bg-gray-300"></div>
101121
</div>
102-
<div class="h-4 rounded-l-sm bg-[#0094FF]"></div>
103-
</div>
104122

105-
<!-- Other Companies Section -->
106-
<div [style.width.%]="card.busFactor.otherCompaniesPercentage" class="flex flex-col gap-1">
107-
<div class="text-xs text-slate-600 text-right">{{ card.busFactor.otherCompaniesCount }} Other Companies</div>
108-
<div class="h-4 bg-slate-200 rounded-r-sm"></div>
123+
<!-- Other Companies Section -->
124+
<div [style.width.%]="card.busFactor.otherCompaniesPercentage" class="flex flex-col gap-1">
125+
<div class="text-xs text-slate-600 text-right">{{ card.busFactor.otherCompaniesCount }} Other Companies</div>
126+
<div class="h-4 bg-slate-200 rounded-r-sm"></div>
127+
</div>
109128
</div>
110129
</div>
111-
</div>
130+
}
112131
}
113-
}
114132

115-
<!-- Project Health Scores -->
116-
@case ('health-scores') {
117-
@if (card.healthScores) {
118-
<div class="flex-1 flex flex-col justify-end p-0">
119-
<div class="flex items-end justify-between gap-2 h-16">
120-
@for (item of healthScoreDistribution(); track item.category) {
121-
<div class="flex-1 flex flex-col items-center gap-1">
122-
<div
123-
class="w-full rounded-t transition-all hover:opacity-80"
124-
[style.backgroundColor]="item.color"
125-
[style.height.px]="item.heightPx"></div>
126-
<div class="text-[10px] text-center whitespace-nowrap">
127-
<div class="font-medium">{{ item.category }}</div>
128-
<div class="text-muted-foreground">{{ item.count }}</div>
133+
<!-- Project Health Scores -->
134+
@case ('health-scores') {
135+
@if (card.healthScores) {
136+
<div class="flex-1 flex flex-col justify-end p-0">
137+
<div class="flex items-end justify-between gap-2 h-16">
138+
@for (item of healthScoreDistribution(); track item.category) {
139+
<div class="flex-1 flex flex-col items-center gap-1">
140+
<div
141+
class="w-full rounded-t transition-all hover:opacity-80"
142+
[style.backgroundColor]="item.color"
143+
[style.height.px]="item.heightPx"></div>
144+
<div class="text-[10px] text-center whitespace-nowrap">
145+
<div class="font-medium">{{ item.category }}</div>
146+
<div class="text-muted-foreground">{{ item.count }}</div>
147+
</div>
129148
</div>
130-
</div>
131-
}
149+
}
150+
</div>
132151
</div>
133-
</div>
152+
}
134153
}
135154
}
136-
}
137155

138-
<!-- Card Footer (Value and Subtitle) -->
139-
@if (card.value || card.subtitle) {
140-
<div class="space-y-1">
141-
@if (card.value) {
142-
<div class="text-xl font-medium">{{ card.value }}</div>
143-
}
144-
@if (card.subtitle) {
145-
<div class="text-xs text-gray-500">{{ card.subtitle }}</div>
146-
}
147-
</div>
148-
}
156+
<!-- Card Footer (Value and Subtitle) -->
157+
@if (card.value || card.subtitle) {
158+
<div class="space-y-1">
159+
@if (card.value) {
160+
<div class="text-xl font-medium">{{ card.value }}</div>
161+
}
162+
@if (card.subtitle) {
163+
<div class="text-xs text-gray-500">{{ card.subtitle }}</div>
164+
}
165+
</div>
166+
}
167+
</div>
149168
</div>
150-
</div>
151-
}
169+
}
170+
</div>
152171
</div>
153-
</div>
172+
}
154173
</section>

0 commit comments

Comments
 (0)