Files
headlamp-rook-plugin/src/index.tsx
T
DevContainer User 62c24e3857 fix: register AppBarClusterBadge, fix CSI label mismatch, improve accessibility and theme support
- Register AppBarClusterBadge via registerAppBarAction (was dead code)
- Add Rook 1.12+ CSI pod labels to CephPodDetailSection alongside legacy labels
- Add sidebar entries for Storage Classes and Volumes pages
- Add role="dialog", aria-modal, aria-labelledby, and Escape key to all detail drawers
- Replace hardcoded hex colors with CSS custom properties for dark/light theme compat
- Remove duplicate parseStorageToBytes from OverviewPage (import from k8s.ts)
- Add endpoints field to CephObjectStoreStatus interface (remove unsafe cast)
- Use ROOK_CEPH_API_GROUP/VERSION constants in API URL construction
- Hoist extractJsonData to module level
- Remove dead extractPoolFromVolumeHandle function
- Fix redundant storageClasses.length guard in OverviewPage
- Fix lint indent warnings
- Update CLAUDE.md and CHANGELOG.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:55:37 +00:00

279 lines
7.6 KiB
TypeScript

/**
* headlamp-rook-plugin — entry point.
*
* Registers sidebar entries, routes, detail view sections, table column
* processors, and app bar action for Rook-Ceph visibility in Headlamp.
*/
import {
registerAppBarAction,
registerDetailsViewSection,
registerResourceTableColumnsProcessor,
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import React from 'react';
import { RookCephDataProvider } from './api/RookCephDataContext';
import AppBarClusterBadge from './components/AppBarClusterBadge';
import BlockPoolsPage from './components/BlockPoolsPage';
import CephPodDetailSection from './components/CephPodDetailSection';
import FilesystemsPage from './components/FilesystemsPage';
import {
buildPVColumns,
buildStorageClassColumns,
} from './components/integrations/StorageClassColumns';
import ObjectStoresPage from './components/ObjectStoresPage';
import OverviewPage from './components/OverviewPage';
import PodsPage from './components/PodsPage';
import PVCDetailSection from './components/PVCDetailSection';
import PVDetailSection from './components/PVDetailSection';
import StorageClassesPage from './components/StorageClassesPage';
import VolumesPage from './components/VolumesPage';
// ---------------------------------------------------------------------------
// Sidebar entries
// ---------------------------------------------------------------------------
registerSidebarEntry({
parent: null,
name: 'rook-ceph',
label: 'Rook',
url: '/rook-ceph',
icon: 'mdi:database-cog',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-overview',
label: 'Overview',
url: '/rook-ceph',
icon: 'mdi:view-dashboard',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-blockpools',
label: 'Block Pools',
url: '/rook-ceph/block-pools',
icon: 'mdi:database',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-filesystems',
label: 'Filesystems',
url: '/rook-ceph/filesystems',
icon: 'mdi:folder-network',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-objectstores',
label: 'Object Stores',
url: '/rook-ceph/object-stores',
icon: 'mdi:bucket',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-storage-classes',
label: 'Storage Classes',
url: '/rook-ceph/storage-classes',
icon: 'mdi:database-settings',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-volumes',
label: 'Volumes',
url: '/rook-ceph/volumes',
icon: 'mdi:harddisk',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-pods',
label: 'Pods',
url: '/rook-ceph/pods',
icon: 'mdi:cube-outline',
});
// ---------------------------------------------------------------------------
// App bar action — cluster health badge
// ---------------------------------------------------------------------------
registerAppBarAction(() => (
<RookCephDataProvider>
<AppBarClusterBadge />
</RookCephDataProvider>
));
// ---------------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------------
registerRoute({
path: '/rook-ceph',
sidebar: 'rook-ceph-overview',
name: 'rook-ceph-overview',
exact: true,
component: () => (
<RookCephDataProvider>
<OverviewPage />
</RookCephDataProvider>
),
});
registerRoute({
path: '/rook-ceph/block-pools',
sidebar: 'rook-ceph-blockpools',
name: 'rook-ceph-blockpools',
exact: true,
component: () => (
<RookCephDataProvider>
<BlockPoolsPage />
</RookCephDataProvider>
),
});
registerRoute({
path: '/rook-ceph/filesystems',
sidebar: 'rook-ceph-filesystems',
name: 'rook-ceph-filesystems',
exact: true,
component: () => (
<RookCephDataProvider>
<FilesystemsPage />
</RookCephDataProvider>
),
});
registerRoute({
path: '/rook-ceph/object-stores',
sidebar: 'rook-ceph-objectstores',
name: 'rook-ceph-objectstores',
exact: true,
component: () => (
<RookCephDataProvider>
<ObjectStoresPage />
</RookCephDataProvider>
),
});
registerRoute({
path: '/rook-ceph/storage-classes',
sidebar: 'rook-ceph-storage-classes',
name: 'rook-ceph-storage-classes',
exact: true,
component: () => (
<RookCephDataProvider>
<StorageClassesPage />
</RookCephDataProvider>
),
});
registerRoute({
path: '/rook-ceph/volumes',
sidebar: 'rook-ceph-volumes',
name: 'rook-ceph-volumes',
exact: true,
component: () => (
<RookCephDataProvider>
<VolumesPage />
</RookCephDataProvider>
),
});
registerRoute({
path: '/rook-ceph/pods',
sidebar: 'rook-ceph-pods',
name: 'rook-ceph-pods',
exact: true,
component: () => (
<RookCephDataProvider>
<PodsPage />
</RookCephDataProvider>
),
});
// ---------------------------------------------------------------------------
// Detail view section — PVC pages
// ---------------------------------------------------------------------------
registerDetailsViewSection(({ resource }) => {
if (resource?.kind !== 'PersistentVolumeClaim') return null;
return (
<RookCephDataProvider>
<PVCDetailSection resource={resource} />
</RookCephDataProvider>
);
});
// ---------------------------------------------------------------------------
// Detail view section — PV pages
// ---------------------------------------------------------------------------
registerDetailsViewSection(({ resource }) => {
if (resource?.kind !== 'PersistentVolume') return null;
return <PVDetailSection resource={resource} />;
});
// ---------------------------------------------------------------------------
// Detail view section — Pod pages (Rook-Ceph daemon pods only)
// ---------------------------------------------------------------------------
registerDetailsViewSection(({ resource }) => {
if (resource?.kind !== 'Pod') return null;
return <CephPodDetailSection resource={resource} />;
});
// ---------------------------------------------------------------------------
// Table column processors — native StorageClass and PV tables
// ---------------------------------------------------------------------------
// Merges incoming columns into existing ones by label.
// If a column with the same label already exists, the incoming getValue/render
// takes priority and falls back to the existing one (for mixed-driver tables).
function mergeColumns<T>(
existing: T[],
incoming: Array<{
label: string;
getValue: (r: unknown) => unknown;
render: (r: unknown) => React.ReactNode;
}>
): T[] {
type ObjCol = {
label: string;
getValue: (r: unknown) => unknown;
render: (r: unknown) => React.ReactNode;
};
const isObjCol = (c: unknown): c is ObjCol => typeof c === 'object' && c !== null && 'label' in c;
const result = [...existing];
const toAppend: typeof incoming = [];
for (const col of incoming) {
const idx = result.findIndex(c => isObjCol(c) && (c as ObjCol).label === col.label);
if (idx !== -1) {
const prev = result[idx] as ObjCol;
result[idx] = {
label: col.label,
getValue: (r: unknown) => col.getValue(r) ?? prev.getValue(r),
render: (r: unknown) => (col.getValue(r) !== null ? col.render(r) : prev.render(r)),
} as unknown as T;
} else {
toAppend.push(col);
}
}
return [...result, ...(toAppend as unknown as T[])];
}
registerResourceTableColumnsProcessor(({ id, columns }) => {
if (id === 'headlamp-storageclasses') {
return mergeColumns(columns, buildStorageClassColumns());
}
if (id === 'headlamp-persistentvolumes') {
return mergeColumns(columns, buildPVColumns());
}
return columns;
});