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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/components/project/ProjectDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ function ProjectDetailsContent({ project }: { project: ProjectDefinition }) {
value={tab.id}
label={
<>
{typeof tab.icon === 'string' ? <Icon icon={tab.icon} /> : tab.icon}
{tab.icon &&
(typeof tab.icon === 'string' ? <Icon icon={tab.icon} /> : tab.icon)}
<Typography>{tab.label}</Typography>
</>
}
Expand Down
28 changes: 27 additions & 1 deletion frontend/src/components/project/ProjectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ import { Box, Button, Typography } from '@mui/material';
import { groupBy, uniq } from 'lodash';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useClustersConf } from '../../lib/k8s';
import Namespace from '../../lib/k8s/namespace';
import { ProjectDefinition } from '../../redux/projectsSlice';
import { RootState } from '../../redux/stores/store';
import { StatusLabel } from '../common';
import Link from '../common/Link';
import Table, { TableColumn } from '../common/Table/Table';
import { NewProjectPopup } from './NewProjectPopup';
import { getHealthIcon, getResourcesHealth, PROJECT_ID_LABEL } from './projectUtils';
import { useProjectItems } from './useProjectResources';

const useProjects = (): ProjectDefinition[] => {
const useDefaultProjects = (): ProjectDefinition[] => {
const clusterConf = useClustersConf();
const clusters = Object.values(clusterConf ?? {});

Expand All @@ -53,6 +55,30 @@ const useProjects = (): ProjectDefinition[] => {
return projects;
};

const useProjects = (): ProjectDefinition[] => {
const defaultProjects = useDefaultProjects();
const projectListProcessors = useSelector(
(state: RootState) => state.projects.projectListProcessors
);

return useMemo(() => {
// Start with the default namespace-based projects
let projects = defaultProjects;

// Apply processors in order, each one can modify the project list
for (const processor of projectListProcessors) {
try {
projects = processor.processor(projects);
} catch (error) {
console.error(`Project List: Error in list processor ${processor.id}:`, error);
}
}

console.log('Final projects:', projects);
Copy link

Copilot AI Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement should be removed from production code.

Suggested change
console.log('Final projects:', projects);

Copilot uses AI. Check for mistakes.

return projects;
}, [defaultProjects, projectListProcessors]);
};

export const useProject = (name: string) => {
const clusterConf = useClustersConf();
const clusters = Object.values(clusterConf ?? {});
Expand Down
53 changes: 52 additions & 1 deletion frontend/src/plugin/registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ import {
addCustomCreateProject,
addDetailsTab,
addOverviewSection,
addProjectListProcessor,
CustomCreateProject,
ProjectDetailsTab,
ProjectListProcessor,
ProjectListProcessorFunction,
ProjectOverviewSection,
} from '../redux/projectsSlice';
import { setRoute, setRouteFilter } from '../redux/routesSlice';
Expand Down Expand Up @@ -161,6 +164,7 @@ export type sectionFunc = (resource: KubeObject) => SectionFuncProps | null | un
// @todo: HeaderActionType should be deprecated.
export type DetailsViewHeaderActionType = HeaderActionType;
export type DetailsViewHeaderActionsProcessor = HeaderActionsProcessor;
export type { ProjectListProcessor, ProjectListProcessorFunction };

export default class Registry {
/**
Expand Down Expand Up @@ -1072,7 +1076,7 @@ export function registerCustomCreateProject(customCreateProject: CustomCreatePro
* @param projectDetailsTab - The tab configuration to register
* @param projectDetailsTab.id - Unique identifier for the tab
* @param projectDetailsTab.label - Display label for the tab
* @param projectDetailsTab.icon - Display icon for the tab
* @param projectDetailsTab.icon - Display icon for the tab (optional)
* @param projectDetailsTab.component - React component to render in the tab content
*
* @example
Expand Down Expand Up @@ -1110,6 +1114,53 @@ export function registerProjectOverviewSection(projectOverviewSection: ProjectOv
store.dispatch(addOverviewSection(projectOverviewSection));
}

/**
* Register a project list processor.
*
* This allows plugins to override or modify how projects are discovered and listed.
* Processors receive the current list of projects (from namespaces or previous processors)
* and can modify, filter, or extend the list as needed.
*
* @param processor - The processor function or processor object to register
*
* @example
* ```tsx
* import { registerProjectListProcessor } from '@kinvolk/headlamp-plugin/lib';
*
* // Add new projects while keeping existing ones
* registerProjectListProcessor((currentProjects) => {
* const newProjects = [
* {
* id: 'my-custom-project',
* namespaces: ['default', 'kube-system'],
* clusters: ['cluster1']
* }
* ];
*
* // Add only if the project doesn't already exist
* const existingIds = currentProjects.map(p => p.id);
* const projectsToAdd = newProjects.filter(p => !existingIds.includes(p.id));
*
* return [...currentProjects, ...projectsToAdd];
* });
*
* // Object-based processor with ID
* registerProjectListProcessor({
* id: 'custom-resource-projects',
* processor: (currentProjects) => {
* // Fetch projects from a Custom Resource or external API
* const customProjects = getProjectsFromCustomResource();
* return [...currentProjects, ...customProjects];
* }
* });
* ```
*/
export function registerProjectListProcessor(
processor: ProjectListProcessor | ProjectListProcessorFunction
) {
store.dispatch(addProjectListProcessor(processor));
}

export {
DefaultAppBarAction,
DefaultDetailsViewSection,
Expand Down
49 changes: 47 additions & 2 deletions frontend/src/redux/projectsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export interface ProjectDefinition {
clusters: string[];
}

export type ProjectListProcessorFunction = (
currentProjects: ProjectDefinition[]
) => ProjectDefinition[];

export interface ProjectListProcessor {
id?: string;
processor: ProjectListProcessorFunction;
}

/** Define custom way to create new Projects */
export interface CustomCreateProject {
id: string;
Expand All @@ -49,22 +58,45 @@ export interface ProjectOverviewSection {
export interface ProjectDetailsTab {
id: string;
label: string;
icon: string | ReactNode;
icon?: string | ReactNode;
component: (props: { project: ProjectDefinition; projectResources: KubeObject[] }) => ReactNode;
}

export interface ProjectsState {
customCreateProject: Record<string, CustomCreateProject>;
overviewSections: Record<string, ProjectOverviewSection>;
detailsTabs: Record<string, ProjectDetailsTab>;
projectListProcessors: ProjectListProcessor[];
}

const initialState: ProjectsState = {
customCreateProject: {},
detailsTabs: {},
overviewSections: {},
projectListProcessors: [],
};

/**
* Normalizes a project list processor by ensuring it has an 'id' and a processor function.
*/
function _normalizeProjectListProcessor(
action: PayloadAction<ProjectListProcessor | ProjectListProcessorFunction>
): ProjectListProcessor {
let processor: ProjectListProcessor = action.payload as ProjectListProcessor;
if (typeof action.payload === 'function') {
processor = {
id: `generated-id-${Date.now().toString(36)}`,
Copy link

Copilot AI Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Date.now() for ID generation could create duplicate IDs if multiple processors are registered simultaneously. Consider using crypto.randomUUID() or a counter-based approach for guaranteed uniqueness.

Copilot uses AI. Check for mistakes.

processor: action.payload,
};
}

if (!processor.id) {
processor.id = `generated-id-${Date.now().toString(36)}`;
Comment on lines +88 to +94
Copy link

Copilot AI Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Date.now() for ID generation could create duplicate IDs if multiple processors are registered simultaneously. Consider using crypto.randomUUID() or a counter-based approach for guaranteed uniqueness.

Suggested change
id: `generated-id-${Date.now().toString(36)}`,
processor: action.payload,
};
}
if (!processor.id) {
processor.id = `generated-id-${Date.now().toString(36)}`;
id: `generated-id-${crypto.randomUUID()}`,
processor: action.payload,
};
}
if (!processor.id) {
processor.id = `generated-id-${crypto.randomUUID()}`;

Copilot uses AI. Check for mistakes.

}

return processor;
}

const projectsSlice = createSlice({
name: 'projects',
initialState,
Expand All @@ -83,9 +115,22 @@ const projectsSlice = createSlice({
addOverviewSection(state, action: PayloadAction<ProjectOverviewSection>) {
state.overviewSections[action.payload.id] = action.payload;
},

/** Register a project list processor */
addProjectListProcessor(
state,
action: PayloadAction<ProjectListProcessor | ProjectListProcessorFunction>
) {
state.projectListProcessors.push(_normalizeProjectListProcessor(action));
},
},
});

export const { addCustomCreateProject, addDetailsTab, addOverviewSection } = projectsSlice.actions;
export const {
addCustomCreateProject,
addDetailsTab,
addOverviewSection,
addProjectListProcessor,
} = projectsSlice.actions;

export default projectsSlice.reducer;
46 changes: 45 additions & 1 deletion plugins/examples/projects/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,54 @@
# Projects customization example

This plugin demonstrates how to customize projects feature
This plugin demonstrates how to customize projects feature including:

- Custom project creation workflows
- Custom project details tabs
- Custom project overview sections
- Custom project list processors (extend or modify project discovery)

```bash
cd plugins/examples/projects
npm start
```

The main code for the example plugin is in [src/index.tsx](src/index.tsx).

## Project List Processors

The `registerProjectListProcessor` function allows plugins to extend or modify how projects are discovered and listed. Processors receive the current list of projects (from namespaces or previous processors) and can:

- Add new projects from Custom Resources, external APIs, or other sources
- Filter existing projects based on conditions
- Modify project properties like namespaces or clusters
- Completely replace the project list if needed

### Key Features

- **Additive by default**: Processors receive existing projects and can extend the list
- **Chainable**: Multiple processors can be registered and will run in sequence
- **Error handling**: Failed processors don't break the application
- **Duplicate prevention**: Easy to check for existing projects by ID

### Example Usage

```typescript
// Add new projects while keeping existing ones
registerProjectListProcessor((currentProjects) => {
const newProjects = [
{
id: 'my-custom-project',
namespaces: ['default', 'kube-system'],
clusters: ['cluster1']
}
];

// Only add projects that don't already exist
const existingIds = currentProjects.map(p => p.id);
const projectsToAdd = newProjects.filter(p => !existingIds.includes(p.id));

return [...currentProjects, ...projectsToAdd];
});
```

This approach ensures backward compatibility while providing maximum flexibility for project customization.
29 changes: 27 additions & 2 deletions plugins/examples/projects/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ApiProxy,
registerCustomCreateProject,
registerProjectDetailsTab,
registerProjectListProcessor,
registerProjectOverviewSection,
} from '@kinvolk/headlamp-plugin/lib';

Expand Down Expand Up @@ -55,10 +56,34 @@ registerCustomCreateProject({
registerProjectDetailsTab({
id: 'my-tab',
label: 'Metrics',
component: ({ project }) => <div>Metrics for project {project.name}</div>,
icon: 'mdi:chart-line',
component: ({ project }) => <div>Metrics for project {project.id}</div>,
});

registerProjectOverviewSection({
id: 'resource-usage',
component: ({ project }) => <div>Custom resource usage for project {project.name}</div>,
component: ({ project }) => <div>Custom resource usage for project {project.id}</div>,
});

// Example 1: Extend the project list with additional projects
// This keeps the existing namespace-based projects and adds new ones
registerProjectListProcessor((currentProjects) => {
const newProjects = [
{
id: 'example-project-1',
namespaces: ['default', 'kube-system'],
clusters: ['cluster1']
},
{
id: 'example-project-2',
namespaces: ['example-ns'],
clusters: ['cluster1', 'cluster2']
}
];

// Only add projects that don't already exist
const existingIds = currentProjects.map(p => p.id);
const projectsToAdd = newProjects.filter(p => !existingIds.includes(p.id));

return [...currentProjects, ...projectsToAdd];
});
6 changes: 6 additions & 0 deletions plugins/headlamp-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import Registry, {
DetailsViewSectionProps,
getHeadlampAPIHeaders,
PluginManager,
ProjectListProcessor,
ProjectListProcessorFunction,
registerAddClusterProvider,
registerAppBarAction,
registerAppLogo,
Expand All @@ -55,6 +57,7 @@ import Registry, {
registerMapSource,
registerPluginSettings,
registerProjectDetailsTab,
registerProjectListProcessor,
registerProjectOverviewSection,
registerResourceTableColumnsProcessor,
registerRoute,
Expand Down Expand Up @@ -101,6 +104,7 @@ export {
registerKindIcon,
registerMapSource,
PluginManager,
registerProjectListProcessor,
registerUIPanel,
registerAppTheme,
registerKubeObjectGlance,
Expand All @@ -115,4 +119,6 @@ export type {
ClusterChooserProps,
DetailsViewSectionProps,
DefaultSidebars,
ProjectListProcessor,
ProjectListProcessorFunction,
};