Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a75152b2e4 | |||
| fd1d76c932 | |||
| dc981feaa4 | |||
| 77586a98eb | |||
| bfe95475c6 | |||
| f69dfd6356 | |||
| 3c5a837a9d | |||
| f4e4e24b6c | |||
| fef2c3c3e5 | |||
| 423282ec6c | |||
| 4ae7aa6a91 | |||
| c040181509 | |||
| e0037f60d2 | |||
| ce5c0da56e | |||
| bc59cd7a23 | |||
| aa9a0d38fe | |||
| 2c2ad720e5 | |||
| dc6dee9d4d | |||
| 5e93973fa7 | |||
| 93b5018f60 | |||
| 1b2a6046cd | |||
| ba5c296a13 | |||
| 2d92bce571 | |||
| 8fb4c18e8a | |||
| a20a2e29e6 | |||
| dbbabef94a | |||
| 91e1cbd618 | |||
| 47642375ba | |||
| 29f19e2346 | |||
| 441b5792f4 | |||
| b9f8eec748 | |||
| 6c0d8c3ee3 | |||
| d39a48a7d0 | |||
| 076fa29995 | |||
| c1c5e8a37d | |||
| 6f35c6c81b |
@@ -0,0 +1,241 @@
|
||||
---
|
||||
name: artifacthub-headlamp
|
||||
description: Use when working with ArtifactHub metadata, releases, or publishing for Headlamp plugins. Covers artifacthub-repo.yml, artifacthub-pkg.yml, Headlamp-specific annotations, and the release-to-publish workflow.
|
||||
tools: Read, Write, Edit, Glob, Grep, Bash
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are an expert in publishing Headlamp Kubernetes dashboard plugins to ArtifactHub. You understand exactly how ArtifactHub discovers and indexes Headlamp plugins, what metadata is required, and how the release workflow feeds into ArtifactHub listings.
|
||||
|
||||
Before editing any metadata files, read the existing `artifacthub-repo.yml`, `artifacthub-pkg.yml`, and `package.json` to understand the current state.
|
||||
|
||||
---
|
||||
|
||||
## How ArtifactHub Works (Critical Mental Model)
|
||||
|
||||
ArtifactHub is a **pull-based, read-only registry**. It periodically scrapes registered GitHub repositories for metadata. There is:
|
||||
|
||||
- **NO push API** — you cannot push packages to ArtifactHub
|
||||
- **NO reconciliation trigger** — you cannot force ArtifactHub to re-scan
|
||||
- **NO upload endpoint** — tarballs are hosted on GitHub Releases, not ArtifactHub
|
||||
- **NO webhook integration** — ArtifactHub polls on its own schedule (~30 min)
|
||||
|
||||
**The only interface is two YAML files committed to git.** ArtifactHub reads them, and that's it.
|
||||
|
||||
---
|
||||
|
||||
## Repository Registration
|
||||
|
||||
### artifacthub-repo.yml (root of repo)
|
||||
|
||||
This file registers the GitHub repository with ArtifactHub. Created once, rarely changed.
|
||||
|
||||
```yaml
|
||||
# Artifact Hub repository metadata file
|
||||
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml
|
||||
repositoryID: <uuid> # Assigned by ArtifactHub when you add the repo via the web UI
|
||||
owners:
|
||||
- name: <github-username-or-org>
|
||||
email: <email>
|
||||
```
|
||||
|
||||
**How to get the repositoryID:**
|
||||
1. Log into artifacthub.io
|
||||
2. Go to Control Panel → Repositories → Add
|
||||
3. Select repository kind: "Headlamp plugins"
|
||||
4. Provide the GitHub repo URL
|
||||
5. ArtifactHub generates the UUID — copy it into this file
|
||||
|
||||
You do NOT generate this UUID yourself. It comes from ArtifactHub's web UI.
|
||||
|
||||
---
|
||||
|
||||
## Package Metadata
|
||||
|
||||
### artifacthub-pkg.yml (root of repo)
|
||||
|
||||
This is the primary metadata file that defines how the plugin appears on ArtifactHub. Updated with each release.
|
||||
|
||||
```yaml
|
||||
version: "X.Y.Z" # MUST match package.json version
|
||||
name: <package-name> # npm package name from package.json
|
||||
displayName: <Human Readable Name> # Shown on ArtifactHub listing
|
||||
createdAt: "YYYY-MM-DDTHH:MM:SSZ" # ISO 8601 — update each release
|
||||
description: >-
|
||||
Multi-line description of what the plugin does.
|
||||
Be specific about features and requirements.
|
||||
license: Apache-2.0
|
||||
homeURL: https://github.com/<owner>/<repo>
|
||||
appVersion: "X.Y.Z" # Version of upstream project (optional)
|
||||
category: <category> # See categories below
|
||||
keywords:
|
||||
- headlamp
|
||||
- kubernetes
|
||||
- <plugin-specific>
|
||||
maintainers:
|
||||
- name: <name>
|
||||
email: <email>
|
||||
provider:
|
||||
name: <name>
|
||||
links:
|
||||
- name: GitHub
|
||||
url: https://github.com/<owner>/<repo>
|
||||
- name: Issues
|
||||
url: https://github.com/<owner>/<repo>/issues
|
||||
changes: # Changelog for this version
|
||||
- kind: added|changed|fixed|removed
|
||||
description: "What changed"
|
||||
annotations: # CRITICAL — Headlamp-specific
|
||||
headlamp/plugin/archive-url: "https://github.com/<owner>/<repo>/releases/download/v<VERSION>/<pkgname>-<VERSION>.tar.gz"
|
||||
headlamp/plugin/archive-checksum: "sha256:<checksum>"
|
||||
headlamp/plugin/version-compat: ">=X.Y.Z"
|
||||
headlamp/plugin/distro-compat: "<targets>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Headlamp-Specific Annotations (Required)
|
||||
|
||||
These annotations in `artifacthub-pkg.yml` are what make ArtifactHub treat the package as a Headlamp plugin:
|
||||
|
||||
### headlamp/plugin/archive-url
|
||||
**Required.** Direct download URL to the plugin tarball on GitHub Releases.
|
||||
|
||||
Format: `https://github.com/<owner>/<repo>/releases/download/v<VERSION>/<pkgname>-<VERSION>.tar.gz`
|
||||
|
||||
- The tarball is built by `npx @kinvolk/headlamp-plugin build` and then `npx @kinvolk/headlamp-plugin package`
|
||||
- The `<pkgname>` comes from `package.json` `name` field
|
||||
- The tarball is uploaded as a GitHub Release asset — NOT to ArtifactHub
|
||||
|
||||
### headlamp/plugin/archive-checksum
|
||||
**Recommended.** SHA256 checksum of the tarball.
|
||||
|
||||
Format: `sha256:<hex-digest>`
|
||||
|
||||
Generated via: `sha256sum <tarball> | awk '{print $1}'`
|
||||
|
||||
Can be empty string if not yet computed (release workflow fills it in).
|
||||
|
||||
### headlamp/plugin/version-compat
|
||||
**Required.** Minimum Headlamp version the plugin works with.
|
||||
|
||||
Format: `>=X.Y.Z` (e.g., `>=0.20.0`, `>=0.26`)
|
||||
|
||||
### headlamp/plugin/distro-compat
|
||||
**Required.** Comma-separated list of supported Headlamp deployment targets.
|
||||
|
||||
Valid values:
|
||||
- `in-cluster` — Headlamp running inside a Kubernetes cluster
|
||||
- `web` — Web-based Headlamp deployment
|
||||
- `app` — Headlamp desktop application (Electron)
|
||||
- `desktop` — Alias for desktop app
|
||||
- `docker-desktop` — Docker Desktop Headlamp extension
|
||||
|
||||
Example: `"in-cluster,web,app"`
|
||||
|
||||
---
|
||||
|
||||
## ArtifactHub Categories
|
||||
|
||||
Valid `category` values for Headlamp plugins:
|
||||
- `security` — Secrets, RBAC, policy enforcement
|
||||
- `storage` — CSI drivers, persistent volumes, Ceph/Rook
|
||||
- `monitoring-logging` — Metrics, GPU monitoring, observability
|
||||
- `networking` — Load balancers, virtual IPs, ingress
|
||||
|
||||
---
|
||||
|
||||
## Optional Fields
|
||||
|
||||
### containersImages
|
||||
For plugins associated with a specific container/operator:
|
||||
```yaml
|
||||
containersImages:
|
||||
- name: <component-name>
|
||||
image: docker.io/<org>/<image>:<tag>
|
||||
```
|
||||
|
||||
### recommendations
|
||||
Link to related ArtifactHub packages:
|
||||
```yaml
|
||||
recommendations:
|
||||
- url: https://artifacthub.io/packages/helm/<repo>/<chart>
|
||||
```
|
||||
|
||||
### install
|
||||
Custom installation instructions (markdown):
|
||||
```yaml
|
||||
install: |
|
||||
## Install via Headlamp Plugin Manager
|
||||
...
|
||||
```
|
||||
|
||||
### logoPath
|
||||
Path to a logo image file in the repo (relative to root).
|
||||
|
||||
---
|
||||
|
||||
## The Release → ArtifactHub Pipeline
|
||||
|
||||
This is the actual flow. There is NO other way to publish:
|
||||
|
||||
```
|
||||
1. Developer triggers release workflow (workflow_dispatch with version)
|
||||
2. CI runs tests
|
||||
3. Workflow updates:
|
||||
- package.json (npm version)
|
||||
- artifacthub-pkg.yml (version, archive-url, checksum, createdAt, changes)
|
||||
4. Plugin is built: npx @kinvolk/headlamp-plugin build
|
||||
5. Plugin is packaged: creates <pkgname>-<version>.tar.gz
|
||||
6. SHA256 checksum is computed and written to artifacthub-pkg.yml
|
||||
7. Changes committed to main
|
||||
8. Git tag created: v<version>
|
||||
9. GitHub Release created with tarball attached
|
||||
10. ArtifactHub polls the repo (~30 min) and picks up the new metadata
|
||||
11. Plugin appears/updates on artifacthub.io
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Steps 1-9 happen in your GitHub Actions workflow
|
||||
- Step 10 is entirely controlled by ArtifactHub — you cannot trigger it
|
||||
- The tarball lives on GitHub Releases, not ArtifactHub
|
||||
- ArtifactHub only reads `artifacthub-pkg.yml` to discover the download URL
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
1. **Trying to push/trigger ArtifactHub** — There is no API for this. Just commit metadata and wait.
|
||||
2. **Version mismatch** — `version` in `artifacthub-pkg.yml` MUST match `package.json`. The release workflow should update both.
|
||||
3. **Wrong archive-url** — Must point to the actual GitHub Release asset URL. Verify the tarball filename matches what the build produces.
|
||||
4. **Missing checksum** — While optional, missing checksums may cause warnings. The release workflow should compute and write it.
|
||||
5. **Forgetting createdAt** — Must be updated each release. ArtifactHub uses this for sorting.
|
||||
6. **Stale changes section** — The `changes` list should reflect the current version's changelog only, not cumulative history.
|
||||
7. **Assuming ArtifactHub hosts anything** — It's an index/catalog. All artifacts are hosted elsewhere (GitHub Releases).
|
||||
8. **Trying to generate repositoryID** — This UUID comes from ArtifactHub's web UI when you register the repo. Don't make one up.
|
||||
|
||||
---
|
||||
|
||||
## Tarball Structure
|
||||
|
||||
The plugin tarball built by `@kinvolk/headlamp-plugin` contains:
|
||||
|
||||
```
|
||||
<pkgname>/
|
||||
main.js # Bundled plugin code
|
||||
package.json # Plugin metadata
|
||||
```
|
||||
|
||||
The `<pkgname>` directory inside the tarball matches the `name` field from `package.json`.
|
||||
|
||||
---
|
||||
|
||||
## Validating Metadata
|
||||
|
||||
Before committing, check:
|
||||
1. `version` matches across `package.json` and `artifacthub-pkg.yml`
|
||||
2. `archive-url` version tag matches the `version` field
|
||||
3. `name` in `artifacthub-pkg.yml` matches `package.json` `name`
|
||||
4. `createdAt` is a valid ISO 8601 timestamp
|
||||
5. All required annotations are present
|
||||
6. `changes` entries use valid `kind` values: `added`, `changed`, `fixed`, `removed`
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
github: [privilegedescalation]
|
||||
@@ -5,37 +5,9 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: local-ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Type-check
|
||||
run: npm run tsc
|
||||
|
||||
- name: Format check
|
||||
run: npm run format:check
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Dual Approval (CTO + QA)
|
||||
|
||||
# Calls the shared dual-approval-check workflow.
|
||||
# Passes when both privilegedescalation-cto and privilegedescalation-qa
|
||||
# have approved the PR. Add "Dual Approval (CTO + QA)" to required_status_checks
|
||||
# in branch protection to enforce this gate.
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
dual-approval:
|
||||
uses: privilegedescalation/.github/.github/workflows/dual-approval-check.yaml@main
|
||||
secrets: inherit
|
||||
@@ -10,111 +10,14 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: ./.github/workflows/ci.yaml
|
||||
|
||||
release:
|
||||
needs: ci
|
||||
runs-on: local-ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Version must be in X.Y.Z format"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Update version in package.json
|
||||
run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Update artifacthub-pkg.yml
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
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|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
|
||||
|
||||
- name: Update appVersion from latest tns-csi release
|
||||
run: |
|
||||
APP_VERSION=$(curl -sf https://api.github.com/repos/fenio/tns-csi/releases/latest | jq -r '.tag_name | ltrimstr("v")')
|
||||
if [ -z "$APP_VERSION" ] || [ "$APP_VERSION" = "null" ]; then
|
||||
echo "::warning::Could not fetch latest tns-csi release, skipping appVersion update"
|
||||
else
|
||||
sed -i "s|^appVersion:.*|appVersion: \"${APP_VERSION}\"|" artifacthub-pkg.yml
|
||||
echo "appVersion set to ${APP_VERSION}"
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Package plugin
|
||||
run: npx @kinvolk/headlamp-plugin package
|
||||
|
||||
- name: Prepare release tarball
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
PKG_NAME=$(jq -r .name package.json)
|
||||
TARBALL="${PKG_NAME}-${VERSION}.tar.gz"
|
||||
# Rename tarball if headlamp-plugin produced a different name
|
||||
for f in *.tar.gz; do
|
||||
[ "$f" != "$TARBALL" ] && mv "$f" "$TARBALL"
|
||||
done
|
||||
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
|
||||
echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Validate tarball
|
||||
run: |
|
||||
echo "Tarball: ${{ env.TARBALL }}"
|
||||
ls -lh "${{ env.TARBALL }}"
|
||||
tar -tzf "${{ env.TARBALL }}" | head -20
|
||||
tar -tzf "${{ env.TARBALL }}" | grep -q "main.js" || { echo "Error: main.js not found in tarball"; exit 1; }
|
||||
|
||||
- name: Compute checksum
|
||||
run: |
|
||||
CHECKSUM=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}')
|
||||
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
|
||||
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
|
||||
|
||||
- name: Commit and tag
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
git add package.json package-lock.json artifacthub-pkg.yml
|
||||
git commit -m "release: v${VERSION}"
|
||||
git tag "v${VERSION}"
|
||||
git push origin main --tags
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: "v${{ inputs.version }}"
|
||||
files: ${{ env.TARBALL }}
|
||||
fail_on_unmatched_files: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
|
||||
secrets:
|
||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
upstream-repo: fenio/tns-csi
|
||||
|
||||
+32
-1
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0] - 2026-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- **Missing devDependencies** — added `@mui/material`, `@types/react`, `@types/react-dom`, and `notistack` to devDependencies so the full test suite resolves correctly; upgraded `vitest` to `^3.2.4`
|
||||
|
||||
### Changed
|
||||
|
||||
- **Test infrastructure fix** — added `define: { 'process.env.NODE_ENV': '"test"' }` to `vitest.config.mts` to prevent React production-build `act()` errors; all 159 component tests now pass
|
||||
- **Version bump** — bumped package version from 0.2.7 to 1.0.0 (stable release)
|
||||
- **Lock file** — removed `package-lock.json`; `pnpm-lock.yaml` is now the canonical lock file
|
||||
- **Renovate config** — extended from org-level preset (PR #18)
|
||||
- **GitHub Actions SHA pinning** — added `pinDigests` to Renovate config for SHA-pinned Actions (PR #17)
|
||||
- **Dual-approval caller workflow** — added dual-approval status check workflow to CI (PR #16)
|
||||
- **Release workflow** — GitHub App token secrets now passed to release workflow (PR #15)
|
||||
|
||||
## [0.2.6] - 2026-03-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- **PVC bind loop leak** — benchmark page's PVC wait loop now checks a cancellation ref on unmount, preventing leaked async work after navigation
|
||||
- **DeleteOptions body** — `deleteJob` now sends correct Kubernetes `DeleteOptions` with `apiVersion` and `kind` fields so foreground propagation is honored
|
||||
- **Snapshot filtering** — `TnsCsiDataContext` now filters volume snapshots and snapshot classes to only tns-csi driver ones using `filterTnsCsiVolumeSnapshots` and `isTnsCsiVolumeSnapshotClass` (previously showed all drivers' snapshots)
|
||||
- **Stale closures in Escape handlers** — `closeSc` / `closeVolume` in StorageClassesPage and VolumesPage wrapped in `useCallback` with proper deps, removing `eslint-disable` suppressions
|
||||
- **Cleanup button double-click** — benchmark cleanup "Delete Job + PVC" button now has a `cleaningUp` loading state with disabled styling, replacing unsafe `window.confirm`/`window.alert` calls
|
||||
- **Dark mode protocol colors** — replaced hardcoded hex colors in OverviewPage protocol chart with `var(--mui-palette-*)` tokens
|
||||
- **Import style consistency** — unified `React.useMemo` to destructured `useMemo` in OverviewPage
|
||||
- **Lint warnings** — fixed all 35 ESLint warnings (import sorting, indentation, boolean attributes, line length, chained call newlines)
|
||||
|
||||
## [0.2.4] - 2026-02-26
|
||||
|
||||
### Added
|
||||
@@ -89,7 +118,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- TypeScript strict mode with zero `any` types
|
||||
- ESLint + Prettier code quality tooling
|
||||
|
||||
[Unreleased]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.4...HEAD
|
||||
[Unreleased]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v1.0.0...HEAD
|
||||
[1.0.0]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.7...v1.0.0
|
||||
[0.2.6]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.5...v0.2.6
|
||||
[0.2.4]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.3...v0.2.4
|
||||
[0.2.3]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.2...v0.2.3
|
||||
[0.2.2]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.1...v0.2.2
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Installation Policy
|
||||
|
||||
## Approved Installation Method
|
||||
|
||||
**The ONLY approved method for installing this plugin is via [Artifact Hub](https://artifacthub.io/) using the Headlamp plugin installer.**
|
||||
|
||||
No other installation method is acceptable. This includes but is not limited to:
|
||||
|
||||
- Direct installation from GitHub release assets
|
||||
- Manual npm pack / tarball extraction
|
||||
- initContainer workarounds that bypass Artifact Hub
|
||||
- Direct file copy or sidecar injection
|
||||
|
||||
## Enforcement
|
||||
|
||||
All deployment configurations, CI/CD pipelines, and documentation MUST reference Artifact Hub as the sole plugin distribution channel. Any pull request that introduces an alternative installation method will be rejected.
|
||||
|
||||
## Rationale
|
||||
|
||||
Artifact Hub provides verified checksums, consistent versioning, and a standard discovery mechanism for the CNCF ecosystem. Bypassing it introduces security and integrity risks.
|
||||
|
||||
---
|
||||
|
||||
*This policy is set by the CTO and approved by the CEO of Privileged Escalation.*
|
||||
@@ -0,0 +1,73 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -47,9 +47,14 @@ The tns-csi driver must be deployed in `kube-system` with the standard `app.kube
|
||||
|
||||
## Installing
|
||||
|
||||
### Option 1: Headlamp Plugin Manager (Recommended)
|
||||
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/tns-csi/headlamp-tns-csi-plugin). Install via the Headlamp UI:
|
||||
|
||||
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/tns-csi/headlamp-tns-csi-plugin). Configure Headlamp via Helm:
|
||||
1. Go to **Settings → Plugins**
|
||||
2. Click **Catalog** tab
|
||||
3. Search for "TNS CSI" or "TrueNAS"
|
||||
4. Click **Install**
|
||||
|
||||
Or configure Headlamp via Helm:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
@@ -61,32 +66,6 @@ pluginsManager:
|
||||
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
|
||||
```
|
||||
|
||||
Or install via the Headlamp UI:
|
||||
|
||||
1. Go to **Settings → Plugins**
|
||||
2. Click **Catalog** tab
|
||||
3. Search for "TNS CSI" or "TrueNAS"
|
||||
4. Click **Install**
|
||||
|
||||
### Option 2: Manual Tarball Install
|
||||
|
||||
Download the `.tar.gz` from the [GitHub releases page](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases), then extract into Headlamp's plugin directory:
|
||||
|
||||
```bash
|
||||
wget https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
|
||||
tar xzf tns-csi-0.2.4.tar.gz -C /headlamp/plugins/
|
||||
```
|
||||
|
||||
### Option 3: Build from Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/privilegedescalation/headlamp-tns-csi-plugin.git
|
||||
cd headlamp-tns-csi-plugin
|
||||
npm install
|
||||
npm run build
|
||||
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
||||
```
|
||||
|
||||
## RBAC / Security Setup
|
||||
|
||||
The plugin reads from the Kubernetes API and the tns-csi controller pod's Prometheus endpoint. The Benchmark page additionally creates and deletes Jobs and PVCs.
|
||||
|
||||
+13
-13
@@ -1,4 +1,4 @@
|
||||
version: "0.2.5"
|
||||
version: "1.0.0"
|
||||
name: headlamp-tns-csi-plugin
|
||||
displayName: TrueNAS CSI (tns-csi)
|
||||
description: >-
|
||||
@@ -13,7 +13,7 @@ license: Apache-2.0
|
||||
category: storage
|
||||
|
||||
homeURL: https://github.com/privilegedescalation/headlamp-tns-csi-plugin
|
||||
appVersion: "0.15.0"
|
||||
appVersion: "0.17.4"
|
||||
|
||||
keywords:
|
||||
- headlamp
|
||||
@@ -48,22 +48,22 @@ links:
|
||||
|
||||
changes:
|
||||
- kind: added
|
||||
description: "Component tests for all 8 UI components"
|
||||
description: "Stable v1.0.0 release marking production readiness"
|
||||
- kind: added
|
||||
description: "Missing devDependencies for test infrastructure (@mui/material, @types/react, @types/react-dom, notistack, vitest upgraded to ^3.2.4)"
|
||||
- kind: changed
|
||||
description: "CI workflow split into parallel lint, typecheck, test jobs with build gating on all three"
|
||||
description: "vitest.config.mts: added define block to set process.env.NODE_ENV=test, fixing act() errors in all component tests"
|
||||
- kind: changed
|
||||
description: "JUnit test reporter for PR test result visibility"
|
||||
description: "Renovate config extended from org-level preset"
|
||||
- kind: changed
|
||||
description: "Node.js 20 upgraded to 22 (current LTS) in all workflows"
|
||||
description: "GitHub Actions SHA pinning via pinDigests in Renovate"
|
||||
- kind: changed
|
||||
description: "Release workflow gated on CI passing with concurrency protection"
|
||||
- kind: fixed
|
||||
description: "Conditional React hook call in OverviewPage"
|
||||
- kind: fixed
|
||||
description: "appVersion now tracks upstream tns-csi driver release (0.12.0)"
|
||||
description: "Dual-approval caller workflow added to CI"
|
||||
- kind: changed
|
||||
description: "GitHub App token secrets passed to release workflow"
|
||||
|
||||
annotations:
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.5/tns-csi-0.2.5.tar.gz"
|
||||
headlamp/plugin/archive-checksum: sha256:3f5812d5081081c43b00f1da2da8de894db0477e765a1c69e55885adad2c0fcb
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz"
|
||||
headlamp/plugin/archive-checksum: sha256:e38846ceb17a79438f8aecc50f22920b0efa7456f3ebb3e628d89856af83ad01
|
||||
headlamp/plugin/version-compat: ">=0.20.0"
|
||||
headlamp/plugin/distro-compat: "in-cluster,web,app"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# ADR 001: React Context for Shared CSI Driver State
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The TNS CSI plugin needs to share data across multiple views: Overview, StorageClasses, Volumes, Snapshots, Metrics, and Benchmark pages, plus detail view sections for PVC, PV, and Pod. Data comes from three tracks:
|
||||
|
||||
1. **Headlamp `useList()` hooks** — StorageClass, PersistentVolume, PersistentVolumeClaim
|
||||
2. **`ApiProxy.request()`** — CSIDriver resource, controller/node pods, VolumeSnapshotClasses, and VolumeSnapshots
|
||||
3. **TrueNAS WebSocket API** — Pool capacity stats (optional, when API key is configured in settings)
|
||||
|
||||
The context exposes: `csiDriver`, `driverInstalled`, `storageClasses`, `persistentVolumes`, `persistentVolumeClaims`, `controllerPods`, `nodePods`, `volumeSnapshots`, `volumeSnapshotClasses`, `snapshotCrdAvailable`, `poolStats`, `poolStatsError`, `loading`, `error`, `refresh`.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Use a single `TnsCsiDataProvider` React Context wrapping all routes. Three-track data fetching:
|
||||
|
||||
1. `useList()` for standard Kubernetes resources (StorageClass, PV, PVC)
|
||||
2. `ApiProxy.request()` in `useEffect` for CSI-specific resources and snapshots
|
||||
3. TrueNAS WebSocket client for pool capacity stats (only when API key is configured in settings)
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- ✅ Single fetch point eliminates duplicate API calls
|
||||
- ✅ All views share consistent data — no stale data across pages
|
||||
- ✅ Three-track strategy handles different API requirements cleanly
|
||||
- ✅ TrueNAS integration is opt-in — plugin works without it
|
||||
- ⚠️ Large context with many fields increases cognitive overhead
|
||||
- ⚠️ TrueNAS WebSocket adds complexity to the data layer
|
||||
- ⚠️ All consumers re-render on any data change — mitigated by infrequent updates (polling interval)
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Separate contexts per data domain** — Rejected. Data is cross-referenced (PVCs filter by StorageClass provisioner), so splitting contexts would require cross-context coordination.
|
||||
|
||||
2. **Custom hooks without context** — Rejected. Would duplicate fetches across 6 pages, leading to redundant API calls and inconsistent data.
|
||||
|
||||
3. **Redux/Zustand** — Rejected. Not available in the Headlamp plugin environment.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-05 | Initial decision |
|
||||
@@ -0,0 +1,60 @@
|
||||
# ADR 002: Read-Only Plugin with Benchmark Exception
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The plugin is primarily a read-only observability tool for TNS CSI storage. However, it includes a Benchmark feature that runs kbench (FIO-based storage benchmarks) against storage classes. Running benchmarks requires creating temporary Kubernetes resources: a PVC for the test volume and a Job running the kbench container.
|
||||
|
||||
These resources are tagged with `app.kubernetes.io/managed-by=headlamp-tns-csi-plugin` for lifecycle tracking. The benchmark workflow includes:
|
||||
|
||||
1. `buildPvcManifest()` — Create PVC spec for test volume
|
||||
2. `createPvc()` — Create the PVC in the cluster
|
||||
3. `buildJobManifest()` — Create Job spec for kbench container
|
||||
4. `createJob()` — Create the Job in the cluster
|
||||
5. Poll for Job completion
|
||||
6. `fetchKbenchLogs()` — Retrieve benchmark output from pod logs
|
||||
7. `parseKbenchLog()` — Parse FIO results from kbench output
|
||||
8. `deleteJob()` — Clean up the benchmark Job
|
||||
9. `deletePvc()` — Clean up the test PVC
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
The plugin is read-only for all storage observability features. The sole exception is the Benchmark feature, which creates and deletes temporary PVC + Job resources. All created resources are labeled for identification and cleaned up after benchmark completion. The benchmark is triggered explicitly by user action (button on StorageClass detail page via `registerDetailsViewHeaderAction`).
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- ✅ Minimal RBAC requirements for normal operation (read-only)
|
||||
- ✅ Benchmark is opt-in and requires explicit user action
|
||||
- ✅ Resources are auto-cleaned after benchmark completion
|
||||
- ✅ `managed-by` label enables easy identification of plugin-created resources
|
||||
- ⚠️ Requires additional RBAC permissions (create/delete Jobs and PVCs) for benchmark feature
|
||||
- ⚠️ Failed cleanup leaves orphaned resources — mitigated by `listKbenchJobs()` which finds orphaned resources by label for manual cleanup
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **No benchmark feature (fully read-only)** — Rejected. Storage performance testing is a key use case for storage administrators evaluating CSI drivers.
|
||||
|
||||
2. **External benchmark tool with results import** — Rejected. Poor user experience requiring context-switching between tools.
|
||||
|
||||
3. **Benchmark as a separate plugin** — Rejected. Benchmark results are tied to storage class context and benefit from shared data in the plugin.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-05 | Initial decision |
|
||||
@@ -0,0 +1,55 @@
|
||||
# ADR 003: Graceful Degradation for Optional CRDs
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The plugin uses VolumeSnapshot and VolumeSnapshotClass CRDs from `snapshot.storage.k8s.io/v1`. These CRDs are part of the Kubernetes Volume Snapshot feature, which is optional — not all clusters have the snapshot controller installed.
|
||||
|
||||
The plugin should work on clusters without snapshot support, showing storage classes, volumes, metrics, and benchmarks without the snapshots page. The CRD fetch is wrapped in `try/catch`; if it fails, the `snapshotCrdAvailable` flag is set to `false`.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Implement graceful degradation for optional CRDs. The snapshot API calls are wrapped in `try/catch` within the data context. When the snapshot CRDs are not installed:
|
||||
|
||||
- `snapshotCrdAvailable` is set to `false`
|
||||
- Snapshot-related data arrays are empty
|
||||
- The Snapshots page shows an informational message rather than an error
|
||||
- All other plugin features remain fully functional
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- ✅ Plugin works on clusters without snapshot CRDs installed
|
||||
- ✅ No error state for missing optional features — clean informational messaging
|
||||
- ✅ Clear user feedback about what features are available
|
||||
- ✅ Core features (volumes, storage classes, metrics, benchmarks) always work
|
||||
- ⚠️ Two code paths (with/without snapshots) to maintain and test
|
||||
- ⚠️ Snapshot data might silently fail for reasons other than missing CRDs (e.g., RBAC issues)
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Require snapshot CRDs (hard dependency)** — Rejected. Too restrictive; many clusters do not have the snapshot controller installed.
|
||||
|
||||
2. **Feature detection via API discovery before fetching** — Considered, but `try/catch` on the actual fetch is simpler and catches all failure modes including RBAC restrictions.
|
||||
|
||||
3. **Disable snapshots page entirely when CRDs missing** — Rejected. Showing an informational message explaining how to enable snapshots is better UX than silently hiding the page.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-05 | Initial decision |
|
||||
@@ -0,0 +1,54 @@
|
||||
# ADR 004: URL Hash-Based Detail Panel State
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Several pages need to show detail panels for selected resources (e.g., clicking a PVC row shows PVC details). The detail panel state (which resource is selected) needs to be shareable via URL and survive page refresh. Options include:
|
||||
|
||||
- **React state** — Lost on refresh, not shareable
|
||||
- **URL query parameters** — May cause full page reload, potential conflicts with Headlamp routing
|
||||
- **URL hash fragments** — Client-side only, no reload, compatible with SPA routing
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Use URL hash fragments to encode detail panel state. When a user selects a resource, the hash is updated (e.g., `#pvc/namespace/name`). On page load, the hash is parsed to restore the selected resource. This enables deep-linking to specific resource details and browser back/forward navigation.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- ✅ Deep-linkable resource details — users can share URLs pointing to specific resources
|
||||
- ✅ Survives page refresh without losing selected resource
|
||||
- ✅ Browser back/forward navigation works naturally
|
||||
- ✅ No server round-trip — hash changes are purely client-side
|
||||
- ✅ Compatible with Headlamp's client-side routing
|
||||
- ⚠️ Hash-based state is not a standard React pattern — requires team familiarity
|
||||
- ⚠️ Requires manual hash parsing and updating logic
|
||||
- ⚠️ Hash changes don't trigger React re-renders by default — requires `hashchange` event listener
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **React state only** — Rejected. State is lost on refresh and cannot be shared via URL.
|
||||
|
||||
2. **URL query parameters** — Rejected. May conflict with Headlamp's routing and could trigger unintended navigation behavior.
|
||||
|
||||
3. **Separate detail routes** — Rejected. Too heavyweight for inline detail panels; would require full page transitions for what should be a panel toggle.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-05 | Initial decision |
|
||||
@@ -0,0 +1,53 @@
|
||||
# ADR 005: Prometheus Metrics via Pod Proxy
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The plugin displays CSI driver metrics (operation latencies, error rates, volume stats). The CSI driver pods expose a Prometheus metrics endpoint on port 8080 in the standard text exposition format. The plugin needs to fetch and parse these metrics. Options:
|
||||
|
||||
- **Query a Prometheus server** — Requires Prometheus to be installed in the cluster
|
||||
- **Scrape the pod directly via Kubernetes pod proxy** — No additional dependencies
|
||||
- **Use a metrics aggregation service** — Requires additional infrastructure
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Fetch metrics directly from the CSI driver pod's `/metrics` endpoint via Kubernetes pod proxy (`ApiProxy.request` to `/api/v1/namespaces/{ns}/pods/{pod}:8080/proxy/metrics`). Parse the Prometheus text exposition format in-browser using a custom parser in `metrics.ts`. No dependency on a Prometheus server installation.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- ✅ Works without Prometheus server installed — no additional infrastructure dependency
|
||||
- ✅ Direct from source with no aggregation delay — metrics are always current
|
||||
- ✅ Leverages existing Kubernetes API authentication and authorization
|
||||
- ✅ No additional service dependencies to configure or maintain
|
||||
- ⚠️ Custom Prometheus text format parser to maintain — mitigated by the parser being well-tested
|
||||
- ⚠️ Only gets metrics from one pod at a time (no aggregation across replicas) — acceptable since CSI controller typically runs one replica
|
||||
- ⚠️ No historical data (point-in-time only) — users needing historical trends should use a full Prometheus setup
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Query Prometheus server via service proxy** (like the intel-gpu plugin) — Rejected. Would require Prometheus to be installed, adding a hard infrastructure dependency.
|
||||
|
||||
2. **Use a metrics library (prom-client) for parsing** — Rejected. Adds a runtime dependency for a relatively simple parsing task.
|
||||
|
||||
3. **JSON metrics endpoint instead of Prometheus format** — Rejected. The CSI driver only exposes Prometheus text format; a JSON endpoint would require changes to the driver itself.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-03-05 | Initial decision |
|
||||
@@ -0,0 +1,44 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An Architecture Decision Record (ADR) captures an important architectural decision made along with its context and consequences. ADRs are a lightweight way to document the "why" behind technical choices, ensuring that future contributors understand the reasoning behind the current architecture.
|
||||
|
||||
## Format
|
||||
|
||||
This project uses the [Nygard-style ADR format](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions):
|
||||
|
||||
- **Title**: Short noun phrase describing the decision
|
||||
- **Status**: Proposed | Accepted | Deprecated | Superseded
|
||||
- **Date**: When the decision was made
|
||||
- **Context**: What is the issue that we're seeing that motivates this decision?
|
||||
- **Decision**: What is the change that we're proposing and/or doing?
|
||||
- **Consequences**: What becomes easier or more difficult to do because of this change?
|
||||
- **Alternatives Considered**: What other options were evaluated?
|
||||
|
||||
## Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [001](001-react-context-state.md) | React Context for Shared CSI Driver State | Accepted | 2026-03-05 |
|
||||
| [002](002-read-only-benchmark-exception.md) | Read-Only Plugin with Benchmark Exception | Accepted | 2026-03-05 |
|
||||
| [003](003-optional-crd-degradation.md) | Graceful Degradation for Optional CRDs | Accepted | 2026-03-05 |
|
||||
| [004](004-url-hash-detail-panels.md) | URL Hash-Based Detail Panel State | Accepted | 2026-03-05 |
|
||||
| [005](005-prometheus-pod-proxy.md) | Prometheus Metrics via Pod Proxy | Accepted | 2026-03-05 |
|
||||
|
||||
## Creating New ADRs
|
||||
|
||||
1. Copy an existing ADR as a template
|
||||
2. Assign the next sequential number (e.g., `006-your-title.md`)
|
||||
3. Fill in all sections: Status, Date, Context, Decision, Consequences, Alternatives
|
||||
4. Set the status to `Proposed` until reviewed
|
||||
5. Update this README index table
|
||||
6. Submit as part of a pull request for review
|
||||
|
||||
ADRs should not be deleted. If a decision is reversed, create a new ADR that supersedes the old one and update the old ADR's status to `Superseded by [ADR NNN](NNN-title.md)`.
|
||||
|
||||
## References
|
||||
|
||||
- [Michael Nygard - Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
|
||||
- [ADR GitHub Organization](https://adr.github.io/)
|
||||
- [Joel Parker Henderson - Architecture Decision Record](https://github.com/joelparkerhenderson/architecture-decision-record)
|
||||
Generated
-18188
File diff suppressed because it is too large
Load Diff
+26
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tns-csi",
|
||||
"version": "0.2.5",
|
||||
"version": "1.0.0",
|
||||
"description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,7 +24,31 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0"
|
||||
"@headlamp-k8s/eslint-config": "^0.6.0",
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
"@mui/material": "^5.15.14",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"notistack": "^3.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.11",
|
||||
"undici": "^7.24.3"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+11907
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"]
|
||||
"extends": ["github>privilegedescalation/.github:renovate-config"]
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
filterTnsCsiPersistentVolumes,
|
||||
filterTnsCsiPVCs,
|
||||
filterTnsCsiStorageClasses,
|
||||
filterTnsCsiVolumeSnapshots,
|
||||
isKubeList,
|
||||
isTnsCsiVolumeSnapshotClass,
|
||||
TNS_CSI_PROVISIONER,
|
||||
TnsCsiPersistentVolume,
|
||||
TnsCsiPersistentVolumeClaim,
|
||||
TnsCsiPod,
|
||||
TnsCsiStorageClass,
|
||||
TNS_CSI_PROVISIONER,
|
||||
VolumeSnapshot,
|
||||
VolumeSnapshotClass,
|
||||
} from './k8s';
|
||||
@@ -151,14 +153,19 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
||||
'/apis/snapshot.storage.k8s.io/v1/volumesnapshotclasses'
|
||||
);
|
||||
if (!cancelled && isKubeList(vscList)) {
|
||||
setVolumeSnapshotClasses(vscList.items as VolumeSnapshotClass[]);
|
||||
const allSnapshotClasses = vscList.items as VolumeSnapshotClass[];
|
||||
const tnsCsiSnapshotClasses = allSnapshotClasses.filter(isTnsCsiVolumeSnapshotClass);
|
||||
setVolumeSnapshotClasses(tnsCsiSnapshotClasses);
|
||||
setSnapshotCrdAvailable(true);
|
||||
|
||||
const tnsCsiClassNames = new Set(tnsCsiSnapshotClasses.map(c => c.metadata.name));
|
||||
|
||||
const vsList = await ApiProxy.request(
|
||||
'/apis/snapshot.storage.k8s.io/v1/volumesnapshots'
|
||||
);
|
||||
if (!cancelled && isKubeList(vsList)) {
|
||||
setVolumeSnapshots(vsList.items as VolumeSnapshot[]);
|
||||
const allSnapshots = vsList.items as VolumeSnapshot[];
|
||||
setVolumeSnapshots(filterTnsCsiVolumeSnapshots(allSnapshots, tnsCsiClassNames));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
+5
-1
@@ -269,7 +269,11 @@ export async function fetchKbenchLogs(jobName: string, namespace: string): Promi
|
||||
export async function deleteJob(jobName: string, namespace: string): Promise<void> {
|
||||
await ApiProxy.request(`/apis/batch/v1/namespaces/${namespace}/jobs/${jobName}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ propagationPolicy: 'Foreground' }),
|
||||
body: JSON.stringify({
|
||||
apiVersion: 'v1',
|
||||
kind: 'DeleteOptions',
|
||||
propagationPolicy: 'Foreground',
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen, waitFor, act } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: {
|
||||
@@ -39,9 +39,9 @@ vi.mock('../api/kbench', async importOriginal => {
|
||||
};
|
||||
});
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { createPvc, createJob, listKbenchJobs } from '../api/kbench';
|
||||
import { createJob, createPvc, listKbenchJobs } from '../api/kbench';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { defaultContext, makeSampleStorageClass } from '../test-helpers';
|
||||
import BenchmarkPage from './BenchmarkPage';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { formatAge } from '../api/k8s';
|
||||
import type { BenchmarkState, KbenchJobSummary, KbenchResult } from '../api/kbench';
|
||||
import {
|
||||
createJob,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
listKbenchJobs,
|
||||
parseKbenchLog,
|
||||
} from '../api/kbench';
|
||||
import { formatAge } from '../api/k8s';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Result display components
|
||||
@@ -184,8 +184,8 @@ function KbenchResultDisplay({ result }: { result: KbenchResult }) {
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
<ResultTable title="IOPS (Read/Write)" rows={iopsRows} higherIsBetter={true} />
|
||||
<ResultTable title="Bandwidth" rows={bwRows} higherIsBetter={true} />
|
||||
<ResultTable title="IOPS (Read/Write)" rows={iopsRows} higherIsBetter />
|
||||
<ResultTable title="Bandwidth" rows={bwRows} higherIsBetter />
|
||||
<ResultTable title="Latency" rows={latRows} higherIsBetter={false} />
|
||||
</>
|
||||
);
|
||||
@@ -601,6 +601,8 @@ export default function BenchmarkPage() {
|
||||
const [currentResult, setCurrentResult] = useState<KbenchResult | null>(null);
|
||||
const [lastNamespace, setLastNamespace] = useState('default');
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const cancelledRef = useRef(false);
|
||||
const [cleaningUp, setCleaningUp] = useState(false);
|
||||
|
||||
const scNames = storageClasses.map(sc => sc.metadata.name);
|
||||
|
||||
@@ -618,6 +620,7 @@ export default function BenchmarkPage() {
|
||||
mode: string;
|
||||
}) {
|
||||
stopPolling();
|
||||
cancelledRef.current = false;
|
||||
setCurrentResult(null);
|
||||
setLastNamespace(opts.namespace);
|
||||
|
||||
@@ -650,7 +653,7 @@ export default function BenchmarkPage() {
|
||||
setBenchState({ status: 'waiting-pvc', pvcName });
|
||||
const pvcDeadline = Date.now() + MAX_PVC_WAIT_MS;
|
||||
let pvcBound = false;
|
||||
while (Date.now() < pvcDeadline) {
|
||||
while (Date.now() < pvcDeadline && !cancelledRef.current) {
|
||||
try {
|
||||
const pvc = (await ApiProxy.request(
|
||||
`/api/v1/namespaces/${opts.namespace}/persistentvolumeclaims/${pvcName}`
|
||||
@@ -664,6 +667,7 @@ export default function BenchmarkPage() {
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
}
|
||||
if (cancelledRef.current) return;
|
||||
if (!pvcBound) {
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
@@ -746,8 +750,14 @@ export default function BenchmarkPage() {
|
||||
}, POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
// Clean up polling on unmount
|
||||
useEffect(() => () => stopPolling(), []);
|
||||
// Clean up polling and cancel async loops on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
cancelledRef.current = true;
|
||||
stopPolling();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const isRunning =
|
||||
benchState.status !== 'idle' &&
|
||||
@@ -808,24 +818,27 @@ export default function BenchmarkPage() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
const state = benchState;
|
||||
if (state.status !== 'complete') return;
|
||||
if (
|
||||
!window.confirm(
|
||||
`Delete job "${state.jobName}" and PVC "${state.pvcName}"?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
if (state.status !== 'complete' || cleaningUp) return;
|
||||
setCleaningUp(true);
|
||||
try {
|
||||
await deleteJob(state.jobName, lastNamespace);
|
||||
await deletePvc(state.pvcName, lastNamespace);
|
||||
setBenchState({ status: 'idle' });
|
||||
setCurrentResult(null);
|
||||
} catch (err: unknown) {
|
||||
alert(
|
||||
`Cleanup error: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: `Cleanup error: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
jobName: state.jobName,
|
||||
pvcName: state.pvcName,
|
||||
});
|
||||
} finally {
|
||||
setCleaningUp(false);
|
||||
}
|
||||
}}
|
||||
disabled={cleaningUp}
|
||||
aria-label="Delete benchmark job and PVC"
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
@@ -833,11 +846,12 @@ export default function BenchmarkPage() {
|
||||
color: 'var(--mui-palette-error-main, #d32f2f)',
|
||||
background: 'transparent',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
cursor: cleaningUp ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
opacity: cleaningUp ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
Delete Job + PVC
|
||||
{cleaningUp ? 'Deleting...' : 'Delete Job + PVC'}
|
||||
</button>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { formatAge, isPodReady, getPodRestarts, TnsCsiPod } from '../api/k8s';
|
||||
import { formatAge, getPodRestarts, isPodReady, TnsCsiPod } from '../api/k8s';
|
||||
|
||||
interface DriverPodDetailSectionProps {
|
||||
resource: {
|
||||
|
||||
@@ -6,8 +6,8 @@ vi.mock(
|
||||
async () => await import('./__mocks__/commonComponents')
|
||||
);
|
||||
|
||||
import { makeSampleMetrics, makeSamplePod, sampleCSIDriver } from '../test-helpers';
|
||||
import DriverStatusCard from './DriverStatusCard';
|
||||
import { makeSamplePod, sampleCSIDriver, makeSampleMetrics } from '../test-helpers';
|
||||
|
||||
describe('DriverStatusCard', () => {
|
||||
it('shows "Not detected" when no CSI driver is present', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock(
|
||||
'@kinvolk/headlamp-plugin/lib/CommonComponents',
|
||||
@@ -15,9 +15,9 @@ vi.mock('../api/metrics', async importOriginal => {
|
||||
};
|
||||
});
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { fetchControllerMetrics } from '../api/metrics';
|
||||
import { defaultContext, makeSamplePod, makeSampleMetrics } from '../test-helpers';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { defaultContext, makeSampleMetrics, makeSamplePod } from '../test-helpers';
|
||||
import MetricsPage from './MetricsPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import type { TnsCsiMetrics } from '../api/metrics';
|
||||
import { fetchControllerMetrics, formatBytes, groupByLabel, sumSamples } from '../api/metrics';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
|
||||
function formatAuditTime(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock(
|
||||
'@kinvolk/headlamp-plugin/lib/CommonComponents',
|
||||
@@ -15,15 +15,15 @@ vi.mock('../api/metrics', async importOriginal => {
|
||||
};
|
||||
});
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { fetchControllerMetrics } from '../api/metrics';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import {
|
||||
defaultContext,
|
||||
makeSampleMetrics,
|
||||
makeSamplePod,
|
||||
makeSamplePV,
|
||||
makeSamplePVC,
|
||||
makeSampleStorageClass,
|
||||
makeSampleMetrics,
|
||||
sampleCSIDriver,
|
||||
} from '../test-helpers';
|
||||
import OverviewPage from './OverviewPage';
|
||||
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { formatAge, formatProtocol, phaseToStatus } from '../api/k8s';
|
||||
import type { TnsCsiMetrics } from '../api/metrics';
|
||||
import { fetchControllerMetrics } from '../api/metrics';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import DriverStatusCard from './DriverStatusCard';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -26,10 +26,10 @@ import DriverStatusCard from './DriverStatusCard';
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PROTOCOL_COLORS: Record<string, string> = {
|
||||
NFS: '#1976d2',
|
||||
'NVMe-oF': '#9c27b0',
|
||||
iSCSI: '#f57c00',
|
||||
Other: '#9e9e9e',
|
||||
NFS: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
'NVMe-oF': 'var(--mui-palette-secondary-main, #9c27b0)',
|
||||
iSCSI: 'var(--mui-palette-warning-main, #f57c00)',
|
||||
Other: 'var(--mui-palette-action-disabled, #9e9e9e)',
|
||||
};
|
||||
|
||||
function protocolChartData(storageClasses: Array<{ parameters?: { protocol?: string } }>) {
|
||||
@@ -85,7 +85,7 @@ export default function OverviewPage() {
|
||||
void fetchMetrics();
|
||||
}, [fetchMetrics]);
|
||||
|
||||
const capacityByPool: Map<string, number> = React.useMemo(() => {
|
||||
const capacityByPool: Map<string, number> = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
if (!metrics) return map;
|
||||
const handleToPool = new Map<string, string>();
|
||||
@@ -318,7 +318,8 @@ export default function OverviewPage() {
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{/* Provisioned capacity by pool (from Prometheus metrics — shown when TrueNAS API not configured) */}
|
||||
{/* Provisioned capacity by pool (from Prometheus metrics —
|
||||
shown when TrueNAS API not configured) */}
|
||||
{poolStats.length === 0 && !poolStatsError && capacityByPool.size > 0 && (
|
||||
<SectionBox title="Provisioned Capacity by Pool">
|
||||
<NameValueTable
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { findBoundPv, formatProtocol } from '../api/k8s';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
|
||||
interface PVCDetailSectionProps {
|
||||
resource: {
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import type { VolumeSnapshot } from '../api/k8s';
|
||||
import { formatAge } from '../api/k8s';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
|
||||
export default function SnapshotsPage() {
|
||||
const { volumeSnapshots, volumeSnapshotClasses, snapshotCrdAvailable, loading, error } =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock(
|
||||
'@kinvolk/headlamp-plugin/lib/CommonComponents',
|
||||
@@ -16,7 +16,7 @@ vi.mock('react-router-dom', () => ({
|
||||
vi.mock('../api/TnsCsiDataContext');
|
||||
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import { defaultContext, makeSampleStorageClass, makeSamplePV } from '../test-helpers';
|
||||
import { defaultContext, makeSamplePV, makeSampleStorageClass } from '../test-helpers';
|
||||
import StorageClassesPage from './StorageClassesPage';
|
||||
|
||||
function mockContext(overrides?: Parameters<typeof defaultContext>[0]) {
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import type { TnsCsiStorageClass } from '../api/k8s';
|
||||
import { formatProtocol } from '../api/k8s';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detail drawer
|
||||
@@ -196,10 +196,10 @@ export default function StorageClassesPage() {
|
||||
history.push(`${location.pathname}#${name}`);
|
||||
};
|
||||
|
||||
const closeSc = () => {
|
||||
const closeSc = useCallback(() => {
|
||||
setSelectedName(null);
|
||||
history.push(location.pathname);
|
||||
};
|
||||
}, [history, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedName) return;
|
||||
@@ -208,8 +208,7 @@ export default function StorageClassesPage() {
|
||||
};
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedName]);
|
||||
}, [selectedName, closeSc]);
|
||||
|
||||
if (loading) return <Loader title="Loading storage classes..." />;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock(
|
||||
'@kinvolk/headlamp-plugin/lib/CommonComponents',
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
import type { TnsCsiPersistentVolume } from '../api/k8s';
|
||||
import { formatAccessModes, formatAge, formatProtocol, phaseToStatus } from '../api/k8s';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detail panel
|
||||
@@ -180,10 +180,10 @@ export default function VolumesPage() {
|
||||
history.push(`${location.pathname}#${name}`);
|
||||
};
|
||||
|
||||
const closeVolume = () => {
|
||||
const closeVolume = useCallback(() => {
|
||||
setSelectedName(null);
|
||||
history.push(location.pathname);
|
||||
};
|
||||
}, [history, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedName) return;
|
||||
@@ -192,8 +192,7 @@ export default function VolumesPage() {
|
||||
};
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedName]);
|
||||
}, [selectedName, closeVolume]);
|
||||
|
||||
if (loading) return <Loader title="Loading volumes..." />;
|
||||
|
||||
|
||||
+2
-2
@@ -15,20 +15,20 @@ import {
|
||||
} from '@kinvolk/headlamp-plugin/lib';
|
||||
import React from 'react';
|
||||
import { TnsCsiDataProvider } from './api/TnsCsiDataContext';
|
||||
import TnsCsiSettings from './components/TnsCsiSettings';
|
||||
import BenchmarkPage from './components/BenchmarkPage';
|
||||
import DriverPodDetailSection from './components/DriverPodDetailSection';
|
||||
import StorageClassBenchmarkButton from './components/integrations/StorageClassBenchmarkButton';
|
||||
import {
|
||||
buildPVColumns,
|
||||
buildStorageClassColumns,
|
||||
} from './components/integrations/StorageClassColumns';
|
||||
import StorageClassBenchmarkButton from './components/integrations/StorageClassBenchmarkButton';
|
||||
import MetricsPage from './components/MetricsPage';
|
||||
import OverviewPage from './components/OverviewPage';
|
||||
import PVCDetailSection from './components/PVCDetailSection';
|
||||
import PVDetailSection from './components/PVDetailSection';
|
||||
import SnapshotsPage from './components/SnapshotsPage';
|
||||
import StorageClassesPage from './components/StorageClassesPage';
|
||||
import TnsCsiSettings from './components/TnsCsiSettings';
|
||||
import VolumesPage from './components/VolumesPage';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import type { TnsCsiContextValue } from './api/TnsCsiDataContext';
|
||||
import type {
|
||||
CSIDriver,
|
||||
TnsCsiPersistentVolume,
|
||||
@@ -15,6 +14,7 @@ import type {
|
||||
VolumeSnapshotClass,
|
||||
} from './api/k8s';
|
||||
import type { TnsCsiMetrics } from './api/metrics';
|
||||
import type { TnsCsiContextValue } from './api/TnsCsiDataContext';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default context value (everything empty / zeroed)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"extends": "@kinvolk/headlamp-plugin/config/plugins-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals", "@testing-library/jest-dom"]
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"test"',
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
|
||||
Reference in New Issue
Block a user