Skip to content

Commit 53a3893

Browse files
asithadejordaneclaude
authored
feat(dashboards): add board member persona with snowflake integration (#134)
* feat(dashboards): add board member persona with snowflake integration Implement Board Member persona dashboard with three key components: - Organization Involvement with Snowflake integration - Foundation Health metrics - Pending Actions (persona-aware) Backend changes: - Add organization maintainers endpoint - Add membership tier endpoint - Integrate Snowflake queries with proper error handling Frontend changes: - Create board-member-dashboard component - Create organization-involvement component with loading state - Create foundation-health component - Create health-score-tag component - Add persona routing and navigation Shared package updates: - Add OrganizationMaintainersResponse interface - Add MembershipTierResponse interface - Add organization involvement constants - Add foundation health constants - Update persona configuration LFXV2-687 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * feat(dashboard): integrate snowflake analytics for org involvement - Add organization contributors, event attendance, and technical committee endpoints - Implement visual connection status indicators for live vs placeholder data - Update bar charts for Active Contributors and Maintainers with solid fills and rounded corners - Refactor membership tier to use consolidated dashboard table - Create shared color utility functions with validation - Add comprehensive TypeScript interfaces for Snowflake data LFXV2-687 Signed-off-by: Asitha de Silva <[email protected]> * feat(dashboard): add organization selector to board member dashboard Add dynamic organization selection dropdown to board member dashboard that allows users to filter analytics data by different organizations. The dropdown displays 12 predefined organizations and persists the selection to localStorage. Key changes: - Add Account interface and constants with 12 organizations - Create AccountContextService to manage selected organization state - Add organization dropdown to board member dashboard header - Update analytics service methods to accept optional accountId parameter - Modify backend analytics controller to read accountId from query params - Fix reactive data flow using toObservable and RxJS operators - Update membership tier to use correct Snowflake column names (CURRENT_MEMBERSHIP_START_DATE, CURRENT_MEMBERSHIP_END_DATE) The implementation uses Angular signals for reactive state management, automatically refreshing all dashboard metrics when organization changes. Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Jordan Evans <[email protected]> * fix(snowflake): improve connection handling and error logging - Add pool configuration for connection validation (testOnBorrow, evictionRunIntervalMillis, idleTimeoutMillis) - Remove lock TTL buffer to prevent false deduplication hits on first requests - Add comprehensive environment variable validation with clear error messages - Wrap Snowflake SDK errors in MicroserviceError for proper ERROR-level logging - Fix "Unhandled API error" by providing structured error responses Resolves issues: - "Unable to perform operation using terminated connection" errors - False "Query deduplication hit" messages on first-time requests - Missing SNOWFLAKE_USER validation causing unclear error messages - INFO-level logging for errors instead of proper ERROR-level logs LFXV2-687 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix(snowflake): preserve stack traces and use validated env vars - Pass originalError to MicroserviceError constructor to preserve full stack traces - Use validated requiredEnvVars instead of direct process.env access in logging - Ensures consistency between logged values and actual configuration used LFXV2-687 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]> Signed-off-by: Jordan Evans <[email protected]> Co-authored-by: Jordan Evans <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 0df5778 commit 53a3893

35 files changed

+2385
-27
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"PostgreSQL",
2626
"PostgREST",
2727
"primeng",
28+
"sparkline",
2829
"styleclass",
2930
"supabase",
3031
"timegrid",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<div class="container mx-auto px-4 sm:px-6 lg:px-8" data-testid="dashboard-container">
5+
<!-- Organization Selector -->
6+
<div class="mb-6 flex items-center gap-4" data-testid="organization-selector">
7+
<label for="organization-select" class="text-sm font-semibold text-gray-700">Organization:</label>
8+
<lfx-select
9+
[form]="accountForm"
10+
control="selectedAccountId"
11+
[options]="availableAccounts()"
12+
optionLabel="accountName"
13+
optionValue="accountId"
14+
[filter]="true"
15+
filterPlaceholder="Search organizations..."
16+
placeholder="Select an organization"
17+
[showClear]="false"
18+
styleClass="min-w-[300px]"
19+
inputId="organization-select"
20+
data-testid="organization-select"
21+
(onChange)="handleAccountChange($event)" />
22+
</div>
23+
24+
<!-- Dashboard Sections -->
25+
<div class="flex flex-col gap-6" data-testid="dashboard-sections-grid">
26+
<!-- Organization Involvement - Full Width -->
27+
<lfx-organization-involvement />
28+
29+
<!-- Middle Row - Two Cards -->
30+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
31+
<!-- My Meetings -->
32+
<lfx-my-meetings class="h-full" />
33+
34+
<!-- Pending Actions -->
35+
<lfx-pending-actions class="h-full" />
36+
</div>
37+
38+
<!-- Foundation Health - Full Width -->
39+
<lfx-foundation-health />
40+
</div>
41+
</div>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Component, computed, inject, Signal } from '@angular/core';
5+
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
6+
import { Account } from '@lfx-one/shared/interfaces';
7+
import { SelectComponent } from '../../../shared/components/select/select.component';
8+
import { AccountContextService } from '../../../shared/services/account-context.service';
9+
import { FoundationHealthComponent } from '../components/foundation-health/foundation-health.component';
10+
import { MyMeetingsComponent } from '../components/my-meetings/my-meetings.component';
11+
import { OrganizationInvolvementComponent } from '../components/organization-involvement/organization-involvement.component';
12+
import { PendingActionsComponent } from '../components/pending-actions/pending-actions.component';
13+
14+
@Component({
15+
selector: 'lfx-board-member-dashboard',
16+
imports: [OrganizationInvolvementComponent, PendingActionsComponent, MyMeetingsComponent, FoundationHealthComponent, SelectComponent, ReactiveFormsModule],
17+
templateUrl: './board-member-dashboard.component.html',
18+
styleUrl: './board-member-dashboard.component.scss',
19+
})
20+
export class BoardMemberDashboardComponent {
21+
private readonly accountContextService = inject(AccountContextService);
22+
23+
protected readonly accountForm = new FormGroup({
24+
selectedAccountId: new FormControl<string>(this.accountContextService.selectedAccount().accountId),
25+
});
26+
27+
protected readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);
28+
29+
/**
30+
* Handle account selection change
31+
*/
32+
protected handleAccountChange(event: any): void {
33+
const selectedAccountId = event.value as string;
34+
const selectedAccount = this.accountContextService.availableAccounts.find((acc) => acc.accountId === selectedAccountId);
35+
if (selectedAccount) {
36+
this.accountContextService.setAccount(selectedAccount);
37+
}
38+
}
39+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<section data-testid="dashboard-foundation-health-section">
5+
<!-- Header with title and optional View All button -->
6+
<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()) {
9+
<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>
15+
</button>
16+
}
17+
</div>
18+
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" />
82+
</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>
90+
</div>
91+
} @else {
92+
<div class="text-xs text-gray-500">{{ foundation.projectCount }} projects</div>
93+
}
94+
</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>
116+
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>
128+
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 }}
137+
</div>
138+
</div>
139+
</td>
140+
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>
148+
}
149+
</div>
150+
</div>
151+
<div class="text-sm font-medium whitespace-nowrap">
152+
{{ foundation.eventsTotal }}
153+
</div>
154+
</div>
155+
</td>
156+
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>
182+
</div>
183+
</div>
184+
</section>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
:host {
5+
display: block;
6+
}

0 commit comments

Comments
 (0)