Skip to content

Commit 1218bd5

Browse files
authored
feat(dashboards): refactor my-projects to use user contribution data (#196)
- Remove pagination from my-projects component, add empty state - Update analytics service and controller to use LF username - Refactor user service to query USER_PROJECT_CONTRIBUTIONS_DAILY table - Update interfaces with new fields (PROJECT_LOGO, IS_MAINTAINER, AFFILIATION) - Fix board member dashboard available accounts binding (LFXV2-874) - Update RSVP disabled message for legacy meetings (LFXV2-875) - Hide data copilot sparkle icon temporarily (LFXV2-876) LFXV2-873 LFXV2-874 LFXV2-875 LFXV2-876 Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 22776a1 commit 1218bd5

File tree

11 files changed

+109
-105
lines changed

11 files changed

+109
-105
lines changed

apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ <h1>{{ selectedFoundation()?.name }} Overview</h1>
1616
<lfx-select
1717
[form]="form"
1818
control="selectedAccountId"
19-
[options]="availableAccounts()"
19+
[options]="availableAccounts"
2020
optionLabel="accountName"
2121
optionValue="accountId"
2222
[filter]="true"

apps/lfx-one/src/app/modules/dashboards/board-member/board-member-dashboard.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Component, computed, inject, Signal } from '@angular/core';
55
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
66
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
77
import { SelectComponent } from '@components/select/select.component';
8+
import { ACCOUNTS } from '@lfx-one/shared';
89
import { Account, PendingActionItem } from '@lfx-one/shared/interfaces';
910
import { AccountContextService } from '@services/account-context.service';
1011
import { FeatureFlagService } from '@services/feature-flag.service';
@@ -35,7 +36,7 @@ export class BoardMemberDashboardComponent {
3536
selectedAccountId: new FormControl<string>(this.accountContextService.selectedAccount().accountId),
3637
});
3738

38-
public readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);
39+
public readonly availableAccounts = ACCOUNTS;
3940
public readonly selectedFoundation = computed(() => this.projectContextService.selectedFoundation());
4041
public readonly selectedProject = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation());
4142
public readonly refresh$: BehaviorSubject<void> = new BehaviorSubject<void>(undefined);

apps/lfx-one/src/app/modules/dashboards/components/my-projects/my-projects.component.html

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,7 @@ <h2 class="font-display font-semibold text-[16px]">My Projects</h2>
1616
</div>
1717
}
1818
<div class="overflow-x-auto">
19-
<lfx-table
20-
[value]="projects()"
21-
[lazy]="true"
22-
[paginator]="true"
23-
[rows]="rows()"
24-
[totalRecords]="totalRecords()"
25-
[rowsPerPageOptions]="[5, 10, 20, 50]"
26-
[showCurrentPageReport]="true"
27-
[currentPageReportTemplate]="'Showing {first} to {last} of {totalRecords} projects'"
28-
(onLazyLoad)="onPageChange($event)"
29-
data-testid="dashboard-my-projects-table">
19+
<lfx-table [value]="projects()" data-testid="dashboard-my-projects-table">
3020
<ng-template #header>
3121
<tr class="border-b border-border">
3222
<th class="text-left py-2 px-6 text-xs font-medium text-gray-500 w-1/4">Project</th>
@@ -87,6 +77,17 @@ <h2 class="font-display font-semibold text-[16px]">My Projects</h2>
8777
</td>
8878
</tr>
8979
</ng-template>
80+
81+
<ng-template #emptymessage>
82+
<tr>
83+
<td colspan="4" class="text-center py-8" data-testid="dashboard-my-projects-empty">
84+
<div class="flex flex-col items-center gap-2">
85+
<i class="fa-light fa-folder-open text-gray-400 text-3xl"></i>
86+
<span class="text-sm text-gray-500">No projects found</span>
87+
</div>
88+
</td>
89+
</tr>
90+
</ng-template>
9091
</lfx-table>
9192
</div>
9293
</div>

