+
diff --git a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts
index 15634e6cf..5195105d1 100644
--- a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts
+++ b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts
@@ -5,10 +5,11 @@ import { NgClass } from '@angular/common';
import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
+import { LensSwitcherComponent } from '@components/lens-switcher/lens-switcher.component';
import { SidebarComponent } from '@components/sidebar/sidebar.component';
import { environment } from '@environments/environment';
-import { COMMITTEE_LABEL, MAILING_LIST_LABEL, MY_ACTIVITY_LABEL, SURVEY_LABEL, VOTE_LABEL } from '@lfx-one/shared/constants';
-import { isBoardScopedPersona, SidebarMenuItem } from '@lfx-one/shared/interfaces';
+import { COMMITTEE_LABEL, MAILING_LIST_LABEL, SURVEY_LABEL, VOTE_LABEL } from '@lfx-one/shared/constants';
+import { SidebarMenuItem } from '@lfx-one/shared/interfaces';
import { AppService } from '@services/app.service';
import { FeatureFlagService } from '@services/feature-flag.service';
import { PersonaService } from '@services/persona.service';
@@ -19,7 +20,7 @@ import { DevToolbarComponent } from '../dev-toolbar/dev-toolbar.component';
@Component({
selector: 'lfx-main-layout',
- imports: [NgClass, RouterModule, SidebarComponent, DrawerModule, DevToolbarComponent],
+ imports: [NgClass, RouterModule, SidebarComponent, LensSwitcherComponent, DrawerModule, DevToolbarComponent],
templateUrl: './main-layout.component.html',
styleUrl: './main-layout.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -30,112 +31,151 @@ export class MainLayoutComponent {
private readonly featureFlagService = inject(FeatureFlagService);
private readonly personaService = inject(PersonaService);
- // Expose mobile sidebar state from service (writable for two-way binding with p-drawer)
protected readonly showMobileSidebar = this.appService.showMobileSidebar;
+ protected readonly activeLens = this.appService.activeLens;
+ protected readonly projectSelectorOpen = this.appService.projectSelectorOpen;
+ protected readonly showDevToolbar = this.appService.showDevToolbar;
// Feature flags
- private readonly showProjectsInSidebar = this.featureFlagService.getBooleanFlag('sidebar-projects', false);
private readonly enableProfileClick = this.featureFlagService.getBooleanFlag('sidebar-profile', false);
- protected readonly showDevToolbar = this.featureFlagService.getBooleanFlag('dev-toolbar', true);
- // Governance section items - Votes, Surveys, Permissions only
- private readonly governanceSectionItems: SidebarMenuItem[] = [
+ // ─── Me Lens ──────────────────────────────────────────────────────────────
+ private readonly meLensItems: SidebarMenuItem[] = [
{
- label: VOTE_LABEL.plural,
- icon: 'fa-light fa-check-to-slot',
- routerLink: '/votes',
+ label: 'Overview',
+ icon: 'fa-light fa-grid-2',
+ routerLink: '/',
},
{
- label: SURVEY_LABEL.plural,
- icon: 'fa-light fa-clipboard-list',
- routerLink: '/surveys',
+ label: 'My Engagement',
+ isSection: true,
+ expanded: true,
+ items: [
+ { label: 'My Actions', icon: 'fa-light fa-bolt', routerLink: '/me/actions' },
+ { label: 'My Meetings', icon: 'fa-light fa-calendar', routerLink: '/meetings' },
+ { label: `My ${COMMITTEE_LABEL.plural}`, icon: 'fa-light fa-users', routerLink: '/groups' },
+ { label: 'My Events', icon: 'fa-light fa-ticket', routerLink: '/me/events' },
+ ],
},
{
- label: 'Permissions',
- icon: 'fa-light fa-shield',
- routerLink: '/settings',
+ label: 'My Growth',
+ isSection: true,
+ expanded: true,
+ items: [
+ { label: 'Trainings & Certifications', icon: 'fa-light fa-graduation-cap', routerLink: '/me/training' },
+ { label: 'Badges', icon: 'fa-light fa-certificate', routerLink: '/me/badges' },
+ { label: 'EasyCLA', icon: 'fa-light fa-file-signature', routerLink: '/me/easycla' },
+ ],
+ },
+ {
+ label: 'My Account',
+ isSection: true,
+ expanded: true,
+ items: [
+ { label: 'My Profile', icon: 'fa-light fa-user', routerLink: '/profile' },
+ { label: 'Transactions', icon: 'fa-light fa-receipt', routerLink: '/me/transactions' },
+ { label: 'Settings', icon: 'fa-light fa-gear', routerLink: '/settings' },
+ ],
},
];
- // Computed sidebar items based on feature flags and persona
- // Order: Overview, Meetings, Mailing Lists, Groups, Projects, My Activity, Insights, Governance
- protected readonly sidebarItems = computed(() => {
- const items: SidebarMenuItem[] = [];
- const isBoardMember = isBoardScopedPersona(this.personaService.currentPersona());
-
- // Overview (Dashboard)
- items.push({
- label: 'Overview',
- icon: 'fa-light fa-grid-2',
- routerLink: '/',
- });
-
- // Meetings
- items.push({
- label: 'Meetings',
- icon: 'fa-light fa-calendar',
- routerLink: '/meetings',
- });
+ // ─── Foundation Lens ──────────────────────────────────────────────────────
+ private readonly foundationLensItems = computed((): SidebarMenuItem[] => {
+ const isBoardMember = this.personaService.currentPersona() === 'board-member' || this.personaService.currentPersona() === 'executive-director';
+
+ const items: SidebarMenuItem[] = [
+ {
+ label: 'Overview',
+ icon: 'fa-light fa-grid-2',
+ routerLink: '/',
+ },
+ {
+ label: 'Community',
+ isSection: true,
+ expanded: true,
+ items: [
+ { label: 'Projects', icon: 'fa-light fa-folder-open', routerLink: '/foundation/projects' },
+ { label: 'Meetings', icon: 'fa-light fa-calendar', routerLink: '/meetings' },
+ { label: MAILING_LIST_LABEL.plural, icon: 'fa-light fa-envelope', routerLink: '/mailing-lists' },
+ { label: COMMITTEE_LABEL.plural, icon: 'fa-light fa-users', routerLink: '/groups' },
+ { label: 'Events', icon: 'fa-light fa-ticket', routerLink: '/foundation/events' },
+ ],
+ },
+ ];
+
+ if (isBoardMember) {
+ items.push({
+ label: 'Governance',
+ isSection: true,
+ expanded: true,
+ items: [
+ { label: VOTE_LABEL.plural, icon: 'fa-light fa-check-to-slot', routerLink: '/votes' },
+ { label: SURVEY_LABEL.plural, icon: 'fa-light fa-clipboard-list', routerLink: '/surveys' },
+ { label: 'Permissions', icon: 'fa-light fa-shield', routerLink: '/settings' },
+ ],
+ });
+ }
- // Mailing Lists
- items.push({
- label: MAILING_LIST_LABEL.plural,
- icon: 'fa-light fa-envelope',
- routerLink: '/mailing-lists',
- });
+ return items;
+ });
- // Groups
- items.push({
- label: COMMITTEE_LABEL.plural,
- icon: 'fa-light fa-users',
- routerLink: '/groups',
- });
+ // ─── Org Lens ─────────────────────────────────────────────────────────────
+ private readonly orgUserType = this.appService.orgUserType;
- // Projects (conditionally shown based on feature flag)
- if (this.showProjectsInSidebar()) {
- items.push({
- label: 'Projects',
- icon: 'fa-light fa-folder-open',
- routerLink: '/projects',
- });
+ private readonly orgLensItems = computed((): SidebarMenuItem[] => {
+ const isAdmin = this.orgUserType() !== 'employee';
+ const items: SidebarMenuItem[] = [
+ { label: 'Overview', icon: 'fa-light fa-grid-2', routerLink: '/org' },
+ ];
+ if (isAdmin) {
+ items.push({ label: 'Memberships', icon: 'fa-light fa-file-contract', routerLink: '/org/membership' });
+ }
+ items.push(
+ { label: 'Key Projects', icon: 'fa-light fa-folder-bookmark', routerLink: '/org/projects' },
+ { label: 'Employee activities', icon: 'fa-light fa-wave-pulse', routerLink: '/org/code' },
+ );
+
+ const settingsItems: SidebarMenuItem[] = [];
+ if (isAdmin) {
+ settingsItems.push(
+ { label: 'Organization settings', icon: 'fa-light fa-buildings', routerLink: '/org/profile' },
+ { label: 'People', icon: 'fa-light fa-people-group', routerLink: '/org/groups' },
+ );
}
+ settingsItems.push(
+ { label: 'OSPO Resources', icon: 'fa-light fa-book', routerLink: '/org/benefits' },
+ { label: 'Open Source Strategy', icon: 'fa-light fa-compass', routerLink: '/org/strategy' },
+ );
- // My Activity
items.push({
- label: MY_ACTIVITY_LABEL.singular,
- icon: 'fa-light fa-clipboard-list',
- routerLink: '/my-activity',
+ label: 'Settings & Resources',
+ isSection: true,
+ expanded: true,
+ items: settingsItems,
});
- // Insights URL
- items.push({
- label: 'Insights',
- icon: 'fa-light fa-chart-column',
- url: 'https://insights.linuxfoundation.org/',
- target: '_blank',
- rel: 'noopener noreferrer',
- });
+ return items;
+ });
- // Governance section (Votes, Surveys, Permissions) - only for non-board-members
- if (!isBoardMember) {
- items.push({
- label: 'Governance',
- isSection: true,
- expanded: true,
- items: this.governanceSectionItems,
- });
+ // ─── Active nav items based on lens ───────────────────────────────────────
+ protected readonly sidebarItems = computed((): SidebarMenuItem[] => {
+ switch (this.activeLens()) {
+ case 'foundation':
+ return this.foundationLensItems();
+ case 'org':
+ return this.orgLensItems();
+ default:
+ return this.meLensItems;
}
-
- return items;
});
- // Sidebar footer items - matching React NavigationSidebar design
+ // ─── Footer items ─────────────────────────────────────────────────────────
protected readonly sidebarFooterItems = computed(() => [
{
label: 'Profile',
icon: 'fa-light fa-user',
routerLink: '/profile',
- disabled: !this.enableProfileClick(), // Disable when feature flag is false
+ disabled: !this.enableProfileClick(),
},
{
label: 'Support',
@@ -154,7 +194,6 @@ export class MainLayoutComponent {
]);
public constructor() {
- // Close mobile sidebar on navigation
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
@@ -162,9 +201,14 @@ export class MainLayoutComponent {
)
.subscribe(() => {
this.appService.closeMobileSidebar();
+ this.appService.setProjectSelectorOpen(false);
});
}
+ public closeProjectSelector(): void {
+ this.appService.setProjectSelectorOpen(false);
+ }
+
public toggleMobileSidebar(): void {
this.appService.toggleMobileSidebar();
}
diff --git a/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html b/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html
index 9c975308c..5da15b9f1 100644
--- a/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html
+++ b/apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html
@@ -13,7 +13,7 @@
-
{{ committeeLabel.plural }}
+
{{ pageTitle() }}
@if (canCreateGroup()) {
+ this.appService.activeLens() === 'me' ? `My ${COMMITTEE_LABEL.plural}` : COMMITTEE_LABEL.plural
+ );
// ── Writable Signals ──────────────────────────────────────────────────────
public committeesLoading = signal(true);
diff --git a/apps/lfx-one/src/app/modules/committees/components/committee-members/committee-members.component.html b/apps/lfx-one/src/app/modules/committees/components/committee-members/committee-members.component.html
index fdf8b6e52..b1b6e7d2d 100644
--- a/apps/lfx-one/src/app/modules/committees/components/committee-members/committee-members.component.html
+++ b/apps/lfx-one/src/app/modules/committees/components/committee-members/committee-members.component.html
@@ -17,14 +17,7 @@
-
+
diff --git a/apps/lfx-one/src/app/modules/dashboards/dashboard.component.ts b/apps/lfx-one/src/app/modules/dashboards/dashboard.component.ts
index fb5f50552..8cbe0e546 100644
--- a/apps/lfx-one/src/app/modules/dashboards/dashboard.component.ts
+++ b/apps/lfx-one/src/app/modules/dashboards/dashboard.component.ts
@@ -3,6 +3,7 @@
import { Component, computed, inject, Signal } from '@angular/core';
import { PersonaType } from '@lfx-one/shared/interfaces';
+import { AppService } from '@services/app.service';
import { PersonaService } from '@services/persona.service';
import { BoardMemberDashboardComponent } from './board-member/board-member-dashboard.component';
@@ -21,6 +22,9 @@ import { MaintainerDashboardComponent } from './maintainer/maintainer-dashboard.
})
export class DashboardComponent {
private readonly personaService = inject(PersonaService);
+ private readonly appService = inject(AppService);
+
+ protected readonly isMeLens = computed(() => this.appService.activeLens() === 'me');
/**
* Computed signal that determines which dashboard to display
diff --git a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html
index 4656763fc..33750f998 100644
--- a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html
+++ b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.html
@@ -2,8 +2,13 @@
-
- @if (selectedProject()) {
+
+ @if (isMeLens()) {
+
+
My LFX Overview
+
Cross-foundation dashboard — your activity, meetings, and actions across all foundations.
+
+ } @else if (selectedProject()) {
{{ selectedProject()?.name }} Overview
@@ -11,40 +16,6 @@
{{ selectedProject()?.name }} Overview
-
- @defer (on idle) {
-
- } @placeholder {
-
-
-
- @for (i of [1, 2, 3, 4]; track i) {
-
- }
-
-
- }
-
diff --git a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts
index 23d8f8ba9..0a77ab9f9 100644
--- a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts
+++ b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
import { Component, computed, inject, signal } from '@angular/core';
+import { AppService } from '@services/app.service';
import { HiddenActionsService } from '@services/hidden-actions.service';
import { ProjectContextService } from '@services/project-context.service';
import { SkeletonModule } from 'primeng/skeleton';
@@ -21,6 +22,9 @@ import { RecentProgressComponent } from '../components/recent-progress/recent-pr
export class MaintainerDashboardComponent {
private readonly projectContextService = inject(ProjectContextService);
private readonly hiddenActionsService = inject(HiddenActionsService);
+ private readonly appService = inject(AppService);
+
+ protected readonly isMeLens = computed(() => this.appService.activeLens() === 'me');
public readonly selectedProject = computed(() => this.projectContextService.selectedFoundation() || this.projectContextService.selectedProject());
public readonly refresh$: BehaviorSubject
= new BehaviorSubject(undefined);
diff --git a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html
index 3c06a9bc1..f144caffe 100644
--- a/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html
+++ b/apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.html
@@ -5,7 +5,7 @@
-
Meetings
+
{{ pageTitle() }}
@if (canCreateMeeting()) {
this.appService.activeLens() === 'me' ? 'My Meetings' : 'Meetings');
private readonly featureFlagService = inject(FeatureFlagService);
public meetingsLoading: WritableSignal;
diff --git a/apps/lfx-one/src/app/modules/org-lens/benefits/org-benefits.component.html b/apps/lfx-one/src/app/modules/org-lens/benefits/org-benefits.component.html
new file mode 100644
index 000000000..0e4f273cf
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/benefits/org-benefits.component.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
OSPO Resources
+
Guides, tools, and best practices for running an effective Open Source Program Office.
+
+
+
+
+
+ @for (card of cards; track card.title) {
+
+
{{ card.title }}
+
{{ card.description }}
+
+
+ }
+
+
+
+
+
+
Key Documentation
+
+ @for (doc of keyDocs; track doc.title) {
+
+ }
+
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/benefits/org-benefits.component.ts b/apps/lfx-one/src/app/modules/org-lens/benefits/org-benefits.component.ts
new file mode 100644
index 000000000..a59312ee1
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/benefits/org-benefits.component.ts
@@ -0,0 +1,38 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component } from '@angular/core';
+import { ButtonComponent } from '@components/button/button.component';
+
+interface OspoCard {
+ title: string;
+ description: string;
+}
+
+interface KeyDoc {
+ title: string;
+ description: string;
+}
+
+@Component({
+ selector: 'lfx-org-benefits',
+ imports: [ButtonComponent],
+ templateUrl: './org-benefits.component.html',
+})
+export class OrgBenefitsComponent {
+ protected readonly cards: OspoCard[] = [
+ { title: 'OSPO Basics', description: 'Learn the fundamentals of Open Source Program Offices, including governance, legal, and community management.' },
+ { title: 'Contribution Guidelines', description: 'Best practices for contributing to open source projects, licensing, and maintaining community standards.' },
+ { title: 'Compliance & Legal', description: 'Understanding open source licenses, compliance requirements, and legal considerations.' },
+ { title: 'Community Engagement', description: 'Strategies for building and maintaining healthy open source communities.' },
+ { title: 'Metrics & Measurement', description: 'Tools and techniques for measuring open source program impact and success.' },
+ { title: 'Resource Library', description: 'Curated collection of templates, guides, and tools for managing open source initiatives.' },
+ ];
+
+ protected readonly keyDocs: KeyDoc[] = [
+ { title: 'Open Source Program Office Handbook', description: 'Comprehensive guide to establishing and running an OSPO' },
+ { title: 'Open Source Compliance Guide', description: 'Best practices for license compliance and risk management' },
+ { title: 'Community Health Assessment', description: 'Tools to evaluate and improve project community health' },
+ { title: 'Funding & Sponsorship Models', description: 'Different approaches to funding open source projects' },
+ ];
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/cla/org-cla.component.html b/apps/lfx-one/src/app/modules/org-lens/cla/org-cla.component.html
new file mode 100644
index 000000000..0d944d36f
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/cla/org-cla.component.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+
CLA Management
+
Corporate Contributor License Agreements signed on behalf of your organization.
+
+
+
+
+
+
+
1
+
Pending Signature
+
+
+
+
393
+
Covered Contributors
+
+
+
+
+
+
+
+
Action required: 1 expired CLA
+
+ The CCLA for FINOS Legend expired on Nov 30, 2024. Contributors from your org cannot commit to this project until renewed.
+
+
+
+
+
+
+
+
+
+
+ | Project |
+ Foundation |
+ Type |
+ Signed By |
+ Signed Date |
+ Contributors |
+ Status |
+
+
+
+ @for (agreement of agreements; track agreement.project) {
+
+ | {{ agreement.project }} |
+ {{ agreement.foundation }} |
+
+ {{ agreement.type }}
+ |
+ {{ agreement.signedBy }} |
+ {{ agreement.signedDate }} |
+ {{ agreement.contributors }} |
+
+
+ {{ agreement.status }}
+ @if (agreement.status === 'Expired') {
+
+ }
+ @if (agreement.status === 'Pending') {
+
+ }
+
+ |
+
+ }
+
+
+
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/cla/org-cla.component.ts b/apps/lfx-one/src/app/modules/org-lens/cla/org-cla.component.ts
new file mode 100644
index 000000000..1474cab2b
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/cla/org-cla.component.ts
@@ -0,0 +1,100 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component } from '@angular/core';
+import { ButtonComponent } from '@components/button/button.component';
+
+interface ClaAgreement {
+ project: string;
+ foundation: string;
+ type: 'CCLA' | 'ICLA';
+ typeClass: string;
+ signedBy: string;
+ signedDate: string;
+ expiresDate: string | null;
+ status: 'Signed' | 'Pending' | 'Expired';
+ statusClass: string;
+ contributors: number;
+}
+
+@Component({
+ selector: 'lfx-org-cla',
+ imports: [ButtonComponent],
+ templateUrl: './org-cla.component.html',
+})
+export class OrgClaComponent {
+ protected readonly agreements: ClaAgreement[] = [
+ {
+ project: 'Kubernetes',
+ foundation: 'CNCF',
+ type: 'CCLA',
+ typeClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ signedBy: 'Jane Smith',
+ signedDate: 'Jan 12, 2020',
+ expiresDate: null,
+ status: 'Signed',
+ statusClass: 'bg-green-50 text-green-700',
+ contributors: 156,
+ },
+ {
+ project: 'Linux Kernel',
+ foundation: 'Linux Foundation',
+ type: 'CCLA',
+ typeClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ signedBy: 'John Doe',
+ signedDate: 'Mar 5, 2018',
+ expiresDate: null,
+ status: 'Signed',
+ statusClass: 'bg-green-50 text-green-700',
+ contributors: 89,
+ },
+ {
+ project: 'Envoy Proxy',
+ foundation: 'CNCF',
+ type: 'CCLA',
+ typeClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ signedBy: 'Jane Smith',
+ signedDate: 'Jun 22, 2021',
+ expiresDate: null,
+ status: 'Signed',
+ statusClass: 'bg-green-50 text-green-700',
+ contributors: 67,
+ },
+ {
+ project: 'Prometheus',
+ foundation: 'CNCF',
+ type: 'CCLA',
+ typeClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ signedBy: 'Alice Chen',
+ signedDate: 'Feb 14, 2022',
+ expiresDate: null,
+ status: 'Signed',
+ statusClass: 'bg-green-50 text-green-700',
+ contributors: 42,
+ },
+ {
+ project: 'OpenTelemetry',
+ foundation: 'CNCF',
+ type: 'CCLA',
+ typeClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ signedBy: 'Bob Wilson',
+ signedDate: 'Sep 1, 2022',
+ expiresDate: null,
+ status: 'Pending',
+ statusClass: 'bg-amber-50 text-amber-700',
+ contributors: 31,
+ },
+ {
+ project: 'FINOS Legend',
+ foundation: 'FINOS',
+ type: 'CCLA',
+ typeClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ signedBy: 'Jane Smith',
+ signedDate: 'Nov 30, 2022',
+ expiresDate: 'Nov 30, 2024',
+ status: 'Expired',
+ statusClass: 'bg-red-50 text-red-700',
+ contributors: 8,
+ },
+ ];
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/code/org-code.component.html b/apps/lfx-one/src/app/modules/org-lens/code/org-code.component.html
new file mode 100644
index 000000000..a84230ec0
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/code/org-code.component.html
@@ -0,0 +1,331 @@
+
+
+
+
+
+
Employee Activities
+
+
+
+
+
+
+
+
+
+
+ @if (activeTab() === 'code') {
+
+
+
+
+
+
+
+
+
+
+
+
+ @for (type of participantTypes; track type.id) {
+
+ }
+
+ Hide former employees
+
+
+
+
+
+
+
+
+
+ | Name |
+ Highest Type |
+ Activities ↓ |
+ Last Active |
+ Most Active Project |
+
+
+
+ @for (c of contributors; track c.name) {
+
+ |
+
+ |
+ {{ c.highestType }} |
+ {{ c.activities | number }} |
+ {{ c.lastActive }} |
+
+ {{ c.mostActiveProject }}
+ {{ c.mostActiveFoundation }}
+ |
+
+ }
+
+
+
+
+
+
Missing a contributor or need another update?
+
Let us know
+
+
+ }
+
+
+ @if (activeTab() === 'training') {
+
+
+
+
+
+
+
+
+
+
+ {{ trainingStats.trainingCourses }}
+ Training Courses
+
+
+ {{ trainingStats.individualsEnrolled }}
+ Individuals Enrolled in Training
+
+
+ {{ trainingStats.certificationCourses }}
+ Certification Courses
+
+
+ {{ trainingStats.individualsIssued }}
+ Individuals Issued Certifications
+
+
+
+
+
+
+
+
Training & Certifications
+
+
+
+
+
+
+
+
+
+ Include All Available
+
+
+
+
+
+
+
+
+ | Name |
+ Enrollees |
+ Type |
+
+
+
+ @for (course of trainingCourses; track course.name) {
+
+ |
+ {{ course.name }}
+ |
+ {{ course.enrollees }} |
+ {{ course.type }} |
+
+ }
+
+
+
+
+ }
+
+
+ @if (activeTab() === 'events') {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (activeEventsSubTab() === 'overview') {
+
+
+
+
Events
+
Send employees or sponsor events to build your brand
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Dates |
+ My Registrants |
+ Speaking Proposals |
+
+
+
+ @for (e of events; track e.name) {
+
+ |
+ {{ e.name }}
+ {{ e.location }}
+ |
+ {{ e.dates }} |
+
+ {{ e.myRegistrants }}
+ / {{ e.totalRegistrants | number }}
+ |
+
+ @if (e.speakingTotal !== null) {
+ {{ e.speakingProposals }}
+ / {{ e.speakingTotal }}
+ } @else {
+ 0
+ }
+ |
+
+ }
+
+
+
+
+ }
+
+
+ @if (activeEventsSubTab() === 'sponsorships') {
+
+
Sponsorships
+
No sponsorship data available for the selected period.
+
+ }
+
+
+ @if (activeEventsSubTab() === 'travel') {
+
+
Travel Funding
+
No travel funding data available for the selected period.
+
+ }
+ }
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/code/org-code.component.ts b/apps/lfx-one/src/app/modules/org-lens/code/org-code.component.ts
new file mode 100644
index 000000000..18556f5ff
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/code/org-code.component.ts
@@ -0,0 +1,106 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { DecimalPipe } from '@angular/common';
+import { Component, computed, inject, signal } from '@angular/core';
+import { ButtonComponent } from '@components/button/button.component';
+import { AppService } from '@services/app.service';
+
+type ActivityTab = 'code' | 'training' | 'events';
+type ParticipantType = 'all' | 'maintainers' | 'contributors' | 'participants';
+type EventsSubTab = 'overview' | 'sponsorships' | 'travel';
+
+interface ContributorRecord {
+ name: string;
+ initials: string;
+ bgColor: string;
+ textColor: string;
+ highestType: string;
+ activities: number;
+ lastActive: string;
+ mostActiveProject: string;
+ mostActiveFoundation: string;
+}
+
+interface TrainingCourse {
+ name: string;
+ enrollees: number;
+ type: 'Training' | 'Certification';
+}
+
+interface EventRecord {
+ name: string;
+ location: string;
+ dates: string;
+ myRegistrants: number;
+ totalRegistrants: number;
+ speakingProposals: number | null;
+ speakingTotal: number | null;
+}
+
+@Component({
+ selector: 'lfx-org-code',
+ imports: [DecimalPipe, ButtonComponent],
+ templateUrl: './org-code.component.html',
+})
+export class OrgCodeComponent {
+ private readonly appService = inject(AppService);
+
+ protected readonly orgUserType = this.appService.orgUserType;
+ protected readonly isAdmin = computed(() => this.orgUserType() !== 'employee');
+ protected readonly canEdit = computed(() => this.orgUserType() === 'admin-edit' || this.orgUserType() === 'conglomerate-admin');
+
+ protected readonly activeTab = signal('code');
+ protected readonly activeParticipantType = signal('all');
+ protected readonly hideFormerEmployees = signal(false);
+ protected readonly activeEventsSubTab = signal('overview');
+
+ protected readonly participantTypes = [
+ { id: 'all', label: 'All Employees', count: 291 },
+ { id: 'maintainers', label: 'Maintainers', count: 12 },
+ { id: 'contributors', label: 'Contributors', count: 240 },
+ { id: 'participants', label: 'Participants', count: 39 },
+ ];
+
+ protected readonly trainingStats = {
+ trainingCourses: 59,
+ individualsEnrolled: 20,
+ certificationCourses: 23,
+ individualsIssued: 10,
+ };
+
+ protected readonly contributors: ContributorRecord[] = [
+ { name: 'Simon Deziel', initials: 'SD', bgColor: '#E0F2FE', textColor: '#0284C7', highestType: 'Contributor', activities: 15882, lastActive: 'Mar 26, 2026', mostActiveProject: 'Node.js', mostActiveFoundation: 'OpenJS Foundation' },
+ { name: 'Thomas Parrott', initials: 'T', bgColor: '#7C3AED', textColor: '#FFFFFF', highestType: 'Contributor', activities: 9422, lastActive: 'Feb 3, 2026', mostActiveProject: 'Kubernetes', mostActiveFoundation: 'CNCF' },
+ { name: 'dann frazier', initials: 'DF', bgColor: '#FED7AA', textColor: '#9A3412', highestType: 'Contributor', activities: 4308, lastActive: 'Mar 8, 2026', mostActiveProject: 'PyTorch Project', mostActiveFoundation: 'PyTorch Foundation' },
+ { name: 'Simon Richardson', initials: 'SR', bgColor: '#DBEAFE', textColor: '#1D4ED8', highestType: 'Contributor', activities: 4635, lastActive: 'Feb 3, 2026', mostActiveProject: 'Linux Kernel', mostActiveFoundation: 'Linux Foundation' },
+ { name: 'kadinsayani', initials: 'K', bgColor: '#E0E7FF', textColor: '#4338CA', highestType: 'Contributor', activities: 2228, lastActive: 'Mar 27, 2026', mostActiveProject: 'Aether Project', mostActiveFoundation: 'Linux Foundation' },
+ ];
+
+ protected readonly trainingCourses: TrainingCourse[] = [
+ { name: 'Certified Kubernetes Security Specialist (CKS)', enrollees: 9, type: 'Certification' },
+ { name: 'Certified Kubernetes Administrator (CKA)', enrollees: 8, type: 'Certification' },
+ { name: 'Certified Kubernetes Application Developer (CKAD)', enrollees: 8, type: 'Certification' },
+ { name: 'Kubernetes and Cloud Native Security Associate (KCSA)', enrollees: 7, type: 'Certification' },
+ { name: 'Getting Started with OpenTofu (LFEL1009)', enrollees: 7, type: 'Training' },
+ { name: 'Kubernetes and Cloud Native Associate Exam (KCNA)', enrollees: 7, type: 'Certification' },
+ ];
+
+ protected readonly events: EventRecord[] = [
+ { name: 'Open Source in Finance Forum Toronto 2026', location: 'Toronto Canada', dates: 'Apr 14, 2026', myRegistrants: 2, totalRegistrants: 325, speakingProposals: 0, speakingTotal: 1 },
+ { name: 'OpenSearchCon Europe 2026', location: 'Nové Město Czechia', dates: 'Apr 16 - Apr 17, 2026', myRegistrants: 1, totalRegistrants: 250, speakingProposals: null, speakingTotal: null },
+ { name: 'Open Source Summit + ELC North America 2026', location: 'Minneapolis United States', dates: 'May 18 - May 20, 2026', myRegistrants: 1, totalRegistrants: 2000, speakingProposals: 0, speakingTotal: 1 },
+ ];
+
+ protected setTab(tab: ActivityTab): void {
+ this.activeTab.set(tab);
+ }
+
+ protected setParticipantType(type: string): void {
+ this.activeParticipantType.set(type as ParticipantType);
+ }
+
+ protected setEventsSubTab(tab: string): void {
+ this.activeEventsSubTab.set(tab as EventsSubTab);
+ }
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/groups/org-groups.component.html b/apps/lfx-one/src/app/modules/org-lens/groups/org-groups.component.html
new file mode 100644
index 000000000..d80ba916e
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/groups/org-groups.component.html
@@ -0,0 +1,455 @@
+
+
+
+
+
+
People
+
Manage LFX access, board & committee seats, and employee data corrections.
+
+
+
+
+
+
+
+
+
+
+ @if (activeTab() === 'access') {
+
+
+
+
Organization Lens Access Holders
+
+
+
+
+
+ Employees
+
+
{{ accessSummary.employees.total }}
+
with org affiliation
+
+
{{ accessSummary.employees.viewedLens }}
+
viewed company lens past 1 year
+
+
+
+
+
+
+ Admins (Read Only)
+
+
{{ accessSummary.adminReadOnly.total }}
+
view-only access
+
+
{{ accessSummary.adminReadOnly.viewedLens }}
+
viewed company lens past 1 year
+
+
+
+
+
+
+ Admins (Write)
+
+
{{ accessSummary.adminWrite.total }}
+
edit permissions
+
+
{{ accessSummary.adminWrite.viewedLens }}
+
viewed company lens past 1 year
+
+
+
+
+
+
+ Conglomerate
+
+
{{ accessSummary.conglomerate.total }}
+
conglomerate admins
+
+
{{ accessSummary.conglomerate.viewedLens }}
+
viewed company lens past 1 year
+
+
+
+
+
+
+
+
+
Organization Lens Access Holders
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (accessSubTab() === 'employee') {
+
+
+
+
+ | Name |
+ Email |
+ Last Viewed Org Lens |
+ Active Projects |
+ Access Level |
+
+
+
+ @for (u of employeeUsers; track u.email) {
+
+ | {{ u.name }} |
+ {{ u.email }} |
+ {{ u.lastViewedOrgLens }} |
+ {{ u.activeProjects }} |
+
+ Employee
+ |
+
+ }
+
+
+
+
Showing 5 of {{ accessSummary.employees.total }} employees ·
View all
+ }
+
+
+ @if (accessSubTab() === 'admin-view') {
+
+
+
+
+ | Name |
+ Email |
+ Last Viewed Org Lens |
+ Permissions |
+
+
+
+ @for (u of adminViewUsers; track u.email) {
+
+ | {{ u.name }} |
+ {{ u.email }} |
+ {{ u.lastViewedOrgLens }} |
+
+ View Only
+ |
+
+ }
+
+
+
+
Showing {{ adminViewUsers.length }} of {{ accessSummary.adminReadOnly.total }} admins ·
View all
+ }
+
+
+ @if (accessSubTab() === 'admin-edit') {
+
+
+
+
+ | Name |
+ Email |
+ Last Viewed Org Lens |
+ Permissions |
+
+
+
+ @for (u of adminEditUsers; track u.email) {
+
+ | {{ u.name }} |
+ {{ u.email }} |
+ {{ u.lastViewedOrgLens }} |
+
+ Edit
+ |
+
+ }
+
+
+
+
Showing {{ adminEditUsers.length }} of {{ accessSummary.adminWrite.total }} admins ·
View all
+ }
+
+
+ @if (accessSubTab() === 'conglomerate') {
+
+
+
+
+ | Name |
+ Email |
+ Subsidiary |
+ Last Viewed Org Lens |
+
+
+
+ @for (u of conglomerateUsers; track u.email) {
+
+ | {{ u.name }} |
+ {{ u.email }} |
+ {{ u.subsidiary }} |
+ {{ u.lastViewedOrgLens }} |
+
+ }
+
+
+
+ }
+
+ }
+
+
+ @if (activeTab() === 'board') {
+
+
+
+
Board & Committee Members
+
+
+
+
+
+ Board Members
+ {{ boardSummary.boardMembers.vacancies }} vacancy
+
+
{{ boardSummary.boardMembers.total }}
+
across {{ boardSummary.boardMembers.foundations }} foundations
+
+
{{ boardSummary.boardMembers.loggedIn }}
+
logged into LFX self-serve past 1 year
+
+
+
+
+
+
+ Committee Members
+ {{ boardSummary.committeeMembers.openSeats }} open seats
+
+
{{ boardSummary.committeeMembers.total }}
+
across {{ boardSummary.committeeMembers.committees }} committees
+
+
{{ boardSummary.committeeMembers.loggedIn }}
+
logged into LFX self-serve past 1 year
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (boardView() === 'by-foundation') {
+
+ @for (group of foundationGroups(); track group.foundation) {
+
+
+ {{ group.foundation }}
+ {{ group.seats.length }} seats
+
+
+
+
+
+ | Seat |
+ Occupant |
+ Type |
+ @if (canEdit()) {
+ |
+ }
+
+
+
+ @for (seat of group.seats; track seat.committee + seat.representative) {
+
+ | {{ seat.committee }} |
+
+ @if (!seat.vacant) {
+
+ {{ seat.representativeInitials }}
+ {{ seat.representative }}
+
+ } @else {
+ Vacant
+ }
+ |
+
+ {{ seat.type }}
+ |
+ @if (canEdit()) {
+
+ @if (!seat.vacant) {
+
+
+
+
+ } @else {
+
+ }
+ |
+ }
+
+ }
+
+
+
+
+ }
+
+ }
+
+
+ @if (boardView() === 'by-employee') {
+
+ @for (emp of employeeGroups(); track emp.name) {
+
+
+
{{ emp.initials }}
+
+
{{ emp.name }}
+
{{ emp.seats.length }} seat{{ emp.seats.length !== 1 ? 's' : '' }}
+
+
+
+ @for (seat of emp.seats; track seat.committee) {
+
+
+ {{ seat.committee }}
+ · {{ seat.foundation }}
+
+
{{ seat.type }}
+
+ }
+
+
+ }
+
+ }
+ }
+
+
+ @if (activeTab() === 'correct') {
+
+
+ Search for an employee to view their full activity profile and flag incorrect data for review.
+
+
+
+
+ @if (correctionSearched()) {
+
+
+
+
AC
+
+
Alice Chen
+
alice.chen@company.com · Principal Engineer
+
+
+
+
+
+ Code Contributions
+ 187 commits · 3 projects
+
+
+ Training / Certifications
+ 1 in progress
+
+
+ Events Attended
+ 3 events
+
+
+ Board / Committee Seats
+ TOC (CNCF)
+
+
+
+
+
+
Flag Incorrect Data
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+ }
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/groups/org-groups.component.ts b/apps/lfx-one/src/app/modules/org-lens/groups/org-groups.component.ts
new file mode 100644
index 000000000..02b905baa
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/groups/org-groups.component.ts
@@ -0,0 +1,149 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, computed, inject, signal } from '@angular/core';
+import { ButtonComponent } from '@components/button/button.component';
+import { MessageComponent } from '@components/message/message.component';
+import { AppService } from '@services/app.service';
+
+type PeopleTab = 'access' | 'board' | 'correct';
+type BoardView = 'by-foundation' | 'by-employee';
+type AccessSubTab = 'employee' | 'admin-view' | 'admin-edit' | 'conglomerate';
+
+interface EmployeeUser {
+ name: string;
+ email: string;
+ lastViewedOrgLens: string;
+ activeProjects: number;
+ never?: boolean;
+}
+
+interface AdminUser {
+ name: string;
+ email: string;
+ lastViewedOrgLens: string;
+ permissions: string;
+}
+
+interface ConglomerateUser {
+ name: string;
+ email: string;
+ subsidiary: string;
+ lastViewedOrgLens: string;
+}
+
+interface BoardSeat {
+ foundation: string;
+ committee: string;
+ type: 'Board' | 'Committee';
+ representative: string | null;
+ representativeInitials: string | null;
+ vacant: boolean;
+ nextMeeting: string;
+}
+
+@Component({
+ selector: 'lfx-org-groups',
+ imports: [ButtonComponent, MessageComponent],
+ templateUrl: './org-groups.component.html',
+})
+export class OrgGroupsComponent {
+ private readonly appService = inject(AppService);
+
+ protected readonly orgUserType = this.appService.orgUserType;
+ protected readonly isAdmin = computed(() => this.orgUserType() !== 'employee');
+ protected readonly canEdit = computed(() => this.orgUserType() === 'admin-edit' || this.orgUserType() === 'conglomerate-admin');
+
+ protected readonly activeTab = signal('access');
+ protected readonly accessSubTab = signal('employee');
+ protected readonly boardView = signal('by-foundation');
+ protected readonly correctionSearch = signal('');
+ protected readonly correctionSearched = signal(false);
+
+ protected readonly accessSummary = {
+ employees: { total: 234, viewedLens: 198 },
+ adminReadOnly: { total: 14, viewedLens: 12 },
+ adminWrite: { total: 8, viewedLens: 8 },
+ conglomerate: { total: 3, viewedLens: 3 },
+ };
+
+ protected readonly boardSummary = {
+ boardMembers: { total: 18, foundations: 15, loggedIn: 14, vacancies: 1 },
+ committeeMembers: { total: 24, committees: 6, loggedIn: 19, openSeats: 3 },
+ };
+
+ protected readonly employeeUsers: EmployeeUser[] = [
+ { name: 'Sarah Chen', email: 'sarah.chen@canonical.com', lastViewedOrgLens: 'Today', activeProjects: 4 },
+ { name: 'Marcus Rivera', email: 'm.rivera@canonical.com', lastViewedOrgLens: 'Yesterday', activeProjects: 3 },
+ { name: 'Priya Sharma', email: 'priya.sharma@canonical.com', lastViewedOrgLens: 'Mar 24, 2026', activeProjects: 5 },
+ { name: 'James Wu', email: 'james.wu@canonical.com', lastViewedOrgLens: 'Mar 20, 2026', activeProjects: 2 },
+ { name: 'Elena Popov', email: 'elena.popov@canonical.com', lastViewedOrgLens: 'Never', activeProjects: 2, never: true },
+ ];
+
+ protected readonly adminViewUsers: AdminUser[] = [
+ { name: 'Sarah Wilson', email: 'sarah.wilson@canonical.com', lastViewedOrgLens: 'Today', permissions: 'View Only' },
+ { name: 'Mark Davis', email: 'mark.davis@canonical.com', lastViewedOrgLens: '2 days ago', permissions: 'View Only' },
+ ];
+
+ protected readonly adminEditUsers: AdminUser[] = [
+ { name: 'Jennifer Lee', email: 'jennifer.lee@canonical.com', lastViewedOrgLens: 'Today', permissions: 'Edit' },
+ { name: 'Michael Brown', email: 'michael.brown@canonical.com', lastViewedOrgLens: 'Yesterday', permissions: 'Edit' },
+ ];
+
+ protected readonly conglomerateUsers: ConglomerateUser[] = [
+ { name: 'David Kumar', email: 'd.kumar@corporation.io', subsidiary: 'Red Hat', lastViewedOrgLens: 'Today' },
+ { name: 'Emily Zhang', email: 'emily.z@corp.net', subsidiary: 'Google', lastViewedOrgLens: '3 days ago' },
+ { name: 'James Miller', email: 'j.miller@intel.com', subsidiary: 'Intel', lastViewedOrgLens: '1 week ago' },
+ ];
+
+ protected readonly boardSeats: BoardSeat[] = [
+ { foundation: 'Linux Foundation', committee: 'Governing Board', type: 'Board', representative: 'Jennifer Lee', representativeInitials: 'JL', vacant: false, nextMeeting: 'Apr 15, 2026' },
+ { foundation: 'Linux Foundation', committee: 'Governing Board', type: 'Board', representative: 'Michael Brown', representativeInitials: 'MB', vacant: false, nextMeeting: 'Apr 15, 2026' },
+ { foundation: 'Linux Foundation', committee: 'Governing Board', type: 'Board', representative: 'Sarah Wilson', representativeInitials: 'SW', vacant: false, nextMeeting: 'Apr 15, 2026' },
+ { foundation: 'Linux Foundation', committee: 'Governing Board', type: 'Board', representative: 'David Kumar', representativeInitials: 'DK', vacant: false, nextMeeting: 'Apr 15, 2026' },
+ { foundation: 'Linux Foundation', committee: 'Governing Board', type: 'Board', representative: null, representativeInitials: null, vacant: true, nextMeeting: 'Apr 15, 2026' },
+ { foundation: 'CNCF', committee: 'Technical Steering Committee', type: 'Committee', representative: 'Emily Zhang', representativeInitials: 'EZ', vacant: false, nextMeeting: 'Apr 8, 2026' },
+ { foundation: 'CNCF', committee: 'Technical Steering Committee', type: 'Committee', representative: 'James Miller', representativeInitials: 'JM', vacant: false, nextMeeting: 'Apr 8, 2026' },
+ { foundation: 'CNCF', committee: 'Governing Board', type: 'Board', representative: 'Lars Andersen', representativeInitials: 'LA', vacant: false, nextMeeting: 'Apr 20, 2026' },
+ { foundation: 'CNCF', committee: 'Governing Board', type: 'Board', representative: null, representativeInitials: null, vacant: true, nextMeeting: 'Apr 20, 2026' },
+ ];
+
+ protected readonly foundationGroups = computed(() => {
+ const map = new Map();
+ for (const seat of this.boardSeats) {
+ if (!map.has(seat.foundation)) map.set(seat.foundation, []);
+ map.get(seat.foundation)!.push(seat);
+ }
+ return Array.from(map.entries()).map(([foundation, seats]) => ({ foundation, seats }));
+ });
+
+ protected readonly employeeGroups = computed(() => {
+ const map = new Map();
+ for (const seat of this.boardSeats.filter((s) => !s.vacant)) {
+ const key = seat.representative!;
+ if (!map.has(key)) map.set(key, []);
+ map.get(key)!.push(seat);
+ }
+ return Array.from(map.entries()).map(([name, seats]) => ({
+ name,
+ initials: name.split(' ').map((n) => n[0]).join(''),
+ seats,
+ }));
+ });
+
+ protected setTab(tab: PeopleTab): void {
+ this.activeTab.set(tab);
+ }
+
+ protected setAccessSubTab(tab: string): void {
+ this.accessSubTab.set(tab as AccessSubTab);
+ }
+
+ protected setBoardView(view: BoardView): void {
+ this.boardView.set(view);
+ }
+
+ protected searchCorrection(): void {
+ this.correctionSearched.set(true);
+ }
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/membership/org-membership.component.html b/apps/lfx-one/src/app/modules/org-lens/membership/org-membership.component.html
new file mode 100644
index 000000000..118a2af99
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/membership/org-membership.component.html
@@ -0,0 +1,442 @@
+
+
+
+
+
+
+ @if (!selectedMembership()) {
+
+
Membership
+
+
+
+
+
+
+
+
+
+
+ @if (activeListTab() === 'active') {
+
+
+
+
Active Memberships
+
Projects your organization have active membership for
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Membership Tier |
+ Renewal Date |
+ Your Projects |
+ Board Members |
+ Key Contacts |
+
+
+
+ @for (m of filteredActive(); track m.foundation) {
+
+ |
+ {{ m.foundation }}
+ Member since {{ m.memberSince }}
+ |
+
+ {{ m.tier }}
+ {{ m.tierDateRange }}
+ |
+ {{ m.renewalDate }} |
+ {{ m.projects }} |
+ {{ m.boardMembers }} |
+
+
+ {{ m.keyContacts }}/{{ m.keyContactsTotal }}
+
+ |
+
+ }
+
+
+
+
+ }
+
+
+ @if (activeListTab() === 'expired') {
+
+
+
Expired Memberships
+
Previous memberships that have lapsed or were not renewed
+
+
+
+
+
+ | Name |
+ Membership Tier |
+ Expired |
+ Your Projects |
+ Board Members |
+ @if (canEdit()) {
+ |
+ }
+
+
+
+ @for (m of expiredMemberships; track m.foundation) {
+
+ |
+ {{ m.foundation }}
+ Member since {{ m.memberSince }}
+ |
+
+ {{ m.tier }}
+ {{ m.tierDateRange }}
+ |
+
+ {{ m.renewalDate }}
+ |
+ {{ m.projects }} |
+ {{ m.boardMembers }} |
+ @if (canEdit()) {
+
+
+ |
+ }
+
+ }
+
+
+
+
+ }
+
+
+ @if (activeListTab() === 'discover') {
+
+
Foundations relevant to your organization's open source activity that you are not yet a member of.
+ @for (d of discoverMemberships; track d.foundation) {
+
+
+
{{ d.foundation }}
+
{{ d.description }}
+
+
+ {{ d.relevance }}
+
+
+
+ Learn More
+
+
+ }
+
+ }
+ }
+
+
+ @if (selectedMembership(); as m) {
+
+
+
+
+ {{ m.foundation }}
+
+
+
+
+
+
{{ m.foundation }}
+
+ {{ m.tier }}
+ ·
+ Member since {{ m.memberSince }}
+ @if (m.expired) {
+
+ }
+
+
+ @if (canEdit() && !m.expired) {
+
+ }
+ @if (canEdit() && m.expired) {
+
+ }
+
+
+
+
+ @for (tab of [
+ {id: 'docs', label: 'Documentation', icon: 'fa-light fa-file-lines'},
+ {id: 'contacts', label: 'Key Contacts', icon: 'fa-light fa-address-book'},
+ {id: 'board', label: 'Board & Committees', icon: 'fa-light fa-people-roof'},
+ {id: 'onboarding', label: 'Onboarding', icon: 'fa-light fa-list-check'},
+ {id: 'roi', label: 'ROI', icon: 'fa-light fa-chart-line'}
+ ]; track tab.id) {
+
+ }
+
+
+
+ @if (activeDetailTab() === 'docs') {
+
+
+
Membership Agreements
+
+
+
+
Membership Agreement 2024
+
Signed Jan 14, 2024
+
+
+
+
+
+
+
Foundation Charter
+
Uploaded Mar 2022
+
+
+
+ @if (canEdit()) {
+
+ }
+
+
+
Renewal Dates
+
+ Renewal Date
+ {{ m.renewalDate }}
+
+
+ Annual Fee
+ {{ m.annualFee }}
+
+
+ Member Since
+ {{ m.memberSince }}
+
+
+
+ }
+
+
+ @if (activeDetailTab() === 'contacts') {
+
+
+ Key Contact Roles
+ @if (canEdit()) {
+
+ }
+
+
+ @for (c of detailKeyContacts; track c.role) {
+
+ @if (c.filled) {
+
{{ initials(c.name!) }}
+
+
{{ c.name }}
+
{{ c.email }}
+
+
{{ c.role }}
+ @if (canEdit()) {
+
+ }
+ } @else {
+
+
+
+
+
{{ c.role }}
+ @if (canEdit()) {
+
+ }
+ }
+
+ }
+
+
+ }
+
+
+ @if (activeDetailTab() === 'board') {
+
+
+ Board & Committee Seats
+ @if (canEdit()) {
+
+ }
+
+
+
+
+
+ | Committee / Board |
+ Type |
+ Representative |
+ Status |
+
+
+
+ @for (seat of detailBoardSeats; track seat.committee) {
+
+ | {{ seat.committee }} |
+
+ {{ seat.type }}
+ |
+
+ @if (seat.filled) {
+
+ {{ initials(seat.member!) }}
+ {{ seat.member }}
+
+ } @else {
+ Vacant
+ }
+ |
+
+ @if (seat.filled) {
+ Filled
+ } @else {
+ Vacant
+ }
+ |
+
+ }
+
+
+
+
+ }
+
+
+ @if (activeDetailTab() === 'onboarding') {
+
+
+
+ Overall Progress
+ {{ onboardingPercent() }}%
+
+
+
{{ onboardingDoneCount() }} of {{ detailOnboardingSteps.length }} steps complete
+
+
+ @for (category of ['Documentation', 'Key Contacts', 'Board & Committees', 'Communications']; track category) {
+
+
{{ category }}
+
+ @for (step of detailOnboardingSteps; track step.label) {
+ @if (step.category === category) {
+
+ @if (step.done) {
+
+ } @else {
+
+ }
+ {{ step.label }}
+ @if (!step.done && canEdit()) {
+
+ }
+
+ }
+ }
+
+
+ }
+
+ }
+
+
+ @if (activeDetailTab() === 'roi') {
+
+
+
ROI Calculator
+
+
+
+
+ $
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Estimated Benefit Value$1,890,000
+
Total Investment$979,000
+
Net ROI+93%
+
+ @if (canEdit()) {
+
+ }
+
+
+
+
Influence Summary
+
+
Projects Leading8
+
Projects Contributing14
+
Active Maintainers15
+
Annual Commits8,423
+
+
+
+
Goals Progress
+
+ @for (goal of [
+ {label: 'Increase maintainer count to 20', progress: 75},
+ {label: 'Fill all key contact roles', progress: 75},
+ {label: 'Complete onboarding checklist', progress: 92},
+ {label: 'Achieve Leading status on 10 projects', progress: 80}
+ ]; track goal.label) {
+
+
{{ goal.label }}{{ goal.progress }}%
+
+
+ }
+
+
+
+
+ }
+ }
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/membership/org-membership.component.ts b/apps/lfx-one/src/app/modules/org-lens/membership/org-membership.component.ts
new file mode 100644
index 000000000..6410d6a91
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/membership/org-membership.component.ts
@@ -0,0 +1,287 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, computed, inject, signal } from '@angular/core';
+import { ButtonComponent } from '@components/button/button.component';
+import { TagComponent } from '@components/tag/tag.component';
+import { AppService } from '@services/app.service';
+
+type MembershipListTab = 'active' | 'expired' | 'discover';
+type MembershipDetailTab = 'docs' | 'contacts' | 'board' | 'onboarding' | 'roi';
+
+interface Membership {
+ foundation: string;
+ tier: string;
+ tierLabel: string;
+ tierDateRange: string;
+ memberSince: string;
+ renewalDate: string;
+ projects: number;
+ boardMembers: number;
+ keyContacts: number;
+ keyContactsTotal: number;
+ annualFee: string;
+ onboarding: number;
+ expired?: boolean;
+}
+
+interface DiscoverMembership {
+ foundation: string;
+ description: string;
+ relevance: string;
+}
+
+interface KeyContact {
+ role: string;
+ name: string | null;
+ email: string | null;
+ filled: boolean;
+}
+
+interface BoardSeat {
+ committee: string;
+ type: 'Board' | 'Committee';
+ member: string | null;
+ filled: boolean;
+}
+
+interface OnboardingStep {
+ label: string;
+ done: boolean;
+ category: string;
+}
+
+@Component({
+ selector: 'lfx-org-membership',
+ imports: [ButtonComponent, TagComponent],
+ templateUrl: './org-membership.component.html',
+})
+export class OrgMembershipComponent {
+ private readonly appService = inject(AppService);
+
+ protected readonly orgUserType = this.appService.orgUserType;
+ protected readonly isAdmin = computed(() => this.orgUserType() !== 'employee');
+ protected readonly canEdit = computed(() => this.orgUserType() === 'admin-edit' || this.orgUserType() === 'conglomerate-admin');
+
+ protected readonly activeListTab = signal('active');
+ protected readonly selectedMembership = signal(null);
+ protected readonly activeDetailTab = signal('docs');
+ protected readonly searchQuery = signal('');
+
+ protected readonly activeMemberships: Membership[] = [
+ {
+ foundation: 'Academy Software Foundation (ASWF)',
+ tier: 'General Membership',
+ tierLabel: 'General',
+ tierDateRange: 'Jun 30, 2022 – Dec 31, 2026',
+ memberSince: 'Jun 30, 2022',
+ renewalDate: 'Jan 1, 2027',
+ projects: 2,
+ boardMembers: 0,
+ keyContacts: 8,
+ keyContactsTotal: 9,
+ annualFee: '$25,000',
+ onboarding: 60,
+ },
+ {
+ foundation: 'Ceph Foundation',
+ tier: 'Silver Membership',
+ tierLabel: 'Silver',
+ tierDateRange: 'Oct 31, 2018 – Dec 31, 2026',
+ memberSince: 'Oct 31, 2018',
+ renewalDate: 'Jan 1, 2027',
+ projects: 1,
+ boardMembers: 0,
+ keyContacts: 8,
+ keyContactsTotal: 9,
+ annualFee: '$15,000',
+ onboarding: 100,
+ },
+ {
+ foundation: 'Cloud Native Computing Foundation (CNCF)',
+ tier: 'Silver Membership',
+ tierLabel: 'Silver',
+ tierDateRange: 'Dec 31, 2016 – Dec 31, 2026',
+ memberSince: 'Dec 31, 2016',
+ renewalDate: 'Jan 1, 2027',
+ projects: 28,
+ boardMembers: 0,
+ keyContacts: 9,
+ keyContactsTotal: 9,
+ annualFee: '$370,000',
+ onboarding: 100,
+ },
+ {
+ foundation: 'Confidential Computing Consortium (CCC)',
+ tier: 'General Membership',
+ tierLabel: 'General',
+ tierDateRange: 'Dec 31, 2022 – Dec 31, 2026',
+ memberSince: 'Dec 31, 2022',
+ renewalDate: 'Jan 1, 2027',
+ projects: 3,
+ boardMembers: 0,
+ keyContacts: 8,
+ keyContactsTotal: 9,
+ annualFee: '$25,000',
+ onboarding: 85,
+ },
+ {
+ foundation: 'ELISA Fund',
+ tier: 'General Membership',
+ tierLabel: 'General',
+ tierDateRange: 'Feb 29, 2024 – Dec 31, 2026',
+ memberSince: 'Feb 29, 2024',
+ renewalDate: 'Jan 1, 2027',
+ projects: 2,
+ boardMembers: 1,
+ keyContacts: 8,
+ keyContactsTotal: 9,
+ annualFee: '$15,000',
+ onboarding: 90,
+ },
+ {
+ foundation: 'FINOS',
+ tier: 'Silver Membership',
+ tierLabel: 'Silver',
+ tierDateRange: 'Mar 31, 2021 – Dec 31, 2026',
+ memberSince: 'Mar 31, 2021',
+ renewalDate: 'Jan 1, 2027',
+ projects: 1,
+ boardMembers: 0,
+ keyContacts: 7,
+ keyContactsTotal: 9,
+ annualFee: '$50,000',
+ onboarding: 20,
+ },
+ {
+ foundation: 'LF Edge',
+ tier: 'General Membership',
+ tierLabel: 'General',
+ tierDateRange: 'Jan 31, 2019 – Dec 31, 2026',
+ memberSince: 'Jan 31, 2019',
+ renewalDate: 'Jan 1, 2027',
+ projects: 3,
+ boardMembers: 0,
+ keyContacts: 5,
+ keyContactsTotal: 9,
+ annualFee: '$15,000',
+ onboarding: 75,
+ },
+ {
+ foundation: 'LF Networking (LFN)',
+ tier: 'Silver Membership',
+ tierLabel: 'Silver',
+ tierDateRange: 'Dec 31, 2017 – Dec 31, 2026',
+ memberSince: 'Dec 31, 2017',
+ renewalDate: 'Jan 1, 2027',
+ projects: 2,
+ boardMembers: 0,
+ keyContacts: 8,
+ keyContactsTotal: 9,
+ annualFee: '$50,000',
+ onboarding: 95,
+ },
+ ];
+
+ protected readonly expiredMemberships: Membership[] = [
+ {
+ foundation: 'Open Source Security Foundation (OpenSSF)',
+ tier: 'Premier Membership',
+ tierLabel: 'Premier',
+ tierDateRange: 'Jan 1, 2020 – Apr 30, 2025',
+ memberSince: 'Jan 1, 2020',
+ renewalDate: 'Apr 30, 2025',
+ projects: 4,
+ boardMembers: 1,
+ keyContacts: 2,
+ keyContactsTotal: 9,
+ annualFee: '$150,000',
+ onboarding: 100,
+ expired: true,
+ },
+ {
+ foundation: 'Open Mainframe Project',
+ tier: 'Silver Membership',
+ tierLabel: 'Silver',
+ tierDateRange: 'Mar 1, 2019 – Mar 1, 2024',
+ memberSince: 'Mar 1, 2019',
+ renewalDate: 'Mar 1, 2024',
+ projects: 1,
+ boardMembers: 0,
+ keyContacts: 3,
+ keyContactsTotal: 9,
+ annualFee: '$20,000',
+ onboarding: 100,
+ expired: true,
+ },
+ ];
+
+ protected readonly discoverMemberships: DiscoverMembership[] = [
+ { foundation: 'OpenSSF (Open Source Security Foundation)', description: 'Strengthen security of open source software through community-driven best practices and tooling.', relevance: 'Matches your security & cloud-native focus' },
+ { foundation: 'PyTorch Foundation', description: 'Advance open source deep learning and AI frameworks with a strong contributor community.', relevance: 'Matches your AI/ML engineering team activity' },
+ { foundation: 'LF AI & Data Foundation', description: 'Neutral home for AI, ML, and data open source projects under LF governance.', relevance: 'Aligns with your key project portfolio' },
+ { foundation: 'OpenWallet Foundation', description: 'Open standards and open source components for digital wallet development.', relevance: 'New membership opportunity' },
+ ];
+
+ protected readonly filteredActive = computed(() => {
+ const q = this.searchQuery().toLowerCase();
+ if (!q) return this.activeMemberships;
+ return this.activeMemberships.filter((m) => m.foundation.toLowerCase().includes(q));
+ });
+
+ protected readonly detailKeyContacts: KeyContact[] = [
+ { role: 'Primary Contact', name: 'Jane Smith', email: 'jane.smith@company.com', filled: true },
+ { role: 'Technical Contact', name: 'Alice Chen', email: 'alice.chen@company.com', filled: true },
+ { role: 'Billing Contact', name: 'Mark Johnson', email: 'mark.johnson@company.com', filled: true },
+ { role: 'Legal Contact', name: null, email: null, filled: false },
+ ];
+
+ protected readonly detailBoardSeats: BoardSeat[] = [
+ { committee: 'Governing Board', type: 'Board', member: 'Jane Smith', filled: true },
+ { committee: 'TOC (Technical Oversight Committee)', type: 'Committee', member: 'Alice Chen', filled: true },
+ { committee: 'Security TAG', type: 'Committee', member: 'Bob Wilson', filled: true },
+ { committee: 'End User TAG', type: 'Committee', member: null, filled: false },
+ ];
+
+ protected readonly detailOnboardingSteps: OnboardingStep[] = [
+ { label: 'Sign membership agreement', done: true, category: 'Documentation' },
+ { label: 'Upload executed charter', done: true, category: 'Documentation' },
+ { label: 'Set renewal date reminder', done: true, category: 'Documentation' },
+ { label: 'Assign Primary Contact', done: true, category: 'Key Contacts' },
+ { label: 'Assign Technical Contact', done: true, category: 'Key Contacts' },
+ { label: 'Assign Billing Contact', done: true, category: 'Key Contacts' },
+ { label: 'Assign Legal Contact', done: false, category: 'Key Contacts' },
+ { label: 'Assign Governing Board representative', done: true, category: 'Board & Committees' },
+ { label: 'Assign TOC representative', done: true, category: 'Board & Committees' },
+ { label: 'Assign End User TAG representative', done: false, category: 'Board & Committees' },
+ { label: 'Join Slack workspace', done: true, category: 'Communications' },
+ { label: 'Subscribe to foundation mailing list', done: false, category: 'Communications' },
+ ];
+
+ protected readonly onboardingDoneCount = computed(() => this.detailOnboardingSteps.filter((s) => s.done).length);
+
+ protected readonly onboardingPercent = computed(() => {
+ return Math.round((this.onboardingDoneCount() / this.detailOnboardingSteps.length) * 100);
+ });
+
+ protected setListTab(tab: MembershipListTab): void {
+ this.activeListTab.set(tab);
+ }
+
+ protected selectMembership(m: Membership): void {
+ this.selectedMembership.set(m);
+ this.activeDetailTab.set('docs');
+ }
+
+ protected clearMembership(): void {
+ this.selectedMembership.set(null);
+ }
+
+ protected setDetailTab(tab: string): void {
+ this.activeDetailTab.set(tab as MembershipDetailTab);
+ }
+
+ protected initials(name: string): string {
+ return name.split(' ').map((n) => n[0]).join('');
+ }
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/org-lens.routes.ts b/apps/lfx-one/src/app/modules/org-lens/org-lens.routes.ts
new file mode 100644
index 000000000..8734510cc
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/org-lens.routes.ts
@@ -0,0 +1,51 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Routes } from '@angular/router';
+
+export const ORG_LENS_ROUTES: Routes = [
+ {
+ path: '',
+ loadComponent: () => import('./overview/org-overview.component').then((m) => m.OrgOverviewComponent),
+ },
+ {
+ path: 'projects',
+ loadComponent: () => import('./projects/org-projects.component').then((m) => m.OrgProjectsComponent),
+ },
+ {
+ path: 'projects/:slug',
+ loadComponent: () => import('./project-detail/org-project-detail.component').then((m) => m.OrgProjectDetailComponent),
+ },
+ {
+ path: 'code',
+ loadComponent: () => import('./code/org-code.component').then((m) => m.OrgCodeComponent),
+ },
+ {
+ path: 'membership',
+ loadComponent: () => import('./membership/org-membership.component').then((m) => m.OrgMembershipComponent),
+ },
+ {
+ path: 'benefits',
+ loadComponent: () => import('./benefits/org-benefits.component').then((m) => m.OrgBenefitsComponent),
+ },
+ {
+ path: 'strategy',
+ loadComponent: () => import('./strategy/org-strategy.component').then((m) => m.OrgStrategyComponent),
+ },
+ {
+ path: 'groups',
+ loadComponent: () => import('./groups/org-groups.component').then((m) => m.OrgGroupsComponent),
+ },
+ {
+ path: 'cla',
+ loadComponent: () => import('./cla/org-cla.component').then((m) => m.OrgClaComponent),
+ },
+ {
+ path: 'permissions',
+ loadComponent: () => import('./permissions/org-permissions.component').then((m) => m.OrgPermissionsComponent),
+ },
+ {
+ path: 'profile',
+ loadComponent: () => import('./profile/org-profile.component').then((m) => m.OrgProfileComponent),
+ },
+];
diff --git a/apps/lfx-one/src/app/modules/org-lens/overview/org-overview.component.html b/apps/lfx-one/src/app/modules/org-lens/overview/org-overview.component.html
new file mode 100644
index 000000000..17b8cd554
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/overview/org-overview.component.html
@@ -0,0 +1,355 @@
+
+
+
+
+
+
{{ orgName().accountName }} Overview
+ @if (!isAdmin()) {
+
+ Employee view
+
+ }
+
+
+
+
+
Your LF Open Source Involvement
+
+
+
+
Today
+
+
+
{{ summaryStats.today.foundations }}
+
Foundations
+
Active Memberships
+
+ @if (isAdmin()) {
+
+
{{ summaryStats.today.committeeMembers }}
+
Committee Members
+
Within {{ summaryStats.today.withinFoundations }} Foundations
+
+ }
+
+
+
+
Active past 1 year
+
+
+
{{ summaryStats.pastYear.lfProjects }}
+
LF Projects
+
{{ summaryStats.pastYear.nonLfProjects }} Non-LF Projects
+
+
+ {{ summaryStats.pastYear.contributors }}
+ Contributors
+
+
+ {{ summaryStats.pastYear.maintainers }}
+ Maintainers
+
+
+
+
+
+
+
+ @if (isAdmin()) {
+
Data Maintenance
+
+ @for (card of dataMaintenanceCards; track card.label) {
+
+ }
+
+
+
+
+
+
+ @for (item of membershipOnboarding; track item.id) {
+
+
+
+
+ @if (item.status === 'action-needed') {
+
+ } @else {
+
+ }
+
+
+
+ {{ item.name }}
+ @if (item.status === 'action-needed') {
+ {{ item.stepsRemaining }} {{ item.stepsRemaining === 1 ? 'step' : 'steps' }} remaining
+ } @else {
+ Complete
+ }
+
+ @if (item.status === 'action-needed') {
+
Missing: {{ item.missingItems }}
+ } @else {
+
All onboarding steps complete · Member since {{ item.memberSince }}
+ }
+
+
+
+
+
{{ item.progress }}%
+
+
+
+ }
+
+
Showing 4 of 18 memberships · Sorted by action needed first
+
+ }
+
+
+
+
+
+
+
+
+
{{ influenceCounts.leading }}
+
Leading
+
+
+
{{ influenceCounts.contributing }}
+
Contributing
+
+
+
{{ influenceCounts.participating }}
+
Participating
+
+
+
{{ influenceCounts.silent }}
+
Silent
+
+
+
+
+
+
+
+
Most Active Projects
+ @for (project of mostActiveProjects; track project.name; let last = $last) {
+
+
+
{{ project.name }}
+
{{ project.detail }}
+
+
{{ project.level }}
+
+ }
+
+
+
+
+
Most Influential Projects
+ @for (project of mostInfluentialProjects; track project.name; let last = $last) {
+
+
+
{{ project.name }}
+
Ecosystem: {{ project.ecosystemScore }} · Technical: {{ project.technicalScore }}
+
+
{{ project.totalScore }}
+
+ }
+
+
+
+
+
Influence Changes
+
+ @for (change of influenceChanges; track change.name) {
+
+
+ {{ change.name }}
+
+ {{ change.positive ? '↑ +' : '↓ -' }}{{ change.percentChange }}%
+
+
+
+ {{ change.fromLevel }} → {{ change.toLevel }}
+
+
+ Why: {{ change.why }}
+
+
+ }
+
+
+
+
+
+
+
+
+ AI Summary
+
+
+ Your organization holds Leading influence in 8 projects, driven primarily by Sarah Chen (Kubernetes maintainer, 847 commits) and Marcus Rivera (Linux Kernel subsystem lead). Envoy saw the strongest growth this year after Priya Sharma joined as a core reviewer and James Wu increased commit activity by 3x. Conversely, OpenTelemetry influence declined 18% following the departure of Alex Torres, who was the primary contributor — consider backfilling this role to maintain your position. Overall, your organization's technical footprint is strong across CNCF projects but thinning in observability tooling.
+
+
+
+
+
+
+
+
+
+
+
+
{{ employeeActivitySummary.activeProjects }}
+
Active Projects
+
+
+
{{ employeeActivitySummary.commits }}
+
Commits
+
+
+
{{ employeeActivitySummary.meetingsAttended }}
+
Meetings Attended
+
+
+
{{ employeeActivitySummary.activeContributors }}
+
Active Contributors
+
+
+
+
+
+
+
+
Top 10 – Most Commits
+
+
+
+ | # |
+ Name |
+ Commits |
+
+
+
+ @for (contributor of topCommitters; track contributor.rank; let last = $last) {
+
+ | {{ contributor.rank }} |
+ {{ contributor.name }} |
+ {{ contributor.value }} |
+
+ }
+
+
+
+
+
+
+
Top 10 – Most Meetings Attended
+
+
+
+ | # |
+ Name |
+ Meetings |
+
+
+
+ @for (contributor of topMeetingAttendees; track contributor.rank; let last = $last) {
+
+ | {{ contributor.rank }} |
+ {{ contributor.name }} |
+ {{ contributor.value }} |
+
+ }
+
+
+
+
+
+
+
+
+
+ Events
+
+
Send employees or sponsor events to build your brand
+
+
+
+ | Name |
+ Dates |
+ My Registrants |
+ Speaking Proposals |
+
+
+
+ @for (event of events; track event.name) {
+
+ |
+ {{ event.name }}
+ {{ event.location }}
+ |
+
+ {{ event.startDate }}{{ event.endDate ? ' - ' + event.endDate : '' }}
+ |
+
+ {{ event.myRegistrants }}
+ / {{ event.totalRegistrants }}
+ |
+
+ @if (event.speakingTotal !== undefined) {
+ {{ event.speakingProposals }}
+ / {{ event.speakingTotal }}
+ } @else {
+ 0
+ }
+ |
+
+ }
+
+
+
+
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/overview/org-overview.component.ts b/apps/lfx-one/src/app/modules/org-lens/overview/org-overview.component.ts
new file mode 100644
index 000000000..c0441645c
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/overview/org-overview.component.ts
@@ -0,0 +1,276 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, computed, inject } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { AccountContextService } from '@services/account-context.service';
+import { AppService } from '@services/app.service';
+
+interface DataMaintenanceCard {
+ badge: string;
+ badgeClass: string;
+ count: number;
+ label: string;
+ actionLabel: string;
+ actionRoute: string;
+ actionClass: string;
+}
+
+interface MembershipOnboarding {
+ id: string;
+ name: string;
+ status: 'action-needed' | 'complete';
+ stepsRemaining: number;
+ missingItems: string;
+ progress: number;
+ memberSince?: string;
+}
+
+interface MostActiveProject {
+ name: string;
+ detail: string;
+ level: 'Leading' | 'Contributing' | 'Participating' | 'Silent';
+}
+
+interface MostInfluentialProject {
+ name: string;
+ ecosystemScore: string;
+ technicalScore: string;
+ totalScore: number;
+ scoreColor: string;
+}
+
+interface InfluenceChange {
+ name: string;
+ percentChange: number;
+ fromLevel: string;
+ toLevel: string;
+ why: string;
+ positive: boolean;
+}
+
+interface TopContributor {
+ rank: number;
+ name: string;
+ value: number;
+}
+
+interface OrgEvent {
+ name: string;
+ location: string;
+ startDate: string;
+ endDate?: string;
+ myRegistrants: number;
+ totalRegistrants: string;
+ speakingProposals?: number;
+ speakingTotal?: number;
+}
+
+@Component({
+ selector: 'lfx-org-overview',
+ imports: [RouterModule],
+ templateUrl: './org-overview.component.html',
+})
+export class OrgOverviewComponent {
+ private readonly accountContextService = inject(AccountContextService);
+ private readonly appService = inject(AppService);
+
+ protected readonly orgName = this.accountContextService.selectedAccount;
+ protected readonly orgUserType = this.appService.orgUserType;
+ protected readonly isAdmin = computed(() => this.orgUserType() !== 'employee');
+
+ protected readonly summaryStats = {
+ today: { foundations: 18, committeeMembers: 44, withinFoundations: 15 },
+ pastYear: { lfProjects: 75, nonLfProjects: 578, contributors: 287, maintainers: 15 },
+ };
+
+ protected readonly dataMaintenanceCards: DataMaintenanceCard[] = [
+ {
+ badge: 'Action needed',
+ badgeClass: 'bg-orange-50 text-orange-700 border border-orange-200',
+ count: 7,
+ label: 'Key Contacts Unfilled',
+ actionLabel: 'Review Contacts',
+ actionRoute: '/org/membership',
+ actionClass: 'text-orange-700 bg-orange-50 border border-orange-200 hover:bg-orange-100',
+ },
+ {
+ badge: 'Past 1 year',
+ badgeClass: 'bg-green-50 text-green-700 border border-green-200',
+ count: 2,
+ label: 'Seats to Reassign',
+ actionLabel: 'View Details',
+ actionRoute: '/org/membership',
+ actionClass: 'text-slate-600 bg-slate-50 border border-slate-200 hover:bg-slate-100',
+ },
+ {
+ badge: '1 vacancy',
+ badgeClass: 'bg-green-50 text-green-700 border border-green-200',
+ count: 18,
+ label: 'Board Members',
+ actionLabel: 'Fill Vacancy',
+ actionRoute: '/org/groups',
+ actionClass: 'text-blue-700 bg-blue-50 border border-blue-200 hover:bg-blue-100',
+ },
+ {
+ badge: '3 open seats',
+ badgeClass: 'bg-green-50 text-green-700 border border-green-200',
+ count: 24,
+ label: 'Committee Members',
+ actionLabel: 'Fill Seats',
+ actionRoute: '/org/groups',
+ actionClass: 'text-blue-700 bg-blue-50 border border-blue-200 hover:bg-blue-100',
+ },
+ ];
+
+ protected readonly membershipOnboarding: MembershipOnboarding[] = [
+ {
+ id: 'aswf',
+ name: 'Academy Software Foundation (ASWF)',
+ status: 'action-needed',
+ stepsRemaining: 3,
+ missingItems: 'Board seat unfilled · 2 key contacts needed · Mailing list not joined',
+ progress: 60,
+ },
+ {
+ id: 'ccc',
+ name: 'Confidential Computing Consortium (CCC)',
+ status: 'action-needed',
+ stepsRemaining: 1,
+ missingItems: 'Primary technical contact not assigned',
+ progress: 85,
+ },
+ {
+ id: 'cncf',
+ name: 'Cloud Native Computing Foundation (CNCF)',
+ status: 'complete',
+ stepsRemaining: 0,
+ missingItems: '',
+ progress: 100,
+ memberSince: 'Dec 2016',
+ },
+ {
+ id: 'ceph',
+ name: 'Ceph Foundation',
+ status: 'complete',
+ stepsRemaining: 0,
+ missingItems: '',
+ progress: 100,
+ memberSince: 'Oct 2018',
+ },
+ ];
+
+ // ─── Key Projects ─────────────────────────────────────────────────────────
+ protected readonly influenceCounts = { leading: 8, contributing: 15, participating: 18, silent: 42 };
+
+ protected readonly mostActiveProjects: MostActiveProject[] = [
+ { name: 'Kubernetes', detail: '2,847 commits · 156 contributors', level: 'Leading' },
+ { name: 'Linux Kernel', detail: '1,923 commits · 243 contributors', level: 'Leading' },
+ { name: 'Envoy', detail: '1,456 commits · 98 contributors', level: 'Contributing' },
+ ];
+
+ protected readonly mostInfluentialProjects: MostInfluentialProject[] = [
+ { name: 'Kubernetes', ecosystemScore: '24/27', technicalScore: '11/12', totalScore: 35, scoreColor: 'text-green-700' },
+ { name: 'Linux Kernel', ecosystemScore: '22/27', technicalScore: '10/12', totalScore: 32, scoreColor: 'text-green-700' },
+ { name: 'Prometheus', ecosystemScore: '20/27', technicalScore: '9/12', totalScore: 29, scoreColor: 'text-blue-700' },
+ ];
+
+ protected readonly influenceChanges: InfluenceChange[] = [
+ {
+ name: 'Envoy',
+ percentChange: 34,
+ fromLevel: 'Participating',
+ toLevel: 'Contributing',
+ why: '+3 new code contributors (Priya Sharma, James Wu, Mei Lin) · Commits up 210% · 2 employees accepted as conference speakers · Priya Sharma promoted to core reviewer',
+ positive: true,
+ },
+ {
+ name: 'OpenTelemetry',
+ percentChange: 18,
+ fromLevel: 'Contributing',
+ toLevel: 'Participating',
+ why: 'Alex Torres (primary contributor) left the company · Commits down 62% · Meeting attendance dropped from 18 to 4 · No event speakers submitted this year',
+ positive: false,
+ },
+ ];
+
+ // ─── Employee Activities Summary ──────────────────────────────────────────
+ protected readonly employeeActivitySummary = {
+ activeProjects: 75,
+ commits: '2,847',
+ meetingsAttended: 127,
+ activeContributors: 156,
+ };
+
+ protected readonly topCommitters: TopContributor[] = [
+ { rank: 1, name: 'Sarah Chen', value: 847 },
+ { rank: 2, name: 'Marcus Rivera', value: 623 },
+ { rank: 3, name: 'Priya Sharma', value: 412 },
+ { rank: 4, name: 'James Wu', value: 389 },
+ { rank: 5, name: 'Elena Popov', value: 287 },
+ { rank: 6, name: 'David Kim', value: 234 },
+ { rank: 7, name: 'Aisha Okafor', value: 198 },
+ { rank: 8, name: 'Lars Andersen', value: 176 },
+ { rank: 9, name: 'Mei Lin', value: 154 },
+ { rank: 10, name: 'Tom Bradley', value: 132 },
+ ];
+
+ protected readonly topMeetingAttendees: TopContributor[] = [
+ { rank: 1, name: 'Marcus Rivera', value: 48 },
+ { rank: 2, name: 'Sarah Chen', value: 42 },
+ { rank: 3, name: 'Elena Popov', value: 36 },
+ { rank: 4, name: 'David Kim', value: 31 },
+ { rank: 5, name: 'James Wu', value: 28 },
+ { rank: 6, name: 'Priya Sharma', value: 24 },
+ { rank: 7, name: 'Aisha Okafor', value: 21 },
+ { rank: 8, name: 'Tom Bradley', value: 18 },
+ { rank: 9, name: 'Lars Andersen', value: 15 },
+ { rank: 10, name: 'Mei Lin', value: 12 },
+ ];
+
+ // ─── Events ───────────────────────────────────────────────────────────────
+ protected readonly events: OrgEvent[] = [
+ {
+ name: 'Open Source in Finance Forum Toronto 2026',
+ location: 'Toronto Canada',
+ startDate: 'Apr 14, 2026',
+ myRegistrants: 2,
+ totalRegistrants: '325',
+ speakingProposals: 0,
+ speakingTotal: 1,
+ },
+ {
+ name: 'OpenSearchCon Europe 2026',
+ location: 'Nové Město Czechia',
+ startDate: 'Apr 16, 2026',
+ endDate: 'Apr 17, 2026',
+ myRegistrants: 1,
+ totalRegistrants: '250',
+ },
+ {
+ name: 'Open Source Summit + Embedded Linux Conference North America 2026',
+ location: 'Minneapolis United States',
+ startDate: 'May 18, 2026',
+ endDate: 'May 20, 2026',
+ myRegistrants: 1,
+ totalRegistrants: '2,000',
+ speakingProposals: 0,
+ speakingTotal: 1,
+ },
+ ];
+
+ // ─── Helpers ──────────────────────────────────────────────────────────────
+ protected levelClass(level: string): string {
+ switch (level) {
+ case 'Leading':
+ return 'bg-green-50 text-green-700 border border-green-200';
+ case 'Contributing':
+ return 'bg-blue-50 text-blue-700 border border-blue-200';
+ case 'Participating':
+ return 'bg-amber-50 text-amber-700 border border-amber-200';
+ default:
+ return 'bg-slate-100 text-slate-500';
+ }
+ }
+
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/permissions/org-permissions.component.html b/apps/lfx-one/src/app/modules/org-lens/permissions/org-permissions.component.html
new file mode 100644
index 000000000..e39022931
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/permissions/org-permissions.component.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
Access & Permissions
+
Manage who in your organization can access and administer LFX data.
+
+
+
+
+
+
+
+
{{ users.length }}
+
Total Users
+
+
+
+
+
18
+
Foundations Covered
+
+
+
+
+
+
+
+
+
+
+ | User |
+ Role |
+ Access |
+ Last Active |
+ Status |
+
+
+
+ @for (user of users; track user.email) {
+
+
+
+
+ {{ user.avatarInitials }}
+
+
+ {{ user.name }}
+ {{ user.email }}
+
+
+ |
+
+ {{ user.role }}
+ |
+
+
+ @for (a of user.access; track a) {
+ {{ a }}
+ }
+
+ |
+ {{ user.lastActive }} |
+
+
+ {{ user.status }}
+
+
+ |
+
+ }
+
+
+
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/permissions/org-permissions.component.ts b/apps/lfx-one/src/app/modules/org-lens/permissions/org-permissions.component.ts
new file mode 100644
index 000000000..f218c6548
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/permissions/org-permissions.component.ts
@@ -0,0 +1,91 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component } from '@angular/core';
+
+interface OrgUser {
+ name: string;
+ email: string;
+ avatarInitials: string;
+ role: 'Admin' | 'Manager' | 'Contributor' | 'Viewer';
+ roleClass: string;
+ access: string[];
+ lastActive: string;
+ status: 'Active' | 'Pending';
+ statusClass: string;
+}
+
+@Component({
+ selector: 'lfx-org-permissions',
+ templateUrl: './org-permissions.component.html',
+})
+export class OrgPermissionsComponent {
+ protected readonly users: OrgUser[] = [
+ {
+ name: 'Jane Smith',
+ email: 'jane.smith@company.com',
+ avatarInitials: 'JS',
+ role: 'Admin',
+ roleClass: 'bg-purple-50 text-purple-700 border border-purple-200',
+ access: ['All Foundations', 'CLA Signing', 'Seat Management'],
+ lastActive: 'Today',
+ status: 'Active',
+ statusClass: 'bg-green-50 text-green-700',
+ },
+ {
+ name: 'John Doe',
+ email: 'john.doe@company.com',
+ avatarInitials: 'JD',
+ role: 'Admin',
+ roleClass: 'bg-purple-50 text-purple-700 border border-purple-200',
+ access: ['All Foundations', 'CLA Signing', 'Seat Management'],
+ lastActive: 'Yesterday',
+ status: 'Active',
+ statusClass: 'bg-green-50 text-green-700',
+ },
+ {
+ name: 'Alice Chen',
+ email: 'alice.chen@company.com',
+ avatarInitials: 'AC',
+ role: 'Manager',
+ roleClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ access: ['CNCF', 'OpenSSF'],
+ lastActive: '3 days ago',
+ status: 'Active',
+ statusClass: 'bg-green-50 text-green-700',
+ },
+ {
+ name: 'Bob Wilson',
+ email: 'bob.wilson@company.com',
+ avatarInitials: 'BW',
+ role: 'Contributor',
+ roleClass: 'bg-teal-50 text-teal-700 border border-teal-200',
+ access: ['CNCF'],
+ lastActive: '1 week ago',
+ status: 'Active',
+ statusClass: 'bg-green-50 text-green-700',
+ },
+ {
+ name: 'Carol Martinez',
+ email: 'carol.martinez@company.com',
+ avatarInitials: 'CM',
+ role: 'Viewer',
+ roleClass: 'bg-slate-50 text-slate-600 border border-slate-200',
+ access: ['All Foundations (read-only)'],
+ lastActive: '2 weeks ago',
+ status: 'Active',
+ statusClass: 'bg-green-50 text-green-700',
+ },
+ {
+ name: 'David Park',
+ email: 'david.park@company.com',
+ avatarInitials: 'DP',
+ role: 'Manager',
+ roleClass: 'bg-blue-50 text-blue-700 border border-blue-200',
+ access: ['Linux Foundation', 'ASWF'],
+ lastActive: 'Never',
+ status: 'Pending',
+ statusClass: 'bg-amber-50 text-amber-700',
+ },
+ ];
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/profile/org-profile.component.html b/apps/lfx-one/src/app/modules/org-lens/profile/org-profile.component.html
new file mode 100644
index 000000000..58044895a
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/profile/org-profile.component.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
Organization settings
+
Manage your organization's profile, domains, and structure across LF foundations.
+
+
+
+
+
Company Profile
+
+
+
+

+
LF
+
UID: {{ profile.uid.slice(0, 18) }}...
+
+
+
+
+
Name
+
{{ profile.name }}
+
+
+
Founded
+
{{ profile.founded }}
+
+
+
+
Description
+
{{ profile.description }}
+
+
+
+
+
+
+
+
Organization Structure & Domains
+
+
+
+
Conglomerate Structure
+
+
+ {{ profile.name }}
+
+
+
+
+
Primary Organization
+
{{ profile.name }} (HQ)
+
+
+
Related Entities
+
{{ linkedOrgNames }}
+
+
+
Employee Count
+
234 employees globally
+
+
+
+
+
+
+
+
Email Domains
+
+
+
+
+ | Domain |
+ Primary Use |
+ Employee Count |
+ Status |
+
+
+
+ @for (d of domains; track d.domain) {
+
+ | {{ d.domain }} |
+ {{ d.primaryUse }} |
+ {{ d.employeeCount }} |
+
+
+ {{ d.status }}
+
+ |
+
+ }
+
+
+
+
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/profile/org-profile.component.ts b/apps/lfx-one/src/app/modules/org-lens/profile/org-profile.component.ts
new file mode 100644
index 000000000..fba136804
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/profile/org-profile.component.ts
@@ -0,0 +1,53 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, computed, inject } from '@angular/core';
+import { AppService } from '@services/app.service';
+
+interface Domain {
+ domain: string;
+ primaryUse: string;
+ employeeCount: number;
+ status: 'Active' | 'Shared';
+}
+
+interface LinkedOrg {
+ name: string;
+ relationship: string;
+}
+
+@Component({
+ selector: 'lfx-org-profile',
+ templateUrl: './org-profile.component.html',
+})
+export class OrgProfileComponent {
+ private readonly appService = inject(AppService);
+
+ protected readonly orgUserType = this.appService.orgUserType;
+ protected readonly canEdit = computed(() => this.orgUserType() === 'admin-edit' || this.orgUserType() === 'conglomerate-admin');
+
+ protected readonly profile = {
+ uid: '451efe4e-9322-4b58-97f5-c8e57b5b99f4',
+ name: 'The Linux Foundation',
+ founded: 'October 2, 2002',
+ website: 'linuxfoundation.org',
+ description: 'The Linux Foundation is the nonprofit consortium dedicated to fostering the growth of Linux and collaborative software development. Founded in 2002, we promote, protect, and advance Linux and open source software and communities.',
+ };
+
+ protected readonly domains: Domain[] = [
+ { domain: 'linuxfoundation.org', primaryUse: 'General corporate', employeeCount: 189, status: 'Active' },
+ { domain: 'linux.com', primaryUse: 'Community & projects', employeeCount: 34, status: 'Active' },
+ { domain: 'lfx.dev', primaryUse: 'Engineering tools', employeeCount: 11, status: 'Active' },
+ { domain: 'cncf.io', primaryUse: 'CNCF program', employeeCount: 0, status: 'Shared' },
+ ];
+
+ protected readonly conglomerateExpanded = true;
+
+ protected readonly linkedOrgs: LinkedOrg[] = [
+ { name: 'Linux Foundation Europe', relationship: 'Related Entity' },
+ { name: 'Linux Foundation Japan', relationship: 'Related Entity' },
+ { name: 'Linux Foundation China', relationship: 'Related Entity' },
+ ];
+
+ protected readonly linkedOrgNames = this.linkedOrgs.map((o) => o.name).join(', ');
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/project-detail/org-project-detail.component.html b/apps/lfx-one/src/app/modules/org-lens/project-detail/org-project-detail.component.html
new file mode 100644
index 000000000..c8b38cb5f
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/project-detail/org-project-detail.component.html
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+
+
Key Projects
+
›
+
{{ projectMeta().name }}
+
+
+
+
+
+
+ {{ projectMeta().initial }}
+
+
+
{{ projectMeta().name }}
+
{{ projectMeta().description }}
+
+
+
+
+
+
First commit
+
{{ projectMeta().firstCommit }}
+
+
+
Software value
+
{{ projectMeta().softwareValue }}
+
+
+
Health score
+
+ {{ projectMeta().healthLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+ @for (range of [{id: '1y', label: 'Past 365 Days'}, {id: '2y', label: 'Past 2 Years'}, {id: 'all', label: 'All Time'}]; track range.id) {
+
+ }
+
+
+
+
+ @if (activeTab() === 'influence') {
+
+
+
+
+
Our Technical Influence
+
+ {{ projectMeta().techLevel }}
+
+
+
+ @for (metric of technicalMetrics; track metric.label) {
+
+
{{ metric.label }}
+
+
+
+
+
+ }
+
+
+
+
+
+
+
Our Ecosystem Influence
+
+ {{ projectMeta().ecoLevel }}
+
+
+ @if (projectMeta().foundation !== 'Non-LF Project') {
+
+ Foundation: {{ projectMeta().foundation }}
+
+ }
+
+ @for (metric of ecosystemMetrics; track metric.label) {
+
+
{{ metric.label }}
+
+ @if (metric.companyData) {
+
+ } @else {
+
No data
+ }
+
+
+
+ }
+
+
+
+
+
+
Our Membership
+ @if (projectMeta().membershipTier !== 'N/A') {
+
+
+
Membership tier
+
{{ projectMeta().membershipTier }}
+
+
+
Membership type
+
Foundation membership
+
+
+
Foundation
+
{{ projectMeta().foundation }}
+
+
+
{{ projectMeta().membershipDescription }}
+
+
+
+ {{ projectMeta().platinumCount }}
+ of {{ projectMeta().platinumTotal }}
+
+
Platinum members
+
1 of {{ projectMeta().memberTotal }} members in all paid tiers
+
+
+
Shared Benefits
+
Voting on governance decisions, recruiting and retaining key talent, and access to leaders and insights.
+
+
+
Shared Support
+
Underwriting costs such as infrastructure, governance, marketing, community growth, training development.
+
+
+ } @else {
+
{{ projectMeta().membershipDescription }}
+ }
+
+ }
+
+
+ @if (activeTab() === 'leaderboards') {
+
+
+
+
+
+
+
+
+
+
Technical Influence Leaderboard
+
+
+
+ | # |
+ Organization |
+ Influence Score |
+
+
+
+ @for (entry of technicalLeaderboard; track entry.rank) {
+
+ | {{ entry.rank }} |
+
+ {{ entry.org }}
+ @if (entry.highlight) { (You) }
+ |
+
+ {{ entry.level }}
+ |
+
+ }
+
+
+
+
+
+
+
Ecosystem Influence Leaderboard
+
+
+
+ | # |
+ Organization |
+ Influence Score |
+
+
+
+ @for (entry of ecosystemLeaderboard; track entry.rank) {
+
+ | {{ entry.rank }} |
+
+ {{ entry.org }}
+ @if (entry.highlight) { (You) }
+ |
+
+ {{ entry.level }}
+ |
+
+ }
+
+
+
+
+
+
+
Combined Influence
+
Size represents combined technical and ecosystem influence compared to all other organizations.
+
+ @for (org of bubbleOrgs; track $index) {
+
50 ? 9 : 7">
+ {{ org.name }}
+
+ }
+
+
+ }
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/project-detail/org-project-detail.component.ts b/apps/lfx-one/src/app/modules/org-lens/project-detail/org-project-detail.component.ts
new file mode 100644
index 000000000..6b2a301f6
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/project-detail/org-project-detail.component.ts
@@ -0,0 +1,245 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, computed, inject, signal } from '@angular/core';
+import { ActivatedRoute, RouterModule } from '@angular/router';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { map } from 'rxjs/operators';
+import { ChartComponent } from '@components/chart/chart.component';
+import { lfxColors } from '@lfx-one/shared/constants';
+import { hexToRgba } from '@lfx-one/shared/utils';
+
+import type { ChartData, ChartOptions } from 'chart.js';
+
+type ActiveTab = 'influence' | 'leaderboards';
+type TimeRange = '1y' | '2y' | 'all';
+type LeaderboardMode = 'calculated' | 'activity';
+
+interface TechMetricCard {
+ label: string;
+ description: string;
+ companyData: number[];
+ avgData: number[];
+ negative?: boolean;
+}
+
+interface EcoMetricCard {
+ label: string;
+ description: string;
+ companyData: number[] | null;
+}
+
+interface LeaderboardEntry {
+ rank: number;
+ org: string;
+ level: 'Leading' | 'Contributing' | 'Participating' | 'Silent';
+ highlight: boolean;
+}
+
+interface BubbleOrg {
+ name: string;
+ size: number;
+ dark: boolean;
+}
+
+const MONTHS = ['Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan'];
+
+const PROJECT_DATA: Record = {
+ 'juju': { name: 'Juju', initial: 'J', description: 'Open source application modelling tool for Kubernetes and bare metal.', foundation: 'Non-LF Project', firstCommit: 'Mar 2011', softwareValue: '$4.2M', healthLabel: 'Excellent', techLevel: 'Leading', ecoLevel: 'Non-LF Project', membershipTier: 'N/A', membershipDescription: 'This is a non-LF project. Your organization contributes directly to the project.', platinumCount: 0, platinumTotal: 0, memberTotal: 0 },
+ 'lxd': { name: 'LXD', initial: 'L', description: 'Powerful system container and virtual machine manager.', foundation: 'Non-LF Project', firstCommit: 'Nov 2014', softwareValue: '$3.1M', healthLabel: 'Excellent', techLevel: 'Leading', ecoLevel: 'Non-LF Project', membershipTier: 'N/A', membershipDescription: 'This is a non-LF project. Your organization contributes directly to the project.', platinumCount: 0, platinumTotal: 0, memberTotal: 0 },
+ 'upstream-multipath': { name: 'Upstream MultiPath TCP Linux Kernel Development', initial: 'U', description: 'Multipath TCP implementation for the Linux kernel.', foundation: 'Non-LF Project', firstCommit: 'Jan 2013', softwareValue: '$1.8M', healthLabel: 'Healthy', techLevel: 'Participating', ecoLevel: 'Non-LF Project', membershipTier: 'N/A', membershipDescription: 'This is a non-LF project. Your organization contributes directly to the project.', platinumCount: 0, platinumTotal: 0, memberTotal: 0 },
+ 'cloud-init': { name: 'cloud-init', initial: 'C', description: 'Industry standard multi-distribution method for cross-platform cloud instance initialization.', foundation: 'Non-LF Project', firstCommit: 'Feb 2012', softwareValue: '$2.9M', healthLabel: 'Excellent', techLevel: 'Leading', ecoLevel: 'Non-LF Project', membershipTier: 'N/A', membershipDescription: 'This is a non-LF project. Your organization contributes directly to the project.', platinumCount: 0, platinumTotal: 0, memberTotal: 0 },
+ 'snapcraft': { name: 'Snapcraft', initial: 'S', description: 'Tool for building and publishing snaps — universal Linux packages.', foundation: 'Non-LF Project', firstCommit: 'Oct 2015', softwareValue: '$2.3M', healthLabel: 'Excellent', techLevel: 'Leading', ecoLevel: 'Non-LF Project', membershipTier: 'N/A', membershipDescription: 'This is a non-LF project. Your organization contributes directly to the project.', platinumCount: 0, platinumTotal: 0, memberTotal: 0 },
+ 'sos': { name: 'SoS', initial: 'S', description: 'A unified tool for collecting system logs and diagnostic information.', foundation: 'Non-LF Project', firstCommit: 'Jun 2007', softwareValue: '$1.2M', healthLabel: 'Excellent', techLevel: 'Leading', ecoLevel: 'Non-LF Project', membershipTier: 'N/A', membershipDescription: 'This is a non-LF project. Your organization contributes directly to the project.', platinumCount: 0, platinumTotal: 0, memberTotal: 0 },
+ 'linux-kernel': { name: 'The Linux Kernel', initial: 'L', description: 'The Linux kernel is a free and open-source, monolithic Unix-like operating system kernel.', foundation: 'Linux Foundation', firstCommit: 'Apr 1991', softwareValue: '$14.7B', healthLabel: 'Healthy', techLevel: 'Contributing', ecoLevel: 'Participating', membershipTier: 'Platinum Member', membershipDescription: 'The Linux Kernel is the core project of the Linux Foundation. Your organization is a Platinum Member.', platinumCount: 1, platinumTotal: 14, memberTotal: 721 },
+ 'islet': { name: 'Islet', initial: 'I', description: 'A software platform to enable Confidential Computing on Arm architecture.', foundation: 'Confidential Computing Consortium', firstCommit: 'Sep 2022', softwareValue: '$0.4M', healthLabel: 'Healthy', techLevel: 'Participating', ecoLevel: 'Silent', membershipTier: 'Silver Member', membershipDescription: 'Islet is a project of the Confidential Computing Consortium. Your organization is a Silver Member.', platinumCount: 1, platinumTotal: 8, memberTotal: 143 },
+ 'kubernetes': { name: 'Kubernetes', initial: 'K', description: 'Open-source container orchestration system for automating deployment, scaling, and management.', foundation: 'CNCF', firstCommit: 'Jun 2014', softwareValue: '$18.2B', healthLabel: 'Excellent', techLevel: 'Leading', ecoLevel: 'Leading', membershipTier: 'Platinum Member', membershipDescription: 'Kubernetes is a Graduated Project of CNCF. Your organization is a Platinum Member.', platinumCount: 1, platinumTotal: 14, memberTotal: 721 },
+ 'prometheus': { name: 'Prometheus', initial: 'P', description: 'Open-source systems monitoring and alerting toolkit.', foundation: 'CNCF', firstCommit: 'Nov 2012', softwareValue: '$2.4M', healthLabel: 'Healthy', techLevel: 'Contributing', ecoLevel: 'Contributing', membershipTier: 'Platinum Member', membershipDescription: 'Prometheus is a Graduated Project of CNCF. Your organization is a Platinum Member.', platinumCount: 1, platinumTotal: 14, memberTotal: 721 },
+ 'envoy': { name: 'Envoy', initial: 'E', description: 'Envoy is an open source edge and service proxy, designed for cloud-native applications.', foundation: 'CNCF', firstCommit: 'Aug 2016', softwareValue: '$3.9B', healthLabel: 'Excellent', techLevel: 'Leading', ecoLevel: 'Leading', membershipTier: 'Platinum Member', membershipDescription: 'Envoy is a Graduated Project of CNCF. Your organization is a Platinum Member.', platinumCount: 1, platinumTotal: 14, memberTotal: 721 },
+ 'opentelemetry': { name: 'OpenTelemetry', initial: 'O', description: 'Observability framework and toolkit designed to create and manage telemetry data.', foundation: 'CNCF', firstCommit: 'May 2019', softwareValue: '$1.9M', healthLabel: 'Healthy', techLevel: 'Participating', ecoLevel: 'Participating', membershipTier: 'Platinum Member', membershipDescription: 'OpenTelemetry is a Graduated Project of CNCF. Your organization is a Platinum Member.', platinumCount: 1, platinumTotal: 14, memberTotal: 721 },
+};
+
+@Component({
+ selector: 'lfx-org-project-detail',
+ imports: [RouterModule, ChartComponent],
+ templateUrl: './org-project-detail.component.html',
+})
+export class OrgProjectDetailComponent {
+ private readonly route = inject(ActivatedRoute);
+ private readonly slug = toSignal(this.route.paramMap.pipe(map((p) => p.get('slug') ?? '')), { initialValue: '' });
+
+ protected readonly projectMeta = computed(() => PROJECT_DATA[this.slug()] ?? {
+ name: this.slug(), initial: '?', description: '', foundation: '', firstCommit: '', softwareValue: '', healthLabel: 'Healthy',
+ techLevel: '', ecoLevel: '', membershipTier: '', membershipDescription: '', platinumCount: 0, platinumTotal: 0, memberTotal: 0,
+ });
+
+ protected readonly activeTab = signal('influence');
+ protected readonly timeRange = signal('2y');
+ protected readonly leaderboardMode = signal('calculated');
+ protected readonly months = MONTHS;
+
+ // ─── Sparkline chart options ────────────────────────────────────────────────
+ protected readonly sparklineTechOptions: ChartOptions<'line'> = {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: { mode: 'index', intersect: false },
+ plugins: { legend: { display: false }, tooltip: { enabled: false } },
+ scales: { x: { display: false }, y: { display: false } },
+ datasets: { line: { tension: 0.4, borderWidth: 2, pointRadius: 0, pointHoverRadius: 0 } },
+ layout: { padding: 2 },
+ };
+
+ protected readonly sparklineEcoOptions: ChartOptions<'line'> = {
+ ...this.sparklineTechOptions,
+ };
+
+ // ─── Technical metric cards ──────────────────────────────────────────────
+ protected readonly technicalMetrics: TechMetricCard[] = [
+ { label: 'Maintainers', description: 'Our company employs 1 or more maintainers for this project.', companyData: [3, 4, 3, 5, 6, 7, 8, 9], avgData: [2, 2, 3, 3, 3, 4, 4, 4] },
+ { label: 'Contributors', description: 'Our company employs 4.7% of contributors to this project.', companyData: [5, 5, 6, 6, 7, 7, 7, 8], avgData: [4, 4, 4, 5, 5, 5, 6, 6] },
+ { label: 'Commit Activities', description: 'Employees made 10.56% of all commit activities.', companyData: [4, 5, 6, 7, 8, 9, 9, 10], avgData: [5, 5, 5, 6, 6, 7, 7, 7] },
+ { label: 'Pull Requests Opened', description: 'Employees opened 16.33% of all PRs.', companyData: [5, 6, 7, 8, 9, 10, 11, 12], avgData: [4, 4, 5, 5, 6, 6, 7, 7] },
+ { label: 'Avg Time to Merge PRs', description: 'PRs merged 22.67% slower than average.', companyData: [8, 8, 7, 7, 6, 6, 5, 4], avgData: [7, 7, 7, 7, 7, 7, 7, 7], negative: true },
+ ];
+
+ // ─── Ecosystem metric cards ──────────────────────────────────────────────
+ protected readonly ecosystemMetrics: EcoMetricCard[] = [
+ { label: 'Collaboration Activity', description: 'Employees contributed 9.6% of all collaboration activities.', companyData: [4, 5, 5, 6, 7, 7, 8, 9] },
+ { label: 'Meeting Attendance', description: 'Our company has no meeting attendance for this project.', companyData: null },
+ { label: 'Board Members', description: 'Our company employs 2 board members for this foundation.', companyData: [3, 4, 4, 5, 5, 6, 6, 7] },
+ { label: 'Committee Members', description: 'Employees make up 1.1% of all committee members.', companyData: [3, 3, 4, 4, 5, 5, 5, 6] },
+ { label: 'Event Attendance', description: 'Employees attended 63.0% of all foundation events.', companyData: [5, 7, 8, 10, 12, 14, 15, 17] },
+ { label: 'Event Speakers', description: 'Employees represented 2.4% of all speakers at foundation events.', companyData: [3, 3, 4, 4, 5, 5, 6, 6] },
+ { label: 'Event Sponsorships', description: 'Our company reached 2.6% of attendees through sponsorship.', companyData: [3, 4, 4, 5, 5, 6, 6, 7] },
+ { label: 'Meetup Attendance', description: 'Employees attended 0.8% of all foundation meetups.', companyData: [2, 2, 3, 3, 3, 4, 4, 4] },
+ { label: 'Certified Individuals', description: 'Employees make up 0.3% of all certified individuals.', companyData: [2, 2, 2, 3, 3, 3, 4, 4] },
+ ];
+
+ protected readonly technicalLeaderboard: LeaderboardEntry[] = [
+ { rank: 1, org: 'Tetrate.io Inc', level: 'Leading', highlight: false },
+ { rank: 2, org: 'Google LLC', level: 'Leading', highlight: true },
+ { rank: 3, org: 'Mosaic', level: 'Leading', highlight: false },
+ { rank: 4, org: 'Red Hat', level: 'Leading', highlight: false },
+ { rank: 5, org: 'NetEase, Inc.', level: 'Leading', highlight: false },
+ { rank: 6, org: 'Microsoft Corporation', level: 'Leading', highlight: false },
+ { rank: 7, org: 'Nutanix, Inc.', level: 'Leading', highlight: false },
+ { rank: 8, org: 'The Trade Desk, Inc.', level: 'Leading', highlight: false },
+ { rank: 9, org: 'SAP SE', level: 'Leading', highlight: false },
+ { rank: 10, org: 'Apple Inc.', level: 'Leading', highlight: false },
+ ];
+
+ protected readonly ecosystemLeaderboard: LeaderboardEntry[] = [
+ { rank: 1, org: 'Tetrate.io Inc', level: 'Leading', highlight: false },
+ { rank: 2, org: 'Bloomberg LP', level: 'Leading', highlight: false },
+ { rank: 3, org: 'Nutanix, Inc.', level: 'Leading', highlight: false },
+ { rank: 4, org: 'Google LLC', level: 'Leading', highlight: true },
+ { rank: 5, org: 'Microsoft Corporation', level: 'Contributing', highlight: false },
+ { rank: 6, org: 'IBM', level: 'Contributing', highlight: false },
+ { rank: 7, org: 'Red Hat', level: 'Contributing', highlight: false },
+ { rank: 8, org: 'Apple Inc.', level: 'Participating', highlight: false },
+ { rank: 9, org: 'AWS CloudFormation', level: 'Participating', highlight: false },
+ { rank: 10, org: 'SAP SE', level: 'Participating', highlight: false },
+ ];
+
+ protected readonly bubbleOrgs: BubbleOrg[] = [
+ { name: 'Tetrate.io', size: 100, dark: true },
+ { name: 'Bloomberg', size: 80, dark: true },
+ { name: 'Nutanix', size: 70, dark: true },
+ { name: 'Google', size: 65, dark: true },
+ { name: 'Microsoft', size: 50, dark: false },
+ { name: 'Red Hat', size: 45, dark: false },
+ { name: 'IBM', size: 40, dark: false },
+ { name: '', size: 35, dark: false },
+ { name: '', size: 30, dark: false },
+ { name: '', size: 25, dark: false },
+ ];
+
+ // ─── Chart data builders ─────────────────────────────────────────────────
+ protected techChartData(metric: TechMetricCard): ChartData<'line'> {
+ const color = metric.negative ? lfxColors.red[500] : lfxColors.blue[500];
+ return {
+ labels: MONTHS,
+ datasets: [
+ {
+ data: metric.companyData,
+ borderColor: color,
+ backgroundColor: hexToRgba(color, 0.1),
+ fill: false,
+ },
+ {
+ data: metric.avgData,
+ borderColor: lfxColors.gray[200],
+ backgroundColor: 'transparent',
+ fill: false,
+ },
+ ],
+ };
+ }
+
+ protected ecoChartData(companyData: number[]): ChartData<'line'> {
+ return {
+ labels: MONTHS,
+ datasets: [
+ {
+ data: companyData,
+ borderColor: lfxColors.violet[500],
+ backgroundColor: hexToRgba(lfxColors.violet[500], 0.1),
+ fill: false,
+ },
+ {
+ data: [3, 3, 3, 4, 4, 4, 5, 5],
+ borderColor: lfxColors.gray[200],
+ backgroundColor: 'transparent',
+ fill: false,
+ },
+ ],
+ };
+ }
+
+ protected setTimeRange(range: string): void {
+ this.timeRange.set(range as TimeRange);
+ }
+
+ protected levelClass(level: string): string {
+ switch (level) {
+ case 'Leading': return 'bg-green-50 text-green-700 border border-green-200';
+ case 'Contributing': return 'bg-blue-50 text-blue-700 border border-blue-200';
+ case 'Participating': return 'bg-amber-50 text-amber-700 border border-amber-200';
+ default: return 'bg-slate-100 text-slate-500';
+ }
+ }
+
+ protected healthClass(label: string): string {
+ switch (label) {
+ case 'Excellent': return 'bg-emerald-100 text-emerald-800';
+ case 'Healthy': return 'bg-blue-100 text-blue-800';
+ default: return 'bg-red-100 text-red-700';
+ }
+ }
+
+ protected bubbleOpacity(size: number): string {
+ if (size >= 80) return 'bg-blue-600';
+ if (size >= 60) return 'bg-blue-400';
+ if (size >= 45) return 'bg-blue-300';
+ return 'bg-blue-100';
+ }
+
+ protected bubbleTextColor(size: number): string {
+ return size >= 45 ? 'text-white' : 'text-blue-700';
+ }
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/projects/org-projects.component.html b/apps/lfx-one/src/app/modules/org-lens/projects/org-projects.component.html
new file mode 100644
index 000000000..63f97b83b
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/projects/org-projects.component.html
@@ -0,0 +1,161 @@
+
+
+
+
+
+
Key projects
+
Identify our company's key open source projects, understand their health status, and assess our company's influence.
+
+
+
+
+
+
+
+
+
+
+ @if (workspaceDropdownOpen()) {
+
+ @for (ws of workspaces; track ws.id) {
+
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ Key Projects
+
+
+
+
+
+
+
+ | Project |
+ Health Score |
+ Technical Influence |
+ Ecosystem Influence |
+ Our Contributors |
+ Our Participants |
+ Our Activity (1Y) |
+ |
+
+
+
+ @for (project of visibleProjects; track project.slug) {
+
+
+
+ |
+
+ {{ project.name }}
+
+ |
+
+
+
+
+ {{ project.healthLabel }}
+
+ |
+
+
+
+
+
+ {{ project.technicalInfluence }}
+
+ |
+
+
+ {{ project.ecosystemInfluence }} |
+
+
+
+
+
+ @for (i of [].constructor(project.contributorAvatars); track $index) {
+
+
+ }
+
+ {{ project.contributorCount }}
+
+ |
+
+
+
+
+
+ @for (i of [].constructor(project.participantAvatars); track $index) {
+
+
+ }
+
+ {{ project.participantCount }}
+
+ |
+
+
+
+
+
+
+ {{ project.activityPositive ? '+' : '-' }}{{ project.activityChange }}%
+
+
+ |
+
+
+
+
+ |
+
+
+ }
+
+
+
+
+
+ Showing {{ visibleProjects.length }} projects
+
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/projects/org-projects.component.ts b/apps/lfx-one/src/app/modules/org-lens/projects/org-projects.component.ts
new file mode 100644
index 000000000..0d19a7327
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/projects/org-projects.component.ts
@@ -0,0 +1,258 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, HostListener, signal } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+type HealthLabel = 'Excellent' | 'Healthy' | 'At Risk';
+type InfluenceLevel = 'Leading' | 'Contributing' | 'Participating' | 'Silent' | 'Non-LF Project';
+
+interface Project {
+ name: string;
+ slug: string;
+ healthLabel: HealthLabel;
+ technicalInfluence: InfluenceLevel;
+ ecosystemInfluence: InfluenceLevel | 'Non-LF Project';
+ contributorAvatars: number;
+ contributorCount: number;
+ participantAvatars: number;
+ participantCount: number;
+ activityChange: number;
+ activityPositive: boolean;
+ sparklinePoints: string;
+}
+
+interface Workspace {
+ id: string;
+ label: string;
+ projectSlugs: string[];
+}
+
+@Component({
+ selector: 'lfx-org-projects',
+ imports: [RouterModule],
+ templateUrl: './org-projects.component.html',
+})
+export class OrgProjectsComponent {
+ protected readonly workspaceDropdownOpen = signal(false);
+ protected readonly activeWorkspaceId = signal('most-active');
+
+ protected readonly workspaces: Workspace[] = [
+ { id: 'most-active', label: 'Most Active Projects', projectSlugs: ['juju', 'lxd', 'upstream-multipath', 'cloud-init', 'snapcraft', 'sos', 'linux-kernel', 'islet'] },
+ { id: 'cncf', label: 'CNCF Projects', projectSlugs: ['kubernetes', 'prometheus', 'envoy', 'opentelemetry'] },
+ { id: 'all', label: 'All Projects', projectSlugs: ['juju', 'lxd', 'upstream-multipath', 'cloud-init', 'snapcraft', 'sos', 'linux-kernel', 'islet', 'kubernetes', 'prometheus', 'envoy', 'opentelemetry'] },
+ ];
+
+ protected readonly projects: Project[] = [
+ {
+ name: 'Juju',
+ slug: 'juju',
+ healthLabel: 'Excellent',
+ technicalInfluence: 'Leading',
+ ecosystemInfluence: 'Non-LF Project',
+ contributorAvatars: 3,
+ contributorCount: 41,
+ participantAvatars: 3,
+ participantCount: 76,
+ activityChange: 67.12,
+ activityPositive: true,
+ sparklinePoints: '2,15 8,12 14,10 20,8 26,9 32,7 38,6',
+ },
+ {
+ name: 'LXD',
+ slug: 'lxd',
+ healthLabel: 'Excellent',
+ technicalInfluence: 'Leading',
+ ecosystemInfluence: 'Non-LF Project',
+ contributorAvatars: 2,
+ contributorCount: 31,
+ participantAvatars: 2,
+ participantCount: 63,
+ activityChange: 131.99,
+ activityPositive: true,
+ sparklinePoints: '2,18 8,14 14,11 20,9 26,8 32,7 38,5',
+ },
+ {
+ name: 'Upstream MultiPath TCP Linux Kernel Development',
+ slug: 'upstream-multipath',
+ healthLabel: 'Healthy',
+ technicalInfluence: 'Participating',
+ ecosystemInfluence: 'Non-LF Project',
+ contributorAvatars: 2,
+ contributorCount: 26,
+ participantAvatars: 1,
+ participantCount: 26,
+ activityChange: 4.45,
+ activityPositive: true,
+ sparklinePoints: '2,12 8,11 14,10 20,10 26,10 32,9 38,9',
+ },
+ {
+ name: 'cloud-init',
+ slug: 'cloud-init',
+ healthLabel: 'Excellent',
+ technicalInfluence: 'Leading',
+ ecosystemInfluence: 'Non-LF Project',
+ contributorAvatars: 2,
+ contributorCount: 24,
+ participantAvatars: 1,
+ participantCount: 28,
+ activityChange: 15.94,
+ activityPositive: false,
+ sparklinePoints: '2,8 8,10 14,11 20,12 26,13 32,14 38,15',
+ },
+ {
+ name: 'Snapcraft',
+ slug: 'snapcraft',
+ healthLabel: 'Excellent',
+ technicalInfluence: 'Leading',
+ ecosystemInfluence: 'Non-LF Project',
+ contributorAvatars: 2,
+ contributorCount: 22,
+ participantAvatars: 2,
+ participantCount: 44,
+ activityChange: 65.5,
+ activityPositive: true,
+ sparklinePoints: '2,14 8,11 14,9 20,8 26,7 32,7 38,6',
+ },
+ {
+ name: 'SoS',
+ slug: 'sos',
+ healthLabel: 'Excellent',
+ technicalInfluence: 'Leading',
+ ecosystemInfluence: 'Non-LF Project',
+ contributorAvatars: 2,
+ contributorCount: 20,
+ participantAvatars: 1,
+ participantCount: 21,
+ activityChange: 79.15,
+ activityPositive: true,
+ sparklinePoints: '2,13 8,10 14,8 20,6 26,7 32,6 38,5',
+ },
+ {
+ name: 'The Linux Kernel',
+ slug: 'linux-kernel',
+ healthLabel: 'Healthy',
+ technicalInfluence: 'Contributing',
+ ecosystemInfluence: 'Participating',
+ contributorAvatars: 1,
+ contributorCount: 18,
+ participantAvatars: 2,
+ participantCount: 54,
+ activityChange: 30.4,
+ activityPositive: false,
+ sparklinePoints: '2,6 8,9 14,11 20,13 26,14 32,15 38,17',
+ },
+ {
+ name: 'Islet',
+ slug: 'islet',
+ healthLabel: 'Healthy',
+ technicalInfluence: 'Participating',
+ ecosystemInfluence: 'Silent',
+ contributorAvatars: 1,
+ contributorCount: 16,
+ participantAvatars: 1,
+ participantCount: 20,
+ activityChange: 75.12,
+ activityPositive: false,
+ sparklinePoints: '2,5 8,8 14,10 20,12 26,14 32,16 38,18',
+ },
+ {
+ name: 'Kubernetes',
+ slug: 'kubernetes',
+ healthLabel: 'Excellent',
+ technicalInfluence: 'Leading',
+ ecosystemInfluence: 'Leading',
+ contributorAvatars: 3,
+ contributorCount: 156,
+ participantAvatars: 3,
+ participantCount: 243,
+ activityChange: 12.5,
+ activityPositive: true,
+ sparklinePoints: '2,14 8,12 14,10 20,9 26,8 32,7 38,6',
+ },
+ {
+ name: 'Prometheus',
+ slug: 'prometheus',
+ healthLabel: 'Healthy',
+ technicalInfluence: 'Contributing',
+ ecosystemInfluence: 'Contributing',
+ contributorAvatars: 2,
+ contributorCount: 42,
+ participantAvatars: 2,
+ participantCount: 67,
+ activityChange: 8.3,
+ activityPositive: true,
+ sparklinePoints: '2,13 8,12 14,11 20,10 26,10 32,9 38,8',
+ },
+ {
+ name: 'Envoy',
+ slug: 'envoy',
+ healthLabel: 'Excellent',
+ technicalInfluence: 'Contributing',
+ ecosystemInfluence: 'Contributing',
+ contributorAvatars: 2,
+ contributorCount: 98,
+ participantAvatars: 2,
+ participantCount: 134,
+ activityChange: 34.0,
+ activityPositive: true,
+ sparklinePoints: '2,15 8,12 14,10 20,8 26,7 32,6 38,5',
+ },
+ {
+ name: 'OpenTelemetry',
+ slug: 'opentelemetry',
+ healthLabel: 'Healthy',
+ technicalInfluence: 'Participating',
+ ecosystemInfluence: 'Participating',
+ contributorAvatars: 1,
+ contributorCount: 31,
+ participantAvatars: 2,
+ participantCount: 48,
+ activityChange: 18.0,
+ activityPositive: false,
+ sparklinePoints: '2,6 8,9 14,11 20,13 26,14 32,16 38,17',
+ },
+ ];
+
+ protected get activeWorkspace(): Workspace {
+ return this.workspaces.find((w) => w.id === this.activeWorkspaceId()) ?? this.workspaces[0];
+ }
+
+ protected get visibleProjects(): Project[] {
+ const slugs = this.activeWorkspace.projectSlugs;
+ return this.projects.filter((p) => slugs.includes(p.slug));
+ }
+
+ protected readonly contributorColors = ['#EF4444', '#F97316', '#EAB308'];
+ protected readonly participantColors = ['#3B82F6', '#06B6D4', '#8B5CF6'];
+
+ protected selectWorkspace(id: string): void {
+ this.activeWorkspaceId.set(id);
+ this.workspaceDropdownOpen.set(false);
+ }
+
+ protected healthClass(label: HealthLabel): string {
+ switch (label) {
+ case 'Excellent': return 'bg-emerald-100 text-emerald-800';
+ case 'Healthy': return 'bg-blue-100 text-blue-800';
+ default: return 'bg-red-100 text-red-700';
+ }
+ }
+
+ protected technicalInfluenceClass(level: InfluenceLevel): string {
+ switch (level) {
+ case 'Leading': return 'text-slate-900';
+ case 'Contributing': return 'text-slate-900';
+ case 'Participating': return 'text-slate-900';
+ default: return 'text-slate-400';
+ }
+ }
+
+ @HostListener('document:click', ['$event'])
+ onDocumentClick(event: MouseEvent): void {
+ const target = event.target as HTMLElement;
+ if (!target.closest('[data-workspace-dropdown]')) {
+ this.workspaceDropdownOpen.set(false);
+ }
+ }
+}
diff --git a/apps/lfx-one/src/app/modules/org-lens/strategy/org-strategy.component.html b/apps/lfx-one/src/app/modules/org-lens/strategy/org-strategy.component.html
new file mode 100644
index 000000000..0d0511c87
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/strategy/org-strategy.component.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
Open Source Strategy
+
Discover ways to deepen your organization's open source engagement across the LF ecosystem.
+
+
+
+
+
How to Get More Involved
+
+
+
+ @for (tab of [
+ { id: 'brand', label: 'Build Brand' },
+ { id: 'talent', label: 'Attract & Retain Talent' },
+ { id: 'risk', label: 'Decrease Platform Risk' },
+ { id: 'security', label: 'Increase Security' },
+ { id: 'systems', label: 'Open & Interoperable Systems' },
+ { id: 'insights', label: 'Strategic Insights' }
+ ]; track tab.id) {
+
+ }
+
+
+
+ @if (activeInvolveTab() === 'brand') {
+
+
+
How open source builds your brand
+
+ - Improves software quality
+ - Makes the organization a better place to work
+ - Enables the IT industry to be more innovative
+ - Fulfills a moral obligation to other OSS consumers
+
+
+
+
+
A Deep Dive Into Open Source Community Management
+
+
+
+ }
+
+
+ @if (activeInvolveTab() === 'talent') {
+
Open source participation helps attract top developers and retain existing talent by providing opportunities to work on cutting-edge technology.
+ }
+
+
+ @if (activeInvolveTab() === 'risk') {
+
Active participation in open source reduces vendor lock-in and ensures your organization has influence over the technologies you depend on.
+ }
+
+
+ @if (activeInvolveTab() === 'security') {
+
Contributing to open source security initiatives helps protect your software supply chain and builds trust with customers and partners.
+ }
+
+
+ @if (activeInvolveTab() === 'systems') {
+
Open standards and interoperable systems reduce integration costs and enable innovation across the technology ecosystem.
+ }
+
+
+ @if (activeInvolveTab() === 'insights') {
+
Leverage open source intelligence to inform strategic decisions about technology investments and industry direction.
+ }
+
+
diff --git a/apps/lfx-one/src/app/modules/org-lens/strategy/org-strategy.component.ts b/apps/lfx-one/src/app/modules/org-lens/strategy/org-strategy.component.ts
new file mode 100644
index 000000000..4847d4a12
--- /dev/null
+++ b/apps/lfx-one/src/app/modules/org-lens/strategy/org-strategy.component.ts
@@ -0,0 +1,18 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, signal } from '@angular/core';
+
+type InvolveTab = 'brand' | 'talent' | 'risk' | 'security' | 'systems' | 'insights';
+
+@Component({
+ selector: 'lfx-org-strategy',
+ templateUrl: './org-strategy.component.html',
+})
+export class OrgStrategyComponent {
+ protected readonly activeInvolveTab = signal('brand');
+
+ protected setInvolveTab(tab: string): void {
+ this.activeInvolveTab.set(tab as InvolveTab);
+ }
+}
diff --git a/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.html b/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.html
new file mode 100644
index 000000000..227055ef7
--- /dev/null
+++ b/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+ @for (lens of lenses; track lens.id) {
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ @for (action of footerActions; track action.label) {
+
+
+
+ }
+
+
diff --git a/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.scss b/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.scss
new file mode 100644
index 000000000..9667aca80
--- /dev/null
+++ b/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.scss
@@ -0,0 +1,7 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+:host {
+ display: block;
+ height: 100%;
+}
diff --git a/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.ts b/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.ts
new file mode 100644
index 000000000..9fb759240
--- /dev/null
+++ b/apps/lfx-one/src/app/shared/components/lens-switcher/lens-switcher.component.ts
@@ -0,0 +1,90 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { NgClass } from '@angular/common';
+import { Component, inject } from '@angular/core';
+import { Router, RouterModule } from '@angular/router';
+import { AppService, Lens } from '@services/app.service';
+import { TooltipModule } from 'primeng/tooltip';
+
+interface LensOption {
+ id: Lens;
+ label: string;
+ icon: string;
+ activeIcon: string;
+ testId: string;
+}
+
+interface FooterAction {
+ label: string;
+ icon: string;
+ url: string;
+ target: string;
+ testId: string;
+ tooltipHtml: string;
+}
+
+const EXTERNAL_ICON = '';
+
+function buildTooltip(label: string, external: boolean): string {
+ return external ? `${label}${EXTERNAL_ICON}` : label;
+}
+
+const LENS_DEFAULT_ROUTES: Record = {
+ me: '/',
+ foundation: '/',
+ org: '/org',
+};
+
+@Component({
+ selector: 'lfx-lens-switcher',
+ imports: [NgClass, RouterModule, TooltipModule],
+ templateUrl: './lens-switcher.component.html',
+ styleUrl: './lens-switcher.component.scss',
+})
+export class LensSwitcherComponent {
+ private readonly appService = inject(AppService);
+ private readonly router = inject(Router);
+
+ protected readonly activeLens = this.appService.activeLens;
+
+ protected readonly lenses: LensOption[] = [
+ { id: 'me', label: 'Me', icon: 'fa-light fa-circle-user', activeIcon: 'fa-solid fa-circle-user', testId: 'lens-me' },
+ { id: 'foundation', label: 'Foundation', icon: 'fa-light fa-laptop-code', activeIcon: 'fa-solid fa-laptop-code', testId: 'lens-foundation' },
+ { id: 'org', label: 'Organization', icon: 'fa-light fa-building', activeIcon: 'fa-solid fa-building', testId: 'lens-org' },
+ ];
+
+ protected readonly insightsTooltip = `LFX Insights${EXTERNAL_ICON}
Discover and evaluate the world's most critical open source projects at scale
`;
+
+ protected readonly footerActions: FooterAction[] = [
+ {
+ label: 'Changelog',
+ icon: 'fa-light fa-clock-rotate-left',
+ url: 'https://changelog.lfx.dev/',
+ target: '_blank',
+ testId: 'lens-changelog',
+ tooltipHtml: buildTooltip('Changelog', true),
+ },
+ {
+ label: 'Support',
+ icon: 'fa-light fa-question-circle',
+ url: 'https://jira.linuxfoundation.org/servicedesk/customer/portal/4',
+ target: '_blank',
+ testId: 'lens-support',
+ tooltipHtml: buildTooltip('Support', true),
+ },
+ {
+ label: 'Logout',
+ icon: 'fa-light fa-sign-out',
+ url: '/logout',
+ target: '_self',
+ testId: 'lens-logout',
+ tooltipHtml: buildTooltip('Logout', false),
+ },
+ ];
+
+ protected setLens(lens: Lens): void {
+ this.appService.setLens(lens);
+ void this.router.navigate([LENS_DEFAULT_ROUTES[lens]]);
+ }
+}
diff --git a/apps/lfx-one/src/app/shared/components/placeholder-page/placeholder-page.component.html b/apps/lfx-one/src/app/shared/components/placeholder-page/placeholder-page.component.html
new file mode 100644
index 000000000..cafba97ec
--- /dev/null
+++ b/apps/lfx-one/src/app/shared/components/placeholder-page/placeholder-page.component.html
@@ -0,0 +1,14 @@
+
+
+
+
+ @if (pageTitle) {
+
{{ pageTitle }}
+ }
+
+
+
Coming Soon
+
This page is part of the new navigation proposal and is under development.
+
Back to Overview
+
+
diff --git a/apps/lfx-one/src/app/shared/components/placeholder-page/placeholder-page.component.ts b/apps/lfx-one/src/app/shared/components/placeholder-page/placeholder-page.component.ts
new file mode 100644
index 000000000..7828d86b0
--- /dev/null
+++ b/apps/lfx-one/src/app/shared/components/placeholder-page/placeholder-page.component.ts
@@ -0,0 +1,15 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { Component, inject } from '@angular/core';
+import { ActivatedRoute, RouterModule } from '@angular/router';
+
+@Component({
+ selector: 'lfx-placeholder-page',
+ imports: [RouterModule],
+ templateUrl: './placeholder-page.component.html',
+})
+export class PlaceholderPageComponent {
+ private readonly route = inject(ActivatedRoute);
+ protected readonly pageTitle = this.route.snapshot.data['title'] as string | undefined;
+}
diff --git a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html
index 45dd229f8..33072e5f3 100644
--- a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html
+++ b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html
@@ -2,10 +2,11 @@
@if (projects().length > 1) {
-
+
@@ -19,39 +20,46 @@
-
-
-
-
-
-
-
-
+
+ @if (isOpen()) {
+
+
+
-
+
@for (foundation of foundations(); track foundation.uid) {
@@ -78,7 +86,7 @@
@for (project of childProjectsMap().get(foundation.uid); track project.uid) {
-
+ }
} @else {
@@ -125,10 +133,9 @@
-
}
diff --git a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.scss b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.scss
index 598bff311..8e2eb045d 100644
--- a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.scss
+++ b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.scss
@@ -7,23 +7,5 @@
}
.sidebar-project-name {
- @apply font-medium grow leading-5 relative text-base text-gray-900 tracking-tight line-clamp-2;
-}
-
-::ng-deep {
- .project-selector-panel {
- @apply shadow-lg;
-
- &::before {
- @apply hidden;
- }
-
- &::after {
- @apply hidden;
- }
-
- .p-popover-content {
- @apply rounded-none;
- }
- }
+ @apply font-semibold grow leading-5 relative text-base text-gray-900 tracking-tight line-clamp-2;
}
diff --git a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts
index a4696c049..bf35731ae 100644
--- a/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts
+++ b/apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts
@@ -1,49 +1,90 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT
-import { Component, computed, input, output, signal } from '@angular/core';
+import { Component, computed, DestroyRef, ElementRef, inject, input, OnDestroy, output, signal, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { ButtonComponent } from '@components/button/button.component';
import { Project } from '@lfx-one/shared/interfaces';
+import { AppService } from '@services/app.service';
import { AutoFocus } from 'primeng/autofocus';
import { InputTextModule } from 'primeng/inputtext';
-import { Popover, PopoverModule } from 'primeng/popover';
import { TagComponent } from '../tag/tag.component';
@Component({
selector: 'lfx-project-selector',
- imports: [PopoverModule, ButtonComponent, InputTextModule, FormsModule, AutoFocus, TagComponent],
+ imports: [InputTextModule, FormsModule, AutoFocus, TagComponent],
templateUrl: './project-selector.component.html',
styleUrl: './project-selector.component.scss',
})
-export class ProjectSelectorComponent {
+export class ProjectSelectorComponent implements OnDestroy {
+ @ViewChild('selectorTrigger') private readonly selectorTrigger?: ElementRef
;
+
public readonly projects = input.required();
public readonly selectedProject = input(null);
public readonly projectChange = output();
+ private readonly appService = inject(AppService);
+ private readonly elementRef = inject(ElementRef);
+ private readonly destroyRef = inject(DestroyRef);
+
+ private outsideClickListener: ((e: MouseEvent) => void) | null = null;
+
+ protected readonly isOpen = this.appService.projectSelectorOpen;
+ protected readonly panelTop = signal(0);
protected readonly searchQuery = signal('');
protected readonly displayName = this.initializeDisplayName();
protected readonly displayLogo = this.initializeDisplayLogo();
+ protected readonly displayType = this.initializeDisplayType();
protected readonly foundations = this.initializeFoundations();
protected readonly childProjectsMap = this.initializeChildProjectsMap();
protected readonly hasResults = this.initializeHasResults();
- protected selectProject(project: Project, popover: Popover): void {
+ public ngOnDestroy(): void {
+ this.detachOutsideClickListener();
+ }
+
+ protected selectProject(project: Project): void {
this.projectChange.emit(project);
- this.searchQuery.set(''); // Reset search on selection
- popover.hide();
+ this.closePanel();
}
- protected togglePanel(event: Event, popover: Popover): void {
- popover.toggle(event);
+ protected togglePanel(): void {
+ if (this.appService.projectSelectorOpen()) {
+ this.closePanel();
+ return;
+ }
+ if (this.selectorTrigger) {
+ const rect = this.selectorTrigger.nativeElement.getBoundingClientRect();
+ this.panelTop.set(rect.top);
+ }
+ this.appService.setProjectSelectorOpen(true);
+ // Attach listener on next tick so the current opening click is not caught
+ setTimeout(() => this.attachOutsideClickListener(), 0);
}
- protected onPopoverHide(): void {
- // Reset search when popover closes
+ private closePanel(): void {
this.searchQuery.set('');
+ this.appService.setProjectSelectorOpen(false);
+ this.detachOutsideClickListener();
+ }
+
+ private attachOutsideClickListener(): void {
+ this.outsideClickListener = (event: MouseEvent) => {
+ if (!this.elementRef.nativeElement.contains(event.target as Node)) {
+ this.closePanel();
+ }
+ };
+ document.addEventListener('click', this.outsideClickListener);
+ this.destroyRef.onDestroy(() => this.detachOutsideClickListener());
+ }
+
+ private detachOutsideClickListener(): void {
+ if (this.outsideClickListener) {
+ document.removeEventListener('click', this.outsideClickListener);
+ this.outsideClickListener = null;
+ }
}
private initializeDisplayName() {
@@ -53,6 +94,16 @@ export class ProjectSelectorComponent {
});
}
+ private initializeDisplayType() {
+ return computed(() => {
+ const project = this.selectedProject();
+ if (!project) return 'Foundation';
+ const validProjectIds = new Set(this.projects().map((p) => p.uid));
+ const isFoundation = !project.parent_uid || project.parent_uid === '' || !validProjectIds.has(project.parent_uid);
+ return isFoundation ? 'Foundation' : 'Project';
+ });
+ }
+
private initializeDisplayLogo() {
return computed(() => {
const project = this.selectedProject();
diff --git a/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html
index 603f24124..a4a3798ae 100644
--- a/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html
+++ b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.html
@@ -2,25 +2,15 @@