Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,184 +1,149 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<section data-testid="dashboard-foundation-health-section">
<!-- Header with title and optional View All button -->
<section data-testid="foundation-health-section">
<div class="flex items-center justify-between mb-4">
<h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">Foundation Health</h2>
@if (onViewAll()) {
<div class="flex flex-col md:flex-row md:items-center gap-3">
<h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">{{ title() }}</h2>

<!-- Filter Pills -->
<lfx-filter-pills
[options]="filterOptions"
[selectedFilter]="selectedFilter()"
(filterChange)="handleFilterChange($event)"
data-testid="foundation-health-filters"></lfx-filter-pills>
</div>

<!-- Carousel Controls -->
<div class="flex items-center gap-2">
<button
type="button"
(click)="scrollLeft()"
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"
data-testid="foundation-health-carousel-prev"
aria-label="Scroll left">
<i class="fa-light fa-chevron-left"></i>
</button>
<button
class="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100 rounded transition-colors"
(click)="onViewAll()!()"
data-testid="foundation-health-view-all">
View All
<i class="fa-light fa-chevron-right w-4 h-4"></i>
type="button"
(click)="scrollRight()"
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"
data-testid="foundation-health-carousel-next"
aria-label="Scroll right">
<i class="fa-light fa-chevron-right"></i>
</button>
}
</div>
</div>

