Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bd81ddfa8 | |||
| 62c24e3857 | |||
| fea6df6719 | |||
| 041e7c1f19 | |||
| dc936fb786 |
@@ -0,0 +1,320 @@
|
|||||||
|
---
|
||||||
|
name: headlamp-plugin-developer
|
||||||
|
description: Use when building, extending, debugging, or reviewing Headlamp Kubernetes dashboard plugins. Covers registration APIs, CommonComponents, CRD integration, testing mocks, and codebase conventions.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, WebSearch
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior Headlamp plugin engineer. You produce code matching this codebase's exact conventions. Before writing new code, read `CLAUDE.md` and review existing files in `src/` to understand established patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Registration Functions
|
||||||
|
|
||||||
|
All from `@kinvolk/headlamp-plugin/lib`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerRoute({
|
||||||
|
path: string; // React Router path (e.g., '/myresource/:namespace?/:name?')
|
||||||
|
sidebar?: string; // Sidebar entry name to highlight
|
||||||
|
component: () => JSX.Element; // Arrow function wrapper required
|
||||||
|
exact?: boolean;
|
||||||
|
name?: string; // Used by Link's routeName prop
|
||||||
|
}): void
|
||||||
|
|
||||||
|
registerSidebarEntry({
|
||||||
|
parent: string | null; // null = top-level
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
icon?: string; // Iconify ID (e.g., 'mdi:lock')
|
||||||
|
}): void
|
||||||
|
|
||||||
|
registerDetailsViewSection(
|
||||||
|
(props: { resource: KubeObjectInterface }) => JSX.Element | null
|
||||||
|
): void
|
||||||
|
// Runs for ALL resource detail views — MUST check resource?.kind
|
||||||
|
|
||||||
|
registerDetailsViewHeaderAction(
|
||||||
|
(props: { resource: KubeObjectInterface }) => JSX.Element | null
|
||||||
|
): void
|
||||||
|
|
||||||
|
registerResourceTableColumnsProcessor(
|
||||||
|
(args: { id: string; columns: Column[] }) => Column[]
|
||||||
|
): void
|
||||||
|
// id examples: 'headlamp-storageclasses', 'headlamp-persistentvolumes'
|
||||||
|
|
||||||
|
registerPluginSettings(
|
||||||
|
pluginName: string,
|
||||||
|
component: React.ComponentType<{
|
||||||
|
data?: Record<string, string | number | boolean>;
|
||||||
|
onDataChange?: (data: Record<string, string | number | boolean>) => void;
|
||||||
|
}>,
|
||||||
|
showSaveButton?: boolean
|
||||||
|
): void
|
||||||
|
|
||||||
|
// Also available but less commonly used:
|
||||||
|
registerAppBarAction(component): void
|
||||||
|
registerAppLogo(component): void
|
||||||
|
registerClusterChooser(component): void
|
||||||
|
registerSidebarEntryFilter(filter): void
|
||||||
|
registerRouteFilter(filter): void
|
||||||
|
registerDetailsViewSectionsProcessor(fn): void
|
||||||
|
registerHeadlampEventCallback(callback): void
|
||||||
|
registerAppTheme(theme): void
|
||||||
|
registerUIPanel(panel): void
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## K8s Module
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
```
|
||||||
|
|
||||||
|
### KubeObject Base Class
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class KubeObject<T extends KubeObjectInterface> {
|
||||||
|
jsonData: T; // Raw K8s JSON — use this for spec/status access
|
||||||
|
metadata: KubeMetadata;
|
||||||
|
kind: string;
|
||||||
|
|
||||||
|
getAge(): string;
|
||||||
|
getName(): string;
|
||||||
|
getNamespace(): string | undefined;
|
||||||
|
delete(force?: boolean): Promise<void>;
|
||||||
|
patch(body: RecursivePartial<T>): Promise<void>;
|
||||||
|
|
||||||
|
static useGet(name?, namespace?): [item: T | null, error: ApiError | null];
|
||||||
|
static useList(opts?: { namespace?: string }): [items: T[], error: ApiError | null, loading: boolean];
|
||||||
|
static apiEndpoint: ApiClient | ApiWithNamespaceClient;
|
||||||
|
static className: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL**: Resource hooks return class instances. Raw K8s JSON lives under `.jsonData`. Access fields via `.jsonData.spec`, `.jsonData.status`, or typed getters.
|
||||||
|
|
||||||
|
### ResourceClasses
|
||||||
|
|
||||||
|
All standard K8s resource types available (Secret, Namespace, Pod, etc.):
|
||||||
|
```typescript
|
||||||
|
const [secrets, error, loading] = K8s.ResourceClasses.Secret.useList({ namespace: 'default' });
|
||||||
|
const [secret, error] = K8s.ResourceClasses.Secret.useGet('my-secret', 'default');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ApiProxy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
|
||||||
|
ApiProxy.request(
|
||||||
|
path: string,
|
||||||
|
options?: {
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||||
|
body?: string; // JSON.stringify'd
|
||||||
|
isJSON?: boolean; // false for non-JSON (logs, metrics)
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
): Promise<unknown>
|
||||||
|
|
||||||
|
// CRD endpoint factories
|
||||||
|
ApiProxy.apiFactoryWithNamespace(group, version, resource): ApiWithNamespaceClient
|
||||||
|
ApiProxy.apiFactory(group, version, resource): ApiClient
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service proxy URL** (accessing in-cluster services):
|
||||||
|
```
|
||||||
|
/api/v1/namespaces/${ns}/services/http:${name}:${port}/proxy${path}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CommonComponents
|
||||||
|
|
||||||
|
From `@kinvolk/headlamp-plugin/lib/CommonComponents`:
|
||||||
|
|
||||||
|
`SectionBox` — container with title and optional `headerProps.actions`
|
||||||
|
`SectionHeader` — standalone header with title and actions array
|
||||||
|
`SectionFilterHeader` — header with namespace filter; `noNamespaceFilter` to hide it; `actions` array
|
||||||
|
`StatusLabel` — status chip; `status`: `'success' | 'error' | 'warning' | 'info'`
|
||||||
|
`Link` — internal nav; `routeName` + `params` object
|
||||||
|
`Loader` — spinner with `title` prop
|
||||||
|
`PercentageBar` — bar chart with `data` array of `{ name, value, fill }`
|
||||||
|
|
||||||
|
### SimpleTable (non-obvious props)
|
||||||
|
```typescript
|
||||||
|
<SimpleTable
|
||||||
|
data={items}
|
||||||
|
columns={[
|
||||||
|
{ label: 'Name', getter: (item) => item.metadata.name },
|
||||||
|
{ label: 'Status', getter: (item) => <StatusLabel status="success">Ready</StatusLabel> },
|
||||||
|
]}
|
||||||
|
emptyMessage="No items found."
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### NameValueTable (non-obvious props)
|
||||||
|
```typescript
|
||||||
|
<NameValueTable
|
||||||
|
rows={[
|
||||||
|
{ name: 'Key', value: 'display value' },
|
||||||
|
{ name: 'Hidden', value: 'x', hide: true },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ConfigStore
|
||||||
|
```typescript
|
||||||
|
import { ConfigStore } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
const store = new ConfigStore<MyConfig>('plugin-name');
|
||||||
|
store.get(): MyConfig;
|
||||||
|
store.update(partial: Partial<MyConfig>): void;
|
||||||
|
store.useConfig(): () => MyConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-bundled (no package.json entry needed)
|
||||||
|
react, react-dom, react-router-dom, @iconify/react, react-redux, @material-ui/core, @material-ui/styles, lodash, notistack, recharts, monaco-editor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRD Class Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
const { apiFactoryWithNamespace } = ApiProxy;
|
||||||
|
const { KubeObject } = K8s.cluster;
|
||||||
|
type KubeObjectInterface = K8s.cluster.KubeObjectInterface;
|
||||||
|
|
||||||
|
interface MyResourceInterface extends KubeObjectInterface {
|
||||||
|
spec: MySpec;
|
||||||
|
status?: MyStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MyResource extends KubeObject<MyResourceInterface> {
|
||||||
|
static apiEndpoint = apiFactoryWithNamespace('mygroup.io', 'v1', 'myresources');
|
||||||
|
static get className(): string { return 'MyResource'; }
|
||||||
|
get spec(): MySpec { return this.jsonData.spec; }
|
||||||
|
get status(): MyStatus | undefined { return this.jsonData.status; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Entry Point Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Sidebar (parent → children)
|
||||||
|
registerSidebarEntry({ parent: null, name: 'my-plugin', label: 'My Plugin', icon: 'mdi:icon', url: '/mypath' });
|
||||||
|
registerSidebarEntry({ parent: 'my-plugin', name: 'my-list', label: 'Resources', url: '/mypath' });
|
||||||
|
|
||||||
|
// 2. Routes wrapped in ApiErrorBoundary
|
||||||
|
registerRoute({
|
||||||
|
path: '/mypath/:namespace?/:name?',
|
||||||
|
sidebar: 'my-list',
|
||||||
|
component: () => <ApiErrorBoundary><MyListPage /></ApiErrorBoundary>,
|
||||||
|
exact: true, name: 'my-resource',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Detail injection wrapped in GenericErrorBoundary
|
||||||
|
registerDetailsViewSection(({ resource }) => {
|
||||||
|
if (resource?.kind !== 'Secret') return null;
|
||||||
|
return <GenericErrorBoundary><MySection resource={resource} /></GenericErrorBoundary>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Settings
|
||||||
|
registerPluginSettings('my-plugin', SettingsPage, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Headlamp Test Mocks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||||
|
ApiProxy: { request: vi.fn().mockResolvedValue({}) },
|
||||||
|
K8s: { ResourceClasses: {}, cluster: { KubeObject: class {} } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||||
|
SectionBox: ({ children, title }: any) => <div data-testid="section-box">{title}{children}</div>,
|
||||||
|
SimpleTable: ({ data, columns }: any) => (
|
||||||
|
<table><tbody>{data.map((d: any, i: number) =>
|
||||||
|
<tr key={i}>{columns.map((c: any, j: number) => <td key={j}>{c.getter(d)}</td>)}</tr>
|
||||||
|
)}</tbody></table>
|
||||||
|
),
|
||||||
|
NameValueTable: ({ rows }: any) => (
|
||||||
|
<dl>{rows.filter((r: any) => !r.hide).map((r: any) =>
|
||||||
|
<div key={r.name}><dt>{r.name}</dt><dd>{r.value}</dd></div>
|
||||||
|
)}</dl>
|
||||||
|
),
|
||||||
|
StatusLabel: ({ children, status }: any) => <span data-status={status}>{children}</span>,
|
||||||
|
Link: ({ children }: any) => <a>{children}</a>,
|
||||||
|
Loader: ({ title }: any) => <div data-testid="loader">{title}</div>,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theming & Dark Mode
|
||||||
|
|
||||||
|
Headlamp supports light and dark themes. **Never hardcode colors.** Use CSS custom properties with light-mode fallbacks:
|
||||||
|
|
||||||
|
### Required CSS variables for inline styles
|
||||||
|
```typescript
|
||||||
|
// Text
|
||||||
|
color: 'var(--mui-palette-text-primary)'
|
||||||
|
color: 'var(--mui-palette-text-secondary, #666)'
|
||||||
|
|
||||||
|
// Backgrounds
|
||||||
|
backgroundColor: 'var(--mui-palette-background-default, #fafafa)'
|
||||||
|
backgroundColor: 'var(--mui-palette-background-paper, #fff)'
|
||||||
|
|
||||||
|
// Borders
|
||||||
|
border: '1px solid var(--mui-palette-divider, #e0e0e0)'
|
||||||
|
|
||||||
|
// Interactive
|
||||||
|
backgroundColor: 'var(--mui-palette-primary-main, #1976d2)'
|
||||||
|
color: 'var(--mui-palette-primary-contrastText, #fff)'
|
||||||
|
|
||||||
|
// Disabled states
|
||||||
|
backgroundColor: 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
|
||||||
|
color: 'var(--mui-palette-action-disabled, #9e9e9e)'
|
||||||
|
|
||||||
|
// Links
|
||||||
|
color: 'var(--link-color, #1976d2)'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common mistakes to avoid
|
||||||
|
- **NEVER** use raw `#fff`, `#000`, `#333`, `#666` etc. without wrapping in `var(--mui-palette-*)`
|
||||||
|
- **NEVER** use `rgba(0,0,0,0.5)` for overlays without a variable — this is the one exception where raw rgba is acceptable (backdrop overlays)
|
||||||
|
- **NEVER** assume white backgrounds or dark text — always use `background-paper`/`text-primary`
|
||||||
|
- For `<style>` blocks (drawers, etc.), use the same CSS variables in the stylesheet
|
||||||
|
- Fallback values after the comma are for environments where the variable isn't set — always use the light-mode default
|
||||||
|
|
||||||
|
### Form inputs in custom components
|
||||||
|
```typescript
|
||||||
|
const inputStyle = {
|
||||||
|
border: '1px solid var(--mui-palette-divider, #ccc)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||||
|
color: 'var(--mui-palette-text-primary)',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Quality Rules
|
||||||
|
|
||||||
|
1. **Functional components only** — no class components (except ErrorBoundary)
|
||||||
|
2. **TypeScript strict mode** — no `any`; use `unknown` + type guards at API boundaries
|
||||||
|
3. **Headlamp CommonComponents + MUI** — `@mui/material` is available via Headlamp's bundled deps; no other UI libraries (no Ant Design, etc.)
|
||||||
|
4. **Inline CSS only** — `style={{}}` props, CSS variables (`var(--mui-palette-*)`) for theming
|
||||||
|
5. **Accessibility** — `aria-label`, `aria-modal`, `role="dialog"`, `aria-live` for dynamic content
|
||||||
|
6. **Cancellation safety** — async effects must check a `cancelled` flag
|
||||||
|
7. **Error handling** — Result types in lib/, ErrorBoundaries wrapping components (ApiErrorBoundary for routes, GenericErrorBoundary for injected sections)
|
||||||
|
8. **Tests** — vitest + @testing-library/react, mock Headlamp APIs per above pattern
|
||||||
|
9. Run `npm run tsc` and `npm test` after implementation changes
|
||||||
@@ -47,12 +47,13 @@ jobs:
|
|||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
- name: Update version in package.json
|
- name: Update version in package.json
|
||||||
run: npm version ${{ inputs.version }} --no-git-tag-version
|
run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
- name: Update artifacthub-pkg.yml
|
- name: Update artifacthub-pkg.yml
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ inputs.version }}"
|
VERSION="${{ inputs.version }}"
|
||||||
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/headlamp-rook-plugin-${VERSION}.tar.gz"
|
PKG_NAME=$(jq -r .name package.json)
|
||||||
|
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${PKG_NAME}-${VERSION}.tar.gz"
|
||||||
sed -i "s/^version:.*/version: \"${VERSION}\"/" artifacthub-pkg.yml
|
sed -i "s/^version:.*/version: \"${VERSION}\"/" artifacthub-pkg.yml
|
||||||
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
|
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
|
||||||
|
|
||||||
@@ -68,12 +69,15 @@ jobs:
|
|||||||
- name: Prepare release tarball
|
- name: Prepare release tarball
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ inputs.version }}"
|
VERSION="${{ inputs.version }}"
|
||||||
TARBALL="headlamp-rook-plugin-${VERSION}.tar.gz"
|
PKG_NAME=$(jq -r .name package.json)
|
||||||
GENERATED=$(ls *.tar.gz)
|
TARBALL="${PKG_NAME}-${VERSION}.tar.gz"
|
||||||
if [ "$GENERATED" != "$TARBALL" ]; then
|
if [ ! -f "$TARBALL" ]; then
|
||||||
mv "$GENERATED" "$TARBALL"
|
echo "Error: Expected tarball $TARBALL not found"
|
||||||
|
ls -la *.tar.gz 2>/dev/null || echo "No .tar.gz files found"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
|
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
|
||||||
|
echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Validate tarball
|
- name: Validate tarball
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+34
-8
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.6] - 2026-03-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **AppBarClusterBadge registration** — cluster health badge in the Headlamp top nav bar was implemented but never registered; now wired up via `registerAppBarAction`
|
||||||
|
- **CSI pod label mismatch** — `CephPodDetailSection` now recognizes both legacy (`csi-rbdplugin-provisioner`) and Rook 1.12+ (`rook-ceph.rbd.csi.ceph.com-ctrlplugin`) CSI pod labels
|
||||||
|
- **Duplicate `parseStorageToBytes`** — removed local copy from `OverviewPage`; imports shared implementation from `k8s.ts`
|
||||||
|
- **ObjectStore endpoint type safety** — added `endpoints` field to `CephObjectStoreStatus` interface, eliminating unsafe double-cast
|
||||||
|
- **Redundant guard** — removed duplicate `storageClasses.length > 0` condition in `OverviewPage`
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Sidebar entries** for Storage Classes and Volumes pages — both are now navigable from the sidebar instead of only accessible via direct URL
|
||||||
|
- **Drawer accessibility** — all detail panel drawers now include `role="dialog"`, `aria-modal`, `aria-labelledby`, and Escape key handling
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Theme-aware colors** — replaced hardcoded hex colors with CSS custom properties (`var(--mui-palette-*)`) in `AppBarClusterBadge`, `ClusterStatusCard`, and `OverviewPage` for dark/light theme compatibility
|
||||||
|
- **API URL constants** — `RookCephDataContext` now uses `ROOK_CEPH_API_GROUP` and `ROOK_CEPH_API_VERSION` constants instead of string literals
|
||||||
|
- **`extractJsonData` hoisted** — moved from inside the component render body to module-level function
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **Dead code** — removed unused `extractPoolFromVolumeHandle` function from `k8s.ts`
|
||||||
|
|
||||||
## [0.2.2] - 2026-02-19
|
## [0.2.2] - 2026-02-19
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -72,11 +97,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- TypeScript strict mode with zero `any` types
|
- TypeScript strict mode with zero `any` types
|
||||||
- ESLint + Prettier code quality tooling
|
- ESLint + Prettier code quality tooling
|
||||||
|
|
||||||
[Unreleased]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.2...HEAD
|
[Unreleased]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.6...HEAD
|
||||||
[0.2.2]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.1...v0.2.2
|
[0.2.6]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.5...v0.2.6
|
||||||
[0.2.1]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.0...v0.2.1
|
[0.2.2]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.1...v0.2.2
|
||||||
[0.2.0]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.3...v0.2.0
|
[0.2.1]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.0...v0.2.1
|
||||||
[0.1.3]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.2...v0.1.3
|
[0.2.0]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.3...v0.2.0
|
||||||
[0.1.2]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.1...v0.1.2
|
[0.1.3]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.2...v0.1.3
|
||||||
[0.1.1]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.0...v0.1.1
|
[0.1.2]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.1...v0.1.2
|
||||||
[0.1.0]: https://github.com/cpfarhood/headlamp-rook-plugin/releases/tag/v0.1.0
|
[0.1.1]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.0...v0.1.1
|
||||||
|
[0.1.0]: https://github.com/privilegedescalation/headlamp-rook-plugin/releases/tag/v0.1.0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
Headlamp plugin for Rook-Ceph cluster visibility.
|
Headlamp plugin for Rook-Ceph cluster visibility.
|
||||||
|
|
||||||
- **Plugin name**: `headlamp-rook-plugin`
|
- **Plugin name**: `rook`
|
||||||
- **Rook-Ceph API group**: `ceph.rook.io/v1`
|
- **Rook-Ceph API group**: `ceph.rook.io/v1`
|
||||||
- **Default namespace**: `rook-ceph`
|
- **Default namespace**: `rook-ceph`
|
||||||
- **Reference plugin**: `../headlamp-tns-csi-plugin`
|
- **Reference plugin**: `../headlamp-tns-csi-plugin`
|
||||||
@@ -33,7 +33,7 @@ All tests and `tsc` must pass before committing.
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, etc.
|
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, registerAppBarAction, etc.
|
||||||
├── api/
|
├── api/
|
||||||
│ ├── k8s.ts # Types + filtering helpers (ceph.rook.io)
|
│ ├── k8s.ts # Types + filtering helpers (ceph.rook.io)
|
||||||
│ └── RookCephDataContext.tsx # Shared React context provider
|
│ └── RookCephDataContext.tsx # Shared React context provider
|
||||||
@@ -46,7 +46,7 @@ src/
|
|||||||
├── FilesystemsPage.tsx
|
├── FilesystemsPage.tsx
|
||||||
├── ObjectStoresPage.tsx
|
├── ObjectStoresPage.tsx
|
||||||
├── ClusterStatusCard.tsx
|
├── ClusterStatusCard.tsx
|
||||||
├── AppBarClusterBadge.tsx
|
├── AppBarClusterBadge.tsx # Cluster health badge in Headlamp top nav bar
|
||||||
├── PVCDetailSection.tsx # Injected into Headlamp PVC detail view
|
├── PVCDetailSection.tsx # Injected into Headlamp PVC detail view
|
||||||
├── PVDetailSection.tsx # Injected into Headlamp PV detail view
|
├── PVDetailSection.tsx # Injected into Headlamp PV detail view
|
||||||
├── CephPodDetailSection.tsx # Injected into Headlamp Pod detail view
|
├── CephPodDetailSection.tsx # Injected into Headlamp Pod detail view
|
||||||
@@ -71,7 +71,9 @@ All pages consume data exclusively via `useRookCephContext()`. The provider is r
|
|||||||
- RBD provisioner: `rook-ceph.rbd.csi.ceph.com`
|
- RBD provisioner: `rook-ceph.rbd.csi.ceph.com`
|
||||||
- CephFS provisioner: `rook-ceph.cephfs.csi.ceph.com`
|
- CephFS provisioner: `rook-ceph.cephfs.csi.ceph.com`
|
||||||
- Custom namespace provisioners: any string ending in `.rbd.csi.ceph.com` or `.cephfs.csi.ceph.com`
|
- Custom namespace provisioners: any string ending in `.rbd.csi.ceph.com` or `.cephfs.csi.ceph.com`
|
||||||
- Pod selectors: `app=rook-ceph-operator`, `app=rook-ceph-mon`, `app=rook-ceph-osd`, `app=rook-ceph-mgr`, `app=csi-rbdplugin-provisioner`, `app=csi-cephfsplugin-provisioner`
|
- Pod selectors: `app=rook-ceph-operator`, `app=rook-ceph-mon`, `app=rook-ceph-osd`, `app=rook-ceph-mgr`, `app=rook-ceph-mds`, `app=rook-ceph-rgw`
|
||||||
|
- CSI pod selectors (Rook 1.12+): `app=rook-ceph.rbd.csi.ceph.com-ctrlplugin`, `app=rook-ceph.cephfs.csi.ceph.com-ctrlplugin`
|
||||||
|
- CSI pod selectors (legacy): `app=csi-rbdplugin-provisioner`, `app=csi-cephfsplugin-provisioner`, `app=csi-rbdplugin`, `app=csi-cephfsplugin`
|
||||||
|
|
||||||
## Code conventions
|
## Code conventions
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
version: "0.2.4"
|
version: "0.2.6"
|
||||||
name: headlamp-rook-plugin
|
name: headlamp-rook-plugin
|
||||||
displayName: Rook Plugin
|
displayName: Rook Plugin
|
||||||
createdAt: "2026-02-18T00:00:00Z"
|
createdAt: "2026-02-18T00:00:00Z"
|
||||||
@@ -27,7 +27,7 @@ changes:
|
|||||||
description: "Package renamed to rook so the plugin displays correctly in Headlamp's Plugins list"
|
description: "Package renamed to rook so the plugin displays correctly in Headlamp's Plugins list"
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-rook-plugin/releases/download/v0.2.4/headlamp-rook-plugin-0.2.4.tar.gz"
|
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-rook-plugin/releases/download/v0.2.6/rook-0.2.6.tar.gz"
|
||||||
headlamp/plugin/archive-checksum: sha256:0dd88eecd784bc70557bb4c7ce5eede50fe83944990bc881bbb17313588c79f2
|
headlamp/plugin/archive-checksum: sha256:b3707b65a3fc3e8b8a35975fe52633a6a1fa3c52cef493ef4e5ce979028b6f3e
|
||||||
headlamp/plugin/distro-compat: ""
|
headlamp/plugin/distro-compat: ""
|
||||||
headlamp/plugin/version-compat: ">=0.20"
|
headlamp/plugin/version-compat: ">=0.20"
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "rook",
|
"name": "rook",
|
||||||
"version": "0.2.4",
|
"version": "0.2.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "rook",
|
"name": "rook",
|
||||||
"version": "0.2.4",
|
"version": "0.2.6",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kinvolk/headlamp-plugin": "^0.13.0"
|
"@kinvolk/headlamp-plugin": "^0.13.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rook",
|
"name": "rook",
|
||||||
"version": "0.2.4",
|
"version": "0.2.6",
|
||||||
"description": "Headlamp plugin for Rook-Ceph cluster visibility and CSI driver monitoring",
|
"description": "Headlamp plugin for Rook-Ceph cluster visibility and CSI driver monitoring",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
filterRookCephPVCs,
|
filterRookCephPVCs,
|
||||||
filterRookCephStorageClasses,
|
filterRookCephStorageClasses,
|
||||||
isKubeList,
|
isKubeList,
|
||||||
|
ROOK_CEPH_API_GROUP,
|
||||||
|
ROOK_CEPH_API_VERSION,
|
||||||
ROOK_CEPH_NAMESPACE,
|
ROOK_CEPH_NAMESPACE,
|
||||||
ROOK_CSI_CEPHFS_SELECTOR,
|
ROOK_CSI_CEPHFS_SELECTOR,
|
||||||
ROOK_CSI_RBD_SELECTOR,
|
ROOK_CSI_RBD_SELECTOR,
|
||||||
@@ -79,6 +81,19 @@ export function useRookCephContext(): RookCephContextValue {
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Unwrap Headlamp KubeObject class instances to their raw `.jsonData`. */
|
||||||
|
function extractJsonData(items: unknown[]): unknown[] {
|
||||||
|
return items.map(item =>
|
||||||
|
item && typeof item === 'object' && 'jsonData' in item
|
||||||
|
? (item as { jsonData: unknown }).jsonData
|
||||||
|
: item
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Provider
|
// Provider
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -118,7 +133,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// CephCluster CRDs
|
// CephCluster CRDs
|
||||||
try {
|
try {
|
||||||
const clusterList = await ApiProxy.request(
|
const clusterList = await ApiProxy.request(
|
||||||
`/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephclusters`
|
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephclusters`
|
||||||
);
|
);
|
||||||
if (!cancelled && isKubeList(clusterList)) {
|
if (!cancelled && isKubeList(clusterList)) {
|
||||||
setCephClusters(clusterList.items as CephCluster[]);
|
setCephClusters(clusterList.items as CephCluster[]);
|
||||||
@@ -130,7 +145,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// CephBlockPool CRDs
|
// CephBlockPool CRDs
|
||||||
try {
|
try {
|
||||||
const poolList = await ApiProxy.request(
|
const poolList = await ApiProxy.request(
|
||||||
`/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephblockpools`
|
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephblockpools`
|
||||||
);
|
);
|
||||||
if (!cancelled && isKubeList(poolList)) {
|
if (!cancelled && isKubeList(poolList)) {
|
||||||
setBlockPools(poolList.items as CephBlockPool[]);
|
setBlockPools(poolList.items as CephBlockPool[]);
|
||||||
@@ -142,7 +157,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// CephFilesystem CRDs
|
// CephFilesystem CRDs
|
||||||
try {
|
try {
|
||||||
const fsList = await ApiProxy.request(
|
const fsList = await ApiProxy.request(
|
||||||
`/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephfilesystems`
|
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephfilesystems`
|
||||||
);
|
);
|
||||||
if (!cancelled && isKubeList(fsList)) {
|
if (!cancelled && isKubeList(fsList)) {
|
||||||
setFilesystems(fsList.items as CephFilesystem[]);
|
setFilesystems(fsList.items as CephFilesystem[]);
|
||||||
@@ -154,7 +169,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// CephObjectStore CRDs
|
// CephObjectStore CRDs
|
||||||
try {
|
try {
|
||||||
const osList = await ApiProxy.request(
|
const osList = await ApiProxy.request(
|
||||||
`/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephobjectstores`
|
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephobjectstores`
|
||||||
);
|
);
|
||||||
if (!cancelled && isKubeList(osList)) {
|
if (!cancelled && isKubeList(osList)) {
|
||||||
setObjectStores(osList.items as CephObjectStore[]);
|
setObjectStores(osList.items as CephObjectStore[]);
|
||||||
@@ -255,15 +270,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// Derived / filtered values — memoized to avoid recomputation on every render
|
// Derived / filtered values — memoized to avoid recomputation on every render
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Headlamp useList() returns KubeObject class instances that store raw
|
// Uses module-level extractJsonData below
|
||||||
// Kubernetes JSON under `.jsonData`. Extract it so our plain-object helpers
|
|
||||||
// work correctly.
|
|
||||||
const extractJsonData = (items: unknown[]): unknown[] =>
|
|
||||||
items.map(item =>
|
|
||||||
item && typeof item === 'object' && 'jsonData' in item
|
|
||||||
? (item as { jsonData: unknown }).jsonData
|
|
||||||
: item
|
|
||||||
);
|
|
||||||
|
|
||||||
const storageClasses = useMemo(() => {
|
const storageClasses = useMemo(() => {
|
||||||
if (!allStorageClasses) return [];
|
if (!allStorageClasses) return [];
|
||||||
|
|||||||
+6
-8
@@ -209,10 +209,16 @@ export interface CephObjectStoreSpec {
|
|||||||
gateway?: { port?: number; securePort?: number; instances?: number };
|
gateway?: { port?: number; securePort?: number; instances?: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CephObjectStoreEndpoints {
|
||||||
|
insecure?: string[];
|
||||||
|
secure?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface CephObjectStoreStatus {
|
export interface CephObjectStoreStatus {
|
||||||
phase?: string;
|
phase?: string;
|
||||||
conditions?: CephClusterCondition[];
|
conditions?: CephClusterCondition[];
|
||||||
info?: Record<string, string>;
|
info?: Record<string, string>;
|
||||||
|
endpoints?: CephObjectStoreEndpoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CephObjectStore extends KubeObject {
|
export interface CephObjectStore extends KubeObject {
|
||||||
@@ -463,11 +469,3 @@ export function formatStorageType(type: 'rbd' | 'cephfs' | 'unknown'): string {
|
|||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Extracts pool/subvolume group name from a Rook-Ceph PV volumeHandle. */
|
|
||||||
export function extractPoolFromVolumeHandle(handle: string | undefined): string {
|
|
||||||
if (!handle) return '—';
|
|
||||||
// RBD format: "<csi-vol-id>-<pool>-..." — pool is in volumeAttributes
|
|
||||||
// We rely on volumeAttributes.pool instead; this just provides a fallback.
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import { useRookCephContext } from '../api/RookCephDataContext';
|
|||||||
function getHealthColor(health: string | undefined): string {
|
function getHealthColor(health: string | undefined): string {
|
||||||
switch (health) {
|
switch (health) {
|
||||||
case 'HEALTH_OK':
|
case 'HEALTH_OK':
|
||||||
return '#4caf50';
|
return 'var(--mui-palette-success-main, #4caf50)';
|
||||||
case 'HEALTH_WARN':
|
case 'HEALTH_WARN':
|
||||||
return '#ff9800';
|
return 'var(--mui-palette-warning-main, #ff9800)';
|
||||||
case 'HEALTH_ERR':
|
case 'HEALTH_ERR':
|
||||||
return '#f44336';
|
return 'var(--mui-palette-error-main, #f44336)';
|
||||||
default:
|
default:
|
||||||
return '#9e9e9e';
|
return 'var(--mui-palette-action-disabled, #9e9e9e)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ import { useRookCephContext } from '../api/RookCephDataContext';
|
|||||||
function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () => void }) {
|
function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="drawer-title-blockpool"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -38,7 +44,7 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () =
|
|||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{pool.metadata.name}</strong>
|
<strong id="drawer-title-blockpool">{pool.metadata.name}</strong>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
|
|||||||
@@ -47,10 +47,14 @@ const ROOK_APP_LABELS = new Set([
|
|||||||
'rook-ceph-mgr',
|
'rook-ceph-mgr',
|
||||||
'rook-ceph-mds',
|
'rook-ceph-mds',
|
||||||
'rook-ceph-rgw',
|
'rook-ceph-rgw',
|
||||||
|
// Legacy CSI labels (pre-Rook 1.12)
|
||||||
'csi-rbdplugin-provisioner',
|
'csi-rbdplugin-provisioner',
|
||||||
'csi-cephfsplugin-provisioner',
|
'csi-cephfsplugin-provisioner',
|
||||||
'csi-rbdplugin',
|
'csi-rbdplugin',
|
||||||
'csi-cephfsplugin',
|
'csi-cephfsplugin',
|
||||||
|
// New CSI labels (Rook 1.12+)
|
||||||
|
'rook-ceph.rbd.csi.ceph.com-ctrlplugin',
|
||||||
|
'rook-ceph.cephfs.csi.ceph.com-ctrlplugin',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ROLE_LABELS: Record<string, string> = {
|
const ROLE_LABELS: Record<string, string> = {
|
||||||
@@ -64,6 +68,8 @@ const ROLE_LABELS: Record<string, string> = {
|
|||||||
'csi-cephfsplugin-provisioner': 'CSI CephFS Provisioner',
|
'csi-cephfsplugin-provisioner': 'CSI CephFS Provisioner',
|
||||||
'csi-rbdplugin': 'CSI RBD Node Plugin',
|
'csi-rbdplugin': 'CSI RBD Node Plugin',
|
||||||
'csi-cephfsplugin': 'CSI CephFS Node Plugin',
|
'csi-cephfsplugin': 'CSI CephFS Node Plugin',
|
||||||
|
'rook-ceph.rbd.csi.ceph.com-ctrlplugin': 'CSI RBD Provisioner',
|
||||||
|
'rook-ceph.cephfs.csi.ceph.com-ctrlplugin': 'CSI CephFS Provisioner',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CephPodDetailSection({ resource }: CephPodDetailSectionProps) {
|
export default function CephPodDetailSection({ resource }: CephPodDetailSectionProps) {
|
||||||
|
|||||||
@@ -110,9 +110,16 @@ export default function ClusterStatusCard({
|
|||||||
{
|
{
|
||||||
name: 'Used',
|
name: 'Used',
|
||||||
value: bytesUsed,
|
value: bytesUsed,
|
||||||
fill: usedPct > 80 ? '#f44336' : '#1976d2',
|
fill:
|
||||||
|
usedPct > 80
|
||||||
|
? 'var(--mui-palette-error-main, #f44336)'
|
||||||
|
: 'var(--mui-palette-primary-main, #1976d2)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Free',
|
||||||
|
value: bytesAvail,
|
||||||
|
fill: 'var(--mui-palette-action-disabledBackground, #e0e0e0)',
|
||||||
},
|
},
|
||||||
{ name: 'Free', value: bytesAvail, fill: '#e0e0e0' },
|
|
||||||
]}
|
]}
|
||||||
total={bytesTotal}
|
total={bytesTotal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ import { useRookCephContext } from '../api/RookCephDataContext';
|
|||||||
function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () => void }) {
|
function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="drawer-title-filesystem"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -38,7 +44,7 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
|
|||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{fs.metadata.name}</strong>
|
<strong id="drawer-title-filesystem">{fs.metadata.name}</strong>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
|
|||||||
@@ -15,12 +15,16 @@ import { CephObjectStore, formatAge, phaseToStatus } from '../api/k8s';
|
|||||||
import { useRookCephContext } from '../api/RookCephDataContext';
|
import { useRookCephContext } from '../api/RookCephDataContext';
|
||||||
|
|
||||||
function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose: () => void }) {
|
function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose: () => void }) {
|
||||||
const endpoints = (store.status as unknown as Record<string, unknown>)?.endpoints as
|
const endpoints = store.status?.endpoints;
|
||||||
| { insecure?: string[]; secure?: string[] }
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="drawer-title-objectstore"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -42,7 +46,7 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
|
|||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{store.metadata.name}</strong>
|
<strong id="drawer-title-objectstore">{store.metadata.name}</strong>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
formatAge,
|
formatAge,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
healthToStatus,
|
healthToStatus,
|
||||||
|
parseStorageToBytes,
|
||||||
phaseToStatus,
|
phaseToStatus,
|
||||||
storageClassType,
|
storageClassType,
|
||||||
} from '../api/k8s';
|
} from '../api/k8s';
|
||||||
@@ -162,36 +163,40 @@ export default function OverviewPage() {
|
|||||||
{/* Storage type distribution */}
|
{/* Storage type distribution */}
|
||||||
{storageClasses.length > 0 && (
|
{storageClasses.length > 0 && (
|
||||||
<SectionBox title="Storage Summary">
|
<SectionBox title="Storage Summary">
|
||||||
{storageClasses.length > 0 && (
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
marginBottom: '8px',
|
||||||
marginBottom: '8px',
|
fontSize: '14px',
|
||||||
fontSize: '14px',
|
color: 'var(--mui-palette-text-secondary)',
|
||||||
color: 'var(--mui-palette-text-secondary)',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
StorageClass Type Distribution
|
||||||
StorageClass Type Distribution
|
|
||||||
</div>
|
|
||||||
<PercentageBar
|
|
||||||
data={[
|
|
||||||
...(rbdClasses.length > 0
|
|
||||||
? [{ name: 'Block (RBD)', value: rbdClasses.length, fill: '#1976d2' }]
|
|
||||||
: []),
|
|
||||||
...(cephfsClasses.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: 'Filesystem (CephFS)',
|
|
||||||
value: cephfsClasses.length,
|
|
||||||
fill: '#9c27b0',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
]}
|
|
||||||
total={storageClasses.length}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<PercentageBar
|
||||||
|
data={[
|
||||||
|
...(rbdClasses.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'Block (RBD)',
|
||||||
|
value: rbdClasses.length,
|
||||||
|
fill: 'var(--mui-palette-primary-main, #1976d2)',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(cephfsClasses.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'Filesystem (CephFS)',
|
||||||
|
value: cephfsClasses.length,
|
||||||
|
fill: 'var(--mui-palette-secondary-main, #9c27b0)',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
total={storageClasses.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<NameValueTable
|
<NameValueTable
|
||||||
rows={[
|
rows={[
|
||||||
{
|
{
|
||||||
@@ -334,24 +339,3 @@ export default function OverviewPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStorageToBytes(storage: string): number {
|
|
||||||
const match = /^(\d+(?:\.\d+)?)\s*(Ki|Mi|Gi|Ti|Pi|K|M|G|T|P)?$/.exec(storage.trim());
|
|
||||||
if (!match) return 0;
|
|
||||||
const value = parseFloat(match[1]);
|
|
||||||
const suffix = match[2] ?? '';
|
|
||||||
const multipliers: Record<string, number> = {
|
|
||||||
'': 1,
|
|
||||||
K: 1e3,
|
|
||||||
Ki: 1024,
|
|
||||||
M: 1e6,
|
|
||||||
Mi: 1024 ** 2,
|
|
||||||
G: 1e9,
|
|
||||||
Gi: 1024 ** 3,
|
|
||||||
T: 1e12,
|
|
||||||
Ti: 1024 ** 4,
|
|
||||||
P: 1e15,
|
|
||||||
Pi: 1024 ** 5,
|
|
||||||
};
|
|
||||||
return value * (multipliers[suffix] ?? 1);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ function StorageClassDetail({
|
|||||||
const type = storageClassType(sc);
|
const type = storageClassType(sc);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="drawer-title-storageclass"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -47,7 +53,7 @@ function StorageClassDetail({
|
|||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{sc.metadata.name}</strong>
|
<strong id="drawer-title-storageclass">{sc.metadata.name}</strong>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
|
|||||||
const attrs = pv.spec.csi?.volumeAttributes ?? {};
|
const attrs = pv.spec.csi?.volumeAttributes ?? {};
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="drawer-title-pv"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -39,7 +45,7 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
|
|||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{pv.metadata.name}</strong>
|
<strong id="drawer-title-pv">{pv.metadata.name}</strong>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
|
|||||||
+30
-3
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
registerAppBarAction,
|
||||||
registerDetailsViewSection,
|
registerDetailsViewSection,
|
||||||
registerResourceTableColumnsProcessor,
|
registerResourceTableColumnsProcessor,
|
||||||
registerRoute,
|
registerRoute,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
} from '@kinvolk/headlamp-plugin/lib';
|
} from '@kinvolk/headlamp-plugin/lib';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RookCephDataProvider } from './api/RookCephDataContext';
|
import { RookCephDataProvider } from './api/RookCephDataContext';
|
||||||
|
import AppBarClusterBadge from './components/AppBarClusterBadge';
|
||||||
import BlockPoolsPage from './components/BlockPoolsPage';
|
import BlockPoolsPage from './components/BlockPoolsPage';
|
||||||
import CephPodDetailSection from './components/CephPodDetailSection';
|
import CephPodDetailSection from './components/CephPodDetailSection';
|
||||||
import FilesystemsPage from './components/FilesystemsPage';
|
import FilesystemsPage from './components/FilesystemsPage';
|
||||||
@@ -72,6 +74,22 @@ registerSidebarEntry({
|
|||||||
icon: 'mdi:bucket',
|
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({
|
registerSidebarEntry({
|
||||||
parent: 'rook-ceph',
|
parent: 'rook-ceph',
|
||||||
name: 'rook-ceph-pods',
|
name: 'rook-ceph-pods',
|
||||||
@@ -80,6 +98,16 @@ registerSidebarEntry({
|
|||||||
icon: 'mdi:cube-outline',
|
icon: 'mdi:cube-outline',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// App bar action — cluster health badge
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
registerAppBarAction(() => (
|
||||||
|
<RookCephDataProvider>
|
||||||
|
<AppBarClusterBadge />
|
||||||
|
</RookCephDataProvider>
|
||||||
|
));
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Routes
|
// Routes
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -132,10 +160,9 @@ registerRoute({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Storage Classes and Volumes pages accessible via direct URL
|
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/rook-ceph/storage-classes',
|
path: '/rook-ceph/storage-classes',
|
||||||
sidebar: 'rook-ceph-overview',
|
sidebar: 'rook-ceph-storage-classes',
|
||||||
name: 'rook-ceph-storage-classes',
|
name: 'rook-ceph-storage-classes',
|
||||||
exact: true,
|
exact: true,
|
||||||
component: () => (
|
component: () => (
|
||||||
@@ -147,7 +174,7 @@ registerRoute({
|
|||||||
|
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/rook-ceph/volumes',
|
path: '/rook-ceph/volumes',
|
||||||
sidebar: 'rook-ceph-overview',
|
sidebar: 'rook-ceph-volumes',
|
||||||
name: 'rook-ceph-volumes',
|
name: 'rook-ceph-volumes',
|
||||||
exact: true,
|
exact: true,
|
||||||
component: () => (
|
component: () => (
|
||||||
|
|||||||
Reference in New Issue
Block a user