From 9da27f4186b73043cd60e143939021ca506a7aa9 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Sat, 21 Mar 2026 03:45:12 +0000 Subject: [PATCH] feat: scaffold starter template with TypeScript, CRD list view, CI, ArtifactHub Adds full plugin starter template including: - package.json with @kinvolk/headlamp-plugin devDependency and standard scripts - tsconfig.json extending headlamp plugin config - vitest.config.mts + vitest.setup.ts (jsdom, NODE_ENV=test, localStorage shim) - src/index.tsx: registers sidebar entry and route for ResourceListPage - src/components/ResourceListPage.tsx: placeholder CRD list view with TODO guide - src/components/ResourceListPage.test.tsx: example tests using vi.mock pattern - .github/workflows/ci.yaml: delegates to shared plugin-ci.yaml - .github/workflows/release.yaml: delegates to shared plugin-release.yaml - artifacthub-pkg.yml + artifacthub-repo.yml: ArtifactHub metadata with TODO markers - renovate.json: Mend Renovate config for weekly dependency updates - README.md: complete getting-started guide - CONTRIBUTING.md: local dev, code style, testing, PR process - LICENSE: Apache-2.0 Co-Authored-By: Paperclip --- .github/workflows/ci.yaml | 13 ++ .github/workflows/release.yaml | 18 +++ CONTRIBUTING.md | 89 +++++++++++ LICENSE | 192 +++++++++++++++++++++++ README.md | 174 +++++++++++++++++++- artifacthub-pkg.yml | 24 +++ artifacthub-repo.yml | 6 + package.json | 30 ++++ renovate.json | 19 +++ src/components/ResourceListPage.test.tsx | 51 ++++++ src/components/ResourceListPage.tsx | 40 +++++ src/index.tsx | 43 +++++ tsconfig.json | 8 + vitest.config.mts | 11 ++ vitest.setup.ts | 43 +++++ 15 files changed, 758 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 artifacthub-pkg.yml create mode 100644 artifacthub-repo.yml create mode 100644 package.json create mode 100644 renovate.json create mode 100644 src/components/ResourceListPage.test.tsx create mode 100644 src/components/ResourceListPage.tsx create mode 100644 src/index.tsx create mode 100644 tsconfig.json create mode 100644 vitest.config.mts create mode 100644 vitest.setup.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..899f2b1 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,13 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + workflow_call: + +jobs: + ci: + uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..010e801 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,18 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 1.0.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + release: + uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main + with: + version: ${{ inputs.version }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9f24746 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# Contributing + +Thank you for your interest in contributing. This guide covers how to work on this plugin locally and what to expect from the review process. + +## Running the plugin locally + +### Prerequisites + +- Node.js 18 or later +- A running Headlamp instance (desktop app or `headlamp-server`) + +### Setup + +```bash +npm install +npm start +``` + +`npm start` runs `headlamp-plugin start`, which starts a dev server at `http://localhost:4466`. Headlamp will hot-reload the plugin as you save files. + +To connect Headlamp to your local dev server, use the in-app plugin manager or set the plugin source URL to `http://localhost:4466`. + +### Build + +```bash +npm run build +``` + +Produces a production bundle in `dist/`. Copy the `dist/` folder to your Headlamp plugins directory: + +- **Linux**: `~/.config/Headlamp/plugins//` +- **macOS**: `~/Library/Application Support/Headlamp/plugins//` +- **Windows**: `%APPDATA%\Headlamp\plugins\\` + +## Code style + +This project uses TypeScript, ESLint, and Prettier. All configuration is provided by `@kinvolk/headlamp-plugin`. + +Check types: +```bash +npm run tsc +``` + +Lint: +```bash +npm run lint +npm run lint:fix # auto-fix where possible +``` + +Format: +```bash +npm run format # write changes +npm run format:check # check only (used in CI) +``` + +CI will fail if any of these checks fail. Run them locally before opening a PR. + +## Testing + +All new code must have tests. Run the suite with: + +```bash +npm test +``` + +Watch mode during development: + +```bash +npm run test:watch +``` + +Tests use Vitest with React Testing Library and the jsdom environment. Key conventions: + +- Place test files alongside the component they test, named `ComponentName.test.tsx`. +- Mock `@kinvolk/headlamp-plugin/lib/CommonComponents` using `vi.mock` — see `src/components/ResourceListPage.test.tsx` for the established pattern. +- Avoid testing implementation details; prefer assertions against rendered output. + +## Pull request process + +1. Fork the repository and create a branch from `main`. +2. Make your changes with tests. +3. Ensure `npm run tsc`, `npm run lint`, `npm run format:check`, and `npm test` all pass. +4. Open a pull request against `main` with a clear description of what changed and why. +5. The CI workflow will run automatically. Address any failures before requesting review. +6. A maintainer will review and merge the PR. + +## Releasing + +Releases are triggered manually via the **Release** GitHub Actions workflow. Maintainers run this workflow with the target version number; it packages the plugin, creates a GitHub release, and updates the ArtifactHub metadata. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b240e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,192 @@ + 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 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, as submitted to the 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 developing and + improving the Work, but excluding communication that is conspicuously + marked or designated in writing by the copyright owner as "Not a + Contribution." + + "Contributor" shall mean Licensor and any Legal Entity on behalf of + whom a Contribution has been received by the Licensor and included + 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 the 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 any Work + 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, You must include a readable copy of the attribution + notices contained within such NOTICE file, 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 license statement for Your modifications and may + provide additional grant of rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Work, and to permit + persons to whom the Work is furnished to do so, subject to the following + conditions: You may impose additional terms; however, such terms may not + be terms of the Apache 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 reproducing 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 exemplary 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 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 offer such obligations + 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 in question. It may also be + provided in a machine-readable format where the fields are easily + parsed. + + 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. diff --git a/README.md b/README.md index 500ecef..9d7652b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,173 @@ -# headlamp-plugin-template +# Headlamp Plugin Template -Starter template for [Headlamp](https://headlamp.dev) plugins — TypeScript, React, CRD list view, CI/CD, ArtifactHub packaging. +A starter template for building [Headlamp](https://headlamp.dev) plugins — Kubernetes UI extensions that run inside the Headlamp desktop or web application. -> **See the [getting-started guide](#getting-started) below to scaffold your plugin.** +## What this is + +This repository is a GitHub template that gives you a working Headlamp plugin skeleton with: + +- A sidebar entry and route registered via the Headlamp SDK +- A placeholder resource list component ready to be swapped for your CRD view +- Vitest + React Testing Library test setup +- ESLint + Prettier configured via `@kinvolk/headlamp-plugin` +- CI workflow (lint, typecheck, test) and a manual release workflow +- ArtifactHub metadata files for publishing your plugin +- Renovate for automated dependency updates + +## Getting Started + +### 1. Create your repo from this template + +Click the **"Use this template"** button at the top of this page on GitHub, then create a new repository under your org. + +### 2. Rename the placeholder values + +Search the repo for `YOUR_ORG`, `YOUR_REPO`, and `YOUR_NAME` and replace them with your actual values. Key files to update: + +| File | What to change | +|---|---| +| `package.json` | `name`, `author`, `repository`, `bugs`, `homepage` | +| `artifacthub-pkg.yml` | `name`, `displayName`, `description`, `homeURL`, `annotations` | +| `artifacthub-repo.yml` | `repositoryID` (after registering), `owners` | + +### 3. Install dependencies and start developing + +```bash +npm install +npm start +``` + +`npm start` runs `headlamp-plugin start` which launches a local dev server. Open Headlamp (desktop or `http://localhost:4466`) and load the plugin from `http://localhost:4466`. + +To point Headlamp at your dev server, set the `HEADLAMP_PLUGIN_URL` in your Headlamp config or use the in-app plugin manager to add `http://localhost:4466`. + +## Project Structure + +``` +. +├── src/ +│ ├── index.tsx # Plugin entry point — registers sidebar entries and routes +│ └── components/ +│ ├── ResourceListPage.tsx # Placeholder list view — replace with your CRD UI +│ └── ResourceListPage.test.tsx # Tests for the list view +├── .github/ +│ └── workflows/ +│ ├── ci.yaml # Lint, typecheck, test on every push/PR +│ └── release.yaml # Manual release workflow (builds + publishes artifact) +├── artifacthub-pkg.yml # ArtifactHub package metadata +├── artifacthub-repo.yml # ArtifactHub repository metadata +├── renovate.json # Automated dependency updates via Mend Renovate +├── tsconfig.json # TypeScript config (extends headlamp-plugin base) +├── vitest.config.mts # Vitest configuration +└── vitest.setup.ts # Test setup (jest-dom + localStorage shim) +``` + +## Building your plugin + +### Adding a CRD list view + +The recommended approach uses the Headlamp SDK's `K8s.makeKubeObject` helper: + +```ts +import { K8s } from '@kinvolk/headlamp-plugin/lib'; + +// 1. Define your CRD type +interface MyResourceSpec { + someField: string; +} + +// 2. Create a typed resource class +const MyResource = K8s.makeKubeObject('MyResource'); +MyResource.apiEndpoint = K8s.ApiProxy.apiFactory('your.group.io', 'v1', 'yourresources'); + +// 3. Use it in a component +function MyResourceList() { + const [resources, error] = MyResource.useList(); + // ... +} +``` + +Replace the contents of `src/components/ResourceListPage.tsx` with your real list view, then update `src/index.tsx` if you need additional routes or sidebar entries. + +### Common SDK imports + +```ts +import { + registerRoute, + registerSidebarEntry, + registerDetailsViewSection, +} from '@kinvolk/headlamp-plugin/lib'; + +import { + SectionBox, + SectionHeader, + NameValueTable, + Loader, + StatusLabel, +} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +``` + +Full API reference: https://headlamp-k8s.github.io/headlamp/ + +## Testing + +Run the test suite: + +```bash +npm test +``` + +Run in watch mode during development: + +```bash +npm run test:watch +``` + +Tests use [Vitest](https://vitest.dev) with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). The `jsdom` environment is configured in `vitest.config.mts`. Mock `@kinvolk/headlamp-plugin/lib/CommonComponents` in your test files using `vi.mock` — see `ResourceListPage.test.tsx` for the pattern. + +## Packaging for ArtifactHub + +After your first release: + +1. Update `artifacthub-pkg.yml`: + - Set `version` and `appVersion` to the released version + - Update `annotations.headlamp/plugin/archive-url` to point to the release artifact URL + - Compute the SHA-256 checksum of the `.tar.gz` artifact and set `annotations.headlamp/plugin/archive-checksum` + +2. Register your repository on [ArtifactHub](https://artifacthub.io) and paste the `repositoryID` into `artifacthub-repo.yml`. + +3. ArtifactHub will automatically pick up new versions when you push updated `artifacthub-pkg.yml` files to your default branch. + +Checksum example: +```bash +sha256sum my-headlamp-plugin-0.1.0.tar.gz +``` + +## CI/CD + +### CI (`ci.yaml`) + +Runs on every push to `main` and every pull request. Delegates to the shared `plugin-ci.yaml` workflow which runs: + +- TypeScript typecheck (`tsc --noEmit`) +- ESLint +- Prettier format check +- Vitest test suite +- Production build + +### Release (`release.yaml`) + +Triggered manually from the GitHub Actions UI with a `version` input (e.g. `1.0.0`). Delegates to the shared `plugin-release.yaml` workflow which: + +- Bumps the version in `package.json` +- Runs `headlamp-plugin package` to produce a `.tar.gz` artifact +- Creates a GitHub release with the artifact attached +- Updates `artifacthub-pkg.yml` with the new version and archive URL + +## Resources + +- [Headlamp documentation](https://headlamp.dev/docs/latest/) +- [Headlamp plugin development guide](https://headlamp.dev/docs/latest/development/plugins/) +- [Headlamp SDK API reference](https://headlamp-k8s.github.io/headlamp/) +- [ArtifactHub Headlamp plugins](https://artifacthub.io/docs/topics/repositories/headlamp-plugins/) +- [Headlamp GitHub](https://github.com/headlamp-k8s/headlamp) diff --git a/artifacthub-pkg.yml b/artifacthub-pkg.yml new file mode 100644 index 0000000..37a0909 --- /dev/null +++ b/artifacthub-pkg.yml @@ -0,0 +1,24 @@ +# Artifact Hub package metadata +# https://artifacthub.io/docs/topics/repositories/headlamp-plugins/ +# Replace ALL placeholder values before publishing. + +version: "0.1.0" +name: my-headlamp-plugin # TODO: change to your plugin name (lowercase, hyphens) +displayName: My Headlamp Plugin # TODO: human-readable display name +createdAt: "2026-01-01T00:00:00Z" # TODO: set to your initial release date +description: A Headlamp plugin for Kubernetes # TODO: describe your plugin +license: Apache-2.0 +homeURL: https://github.com/YOUR_ORG/YOUR_REPO # TODO: update +appVersion: "0.1.0" # TODO: version of the app this plugin targets +keywords: + - headlamp + - kubernetes + # TODO: add your plugin-specific keywords +annotations: + headlamp/plugin/archive-url: "https://github.com/YOUR_ORG/YOUR_REPO/releases/download/v0.1.0/my-headlamp-plugin-0.1.0.tar.gz" # TODO: update per release + headlamp/plugin/archive-checksum: "sha256:REPLACE_WITH_ACTUAL_CHECKSUM" # TODO: compute from release artifact + headlamp/plugin/version-compat: ">=0.13.0" + headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop" +links: + - name: Source Code + url: https://github.com/YOUR_ORG/YOUR_REPO # TODO: update diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml new file mode 100644 index 0000000..0355a1d --- /dev/null +++ b/artifacthub-repo.yml @@ -0,0 +1,6 @@ +# Artifact Hub repository metadata +# https://artifacthub.io/docs/topics/repositories/#repository-metadata-file +repositoryID: "" # TODO: fill in after registering on ArtifactHub +owners: + - name: YOUR_NAME # TODO: fill in + email: your@email.com # TODO: fill in diff --git a/package.json b/package.json new file mode 100644 index 0000000..2a1009a --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "my-headlamp-plugin", + "version": "0.1.0", + "description": "A Headlamp plugin for Kubernetes", + "repository": { + "type": "git", + "url": "https://github.com/YOUR_ORG/YOUR_REPO.git" + }, + "bugs": { + "url": "https://github.com/YOUR_ORG/YOUR_REPO/issues" + }, + "homepage": "https://github.com/YOUR_ORG/YOUR_REPO#readme", + "author": "YOUR_NAME", + "license": "Apache-2.0", + "scripts": { + "start": "headlamp-plugin start", + "build": "headlamp-plugin build", + "package": "headlamp-plugin package", + "tsc": "tsc --noEmit", + "lint": "eslint --ext .ts,.tsx src/", + "lint:fix": "eslint --ext .ts,.tsx --fix src/", + "format": "prettier --write src/", + "format:check": "prettier --check src/", + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@kinvolk/headlamp-plugin": "^0.13.0" + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..9ca1ba1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "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" + } + ] +} diff --git a/src/components/ResourceListPage.test.tsx b/src/components/ResourceListPage.test.tsx new file mode 100644 index 0000000..81ee01d --- /dev/null +++ b/src/components/ResourceListPage.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import ResourceListPage from './ResourceListPage'; + +vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({ + Loader: ({ title }: { title: string }) =>
{title}
, + SectionBox: ({ title, children }: { title: React.ReactNode; children?: React.ReactNode }) => ( +
+ {title} + {children} +
+ ), + SectionHeader: ({ title }: { title: string }) =>

{title}

, + NameValueTable: ({ + rows, + }: { + rows: Array<{ name: React.ReactNode; value: React.ReactNode }>; + }) => ( +
+ {rows.map((r, i) => ( +
+
{r.name}
+
{r.value}
+
+ ))} +
+ ), + StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => ( + {children} + ), +})); + +describe('ResourceListPage', () => { + it('renders the "My Plugin" heading', () => { + render(); + expect(screen.getByText('My Plugin')).toBeInTheDocument(); + }); + + it('shows the example resource group row', () => { + render(); + expect(screen.getByText('Resource Group')).toBeInTheDocument(); + expect(screen.getByText('your.group.io/v1')).toBeInTheDocument(); + }); + + it('shows the example Kind row', () => { + render(); + expect(screen.getByText('Kind')).toBeInTheDocument(); + expect(screen.getByText('YourCustomResource')).toBeInTheDocument(); + }); +}); diff --git a/src/components/ResourceListPage.tsx b/src/components/ResourceListPage.tsx new file mode 100644 index 0000000..82f4bfa --- /dev/null +++ b/src/components/ResourceListPage.tsx @@ -0,0 +1,40 @@ +/** + * ResourceListPage — placeholder resource list view. + * + * TODO: Replace this component with a real CRD list view. Typical approaches: + * + * 1. Use K8s.ResourceClasses to build a typed resource class for your CRD: + * ```ts + * const MyResource = K8s.makeKubeObject('MyResource'); + * MyResource.apiEndpoint = K8s.ApiProxy.apiFactory('your.group.io', 'v1', 'yourresources'); + * ``` + * + * 2. Then use the ResourceListView or SectionBox + a data-fetching hook to list instances. + * + * 3. Remove the placeholder NameValueTable below and replace with your real UI. + * + * See https://headlamp.dev/docs/latest/development/plugins/functionality/ for the full SDK guide. + */ + +import { + NameValueTable, + SectionBox, + SectionHeader, +} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import React from 'react'; + +// TODO: Replace this with your CRD list using K8s.ResourceClasses or K8s.makeKubeObject +export default function ResourceListPage() { + const placeholderRows = [ + { name: 'Resource Group', value: 'your.group.io/v1' }, + { name: 'Kind', value: 'YourCustomResource' }, + { name: 'Namespace', value: 'default (or cluster-scoped)' }, + { name: 'Description', value: 'Replace this view with your real CRD list' }, + ]; + + return ( + }> + + + ); +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..2b67fbe --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,43 @@ +/** + * my-headlamp-plugin — entry point. + * + * Registers sidebar entries and routes for this Headlamp plugin. + * Replace the sidebar labels, URLs, and route component with your own. + */ + +import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; +import React from 'react'; +import ResourceListPage from './components/ResourceListPage'; + +// --------------------------------------------------------------------------- +// Sidebar entries +// --------------------------------------------------------------------------- + +// Top-level sidebar section (parent = null creates a new group in the nav) +registerSidebarEntry({ + parent: null, + name: 'my-plugin', + label: 'My Plugin', + url: '/my-plugin', + icon: 'mdi:kubernetes', +}); + +// Child entry — shown nested under the parent section above +registerSidebarEntry({ + parent: 'my-plugin', + name: 'my-plugin-list', + label: 'Resources', + url: '/my-plugin', +}); + +// --------------------------------------------------------------------------- +// Routes +// --------------------------------------------------------------------------- + +registerRoute({ + path: '/my-plugin', + sidebar: 'my-plugin-list', + name: 'my-plugin-list', + exact: true, + component: () => , +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bf3823f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@kinvolk/headlamp-plugin/config/plugins-tsconfig.json", + "compilerOptions": { + "jsx": "react", + "types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals", "@testing-library/jest-dom"] + }, + "include": ["src"] +} diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..755de9d --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + exclude: ['e2e/**', 'node_modules/**'], + env: { NODE_ENV: 'test' }, + }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..1fa97b6 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,43 @@ +import '@testing-library/jest-dom'; + +// Node 22+ ships a minimal built-in `localStorage` global (property-bag only, +// no getItem/setItem/removeItem/clear) that shadows jsdom's Web Storage +// implementation. Provide a spec-compliant shim so code under test works. +if (typeof localStorage !== 'undefined' && typeof localStorage.getItem !== 'function') { + const store = new Map(); + + const storage = { + getItem(key: string): string | null { + return store.get(key) ?? null; + }, + setItem(key: string, value: string): void { + store.set(key, String(value)); + }, + removeItem(key: string): void { + store.delete(key); + }, + clear(): void { + store.clear(); + }, + get length(): number { + return store.size; + }, + key(index: number): string | null { + return [...store.keys()][index] ?? null; + }, + }; + + Object.defineProperty(globalThis, 'localStorage', { + value: storage, + writable: true, + configurable: true, + }); + + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'localStorage', { + value: storage, + writable: true, + configurable: true, + }); + } +}