+ disabledMessage="RSVP functionality will soon be available for all upcoming LFX meetings visible to you">
} @else if (canRegisterForMeeting()) {
diff --git a/apps/lfx-one/src/app/shared/components/data-copilot/data-copilot.component.html b/apps/lfx-one/src/app/shared/components/data-copilot/data-copilot.component.html
index bacbab57..d4aa0894 100644
--- a/apps/lfx-one/src/app/shared/components/data-copilot/data-copilot.component.html
+++ b/apps/lfx-one/src/app/shared/components/data-copilot/data-copilot.component.html
@@ -11,7 +11,7 @@
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">
-
+
}
diff --git a/apps/lfx-one/src/app/shared/services/analytics.service.ts b/apps/lfx-one/src/app/shared/services/analytics.service.ts
index d7a604a2..d8d3efeb 100644
--- a/apps/lfx-one/src/app/shared/services/analytics.service.ts
+++ b/apps/lfx-one/src/app/shared/services/analytics.service.ts
@@ -94,13 +94,10 @@ export class AnalyticsService {
/**
* Get user's projects with activity data
- * @param page - Page number (1-based)
- * @param limit - Number of projects per page
* @returns Observable of user projects response
*/
- public getMyProjects(page: number = 1, limit: number = 10): Observable
{
- const params = { page: page.toString(), limit: limit.toString() };
- return this.http.get('/api/analytics/my-projects', { params }).pipe(
+ public getMyProjects(): Observable {
+ return this.http.get('/api/analytics/my-projects').pipe(
catchError((error) => {
console.error('Failed to fetch my projects:', error);
return of({
diff --git a/apps/lfx-one/src/server/controllers/analytics.controller.ts b/apps/lfx-one/src/server/controllers/analytics.controller.ts
index 1c164b47..fbc2e007 100644
--- a/apps/lfx-one/src/server/controllers/analytics.controller.ts
+++ b/apps/lfx-one/src/server/controllers/analytics.controller.ts
@@ -8,6 +8,7 @@ import { Logger } from '../helpers/logger';
import { OrganizationService } from '../services/organization.service';
import { ProjectService } from '../services/project.service';
import { UserService } from '../services/user.service';
+import { getUsernameFromAuth } from '../utils/auth-helper';
/**
* Controller for handling analytics HTTP requests
@@ -116,22 +117,24 @@ export class AnalyticsController {
/**
* GET /api/analytics/my-projects
- * Get user's projects with activity data for the last 30 days
- * Supports pagination via query parameters: page (default 1) and limit (default 10)
+ * Get user's projects with activity data
*/
public async getMyProjects(req: Request, res: Response, next: NextFunction): Promise {
const startTime = Logger.start(req, 'get_my_projects');
try {
- // Parse pagination parameters
- const page = Math.max(1, parseInt(req.query['page'] as string, 10) || 1);
- const limit = Math.max(1, Math.min(100, parseInt(req.query['limit'] as string, 10) || 10));
+ // Get LF username from OIDC context
+ const lfUsername = await getUsernameFromAuth(req);
- const response = await this.userService.getMyProjects(page, limit);
+ if (!lfUsername) {
+ throw new AuthenticationError('User username not found in authentication context', {
+ operation: 'get_my_projects',
+ });
+ }
+
+ const response = await this.userService.getMyProjects(lfUsername);
Logger.success(req, 'get_my_projects', startTime, {
- page,
- limit,
returned_projects: response.data.length,
total_projects: response.totalProjects,
});
diff --git a/apps/lfx-one/src/server/services/user.service.ts b/apps/lfx-one/src/server/services/user.service.ts
index 82a70674..e6ab362c 100644
--- a/apps/lfx-one/src/server/services/user.service.ts
+++ b/apps/lfx-one/src/server/services/user.service.ts
@@ -11,7 +11,6 @@ import {
MeetingRegistrant,
PendingActionItem,
PersonaType,
- ProjectCountRow,
ProjectItem,
QueryServiceResponse,
UserCodeCommitsResponse,
@@ -19,7 +18,7 @@ import {
UserMetadata,
UserMetadataUpdateRequest,
UserMetadataUpdateResponse,
- UserProjectActivityRow,
+ UserProjectContributionRow,
UserProjectsResponse,
UserPullRequestsResponse,
UserPullRequestsRow,
@@ -372,80 +371,83 @@ export class UserService {
}
/**
- * Get user's projects with activity data for the last 30 days
- * @param page - Page number (1-indexed)
- * @param limit - Number of projects per page
- * @returns Paginated projects with activity data
+ * Get user's projects with activity data
+ * Queries USER_PROJECT_CONTRIBUTIONS_DAILY table filtered by LF username
+ * @param lfUsername - Linux Foundation username from OIDC
+ * @returns All projects with activity data for the user
*/
- public async getMyProjects(page: number, limit: number): Promise {
- const offset = (page - 1) * limit;
-
- // First, get total count of unique projects
- const countQuery = `
- SELECT COUNT(DISTINCT PROJECT_ID) as TOTAL_PROJECTS
- FROM ANALYTICS.PLATINUM_LFX_ONE.PROJECT_CODE_ACTIVITY
- WHERE ACTIVITY_DATE >= DATEADD(DAY, -30, CURRENT_DATE())
- `;
-
- const countResult = await this.snowflakeService.execute(countQuery, []);
- const totalProjects = countResult.rows[0]?.TOTAL_PROJECTS || 0;
-
- // If no projects found, return empty response
- if (totalProjects === 0) {
- return {
- data: [],
- totalProjects: 0,
- };
- }
-
- // Get paginated projects with all their activity data
- // Use CTE to first get paginated project list, then join for activity data
+ public async getMyProjects(lfUsername: string): Promise {
+ // Get all projects with their activity data
+ // Aggregates affiliations per project and sums activities by date
const query = `
- WITH PaginatedProjects AS (
- SELECT DISTINCT PROJECT_ID, PROJECT_NAME, PROJECT_SLUG
- FROM ANALYTICS.PLATINUM_LFX_ONE.PROJECT_CODE_ACTIVITY
- WHERE ACTIVITY_DATE >= DATEADD(DAY, -30, CURRENT_DATE())
+ WITH UserProjects AS (
+ SELECT PROJECT_ID, PROJECT_NAME, PROJECT_SLUG, PROJECT_LOGO,
+ MAX(IS_MAINTAINER) AS IS_MAINTAINER
+ FROM ANALYTICS.PLATINUM_LFX_ONE.USER_PROJECT_CONTRIBUTIONS_DAILY
+ WHERE LF_USERNAME = ?
+ GROUP BY PROJECT_ID, PROJECT_NAME, PROJECT_SLUG, PROJECT_LOGO
ORDER BY PROJECT_NAME, PROJECT_ID
- LIMIT ? OFFSET ?
+ ),
+ ProjectAffiliations AS (
+ SELECT PROJECT_ID, LISTAGG(DISTINCT AFFILIATION, ', ') WITHIN GROUP (ORDER BY AFFILIATION) AS AFFILIATIONS
+ FROM ANALYTICS.PLATINUM_LFX_ONE.USER_PROJECT_CONTRIBUTIONS_DAILY
+ WHERE LF_USERNAME = ?
+ AND AFFILIATION IS NOT NULL
+ AND AFFILIATION != ''
+ GROUP BY PROJECT_ID
+ ),
+ DailyActivities AS (
+ SELECT PROJECT_ID, ACTIVITY_DATE,
+ SUM(DAILY_CODE_ACTIVITIES) AS DAILY_CODE_ACTIVITIES,
+ SUM(DAILY_NON_CODE_ACTIVITIES) AS DAILY_NON_CODE_ACTIVITIES
+ FROM ANALYTICS.PLATINUM_LFX_ONE.USER_PROJECT_CONTRIBUTIONS_DAILY
+ WHERE LF_USERNAME = ?
+ GROUP BY PROJECT_ID, ACTIVITY_DATE
)
SELECT
p.PROJECT_ID,
p.PROJECT_NAME,
p.PROJECT_SLUG,
+ p.PROJECT_LOGO,
+ p.IS_MAINTAINER,
+ COALESCE(pa.AFFILIATIONS, '') AS AFFILIATION,
a.ACTIVITY_DATE,
- a.DAILY_TOTAL_ACTIVITIES,
a.DAILY_CODE_ACTIVITIES,
a.DAILY_NON_CODE_ACTIVITIES
- FROM PaginatedProjects p
- JOIN ANALYTICS.PLATINUM_LFX_ONE.PROJECT_CODE_ACTIVITY a
- ON p.PROJECT_ID = a.PROJECT_ID
- WHERE a.ACTIVITY_DATE >= DATEADD(DAY, -30, CURRENT_DATE())
+ FROM UserProjects p
+ LEFT JOIN ProjectAffiliations pa ON p.PROJECT_ID = pa.PROJECT_ID
+ LEFT JOIN DailyActivities a ON p.PROJECT_ID = a.PROJECT_ID
ORDER BY p.PROJECT_NAME, p.PROJECT_ID, a.ACTIVITY_DATE ASC
`;
- const result = await this.snowflakeService.execute(query, [limit, offset]);
+ const result = await this.snowflakeService.execute(query, [lfUsername, lfUsername, lfUsername]);
// Group rows by PROJECT_ID and transform into ProjectItem[]
const projectsMap = new Map();
for (const row of result.rows) {
if (!projectsMap.has(row.PROJECT_ID)) {
- // Initialize new project with placeholder values
+ // Parse affiliations from comma-separated string
+ const affiliations = row.AFFILIATION ? row.AFFILIATION.split(', ').filter((a) => a.trim()) : [];
+
+ // Initialize new project
projectsMap.set(row.PROJECT_ID, {
name: row.PROJECT_NAME,
- logo: undefined, // Component will show default icon
- role: 'Member', // Placeholder
- affiliations: [], // Placeholder
+ slug: row.PROJECT_SLUG,
+ logo: row.PROJECT_LOGO || undefined,
+ role: row.IS_MAINTAINER ? 'Maintainer' : 'Contributor',
+ affiliations,
codeActivities: [],
nonCodeActivities: [],
- status: 'active', // Placeholder
});
}
- // Add daily activity values to arrays
- const project = projectsMap.get(row.PROJECT_ID)!;
- project.codeActivities.push(row.DAILY_CODE_ACTIVITIES);
- project.nonCodeActivities.push(row.DAILY_NON_CODE_ACTIVITIES);
+ // Add daily activity values to arrays (if there's activity data)
+ if (row.ACTIVITY_DATE) {
+ const project = projectsMap.get(row.PROJECT_ID)!;
+ project.codeActivities.push(row.DAILY_CODE_ACTIVITIES || 0);
+ project.nonCodeActivities.push(row.DAILY_NON_CODE_ACTIVITIES || 0);
+ }
}
// Convert map to array
@@ -453,7 +455,7 @@ export class UserService {
return {
data: projects,
- totalProjects,
+ totalProjects: projects.length,
};
}
diff --git a/packages/shared/src/interfaces/analytics-data.interface.ts b/packages/shared/src/interfaces/analytics-data.interface.ts
index d0baf189..011b9b74 100644
--- a/packages/shared/src/interfaces/analytics-data.interface.ts
+++ b/packages/shared/src/interfaces/analytics-data.interface.ts
@@ -122,10 +122,10 @@ export interface UserCodeCommitsResponse {
}
/**
- * User Project Activity row from Snowflake PROJECT_CODE_ACTIVITY table
- * Represents daily project activity data
+ * User Project Contribution row from Snowflake USER_PROJECT_CONTRIBUTIONS_DAILY table
+ * Represents daily contribution activity for a user's projects
*/
-export interface UserProjectActivityRow {
+export interface UserProjectContributionRow {
/**
* Project unique identifier
*/
@@ -141,15 +141,25 @@ export interface UserProjectActivityRow {
*/
PROJECT_SLUG: string;
+ /**
+ * Project logo URL
+ */
+ PROJECT_LOGO: string | null;
+
/**
* Date of the activity (YYYY-MM-DD format)
*/
ACTIVITY_DATE: string;
/**
- * Total activities (code + non-code) for this date
+ * Whether the user is a maintainer of this project
+ */
+ IS_MAINTAINER: boolean;
+
+ /**
+ * User's affiliation/organization name
*/
- DAILY_TOTAL_ACTIVITIES: number;
+ AFFILIATION: string | null;
/**
* Code-related activities for this date
diff --git a/packages/shared/src/interfaces/components.interface.ts b/packages/shared/src/interfaces/components.interface.ts
index 7ef03e4e..b13d037b 100644
--- a/packages/shared/src/interfaces/components.interface.ts
+++ b/packages/shared/src/interfaces/components.interface.ts
@@ -415,23 +415,23 @@ export interface MeetingItem {
/**
* Project item for project list
- * @description Structure for project information
+ * @description Structure for project information from USER_PROJECT_CONTRIBUTIONS_DAILY
*/
export interface ProjectItem {
/** Project name */
name: string;
+ /** Project URL slug */
+ slug: string;
/** Project logo URL */
logo?: string;
- /** User's role in project */
- role: string;
- /** User's affiliations */
+ /** User's role in project (Maintainer or Contributor) */
+ role: 'Maintainer' | 'Contributor';
+ /** User's affiliations (comma-separated if multiple) */
affiliations: string[];
- /** Code activity data for chart */
+ /** Code activity data for chart (daily values) */
codeActivities: number[];
- /** Non-code activity data for chart */
+ /** Non-code activity data for chart (daily values) */
nonCodeActivities: number[];
- /** Project status */
- status: 'active' | 'archived';
}
/**