apps/lfx-one/src/app/modules/dashboards/components/my-projects/my-projects.component.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import { toSignal } from '@angular/core/rxjs-interop';
77
import { ChartComponent } from '@components/chart/chart.component';
88
import { TableComponent } from '@components/table/table.component';
99
import { AnalyticsService } from '@services/analytics.service';
10-
import { BehaviorSubject, finalize, switchMap, tap } from 'rxjs';
10+
import { finalize, tap } from 'rxjs';
1111

1212
import type { ChartData, ChartOptions } from 'chart.js';
13-
import type { LazyLoadEvent } from 'primeng/api';
1413
import type { ProjectItemWithCharts } from '@lfx-one/shared/interfaces';
1514
import { hexToRgba, lfxColors } from '@lfx-one/shared';
1615

@@ -23,7 +22,6 @@ import { hexToRgba, lfxColors } from '@lfx-one/shared';
2322
})
2423
export class MyProjectsComponent {
2524
private readonly analyticsService = inject(AnalyticsService);
26-
private readonly paginationState$ = new BehaviorSubject({ page: 1, limit: 10 });
2725
protected readonly loading = signal(true);
2826

2927
public readonly chartOptions: ChartOptions<'line'> = {
@@ -36,12 +34,10 @@ export class MyProjectsComponent {
3634
},
3735
};
3836

39-
public readonly rows = signal(10);
40-
4137
private readonly projectsResponse = toSignal(
42-
this.paginationState$.pipe(
38+
this.analyticsService.getMyProjects().pipe(
4339
tap(() => this.loading.set(true)),
44-
switchMap(({ page, limit }) => this.analyticsService.getMyProjects(page, limit).pipe(finalize(() => this.loading.set(false))))
40+
finalize(() => this.loading.set(false))
4541
),
4642
{
4743
initialValue: { data: [], totalProjects: 0 },
@@ -59,12 +55,6 @@ export class MyProjectsComponent {
5955

6056
public readonly totalRecords = computed(() => this.projectsResponse().totalProjects);
6157

62-
public onPageChange(event: LazyLoadEvent): void {
63-
const page = Math.floor((event.first ?? 0) / (event.rows ?? 10)) + 1;
64-
this.rows.set(event.rows ?? 10);
65-
this.paginationState$.next({ page, limit: event.rows ?? 10 });
66-
}
67-
6858
private createChartData(data: number[], borderColor: string, backgroundColor: string): ChartData<'line'> {
6959
return {
7060
labels: Array.from({ length: data.length }, () => ''),

apps/lfx-one/src/app/modules/meetings/components/meeting-card/meeting-card.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
268268
[showAddButton]="!pastMeeting()"
269269
[additionalRegistrantsCount]="additionalRegistrantsCount()"
270270
[disabled]="isLegacyMeeting()"
271-
disabledMessage="Meetings created outside of LFX One do not have RSVP functionality">
271+
disabledMessage="RSVP functionality will soon be available for all upcoming LFX meetings visible to you">
272272
</lfx-meeting-rsvp-details>
273273
} @else if (!pastMeeting()) {
274274
<!-- Show RSVP Selection for authenticated invited non-organizers (upcoming meetings only) -->
@@ -277,7 +277,7 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
277277
[meeting]="meeting()"
278278
[occurrenceId]="occurrence()?.occurrence_id"
279279
[disabled]="isLegacyMeeting()"
280-
disabledMessage="Meetings created outside of LFX One do not have RSVP functionality">
280+
disabledMessage="RSVP functionality will soon be available for all upcoming LFX meetings visible to you">
281281
</lfx-rsvp-button-group>
282282
} @else if (canRegisterForMeeting()) {
283283
<div class="h-full flex items-center justify-center">

apps/lfx-one/src/app/shared/components/data-copilot/data-copilot.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
styleClass="px-5 py-2.5 bg-white hover:bg-blue-50 text-blue-500 rounded-full font-medium flex items-center gap-2 border border-blue-500 transition-all">
1212
<span class="fa-stack" style="font-size: 0.6em; width: 2em; height: 2em; line-height: 2em">
1313
<i class="fa-light fa-comment fa-stack-2x"></i>
14-
<i class="fa-solid fa-sparkle fa-stack-1x text-yellow-400" style="top: -1em; left: 1.25em"></i>
14+
<!-- <i class="fa-solid fa-sparkle fa-stack-1x text-yellow-400" style="top: -1em; left: 1.25em"></i> -->
1515
</span>
1616
</lfx-button>
1717
}

apps/lfx-one/src/app/shared/services/analytics.service.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,10 @@ export class AnalyticsService {
9494

9595
/**
9696
* Get user's projects with activity data
97-
* @param page - Page number (1-based)
98-
* @param limit - Number of projects per page
9997
* @returns Observable of user projects response
10098
*/
101-
public getMyProjects(page: number = 1, limit: number = 10): Observable<UserProjectsResponse> {
102-
const params = { page: page.toString(), limit: limit.toString() };
103-
return this.http.get<UserProjectsResponse>('/api/analytics/my-projects', { params }).pipe(
99+
public getMyProjects(): Observable<UserProjectsResponse> {
100+
return this.http.get<UserProjectsResponse>('/api/analytics/my-projects').pipe(
104101
catchError((error) => {
105102
console.error('Failed to fetch my projects:', error);
106103
return of({

apps/lfx-one/src/server/controllers/analytics.controller.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Logger } from '../helpers/logger';
88
import { OrganizationService } from '../services/organization.service';
99
import { ProjectService } from '../services/project.service';
1010
import { UserService } from '../services/user.service';
11+
import { getUsernameFromAuth } from '../utils/auth-helper';
1112

1213
/**
1314
* Controller for handling analytics HTTP requests
@@ -116,22 +117,24 @@ export class AnalyticsController {
116117

117118
/**
118119
* GET /api/analytics/my-projects
119-
* Get user's projects with activity data for the last 30 days
120-
* Supports pagination via query parameters: page (default 1) and limit (default 10)
120+
* Get user's projects with activity data
121121
*/
122122
public async getMyProjects(req: Request, res: Response, next: NextFunction): Promise<void> {
123123
const startTime = Logger.start(req, 'get_my_projects');
124124

125125
try {
126-
// Parse pagination parameters
127-
const page = Math.max(1, parseInt(req.query['page'] as string, 10) || 1);
128-
const limit = Math.max(1, Math.min(100, parseInt(req.query['limit'] as string, 10) || 10));
126+
// Get LF username from OIDC context
127+
const lfUsername = await getUsernameFromAuth(req);
129128

130-
const response = await this.userService.getMyProjects(page, limit);
129+
if (!lfUsername) {
130+
throw new AuthenticationError('User username not found in authentication context', {
131+
operation: 'get_my_projects',
132+
});
133+
}
134+
135+
const response = await this.userService.getMyProjects(lfUsername);
131136

132137
Logger.success(req, 'get_my_projects', startTime, {
133-
page,
134-
limit,
135138
returned_projects: response.data.length,
136139
total_projects: response.totalProjects,
137140
});

apps/lfx-one/src/server/services/user.service.ts

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ import {
1111
MeetingRegistrant,
1212
PendingActionItem,
1313
PersonaType,
14-
ProjectCountRow,
1514
ProjectItem,
1615
QueryServiceResponse,
1716
UserCodeCommitsResponse,
1817
UserCodeCommitsRow,
1918
UserMetadata,
2019
UserMetadataUpdateRequest,
2120
UserMetadataUpdateResponse,
22-
UserProjectActivityRow,
21+
UserProjectContributionRow,
2322
UserProjectsResponse,
2423
UserPullRequestsResponse,
2524
UserPullRequestsRow,
@@ -372,88 +371,91 @@ export class UserService {
372371
}
373372

374373
/**
375-
* Get user's projects with activity data for the last 30 days
376-
* @param page - Page number (1-indexed)
377-
* @param limit - Number of projects per page
378-
* @returns Paginated projects with activity data
374+
* Get user's projects with activity data
375+
* Queries USER_PROJECT_CONTRIBUTIONS_DAILY table filtered by LF username
376+
* @param lfUsername - Linux Foundation username from OIDC
377+
* @returns All projects with activity data for the user
379378
*/
380-
public async getMyProjects(page: number, limit: number): Promise<UserProjectsResponse> {
381-
const offset = (page - 1) * limit;
382-
383-
// First, get total count of unique projects
384-
const countQuery = `
385-
SELECT COUNT(DISTINCT PROJECT_ID) as TOTAL_PROJECTS
386-
FROM ANALYTICS.PLATINUM_LFX_ONE.PROJECT_CODE_ACTIVITY
387-
WHERE ACTIVITY_DATE >= DATEADD(DAY, -30, CURRENT_DATE())
388-
`;
389-
390-
const countResult = await this.snowflakeService.execute<ProjectCountRow>(countQuery, []);
391-
const totalProjects = countResult.rows[0]?.TOTAL_PROJECTS || 0;
392-
393-
// If no projects found, return empty response
394-
if (totalProjects === 0) {
395-
return {
396-
data: [],
397-
totalProjects: 0,
398-
};
399-
}
400-
401-
// Get paginated projects with all their activity data
402-
// Use CTE to first get paginated project list, then join for activity data
379+
public async getMyProjects(lfUsername: string): Promise<UserProjectsResponse> {
380+
// Get all projects with their activity data
381+
// Aggregates affiliations per project and sums activities by date
403382
const query = `
404-
WITH PaginatedProjects AS (
405-
SELECT DISTINCT PROJECT_ID, PROJECT_NAME, PROJECT_SLUG
406-
FROM ANALYTICS.PLATINUM_LFX_ONE.PROJECT_CODE_ACTIVITY
407-
WHERE ACTIVITY_DATE >= DATEADD(DAY, -30, CURRENT_DATE())
383+
WITH UserProjects AS (
384+
SELECT PROJECT_ID, PROJECT_NAME, PROJECT_SLUG, PROJECT_LOGO,
385+
MAX(IS_MAINTAINER) AS IS_MAINTAINER
386+
FROM ANALYTICS.PLATINUM_LFX_ONE.USER_PROJECT_CONTRIBUTIONS_DAILY
387+
WHERE LF_USERNAME = ?
388+
GROUP BY PROJECT_ID, PROJECT_NAME, PROJECT_SLUG, PROJECT_LOGO
408389
ORDER BY PROJECT_NAME, PROJECT_ID
409-
LIMIT ? OFFSET ?
390+
),
391+
ProjectAffiliations AS (
392+
SELECT PROJECT_ID, LISTAGG(DISTINCT AFFILIATION, ', ') WITHIN GROUP (ORDER BY AFFILIATION) AS AFFILIATIONS
393+
FROM ANALYTICS.PLATINUM_LFX_ONE.USER_PROJECT_CONTRIBUTIONS_DAILY
394+
WHERE LF_USERNAME = ?
395+
AND AFFILIATION IS NOT NULL
396+
AND AFFILIATION != ''
397+
GROUP BY PROJECT_ID
398+
),
399+
DailyActivities AS (
400+
SELECT PROJECT_ID, ACTIVITY_DATE,
401+
SUM(DAILY_CODE_ACTIVITIES) AS DAILY_CODE_ACTIVITIES,
402+
SUM(DAILY_NON_CODE_ACTIVITIES) AS DAILY_NON_CODE_ACTIVITIES
403+
FROM ANALYTICS.PLATINUM_LFX_ONE.USER_PROJECT_CONTRIBUTIONS_DAILY
404+
WHERE LF_USERNAME = ?
405+
GROUP BY PROJECT_ID, ACTIVITY_DATE
410406
)
411407
SELECT
412408
p.PROJECT_ID,
413409
p.PROJECT_NAME,
414410
p.PROJECT_SLUG,
411+
p.PROJECT_LOGO,
412+
p.IS_MAINTAINER,
413+
COALESCE(pa.AFFILIATIONS, '') AS AFFILIATION,
415414
a.ACTIVITY_DATE,
416-
a.DAILY_TOTAL_ACTIVITIES,
417415
a.DAILY_CODE_ACTIVITIES,
418416
a.DAILY_NON_CODE_ACTIVITIES
419-
FROM PaginatedProjects p
420-
JOIN ANALYTICS.PLATINUM_LFX_ONE.PROJECT_CODE_ACTIVITY a
421-
ON p.PROJECT_ID = a.PROJECT_ID
422-
WHERE a.ACTIVITY_DATE >= DATEADD(DAY, -30, CURRENT_DATE())
417+
FROM UserProjects p
418+
LEFT JOIN ProjectAffiliations pa ON p.PROJECT_ID = pa.PROJECT_ID
419+
LEFT JOIN DailyActivities a ON p.PROJECT_ID = a.PROJECT_ID
423420
ORDER BY p.PROJECT_NAME, p.PROJECT_ID, a.ACTIVITY_DATE ASC
424421
`;
425422

426-
const result = await this.snowflakeService.execute<UserProjectActivityRow>(query, [limit, offset]);
423+
const result = await this.snowflakeService.execute<UserProjectContributionRow>(query, [lfUsername, lfUsername, lfUsername]);
427424

428425
// Group rows by PROJECT_ID and transform into ProjectItem[]
429426
const projectsMap = new Map<string, ProjectItem>();
430427

431428
for (const row of result.rows) {
432429
if (!projectsMap.has(row.PROJECT_ID)) {
433-
// Initialize new project with placeholder values
430+
// Parse affiliations from comma-separated string
431+
const affiliations = row.AFFILIATION ? row.AFFILIATION.split(', ').filter((a) => a.trim()) : [];
432+
433+
// Initialize new project
434434
projectsMap.set(row.PROJECT_ID, {
435435
name: row.PROJECT_NAME,
436-
logo: undefined, // Component will show default icon
437-
role: 'Member', // Placeholder
438-
affiliations: [], // Placeholder
436+
slug: row.PROJECT_SLUG,
437+
logo: row.PROJECT_LOGO || undefined,
438+
role: row.IS_MAINTAINER ? 'Maintainer' : 'Contributor',
439+
affiliations,
439440
codeActivities: [],
440441
nonCodeActivities: [],
441-
status: 'active', // Placeholder
442442
});
443443
}
444444

445-
// Add daily activity values to arrays
446-
const project = projectsMap.get(row.PROJECT_ID)!;
447-
project.codeActivities.push(row.DAILY_CODE_ACTIVITIES);
448-
project.nonCodeActivities.push(row.DAILY_NON_CODE_ACTIVITIES);
445+
// Add daily activity values to arrays (if there's activity data)
446+
if (row.ACTIVITY_DATE) {
447+
const project = projectsMap.get(row.PROJECT_ID)!;
448+
project.codeActivities.push(row.DAILY_CODE_ACTIVITIES || 0);
449+
project.nonCodeActivities.push(row.DAILY_NON_CODE_ACTIVITIES || 0);
450+
}
449451
}
450452

451453
// Convert map to array
452454
const projects = Array.from(projectsMap.values());
453455

454456
return {
455457
data: projects,
456-
totalProjects,
458+
totalProjects: projects.length,
457459
};
458460
}
459461

packages/shared/src/interfaces/analytics-data.interface.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ export interface UserCodeCommitsResponse {
122122
}
123123

124124
/**
125-
* User Project Activity row from Snowflake PROJECT_CODE_ACTIVITY table
126-
* Represents daily project activity data
125+
* User Project Contribution row from Snowflake USER_PROJECT_CONTRIBUTIONS_DAILY table
126+
* Represents daily contribution activity for a user's projects
127127
*/
128-
export interface UserProjectActivityRow {
128+
export interface UserProjectContributionRow {
129129
/**
130130
* Project unique identifier
131131
*/
@@ -141,15 +141,25 @@ export interface UserProjectActivityRow {
141141
*/
142142
PROJECT_SLUG: string;
143143

144+
/**
145+
* Project logo URL
146+
*/
147+
PROJECT_LOGO: string | null;
148+
144149
/**
145150
* Date of the activity (YYYY-MM-DD format)
146151
*/
147152
ACTIVITY_DATE: string;
148153

149154
/**
150-
* Total activities (code + non-code) for this date
155+
* Whether the user is a maintainer of this project
156+
*/
157+
IS_MAINTAINER: boolean;
158+
159+
/**
160+
* User's affiliation/organization name
151161
*/
152-
DAILY_TOTAL_ACTIVITIES: number;
162+
AFFILIATION: string | null;
153163

154164
/**
155165
* Code-related activities for this date

0 commit comments

Comments
 (0)