<!-- Foundation Health Table -->
<div class="bg-white rounded-lg border border-slate-200">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-border">
<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">
Foundation
</th>
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
<div class="flex items-center gap-1">
Health Score
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Overall health score of the foundation"></i>
</div>
</th>
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
<div class="flex items-center gap-1">
Software Value
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Estimated total value of software managed by the foundation"></i>
</div>
</th>
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
<div class="flex items-center gap-1">
Total Members
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Total number of member organizations in the foundation"></i>
</div>
</th>
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
<div class="flex items-center gap-1">
Active Contributors
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Average number of active contributors over the past year"></i>
</div>
</th>
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
<div class="flex items-center gap-1">
Maintainers
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" title="Average number of project maintainers over the past year"></i>
</div>
</th>
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[140px]">
<div class="flex items-center gap-1">
Events
<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>
</div>
</th>
<th class="text-left py-2 px-3 text-xs font-medium text-gray-500 min-w-[180px]">
<div class="flex items-center gap-1">
Org Dependency Risk
<i
class="fa-light fa-circle-question w-3 h-3 cursor-help"
title="Risk level based on concentration of contributions from top organizations"></i>
</div>
</th>
</tr>
</thead>
<tbody>
@for (foundation of foundations(); track foundation.id) {
<tr class="border-b border-border last:border-b-0" [attr.data-testid]="'foundation-row-' + foundation.id">
<!-- Foundation Name Column (Sticky) -->
<td class="sticky left-0 z-10 bg-white py-3 px-6 border-r-2 border-slate-200">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full overflow-hidden flex-shrink-0 bg-white p-1">
<img [src]="foundation.logo" [alt]="foundation.name + ' logo'" class="w-full h-full object-contain" />
<!-- Carousel Container -->
<div class="overflow-hidden">
<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="foundation-health-carousel">
@for (card of metricCards(); track card.title) {
<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">
<div class="flex flex-col h-full justify-between">
<!-- Card Header -->
<div class="flex items-center gap-2">
<i [class]="card.icon + ' w-4 h-4 text-muted-foreground'"></i>
<h5 class="text-sm font-medium">{{ card.title }}</h5>
</div>

<!-- Custom Content -->
@switch (card.customContentType) {
<!-- Sparkline Chart -->
@case ('sparkline') {
@if (card.chartData) {
<div class="mt-3 w-full h-16">
<lfx-chart [type]="'line'" [data]="card.chartData" [options]="sparklineOptions" height="100%"></lfx-chart>
</div>
}
}

<!-- Bar Chart -->
@case ('bar-chart') {
@if (card.chartData) {
<div class="mt-3 flex justify-center">
<div class="w-[200px] h-[60px]">
<lfx-chart [type]="'bar'" [data]="card.chartData" [options]="barChartOptions" height="100%"></lfx-chart>
</div>
</div>
<div class="min-w-0">
<div class="font-medium text-sm text-[#009aff] truncate">{{ foundation.name }}</div>
@if (foundation.projectBreakdown) {
<div class="text-xs text-gray-500">
<div>{{ foundation.projectBreakdown.sandbox }} sandbox</div>
<div>{{ foundation.projectBreakdown.incubating }} incubating</div>
<div>{{ foundation.projectBreakdown.graduated }} graduated</div>
}
}

<!-- Top Projects List -->
@case ('top-projects') {
@if (card.topProjects) {
<div class="space-y-0.5 p-0 pr-5 pl-[60px] m-0 mb-[5px]">
<div class="text-xs font-medium text-muted-foreground">Top 3 Projects by Value</div>
@for (project of card.topProjects; track project.name) {
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ project.name }}</span>
<span class="font-medium">{{ project.formattedValue }}</span>
</div>
} @else {
<div class="text-xs text-gray-500">{{ foundation.projectCount }} projects</div>
}
</div>
</div>
</td>

<!-- Health Score -->
<td class="py-3 px-3">
<lfx-health-score-tag [score]="foundation.healthScore"></lfx-health-score-tag>
</td>

<!-- Software Value -->
<td class="py-3 px-3">
<div class="text-sm font-medium whitespace-nowrap">
{{ foundation.softwareValueFormatted }}
</div>
</td>

<!-- Total Members -->
<td class="py-3 px-3">
<div class="text-sm font-medium whitespace-nowrap">
{{ foundation.totalMembersFormatted }}
</div>
</td>
}
}

<!-- Active Contributors with Sparkline -->
<td class="py-3 px-3">
<div class="flex items-center gap-2">
<div class="w-[60px] h-6 flex-shrink-0">
<lfx-chart type="line" [data]="foundation.activeContributorsChartData" [options]="sparklineOptions" height="100%"> </lfx-chart>
</div>
<div class="text-sm font-medium whitespace-nowrap">
{{ foundation.activeContributorsAvg }}
</div>
</div>
</td>
<!-- Company Bus Factor (Inline) -->
@case ('bus-factor') {
@if (card.busFactor) {
<div class="flex-1 flex flex-col justify-end pb-2">
<div class="flex gap-0">
<!-- Top Companies Section -->
<div [style.width.%]="card.busFactor.topCompaniesPercentage" class="flex flex-col gap-1">
<div class="text-xs font-medium text-slate-700">
{{ card.busFactor.topCompaniesCount }} Companies ({{ card.busFactor.topCompaniesPercentage }}%)
</div>
<div class="h-4 rounded-l-sm bg-[#0094FF]"></div>
</div>

<!-- Maintainers with Sparkline -->
<td class="py-3 px-3">
<div class="flex items-center gap-2">
<div class="w-[60px] h-6 flex-shrink-0">
<lfx-chart type="line" [data]="foundation.maintainersChartData" [options]="sparklineOptions" height="100%"> </lfx-chart>
</div>
<div class="text-sm font-medium whitespace-nowrap">
{{ foundation.maintainersAvg }}
<!-- Other Companies Section -->
<div [style.width.%]="card.busFactor.otherCompaniesPercentage" class="flex flex-col gap-1">
<div class="text-xs text-slate-600 text-right">{{ card.busFactor.otherCompaniesCount }} Other Companies</div>
<div class="h-4 bg-slate-200 rounded-r-sm"></div>
</div>
</div>
</div>
</div>
</td>
}
}

<!-- Events with Monthly Bar Chart -->
<td class="py-3 px-3">
<div class="flex items-center gap-2">
<div class="w-[80px] flex-shrink-0">
<div class="flex items-end gap-0.5 h-10">
@for (height of foundation.barHeights; track $index) {
<div class="flex-1 bg-[#0094FF] rounded-sm min-w-[3px]" [style.height.%]="height" [attr.data-testid]="'event-bar-' + $index"></div>
<!-- Project Health Scores -->
@case ('health-scores') {
@if (card.healthScores) {
<div class="flex-1 flex flex-col justify-end p-0">
<div class="flex items-end justify-between gap-2 h-16">
@for (item of healthScoreDistribution(); track item.category) {
<div class="flex-1 flex flex-col items-center gap-1">
<div
class="w-full rounded-t transition-all hover:opacity-80"
[style.backgroundColor]="item.color"
[style.height.px]="item.heightPx"></div>
<div class="text-[10px] text-center whitespace-nowrap">
<div class="font-medium">{{ item.category }}</div>
<div class="text-muted-foreground">{{ item.count }}</div>
</div>
</div>
}
</div>
</div>
<div class="text-sm font-medium whitespace-nowrap">
{{ foundation.eventsTotal }}
</div>
</div>
</td>
}
}
}

<!-- Org Dependency Risk with Pie Chart -->
<td class="py-3 px-3">
<div class="flex items-center gap-3">
<div class="relative w-10 h-10 flex-shrink-0">
<svg class="w-10 h-10" viewBox="0 0 40 40">
<!-- Other orgs slice (light grey) -->
<path [attr.d]="foundation.pieChartPaths.otherPath" fill="#E5E7EB" />
<!-- Top orgs slice (risk color) -->
<path [attr.d]="foundation.pieChartPaths.topPath" [attr.fill]="foundation.orgDependencyColor" />
</svg>
</div>
<div class="flex flex-col gap-0.5 min-w-0">
<div class="text-sm font-medium" [ngClass]="foundation.orgDependencyTextColorClass">
{{ foundation.orgDependency.topOrgsCount }} orgs: {{ foundation.orgDependency.topOrgsPercentage }}%
</div>
<div class="text-xs text-gray-500">
{{ foundation.orgDependency.otherOrgsCount }} orgs: {{ foundation.orgDependency.otherOrgsPercentage }}%
</div>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
<!-- Card Footer (Value and Subtitle) -->
@if (card.value || card.subtitle) {
<div class="space-y-1">
@if (card.value) {
<div class="text-xl font-medium">{{ card.value }}</div>
}
@if (card.subtitle) {
<div class="text-xs text-muted-foreground">{{ card.subtitle }}</div>
}
</div>
}
</div>
</div>
}
</div>
</div>
</section>
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@
:host {
display: block;
}

.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */

&::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
}
Loading