Skip to main content

Plugin Development

Purpose: For contributors, shows how to create a Headlamp plugin from scratch with scaffold, develop, test, and package steps. Covers both the standalone headlamp-plugin CLI and the openCenter monorepo workflow.

Outcome

By the end of this tutorial you will have a working Headlamp plugin that adds UI functionality, built and tested locally. Choose the path that fits your situation:

  • Standalone plugin — fastest way to get started. Uses the official headlamp-plugin CLI to scaffold, build, and package.
  • Monorepo plugin — for openCenter branding and multi-plugin projects. Uses the headlamp-branding-plugin monorepo with shared tooling.

Prerequisites

  • Node.js >= 20.18.1 (download)
  • npm (comes with Node.js) or pnpm >= 9.0.0 (for monorepo path)
  • A running Headlamp instance — desktop app or development setup
  • Familiarity with React and TypeScript

Path A: Standalone Plugin (headlamp-plugin CLI)

This is the recommended starting point. The headlamp-plugin CLI scaffolds a complete plugin project with build tooling, linting, and TypeScript configuration.

Step 1: Scaffold the Plugin

Run this in your projects directory (not inside Headlamp's plugin installation directory):

npx --yes @kinvolk/headlamp-plugin create my-first-plugin
cd my-first-plugin
npm install

Generated structure:

my-first-plugin/
├── src/
│ └── index.tsx # Main plugin entry point
├── package.json # Plugin metadata and dependencies
├── tsconfig.json # TypeScript configuration
└── README.md

Step 2: Examine the Default Code

Open src/index.tsx:

import { registerAppBarAction } from '@kinvolk/headlamp-plugin/lib';

registerAppBarAction(<span>Hello</span>);

This registers a component in the top-right app bar. The plugin registry (covered below) provides many more registration points.

Step 3: Start Development Mode

npm run start

This watches for file changes and automatically rebuilds the plugin. Open Headlamp (desktop app or dev server) — you should see "Hello" in the top navigation bar.

Step 4: Make a Change

Replace src/index.tsx with something interactive:

import { registerAppBarAction } from '@kinvolk/headlamp-plugin/lib';
import { Button } from '@mui/material';

function HelloButton() {
const handleClick = () => {
alert('Hello from your Headlamp plugin!');
};

return (
<Button variant="outlined" size="small" onClick={handleClick} sx={{ mx: 2 }}>
Hello Headlamp!
</Button>
);
}

registerAppBarAction(<HelloButton />);

Save the file. Headlamp reloads automatically.

Step 5: Code Quality and Testing

The scaffolded project includes quality tools:

npm run format      # Format code
npm run lint # Check for linting issues
npm run lint-fix # Auto-fix linting issues
npm run tsc # Type checking
npm run test # Run tests

Step 6: Build and Package for Production

npm run build
npm run package

This creates a tarball (e.g., my-first-plugin-0.1.0.tar.gz) that can be extracted into the Headlamp plugins directory for deployment.

Path B: Monorepo Plugin (openCenter)

Use this path when building plugins for the openCenter Headlamp deployment, where plugins share tooling and are managed together in the headlamp-branding-plugin monorepo.

Step 1: Scaffold the Plugin

Create the plugin directory inside the monorepo:

mkdir -p plugins/my-plugin/src
mkdir -p plugins/my-plugin/assets
mkdir -p plugins/my-plugin/__tests__

Create plugins/my-plugin/package.json:

{
"name": "@opencenter/headlamp-plugin-my-plugin",
"version": "0.1.0",
"description": "My custom Headlamp plugin",
"main": "dist/main.js",
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"test": "jest",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext .ts,.tsx",
"format": "prettier --write \"src/**/*.{ts,tsx}\""
},
"peerDependencies": {
"@kinvolk/headlamp-plugin": ">=0.7.0 <1.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"ts-loader": "^9.5.1",
"typescript": "^5.7.3",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"copy-webpack-plugin": "^14.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.2.5",
"@types/react": "^18.3.18",
"@types/jest": "^29.5.14"
},
"engines": {
"node": ">=20.0.0"
}
}

