Skip to content

Commit 58ed2b3

Browse files
asithadeclaude
andauthored
refactor(dashboards): update foundation health to carousel layout (#148)
* refactor(dashboards): update foundation health to carousel layout LFXV2-719 - Convert foundation health component from table to carousel layout with filtering - Add aggregate metrics across all foundations (8 metric cards) - Implement category-based filters (All, Contributors, Projects, Events) - Follow organization involvement patterns for data initialization - Pre-compute all display values in computed signals - Add various visualization types (sparklines, bar charts, bus factor, health scores) - Use justify-between for proper card vertical spacing - Remove public formatting methods from HTML templates Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Asitha de Silva <[email protected]> * refactor(dashboards): simplify foundation health data structure LFXV2-719 - Change FOUNDATION_HEALTH_DATA from array to single object - Remove array indexing since only aggregate data is used - Simplify Tailwind utility classes for better maintainability Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent e860187 commit 58ed2b3

File tree

6 files changed

+581
-366
lines changed

6 files changed

+581
-366
lines changed
Lines changed: 126 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,184 +1,149 @@
11
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
22
<!-- SPDX-License-Identifier: MIT -->
33

4-
<section data-testid="dashboard-foundation-health-section">
5-
<!-- Header with title and optional View All button -->
4+
<section data-testid="foundation-health-section">
65
<div class="flex items-center justify-between mb-4">
7-
<h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">Foundation Health</h2>
8-
@if (onViewAll()) {
6+
<div class="flex flex-col md:flex-row md:items-center gap-3">
7+
<h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">{{ title() }}</h2>
8+
9+
<!-- Filter Pills -->
10+
<lfx-filter-pills
11+
[options]="filterOptions"
12+
[selectedFilter]="selectedFilter()"
13+
(filterChange)="handleFilterChange($event)"
14+
data-testid="foundation-health-filters"></lfx-filter-pills>
15+
</div>
16+
17+
<!-- Carousel Controls -->
18+
<div class="flex items-center gap-2">
19+
<button
20+
type="button"
21+
(click)="scrollLeft()"
22+
class="h-8 w-8 p-0 flex items-center justify-center rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-100 hover:border-gray-400 transition-colors"
23+
data-testid="foundation-health-carousel-prev"
24+
aria-label="Scroll left">
25+
<i class="fa-light fa-chevron-left"></i>
26+
</button>
927
<button
10-
class="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100 rounded transition-colors"
11-
(click)="onViewAll()!()"
12-
data-testid="foundation-health-view-all">
13-
View All
14-
<i class="fa-light fa-chevron-right w-4 h-4"></i>
28+
type="button"
29+
(click)="scrollRight()"
30+
class="h-8 w-8 p-0 flex items-center justify-center rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-100 hover:border-gray-400 transition-colors"
31+
data-testid="foundation-health-carousel-next"
32+
aria-label="Scroll right">
33+
<i class="fa-light fa-chevron-right"></i>
1534
</button>
16-
}
35+
</div>
1736
</div>
1837

19-
<!-- Foundation Health Table -->
20-
<div class="bg-white rounded-lg border border-slate-200">
21-
<div class="overflow-x-auto">
22-
<table class="w-full">
23-
<thead>
24-
<tr class="border-b border-border">
25-
<th class="sticky left-0 z-10 bg-white text-left py-2 px-6 text-xs font-medium text-muted-foreground min-w-[200px] border-r-2 border-slate-200">
26-
Foundation
27-
</th>
28-
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
29-
<div class="flex items-center gap-1">
30-
Health Score
31-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Overall health score of the foundation"></i>
32-
</div>
33-
</th>
34-
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
35-
<div class="flex items-center gap-1">
36-
Software Value
37-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Estimated total value of software managed by the foundation"></i>
38-
</div>
39-
</th>
40-
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
41-
<div class="flex items-center gap-1">
42-
Total Members
43-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Total number of member organizations in the foundation"></i>
44-
</div>
45-
</th>
46-
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
47-
<div class="flex items-center gap-1">
48-
Active Contributors
49-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Average number of active contributors over the past year"></i>
50-
</div>
51-
</th>
52-
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
53-
<div class="flex items-center gap-1">
54-
Maintainers
55-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Average number of project maintainers over the past year"></i>
56-
</div>
57-
</th>
58-
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
59-
<div class="flex items-center gap-1">
60-
Events
61-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Total number of events hosted by the foundation this year"></i>
62-
</div>
63-
</th>
64-
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[180px]">
65-
<div class="flex items-center gap-1">
66-
Org Dependency Risk
67-
<i
68-
class="fa-light fa-circle-question w-3 h-3 cursor-help"
69-
title="Risk level based on concentration of contributions from top organizations"></i>
70-
</div>
71-
</th>
72-
</tr>
73-
</thead>
74-
<tbody>
75-
@for (foundation of foundations(); track foundation.id) {
76-
<tr class="border-b border-border last:border-b-0" [attr.data-testid]="'foundation-row-' + foundation.id">
77-
<!-- Foundation Name Column (Sticky) -->
78-
<td class="sticky left-0 z-10 bg-white py-3 px-6 border-r-2 border-slate-200">
79-
<div class="flex items-center gap-3">
80-
<div class="w-8 h-8 rounded-full overflow-hidden flex-shrink-0 bg-white p-1">
81-
<img [src]="foundation.logo" [alt]="foundation.name + ' logo'" class="w-full h-full object-contain" />
38+
<!-- Carousel Container -->
39+
<div class="overflow-hidden">
40+
<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="foundation-health-carousel">
41+
@for (card of metricCards(); track card.title) {
42+
<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">
43+
<div class="flex flex-col h-full justify-between">
44+
<!-- Card Header -->
45+
<div class="flex items-center gap-2">
46+
<i [class]="card.icon + ' w-4 h-4 text-muted-foreground'"></i>
47+
<h5 class="text-sm font-medium">{{ card.title }}</h5>
48+
</div>
49+
50+
<!-- Custom Content -->
51+
@switch (card.customContentType) {
52+
<!-- Sparkline Chart -->
53+
@case ('sparkline') {
54+
@if (card.chartData) {
55+
<div class="mt-3 w-full h-16">
56+
<lfx-chart [type]="'line'" [data]="card.chartData" [options]="sparklineOptions" height="100%"></lfx-chart>
57+
</div>
58+
}
59+
}
60+
61+
<!-- Bar Chart -->
62+
@case ('bar-chart') {
63+
@if (card.chartData) {
64+
<div class="mt-3 flex justify-center">
65+
<div class="w-[200px] h-[60px]">
66+
<lfx-chart [type]="'bar'" [data]="card.chartData" [options]="barChartOptions" height="100%"></lfx-chart>
67+
</div>
8268
</div>
83-
<div class="min-w-0">
84-
<div class="font-medium text-sm text-[#009aff] truncate">{{ foundation.name }}</div>
85-
@if (foundation.projectBreakdown) {
86-
<div class="text-xs text-gray-500">
87-
<div>{{ foundation.projectBreakdown.sandbox }} sandbox</div>
88-
<div>{{ foundation.projectBreakdown.incubating }} incubating</div>
89-
<div>{{ foundation.projectBreakdown.graduated }} graduated</div>
69+
}
70+
}
71+
72+
<!-- Top Projects List -->
73+
@case ('top-projects') {
74+
@if (card.topProjects) {
75+
<div class="space-y-0.5 p-0 pr-5 pl-[60px] m-0 mb-[5px]">
76+
<div class="text-xs font-medium text-muted-foreground">Top 3 Projects by Value</div>
77+
@for (project of card.topProjects; track project.name) {
78+
<div class="flex items-center justify-between text-sm">
79+
<span class="text-muted-foreground">{{ project.name }}</span>
80+
<span class="font-medium">{{ project.formattedValue }}</span>
9081
</div>
91-
} @else {
92-
<div class="text-xs text-gray-500">{{ foundation.projectCount }} projects</div>
9382
}
9483
</div>
95-
</div>
96-
</td>
97-
98-
<!-- Health Score -->
99-
<td class="py-3 px-3">
100-
<lfx-health-score-tag [score]="foundation.healthScore"></lfx-health-score-tag>
101-
</td>
102-
103-
<!-- Software Value -->
104-
<td class="py-3 px-3">
105-
<div class="text-sm font-medium whitespace-nowrap">
106-
{{ foundation.softwareValueFormatted }}
107-
</div>
108-
</td>
109-
110-
<!-- Total Members -->
111-
<td class="py-3 px-3">
112-
<div class="text-sm font-medium whitespace-nowrap">
113-
{{ foundation.totalMembersFormatted }}
114-
</div>
115-
</td>
84+
}
85+
}
11686

117-
<!-- Active Contributors with Sparkline -->
118-
<td class="py-3 px-3">
119-
<div class="flex items-center gap-2">
120-
<div class="w-[60px] h-6 flex-shrink-0">
121-
<lfx-chart type="line" [data]="foundation.activeContributorsChartData" [options]="sparklineOptions" height="100%"> </lfx-chart>
122-
</div>
123-
<div class="text-sm font-medium whitespace-nowrap">
124-
{{ foundation.activeContributorsAvg }}
125-
</div>
126-
</div>
127-
</td>
87+
<!-- Company Bus Factor (Inline) -->
88+
@case ('bus-factor') {
89+
@if (card.busFactor) {
90+
<div class="flex-1 flex flex-col justify-end pb-2">
91+
<div class="flex gap-0">
92+
<!-- Top Companies Section -->
93+
<div [style.width.%]="card.busFactor.topCompaniesPercentage" class="flex flex-col gap-1">
94+
<div class="text-xs font-medium text-slate-700">
95+
{{ card.busFactor.topCompaniesCount }} Companies ({{ card.busFactor.topCompaniesPercentage }}%)
96+
</div>
97+
<div class="h-4 rounded-l-sm bg-[#0094FF]"></div>
98+
</div>
12899

129-
<!-- Maintainers with Sparkline -->
130-
<td class="py-3 px-3">
131-
<div class="flex items-center gap-2">
132-
<div class="w-[60px] h-6 flex-shrink-0">
133-
<lfx-chart type="line" [data]="foundation.maintainersChartData" [options]="sparklineOptions" height="100%"> </lfx-chart>
134-
</div>
135-
<div class="text-sm font-medium whitespace-nowrap">
136-
{{ foundation.maintainersAvg }}
100+
<!-- Other Companies Section -->
101+
<div [style.width.%]="card.busFactor.otherCompaniesPercentage" class="flex flex-col gap-1">
102+
<div class="text-xs text-slate-600 text-right">{{ card.busFactor.otherCompaniesCount }} Other Companies</div>
103+
<div class="h-4 bg-slate-200 rounded-r-sm"></div>
104+
</div>
105+
</div>
137106
</div>
138-
</div>
139-
</td>
107+
}
108+
}
140109

141-
<!-- Events with Monthly Bar Chart -->
142-
<td class="py-3 px-3">
143-
<div class="flex items-center gap-2">
144-
<div class="w-[80px] flex-shrink-0">
145-
<div class="flex items-end gap-0.5 h-10">
146-
@for (height of foundation.barHeights; track $index) {
147-
<div class="flex-1 bg-[#0094FF] rounded-sm min-w-[3px]" [style.height.%]="height" [attr.data-testid]="'event-bar-' + $index"></div>
110+
<!-- Project Health Scores -->
111+
@case ('health-scores') {
112+
@if (card.healthScores) {
113+
<div class="flex-1 flex flex-col justify-end p-0">
114+
<div class="flex items-end justify-between gap-2 h-16">
115+
@for (item of healthScoreDistribution(); track item.category) {
116+
<div class="flex-1 flex flex-col items-center gap-1">
117+
<div
118+
class="w-full rounded-t transition-all hover:opacity-80"
119+
[style.backgroundColor]="item.color"
120+
[style.height.px]="item.heightPx"></div>
121+
<div class="text-[10px] text-center whitespace-nowrap">
122+
<div class="font-medium">{{ item.category }}</div>
123+
<div class="text-muted-foreground">{{ item.count }}</div>
124+
</div>
125+
</div>
148126
}
149127
</div>
150128
</div>
151-
<div class="text-sm font-medium whitespace-nowrap">
152-
{{ foundation.eventsTotal }}
153-
</div>
154-
</div>
155-
</td>
129+
}
130+
}
131+
}
156132

157-
<!-- Org Dependency Risk with Pie Chart -->
158-
<td class="py-3 px-3">
159-
<div class="flex items-center gap-3">
160-
<div class="relative w-10 h-10 flex-shrink-0">
161-
<svg class="w-10 h-10" viewBox="0 0 40 40">
162-
<!-- Other orgs slice (light grey) -->
163-
<path [attr.d]="foundation.pieChartPaths.otherPath" fill="#E5E7EB" />
164-
<!-- Top orgs slice (risk color) -->
165-
<path [attr.d]="foundation.pieChartPaths.topPath" [attr.fill]="foundation.orgDependencyColor" />
166-
</svg>
167-
</div>
168-
<div class="flex flex-col gap-0.5 min-w-0">
169-
<div class="text-sm font-medium" [ngClass]="foundation.orgDependencyTextColorClass">
170-
{{ foundation.orgDependency.topOrgsCount }} orgs: {{ foundation.orgDependency.topOrgsPercentage }}%
171-
</div>
172-
<div class="text-xs text-gray-500">
173-
{{ foundation.orgDependency.otherOrgsCount }} orgs: {{ foundation.orgDependency.otherOrgsPercentage }}%
174-
</div>
175-
</div>
176-
</div>
177-
</td>
178-
</tr>
179-
}
180-
</tbody>
181-
</table>
133+
<!-- Card Footer (Value and Subtitle) -->
134+
@if (card.value || card.subtitle) {
135+
<div class="space-y-1">
136+
@if (card.value) {
137+
<div class="text-xl font-medium">{{ card.value }}</div>
138+
}
139+
@if (card.subtitle) {
140+
<div class="text-xs text-muted-foreground">{{ card.subtitle }}</div>
141+
}
142+
</div>
143+
}
144+
</div>
145+
</div>
146+
}
182147
</div>
183148
</div>
184149
</section>

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,12 @@
44
:host {
55
display: block;
66
}
7+
8+
.hide-scrollbar {
9+
-ms-overflow-style: none; /* IE and Edge */
10+
scrollbar-width: none; /* Firefox */
11+
12+
&::-webkit-scrollbar {
13+
display: none; /* Chrome, Safari and Opera */
14+
}
15+
}

0 commit comments

Comments
 (0)