Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a17053e69 | |||
| da041d52c6 | |||
| 6989ec32f1 | |||
| e0f0349a76 | |||
| 9904f8f405 | |||
| 829c0b4825 | |||
| 272b6655eb | |||
| 06c0a69357 | |||
| 0579c3457b | |||
| 901e1bb25e | |||
| 81b30e55c6 | |||
| f2bf4c2e50 | |||
| 84bfc04917 | |||
| ae8f303d51 | |||
| 236638c049 | |||
| 94fe496d41 | |||
| 088197994d | |||
| 0018614c41 | |||
| fa8203aa9b | |||
| 916eaf3848 | |||
| ab12f6ea0b | |||
| 85d5b3f5fd | |||
| 79a3964ce4 | |||
| 7f0e263b78 | |||
| 41adb333f6 | |||
| 9690072504 | |||
| 6940acc780 | |||
| 5676ffd876 | |||
| 428575de95 | |||
| 5c57f52abc | |||
| f26d1414b2 | |||
| aa676e8300 |
@@ -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]
|
||||
@@ -6,36 +6,8 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
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
|
||||
|
||||
@@ -10,95 +10,11 @@ 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: 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"
|
||||
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 }}
|
||||
name: v${{ inputs.version }}
|
||||
generate_release_notes: true
|
||||
files: ${{ env.TARBALL }}
|
||||
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
upstream-repo: 'kube-vip/kube-vip'
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.3] - 2026-03-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix missing `useEffect` dependency array on Escape key listener in ServicesPage (re-registered every render)
|
||||
- Wrap `closePanel` in `useCallback` to stabilize effect dependencies
|
||||
- Fix invalid empty string `StatusLabel` status for non-kube-vip services (now uses `"info"`)
|
||||
- Add ARIA `role="dialog"`, `aria-modal`, and `aria-label` to service detail slide-in panel
|
||||
- Replace invalid `aria-label` on backdrop div with `role="presentation"`
|
||||
- Use `phaseToStatus()` for pod status in NodesPage instead of hardcoded check (Failed pods now correctly show error)
|
||||
- Remove unreachable `startsWith('kube-vip.io/has-ip=')` branch in `getNodeVipLabel` (label keys never contain `=`)
|
||||
- Simplify redundant lease lookup conditions in OverviewPage
|
||||
- Fix 46 ESLint indentation warnings across all source files
|
||||
|
||||
## [0.1.2] - 2025-05-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add `--allow-same-version` flag for idempotent release retries
|
||||
- Use `action-gh-release` instead of `gh` CLI for release creation
|
||||
|
||||
## [0.1.1] - 2025-05-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove redundant `mv` in release workflow
|
||||
- Move Node.js setup before `npm version` in release workflow
|
||||
|
||||
## [0.1.0] - 2025-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release
|
||||
- Overview dashboard with deployment status, VIP mode, leader election
|
||||
- LoadBalancer services page with VIP assignments and detail panel
|
||||
- Nodes page with kube-vip pod status and leader designation
|
||||
- Configuration page with DaemonSet config, IP pools, leases
|
||||
- Service detail section injected into native Headlamp Service views
|
||||
@@ -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.*
|
||||
@@ -15,29 +15,7 @@ A [Headlamp](https://headlamp.dev/) plugin providing visibility into [kube-vip](
|
||||
|
||||
## Installation
|
||||
|
||||
### Plugin Manager (Headlamp UI)
|
||||
|
||||
Search for `kube-vip` in the Headlamp Plugin Manager.
|
||||
|
||||
### Manual
|
||||
|
||||
```bash
|
||||
# Download the latest release tarball
|
||||
curl -LO https://github.com/privilegedescalation/headlamp-kube-vip-plugin/releases/latest/download/kube-vip-*.tar.gz
|
||||
|
||||
# Extract to Headlamp plugins directory
|
||||
mkdir -p ~/.config/Headlamp/plugins
|
||||
tar -xzf kube-vip-*.tar.gz -C ~/.config/Headlamp/plugins/
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/privilegedescalation/headlamp-kube-vip-plugin.git
|
||||
cd headlamp-kube-vip-plugin
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
Search for `kube-vip` in the Headlamp Plugin Manager (Settings → Plugins → Catalog).
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
version: "0.1.2"
|
||||
name: kube-vip
|
||||
version: "0.1.4"
|
||||
name: headlamp-kube-vip
|
||||
displayName: kube-vip
|
||||
createdAt: "2026-03-04T00:00:00Z"
|
||||
description: >-
|
||||
@@ -25,7 +25,7 @@ maintainers:
|
||||
provider:
|
||||
name: privilegedescalation
|
||||
annotations:
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-kube-vip-plugin/releases/download/v0.1.2/kube-vip-0.1.2.tar.gz"
|
||||
headlamp/plugin/archive-checksum: sha256:6d75838cdb8f0a62b1e7ef3af0d114bd1b9fd2c9e8e32cd7ecb44e4ca6ef184b
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-kube-vip-plugin/releases/download/v0.1.4/kube-vip-0.1.4.tar.gz"
|
||||
headlamp/plugin/archive-checksum: sha256:d926d467a091e7b346afd3f6a09abc2205b7a8c290ecf65920f45df6206f7017
|
||||
headlamp/plugin/version-compat: ">=0.26"
|
||||
headlamp/plugin/distro-compat: "in-cluster"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Artifact Hub repository metadata
|
||||
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml
|
||||
repositoryID: 2f3e67f2-3799-4018-b4bb-84dd2fe1ab9b
|
||||
owners:
|
||||
- name: privilegedescalation
|
||||
email: privilegedescalation@users.noreply.github.com
|
||||
@@ -0,0 +1,66 @@
|
||||
# ADR 001: React Context for Centralized State
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The kube-vip plugin shares data across 4 page views (Overview, Services, Nodes, Config) and 1 detail view section (Service). Data comes from standard Kubernetes resources:
|
||||
|
||||
- **Services** (via `useList()`)
|
||||
- **Nodes** (via `useList()`)
|
||||
- **DaemonSet** (via `ApiProxy.request()`)
|
||||
- **Pods** (with fallback, via `ApiProxy.request()`)
|
||||
- **Leases** (`coordination.k8s.io/v1`, via `ApiProxy.request()`)
|
||||
- **ConfigMap** (via `ApiProxy.request()`)
|
||||
|
||||
The context exposes 12 fields: `kubeVipInstalled`, `daemonSetStatus`, `kubeVipPods`, `cloudProviderPods`, `loadBalancerServices`, `nodes`, `leases`, `ipPools`, `configMapData`, `kubeVipConfig`, `loading`, `error`, and `refresh`.
|
||||
|
||||
The plugin uses dual-track fetching: Headlamp `useList()` for Services and Nodes, `ApiProxy.request()` for everything else.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Use a single `KubeVipDataProvider` React Context wrapping all routes and the Service detail section registration. All kube-vip state is computed in the provider and made available to consumers via the `useKubeVipData()` hook.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ Single fetch location eliminates duplicate API calls across views
|
||||
- ✅ Consistent data across all views at any point in time
|
||||
- ✅ Derived state (e.g., `ipPools` from ConfigMap) computed once in the provider
|
||||
- ✅ `refresh()` function updates everything in a single call
|
||||
|
||||
### Negative
|
||||
|
||||
- ⚠️ All consumers re-render on any change to any of the 12 fields
|
||||
- ⚠️ Complex provider component managing 12 fields and multiple fetch strategies
|
||||
|
||||
These negatives are mitigated by the fact that kube-vip state changes infrequently (VIP assignments and pool configurations are relatively stable), so unnecessary re-renders are rare in practice.
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Per-page fetching** — Rejected. Would duplicate DaemonSet, ConfigMap, and Lease fetching across multiple pages, leading to redundant API calls and potential data inconsistency between views.
|
||||
|
||||
2. **Multiple contexts** (e.g., separate contexts for pods, services, config) — Rejected. Data is heavily cross-referenced: `kubeVipInstalled` depends on the DaemonSet, pod discovery depends on labels from the DaemonSet, and IP pool utilization requires both ConfigMap data and Service annotations. Splitting contexts would require complex inter-context dependencies.
|
||||
|
||||
3. **External state library** (e.g., Redux, Zustand) — Rejected. External state management libraries are not available in the Headlamp plugin runtime environment.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
| ---- | ------ |
|
||||
| 2026-03-05 | Initial decision recorded |
|
||||
@@ -0,0 +1,63 @@
|
||||
# ADR 002: Annotation-Based State Without CRDs
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
kube-vip does not define any Custom Resource Definitions. All kube-vip state is inferred from standard Kubernetes resources:
|
||||
|
||||
- **DaemonSet status** indicates whether kube-vip is installed
|
||||
- **`kube-vip.io/*` annotations on Services** indicate VIP configuration (requested IP, interface, load balancer class)
|
||||
- **Lease holder identity** indicates leader election status
|
||||
- **ConfigMap data** contains IP pool configuration
|
||||
|
||||
The plugin must extract kube-vip-specific state from these standard resources without relying on any custom APIs or CRDs.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Parse kube-vip state entirely from annotations on standard Kubernetes resources and ConfigMap data. No CRDs are queried. Annotation parsing functions in `k8s.ts` extract VIP configuration from Service annotations (e.g., `kube-vip.io/vipAddress`, `kube-vip.io/loadbalancerIPs`). IP pool configuration is parsed from the `kubevip` ConfigMap via `parseIPPools()`. Leader election status is read from Lease `.spec.holderIdentity`.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ No CRD dependency — works with any kube-vip installation regardless of version or configuration
|
||||
- ✅ Uses only standard Kubernetes resources accessible via the standard API
|
||||
- ✅ Annotation parsing is implemented as pure functions, making it straightforward to test
|
||||
- ✅ Works across all kube-vip versions that follow the annotation conventions
|
||||
|
||||
### Negative
|
||||
|
||||
- ⚠️ Annotation schema is implicit — there is no API validation or schema enforcement for annotation values
|
||||
- ⚠️ Annotations may change across kube-vip versions without notice, since they are not part of a formal API contract
|
||||
- ⚠️ Parsing logic must handle missing or malformed annotations gracefully
|
||||
|
||||
These negatives are mitigated by defensive parsing with fallbacks for missing annotations and sensible defaults when annotation values are absent or unparseable.
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **kube-vip CRDs** (if they existed) — Not available. kube-vip does not define any Custom Resource Definitions, so there are no custom resources to query.
|
||||
|
||||
2. **kube-vip API server** (if it existed) — Not available. kube-vip does not expose a dedicated API server or endpoint for querying its state.
|
||||
|
||||
3. **Parse kube-vip pod logs** — Rejected. Log parsing is inherently fragile, depends on log format stability, and requires log access RBAC permissions that may not be available in all environments.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
| ---- | ------ |
|
||||
| 2026-03-05 | Initial decision recorded |
|
||||
@@ -0,0 +1,66 @@
|
||||
# ADR 003: Static Pod Discovery with Label Selector Fallback
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
kube-vip can be deployed in two ways:
|
||||
|
||||
1. **DaemonSet deployment**: Pods are managed by a DaemonSet and have standard labels (e.g., `app: kube-vip`) that can be used for label-selector based discovery.
|
||||
2. **Static pod deployment**: Pod manifests are placed in `/etc/kubernetes/manifests/` on each node. Static pods do not have the same labels as DaemonSet-managed pods and follow the naming convention `kube-vip-<node-name>`.
|
||||
|
||||
The plugin needs to discover kube-vip pods regardless of the deployment method. The pod fetch uses a two-level fallback strategy: first try label-selector based discovery, then fall back to listing all pods in `kube-system` and filtering by name prefix.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Implement a two-level pod discovery fallback:
|
||||
|
||||
1. **Primary**: Fetch pods using label selector (matches DaemonSet-deployed kube-vip).
|
||||
2. **Fallback**: If the label-selector fetch fails or returns empty, list all pods in the `kube-system` namespace and filter by `name.startsWith('kube-vip')` (matches static pod naming convention).
|
||||
|
||||
This covers both deployment methods without requiring user configuration.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ Works with both DaemonSet and static pod deployments out of the box
|
||||
- ✅ No user configuration needed — deployment method is auto-detected
|
||||
- ✅ Automatic fallback is transparent to the user and other plugin components
|
||||
|
||||
### Negative
|
||||
|
||||
- ⚠️ Fallback fetches all `kube-system` pods, which is a broader query than necessary
|
||||
- ⚠️ Name-prefix matching (`kube-vip`) is convention-dependent and could produce false positives if other pods share the prefix
|
||||
|
||||
These negatives are mitigated by the fact that `kube-system` typically has a manageable number of pods, and the name prefix `kube-vip` is the standard convention used by the kube-vip project.
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Label selector only** — Rejected. Would miss static pod deployments entirely, leaving users who deploy kube-vip as static pods without visibility.
|
||||
|
||||
2. **Name prefix only** — Rejected. Less efficient than label selector for DaemonSet deployments, as it requires listing all pods in the namespace rather than using server-side filtering.
|
||||
|
||||
3. **User-configured discovery method** — Rejected. Adds unnecessary configuration burden for something that can be reliably auto-detected through the fallback strategy.
|
||||
|
||||
4. **Node filesystem check for static pod manifests** — Rejected. Requires node-level filesystem access, which is not available to Headlamp plugins running in the browser.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
| ---- | ------ |
|
||||
| 2026-03-05 | Initial decision recorded |
|
||||
@@ -0,0 +1,64 @@
|
||||
# ADR 004: Comprehensive Component-Level Testing with Shared Helpers
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
**Date**: 2026-03-05
|
||||
|
||||
**Deciders**: Development Team
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The plugin has extensive test coverage including both unit tests (`k8s.ts`, `KubeVipDataContext`) and component render tests (`ConfigPage`, `NodesPage`, `OverviewPage`, `ServiceDetailSection`, `ServicesPage`). Component tests need to mock both Headlamp's API and CommonComponents.
|
||||
|
||||
A shared `test-helpers.tsx` file provides fixture factories, and a `__mocks__/commonComponents.ts` file provides stub implementations of Headlamp's CommonComponents for render testing.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
Implement comprehensive component-level testing with two shared test infrastructure files:
|
||||
|
||||
1. **`test-helpers.tsx`**: Shared fixture factories for creating test data (mock Services, Nodes, Pods, DaemonSets, ConfigMaps, Leases). Provides consistent test data across all test files.
|
||||
2. **`__mocks__/commonComponents.ts`**: Stub implementations of Headlamp CommonComponents (`SectionBox`, `SimpleTable`, `StatusLabel`, `NameValueTable`) that render as simple HTML elements for testing.
|
||||
|
||||
All component tests render with a `KubeVipDataProvider` wrapper using mocked context values, verifying the component renders correct data from context.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ Consistent test fixtures across all test files eliminate divergent test data
|
||||
- ✅ CommonComponents mocks enable render testing without the full Headlamp runtime
|
||||
- ✅ Comprehensive coverage of component rendering logic and context integration
|
||||
- ✅ Easy to add new component tests by reusing existing fixtures and mocks
|
||||
|
||||
### Negative
|
||||
|
||||
- ⚠️ Mock maintenance burden when Headlamp's CommonComponents API changes
|
||||
- ⚠️ Test helpers may diverge from actual API shapes over time if not kept in sync
|
||||
|
||||
These negatives are mitigated by type-checking test helpers against the actual interfaces, ensuring compile-time detection of API shape mismatches.
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Unit tests only** (no component rendering) — Rejected. Misses rendering bugs and context integration issues that only surface when components are mounted with real context values.
|
||||
|
||||
2. **E2E tests only** — Rejected. Too slow for rapid development feedback and requires a running Headlamp instance with a connected cluster.
|
||||
|
||||
3. **Inline mocks per test file** — Rejected. Leads to inconsistent and duplicated test setup across files, making it harder to maintain and update when APIs change.
|
||||
|
||||
4. **Storybook for component testing** — Rejected. Additional tooling overhead is not justified for the scope of this plugin, and Storybook does not easily integrate with Headlamp's plugin component model.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
| ---- | ------ |
|
||||
| 2026-03-05 | Initial decision recorded |
|
||||
@@ -0,0 +1,39 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An Architecture Decision Record (ADR) is a document that captures an important architectural decision made along with its context and consequences. ADRs are used to record the motivation behind decisions so that future team members can understand why certain choices were made.
|
||||
|
||||
## Format
|
||||
|
||||
This project uses the Nygard-style ADR format:
|
||||
|
||||
- **Title**: Short noun phrase describing the decision
|
||||
- **Status**: Proposed, Accepted, Deprecated, or Superseded
|
||||
- **Context**: The forces at play, including technical, political, social, and project-specific
|
||||
- **Decision**: The change that is being proposed or has been agreed upon
|
||||
- **Consequences**: What becomes easier or harder as a result of this decision
|
||||
- **Alternatives Considered**: Other options that were evaluated
|
||||
|
||||
## Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
| --- | ----- | ------ | ---- |
|
||||
| [001](001-react-context-state.md) | React Context for Centralized State | Accepted | 2026-03-05 |
|
||||
| [002](002-annotation-based-state.md) | Annotation-Based State Without CRDs | Accepted | 2026-03-05 |
|
||||
| [003](003-static-pod-discovery.md) | Static Pod Discovery with Label Selector Fallback | Accepted | 2026-03-05 |
|
||||
| [004](004-component-testing-strategy.md) | Comprehensive Component-Level Testing with Shared Helpers | Accepted | 2026-03-05 |
|
||||
|
||||
## Creating New ADRs
|
||||
|
||||
1. Copy an existing ADR as a template
|
||||
2. Assign the next sequential number (e.g., `005-your-decision.md`)
|
||||
3. Fill in all sections following the Nygard-style format
|
||||
4. Set the status to `Proposed` until the team has reviewed and accepted
|
||||
5. Update the index table in this README
|
||||
6. Add a changelog entry at the bottom of the ADR
|
||||
|
||||
## References
|
||||
|
||||
- [Michael Nygard's article on ADRs](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
|
||||
- [ADR GitHub organization](https://adr.github.io/)
|
||||
Generated
+178
-165
@@ -1,15 +1,27 @@
|
||||
{
|
||||
"name": "kube-vip",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kube-vip",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0"
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"jsdom": "^24.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"vitest": "^3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
@@ -1551,9 +1563,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/@types/node": {
|
||||
"version": "22.19.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
|
||||
"integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2947,16 +2959,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.19.tgz",
|
||||
"integrity": "sha512-kRK3oIq4wlyH5Zr7Dpr44zzk3nhJdegKSNe6b4wi0a1vnIhnuuxgJc0KXqgiyP2jb1GfUTB/1IRfDqUsQEw+vg==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.20.tgz",
|
||||
"integrity": "sha512-eUIOd4u/p9994Nkv8Avn6r/xmS7D+RNmhmu6KGROefN3myLe3JfhSdimal2wDFe/h/OUNZ/LVVKMZrya9oEfKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "9.1.19",
|
||||
"@storybook/csf-plugin": "9.1.20",
|
||||
"@storybook/icons": "^1.4.0",
|
||||
"@storybook/react-dom-shim": "9.1.19",
|
||||
"@storybook/react-dom-shim": "9.1.20",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -2966,13 +2978,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^9.1.19"
|
||||
"storybook": "^9.1.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-9.1.19.tgz",
|
||||
"integrity": "sha512-elFv6OjJEW+5cbEUComCZOkoHQ/a8Faq3M8JLK8MGQBzhN0SWwIjYE/3m/U+FKmnuny3Idm18CMlhFXgbIVxgA==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-9.1.20.tgz",
|
||||
"integrity": "sha512-/fFOqTZQ0Q5JmSAVlyfEFRa0W3hAh2u7kg+OQRLVxvNZVVuW50mOxE3853tAqisw9UX8TOCN6ZflFBeeoGLYfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2984,7 +2996,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^9.1.19"
|
||||
"storybook": "^9.1.20"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -3007,13 +3019,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.19.tgz",
|
||||
"integrity": "sha512-NidYsvbLig1zQjhSammTsbwc8KzYQ6vC07uZjfzarwSmTdhFp7j2II0nek8iMNMPnfP9sxugJL990UyV5Wbhug==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.20.tgz",
|
||||
"integrity": "sha512-cdU3Q2/wEaT8h+mApFToRiF/0hYKH1eAkD0scQn67aODgp7xnkr0YHcdA+8w0Uxd2V7U8crV/cmT/HD0ELVOGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "9.1.19",
|
||||
"@storybook/csf-plugin": "9.1.20",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3021,18 +3033,18 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^9.1.19",
|
||||
"storybook": "^9.1.20",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-9.1.19.tgz",
|
||||
"integrity": "sha512-dmnSZHRw9/tEaO0qN9I1WhhBA7Jw3EazOzYNkvX90noiv1VT4uZqTSK6YzfBhcWWqhyaAVguOztr9rwLCHxoVw==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-9.1.20.tgz",
|
||||
"integrity": "sha512-SN8n6NgfKUD73k9RMDTp0sxHkaEuOLlUWV2VVeXUj+HjacCDLopDXSxMcLsFP5+uSHYLBk4DQiX7EsD0rx8AJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "9.1.19",
|
||||
"@storybook/core-webpack": "9.1.20",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"cjs-module-lexer": "^1.2.3",
|
||||
"css-loader": "^6.7.1",
|
||||
@@ -3053,7 +3065,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^9.1.19"
|
||||
"storybook": "^9.1.20"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -3062,9 +3074,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-webpack": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-9.1.19.tgz",
|
||||
"integrity": "sha512-L1dZY0+vwmZsPcAPoEVoF67nKyjd4FVkSHPC0Q2pg7Rm83M8Me09m7paHxu3uPrRniV1ip+53e5AotpexPlMVA==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-9.1.20.tgz",
|
||||
"integrity": "sha512-GaH54yOx2I/1HUNHdxD3+kbbEE2xoC9sp7+8HxGC0fofEiyK/nlExo0tIX4+LRXC3T7hI+alWEc9bHgkmyLJMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3075,13 +3087,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^9.1.19"
|
||||
"storybook": "^9.1.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.19.tgz",
|
||||
"integrity": "sha512-jQN9JAbyDxjcQ5r48/t/rb4RMow5V/WB7LUir1Sp5GSMMSg5QD2XlfVuSnxsUk2VjhKVZNmwCa8jMCTjBR3L9Q==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.20.tgz",
|
||||
"integrity": "sha512-HHgk50YQhML7mT01Mzf9N7lNMFHWN4HwwRP90kPT9Ct+Jhx7h3LBDbdmWjI96HwujcpY7eoYdTfpB1Sw8Z7nBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3092,7 +3104,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^9.1.19"
|
||||
"storybook": "^9.1.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/global": {
|
||||
@@ -3117,13 +3129,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-9.1.19.tgz",
|
||||
"integrity": "sha512-1n6vUA8uODhwpzn+IGctD2NCxrNz58nNi38dyF5XK27pVqP+Ef4pauLwcpsGg6rQulwYK1XPqYl+N8Ku4CH52g==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-9.1.20.tgz",
|
||||
"integrity": "sha512-/PPsRJVqRhW5P0Ff58AN7wuPxda2et8a5iUN3ebkol9r/zmc17QPzhqbIEDoa1jTC7DYa1pYgXvxbU+fY6lhrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "9.1.19",
|
||||
"@storybook/core-webpack": "9.1.20",
|
||||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"@types/semver": "^7.3.4",
|
||||
"find-up": "^7.0.0",
|
||||
@@ -3144,7 +3156,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^9.1.19"
|
||||
"storybook": "^9.1.20"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -3175,14 +3187,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.19.tgz",
|
||||
"integrity": "sha512-UuTGumzQ1/Nffm1rWjrfFefL1a7uZlJEk34Cjp8N5uA9qPEX68e1OEeFnHX3nBk+8yEadPfs77vJox76aeTC6g==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.20.tgz",
|
||||
"integrity": "sha512-TJhqzggs7HCvLhTXKfx8HodnVq9YizsB2J31s9v6olU0UCxbCY+FYaCF+XdE8qUCyefGRZgHKzGBIczJ/q9e2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "9.1.19"
|
||||
"@storybook/react-dom-shim": "9.1.20"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -3194,7 +3206,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^9.1.19",
|
||||
"storybook": "^9.1.20",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -3224,9 +3236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.19.tgz",
|
||||
"integrity": "sha512-raIZEhHFQANSPBikc1Zn6cEHHnj+MbrUD3bbxj/abQHi/bqRIAf20ALOAfIuKk1oFD7lSP6J22h4FXYZm/TtFA==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.20.tgz",
|
||||
"integrity": "sha512-UYdZavfPwHEqCKMqPssUOlyFVZiJExLxnSHwkICSZBmw3gxXJcp1aXWs7PvoZdWz2K4ztl3IcKErXXHeiY6w+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -3236,20 +3248,20 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^9.1.19"
|
||||
"storybook": "^9.1.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-vite": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.19.tgz",
|
||||
"integrity": "sha512-J7ikh6oWy+GMgEBuNkZ1uhrozHMUu2cQclmMivhm1kJSix9FsjcflFAaO9rzzBbtOVyX1afu+JStNpyOjfj5NQ==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.20.tgz",
|
||||
"integrity": "sha512-buXeNvEJ9kp4FKbGYV7zW4sh/KS01EAjeq8Z6AVxaXOh4W2CIRTKM9maWGz+Rr+YyqQIq/Gl+RqNwxctpxeuHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1",
|
||||
"@rollup/pluginutils": "^5.0.2",
|
||||
"@storybook/builder-vite": "9.1.19",
|
||||
"@storybook/react": "9.1.19",
|
||||
"@storybook/builder-vite": "9.1.20",
|
||||
"@storybook/react": "9.1.20",
|
||||
"find-up": "^7.0.0",
|
||||
"magic-string": "^0.30.0",
|
||||
"react-docgen": "^8.0.0",
|
||||
@@ -3266,20 +3278,20 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^9.1.19",
|
||||
"storybook": "^9.1.20",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-9.1.19.tgz",
|
||||
"integrity": "sha512-f2oEqda5nxEUPuIJx4S9JoT4KKZB/TrLS7jrgiaEUtIPLCvoj6nPmLkKp6JvHZ7i8KiQZALJiGUCuevEY6jI9g==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-9.1.20.tgz",
|
||||
"integrity": "sha512-t5/+UenrE5h0hfsxcB6FOj3pV2YhrrPVpzaHlybgdhzzkPEQSUd34laWi82N74exqcjVLoDwWSkl3M2g1xoaMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-webpack5": "9.1.19",
|
||||
"@storybook/preset-react-webpack": "9.1.19",
|
||||
"@storybook/react": "9.1.19"
|
||||
"@storybook/builder-webpack5": "9.1.20",
|
||||
"@storybook/preset-react-webpack": "9.1.20",
|
||||
"@storybook/react": "9.1.20"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -3291,7 +3303,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
|
||||
"storybook": "^9.1.19",
|
||||
"storybook": "^9.1.20",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -4340,9 +4352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz",
|
||||
"integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==",
|
||||
"version": "20.19.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4507,17 +4519,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||
"integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz",
|
||||
"integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/type-utils": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/type-utils": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
@@ -4530,22 +4542,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
||||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4561,14 +4573,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
|
||||
"integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz",
|
||||
"integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.56.1",
|
||||
"@typescript-eslint/types": "^8.56.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.57.0",
|
||||
"@typescript-eslint/types": "^8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4583,14 +4595,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
|
||||
"integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz",
|
||||
"integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1"
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4601,9 +4613,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4618,15 +4630,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
@@ -4643,9 +4655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
|
||||
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz",
|
||||
"integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4657,16 +4669,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
|
||||
"integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz",
|
||||
"integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.56.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/project-service": "8.57.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -4685,16 +4697,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
|
||||
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz",
|
||||
"integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1"
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4709,13 +4721,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
|
||||
"integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz",
|
||||
"integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5744,9 +5756,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
||||
"version": "2.10.7",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz",
|
||||
"integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -6269,9 +6281,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001776",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz",
|
||||
"integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==",
|
||||
"version": "1.0.30001778",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz",
|
||||
"integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -7642,9 +7654,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.307",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
|
||||
"integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
|
||||
"version": "1.5.313",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz",
|
||||
"integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -7872,9 +7884,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-iterator-helpers": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
|
||||
"integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz",
|
||||
"integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7893,6 +7905,7 @@
|
||||
"has-symbols": "^1.1.0",
|
||||
"internal-slot": "^1.1.0",
|
||||
"iterator.prototype": "^1.1.5",
|
||||
"math-intrinsics": "^1.1.0",
|
||||
"safe-array-concat": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8943,9 +8956,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz",
|
||||
"integrity": "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==",
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz",
|
||||
"integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -8959,7 +8972,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^7.0.0"
|
||||
"pure-rand": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
@@ -9183,9 +9196,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz",
|
||||
"integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
|
||||
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -9755,9 +9768,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.13.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.0.tgz",
|
||||
"integrity": "sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA==",
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz",
|
||||
"integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -12792,9 +12805,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"version": "2.0.36",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
|
||||
"integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -14124,9 +14137,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
|
||||
"integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.0.0.tgz",
|
||||
"integrity": "sha512-7rgWlxG2gAvFPIQfUreo1XYlNvrQ9VnQPFWdncPkdl3icucLK0InOxsaafbvxGTnI6Bk/Rxmslg0lQlRCuzOXw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -14266,9 +14279,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-docgen": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.2.tgz",
|
||||
"integrity": "sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA==",
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.3.tgz",
|
||||
"integrity": "sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15829,9 +15842,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "9.1.19",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.19.tgz",
|
||||
"integrity": "sha512-P7K/b+Pn1sXJzwYCF6hH5Zjdrg4ZlA5Bz9rdOJEdvm6ev27XESDGI+Ql+dfUfUcGOym3Aud4MssJIDEF2ocsyQ==",
|
||||
"version": "9.1.20",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.20.tgz",
|
||||
"integrity": "sha512-6rME2tww6PFhm96iG2Xx44yzwLDWBiDWy+kJ2ub6x90werSTOiuo+tZJ94BgCfFutR0tEfLRIq59s+Zg6YyChA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -16407,9 +16420,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
|
||||
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -16463,9 +16476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.17",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz",
|
||||
"integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz",
|
||||
"integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -17037,9 +17050,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
|
||||
"integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
|
||||
"version": "7.24.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
|
||||
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -17581,9 +17594,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.2.0.tgz",
|
||||
"integrity": "sha512-g2k9z8B/1Bx7D4wnFjPLx9dyYGrqWMLTpwTtPHhcU+ElNZP2O4+4OsyaficiDClus0dzVhdGvoGFYMJxoXZ12Q==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.3.0.tgz",
|
||||
"integrity": "sha512-XiAtZcev7nppxNFgKoD55rfL+ukVp/RtrnTJONRwRuzv/B2FK2h2ZRCYjvxhwBV/Oarse83SiyXBSxMTfeEM0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -17600,7 +17613,7 @@
|
||||
"url": "https://github.com/sponsors/sapphi-red"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-svgr": {
|
||||
|
||||
+18
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kube-vip",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"description": "Headlamp plugin for kube-vip virtual IP and load balancer visibility",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,7 +24,23 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.11",
|
||||
"undici": "^7.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0"
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"jsdom": "^24.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -1,4 +1,19 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"]
|
||||
"extends": ["config:recommended"],
|
||||
"baseBranches": ["main"],
|
||||
"schedule": ["every weekend"],
|
||||
"prConcurrentLimit": 10,
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "npm minor and patch"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "github-actions minor and patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -210,7 +210,6 @@ export function isControlPlaneNode(node: KubeVipNode): boolean {
|
||||
export function getNodeVipLabel(node: KubeVipNode): string | undefined {
|
||||
const labels = node.metadata.labels ?? {};
|
||||
for (const [key, value] of Object.entries(labels)) {
|
||||
if (key.startsWith('kube-vip.io/has-ip=')) return value;
|
||||
if (key === 'kube-vip.io/has-ip') return value;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getNodeVipLabel,
|
||||
isControlPlaneNode,
|
||||
isNodeReady,
|
||||
phaseToStatus,
|
||||
} from '../api/k8s';
|
||||
import { useKubeVipContext } from '../api/KubeVipDataContext';
|
||||
|
||||
@@ -83,7 +84,7 @@ export default function NodesPage() {
|
||||
const pod = podByNode.get(n.metadata.name);
|
||||
if (!pod) return '—';
|
||||
return (
|
||||
<StatusLabel status={pod.status?.phase === 'Running' ? 'success' : 'warning'}>
|
||||
<StatusLabel status={phaseToStatus(pod.status?.phase)}>
|
||||
{pod.status?.phase ?? 'Unknown'}
|
||||
</StatusLabel>
|
||||
);
|
||||
@@ -127,7 +128,7 @@ export default function NodesPage() {
|
||||
const pod = podByNode.get(n.metadata.name);
|
||||
if (!pod) return '—';
|
||||
return (
|
||||
<StatusLabel status={pod.status?.phase === 'Running' ? 'success' : 'warning'}>
|
||||
<StatusLabel status={phaseToStatus(pod.status?.phase)}>
|
||||
{pod.status?.phase ?? 'Unknown'}
|
||||
</StatusLabel>
|
||||
);
|
||||
|
||||
@@ -65,12 +65,8 @@ export default function OverviewPage() {
|
||||
const controlPlaneVIP = kubeVipConfig['address'] ?? '—';
|
||||
|
||||
// Find leader from leases
|
||||
const cpLease = leases.find(
|
||||
l => l.metadata.name.startsWith('plndr-cp-lock') || l.metadata.name === 'plndr-cp-lock'
|
||||
);
|
||||
const svcLease = leases.find(
|
||||
l => l.metadata.name.startsWith('plndr-svcs-lock') || l.metadata.name === 'plndr-svcs-lock'
|
||||
);
|
||||
const cpLease = leases.find(l => l.metadata.name.startsWith('plndr-cp-lock'));
|
||||
const svcLease = leases.find(l => l.metadata.name.startsWith('plndr-svcs-lock'));
|
||||
const leaderNode = cpLease?.spec?.holderIdentity ?? svcLease?.spec?.holderIdentity ?? '—';
|
||||
|
||||
return (
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('ServicesPage', () => {
|
||||
const svc = makeSampleService();
|
||||
mockContext({ loadBalancerServices: [svc] });
|
||||
render(<ServicesPage />);
|
||||
fireEvent.click(screen.getByLabelText('Close panel backdrop'));
|
||||
fireEvent.click(screen.getByTestId('panel-backdrop'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/kube-vip/services');
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
formatAge,
|
||||
@@ -38,7 +38,10 @@ export default function ServicesPage() {
|
||||
? loadBalancerServices.find(s => `${s.metadata.namespace}/${s.metadata.name}` === selectedName)
|
||||
: null;
|
||||
|
||||
const closePanel = () => history.push(location.pathname);
|
||||
const closePanel = useCallback(
|
||||
() => history.push(location.pathname),
|
||||
[history, location.pathname]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedName) return;
|
||||
@@ -47,7 +50,7 @@ export default function ServicesPage() {
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
});
|
||||
}, [selectedName, closePanel]);
|
||||
|
||||
if (loading) {
|
||||
return <Loader title="Loading services..." />;
|
||||
@@ -111,7 +114,7 @@ export default function ServicesPage() {
|
||||
{
|
||||
label: 'kube-vip',
|
||||
getter: s => (
|
||||
<StatusLabel status={isKubeVipService(s) ? 'success' : ''}>
|
||||
<StatusLabel status={isKubeVipService(s) ? 'success' : 'info'}>
|
||||
{isKubeVipService(s) ? 'Yes' : '—'}
|
||||
</StatusLabel>
|
||||
),
|
||||
@@ -130,8 +133,9 @@ export default function ServicesPage() {
|
||||
{selectedService && (
|
||||
<>
|
||||
<div
|
||||
role="presentation"
|
||||
data-testid="panel-backdrop"
|
||||
onClick={closePanel}
|
||||
aria-label="Close panel backdrop"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
@@ -165,6 +169,9 @@ function ServiceDetailPanel({
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Service Details"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
|
||||
+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