Step 2: Add Webpack Configuration

Create plugins/my-plugin/webpack.config.js:

const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = (env, argv) => ({
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
library: { type: 'commonjs2' },
},
externals: {
react: 'react',
'react-dom': 'react-dom',
'@kinvolk/headlamp-plugin/lib': '@kinvolk/headlamp-plugin/lib',
},
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
{ test: /\.(svg|png)$/, type: 'asset/resource' },
],
},
plugins: [
new CopyPlugin({
patterns: [
{ from: 'package.json', to: 'package.json' },
{ from: 'assets', to: 'assets', noErrorOnMissing: true },
],
}),
],
resolve: { extensions: ['.tsx', '.ts', '.js'] },
devtool: argv.mode === 'development' ? 'source-map' : false,
});

The three externals entries are critical. React and the Headlamp plugin library are provided by the host at runtime — bundling them causes version conflicts.

Step 3: Add TypeScript Configuration

Create plugins/my-plugin/tsconfig.json:

{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "__tests__"]
}

Step 4: Write the Plugin Entry Point

Create plugins/my-plugin/src/index.tsx:

import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib';
import MyPage from './components/MyPage';

// Add a sidebar entry
registerSidebarEntry({
parent: null,
name: 'my-plugin',
label: 'My Plugin',
url: '/my-plugin',
});

// Register the route
registerRoute({
path: '/my-plugin',
component: () => <MyPage />,
name: 'my-plugin',
sidebar: 'My Plugin',
});

Create plugins/my-plugin/src/components/MyPage.tsx:

import React from 'react';

const MyPage: React.FC = () => (
<div style={{ padding: '24px' }}>
<h1>My Plugin Page</h1>
<p>This page was added by the my-plugin Headlamp plugin.</p>
</div>
);

export default MyPage;

Step 5: Install Dependencies and Build

From the workspace root:

pnpm install
pnpm --filter @opencenter/headlamp-plugin-my-plugin run build

Confirm the output:

ls plugins/my-plugin/dist/
# Expected: main.js package.json assets/

Step 6: Test Locally with Podman

Run Headlamp with the plugin mounted:

podman run -d \
--name headlamp-dev \
-p 4466:4466 \
-v $(pwd)/plugins/my-plugin/dist:/headlamp/plugins/my-plugin \
-v ~/.kube/config:/root/.kube/config:ro \
ghcr.io/headlamp-k8s/headlamp:latest

Open http://localhost:4466. You should see "My Plugin" in the sidebar. Click it to load the custom page.

For a faster iteration loop, run webpack in watch mode in one terminal and restart the container after changes:

# Terminal 1
pnpm --filter @opencenter/headlamp-plugin-my-plugin run dev

# Terminal 2 (after saving changes)
podman restart headlamp-dev

Clean up when done:

podman stop headlamp-dev && podman rm headlamp-dev

Step 7: Add Tests

Create plugins/my-plugin/jest.config.js:

module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src', '<rootDir>/__tests__'],
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
};

Create plugins/my-plugin/src/__tests__/MyPage.test.tsx:

import React from 'react';
import { render, screen } from '@testing-library/react';
import MyPage from '../components/MyPage';

test('renders the plugin page heading', () => {
render(<MyPage />);
expect(screen.getByText('My Plugin Page')).toBeInTheDocument();
});

Run tests:

pnpm --filter @opencenter/headlamp-plugin-my-plugin test

Plugin Registry API

The plugin registry is the central system for extending Headlamp. All registration functions are imported from @kinvolk/headlamp-plugin/lib.

import {
registerAppBarAction,
registerAppLogo,
registerRoute,
registerSidebarEntry,
registerSidebarEntryFilter,
registerDetailsViewSection,
registerDetailsViewSectionsProcessor,
registerDetailsViewHeaderAction,
registerClusterChooser,
registerResourceTableColumnsProcessor,
registerHeadlampEventCallback,
registerPluginSettings,
registerAppTheme,
registerRouteFilter,
registerUIPanel,
registerProjectDetailsTab,
registerProjectOverviewSection,
} from '@kinvolk/headlamp-plugin/lib';

Registration Functions

FunctionWhat It Does
registerAppBarActionAdd a component to the top-right app bar.
registerAppLogoReplace the logo in the top-left corner.
registerRouteShow a component at a given URL path.
registerRouteFilterFilter or modify registered routes.
registerSidebarEntryAdd items to the left sidebar navigation.
registerSidebarEntryFilterRemove or modify sidebar items.
registerDetailsViewSectionAdd a component to the bottom of a resource details view.
registerDetailsViewSectionsProcessorAdd, remove, update, or reorder sections in a details view.
registerDetailsViewHeaderActionAdd a component to the top-right of a details view.
registerClusterChooserReplace the cluster chooser button in the app bar.
registerResourceTableColumnsProcessorAdd, remove, update, or reorder columns in resource tables.
registerHeadlampEventCallbackReact to Headlamp lifecycle events (e.g., resource changes).
registerPluginSettingsDefine user-configurable settings for the plugin.
registerAppThemeAdd a custom theme (light or dark base). Appears in General Settings.
registerUIPanelRegister a side panel (top, left, right, or bottom).
registerProjectDetailsTabAdd custom tabs to the project details view.
registerProjectOverviewSectionAdd custom sections to the project overview page.

For desktop app deployments, Headlamp.setAppMenu adds custom menus and Headlamp.setCluster configures clusters dynamically (without a config file).

Plugin Lib Modules

The @kinvolk/headlamp-plugin/lib package exposes several modules beyond the registration functions:

ModulePurpose
K8sKubernetes API access — resource classes, hooks (useList, useGet), and CRUD operations.
CommonComponentsReusable React components used throughout the Headlamp UI (SectionBox, etc.).
NotificationCreate and dispatch notifications to the Headlamp notification system.
RouterGet or generate routes programmatically.

Kubernetes API Access

Access Kubernetes resources using the K8s module:

import { K8s } from '@kinvolk/headlamp-plugin/lib';

function PodList() {
const [pods, error] = K8s.ResourceClasses.Pod.useList();

if (error) return <div>Error loading pods</div>;
if (!pods) return <div>Loading...</div>;

return (
<div>
<h3>Pods ({pods.length})</h3>
{pods.map(pod => (
<div key={pod.metadata.uid}>{pod.metadata.name}</div>
))}
</div>
);
}

Shared Dependencies

Headlamp provides common libraries at runtime. Plugins can import these normally in code, but they are not bundled into the plugin output — Headlamp injects them via the pluginLib global. Do not add these to your plugin's dependencies in package.json:

  • React and React DOM
  • React Router
  • Redux and React-Redux
  • Material-UI (@mui/material, @mui/lab)
  • Lodash
  • Notistack (snackbar notifications)
  • Monaco Editor
  • @iconify/react
  • Recharts

If your plugin imports a library that Headlamp already provides, the build process replaces it with the host version. Version mismatches between your plugin and Headlamp's bundled version can cause runtime errors.

Troubleshooting

Plugin Not Loading

  • Confirm Headlamp is running and accessible.
  • Check the browser console for JavaScript errors.
  • Verify package.json has correct metadata (name, version, main).
  • For standalone plugins, confirm npm run start is running in the plugin directory.

Changes Not Reflecting

  • Confirm the dev server (npm run start or webpack --watch) is running without errors.
  • Remove the installed plugin from Headlamp's plugins folder and re-run npm run start.
  • Restart Headlamp if hot reload does not pick up changes.

Build Errors

  • Run npm run lint (or pnpm run lint) to check for code issues.
  • Run npm run tsc to surface TypeScript errors.
  • Verify all imports resolve — shared dependencies should not be in dependencies.

Hot Reloading Issues

  • Restart the dev server.
  • Avoid multiple Headlamp tabs when running in development mode.
  • Clear browser cache.
  • Check file permissions in the plugin directory.

Check Your Work

  • npm run build (or pnpm run build) produces output without errors
  • Tests pass
  • Linting reports no issues
  • The plugin loads in Headlamp (UI element visible, page renders if applicable)
  • Browser console shows no errors related to the plugin

Next Steps