@rxtx4816/cockpit-plugin-base-react 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/docs/wiki/CI-CD.md +87 -0
- package/docs/wiki/Components.md +56 -0
- package/docs/wiki/Getting-Started.md +92 -0
- package/docs/wiki/Home.md +13 -0
- package/docs/wiki/Hooks.md +58 -0
- package/docs/wiki/Systemd.md +43 -0
- package/docs/wiki/Testing.md +51 -0
- package/docs/wiki/VM-Testing.md +99 -0
- package/package.json +7 -3
- package/src/cockpit.d.ts +1 -0
- package/src/systemd/useServiceStatus.test.ts +1 -1
- package/{vitest.config.base.ts → vitest.config.base.js} +1 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RXTX4816
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @rxtx4816/cockpit-plugin-base-react
|
|
2
|
+
|
|
3
|
+
Shared foundation for building [Cockpit](https://cockpit-project.org/) plugins with React and PatternFly v6. Extracts the boilerplate that every plugin needs — bootstrapping, i18n, dark theme, async patterns, systemd integration, shared tooling config, and a full QEMU VM test harness — so each plugin only contains its own logic.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
|
|
7
|
+
**Plugin runtime**
|
|
8
|
+
- `bootstrapPlugin` — mounts your React app into the Cockpit frame with i18n and error boundary wired up
|
|
9
|
+
- `dark-theme` — side-effect module that automatically syncs the `pf-v6-theme-dark` class with the Cockpit shell, responding to user preference changes and system theme
|
|
10
|
+
- `initCockpitI18n` — sets up i18next with Cockpit's locale loading conventions
|
|
11
|
+
|
|
12
|
+
**Hooks**
|
|
13
|
+
- `useAsyncAction` — wraps an async operation with `loading`, `error`, and `execute` state; ideal for buttons that trigger backend calls
|
|
14
|
+
- `useAutoRefresh` — runs a callback on a configurable interval, with manual refresh support
|
|
15
|
+
- `useAsyncStream` — consumes a Cockpit channel as a line-buffered async stream
|
|
16
|
+
- `useConfirmAction` — multi-step confirmation flow with typed state transitions
|
|
17
|
+
- `usePollingFetch` — fetch with automatic polling, refresh, and loading state
|
|
18
|
+
|
|
19
|
+
**Components**
|
|
20
|
+
- `ConfirmDialog` — confirmation modal driven by `useConfirmAction`, supports multi-step flows
|
|
21
|
+
- `ErrorBoundary` — catches render errors and shows a PatternFly alert with details
|
|
22
|
+
- `HelpPopover` — PatternFly popover for contextual help text
|
|
23
|
+
- `LogViewer` — scrollable terminal-style log display backed by an async stream
|
|
24
|
+
- `StatusBadge` — color-coded badge for service or resource states
|
|
25
|
+
- `ToastProvider` + hook — global toast notification system
|
|
26
|
+
|
|
27
|
+
**Systemd layer**
|
|
28
|
+
- `useServiceStatus` — reactive hook for a systemd service state (active, failed, inactive…)
|
|
29
|
+
- `ServiceControl` — start/stop/restart/enable control component
|
|
30
|
+
- `api` — typed wrappers around `cockpit.spawn` for systemctl operations
|
|
31
|
+
|
|
32
|
+
**Shared tooling config**
|
|
33
|
+
- `tsconfig.base.json` — TypeScript base config tuned for Cockpit plugins
|
|
34
|
+
- `eslint.config.base` — `createEslintConfig()` factory with TS, React, and react-hooks rules
|
|
35
|
+
- `vitest.config.base` — Vitest base config with jsdom and PatternFly setup
|
|
36
|
+
|
|
37
|
+
**Testing utilities**
|
|
38
|
+
- Vitest setup file that installs jsdom and jest-dom matchers
|
|
39
|
+
- `mockCockpit` — in-memory Cockpit API mock for unit tests
|
|
40
|
+
- `mockHttpClient` — mock for Cockpit HTTP client used in tests
|
|
41
|
+
|
|
42
|
+
**QEMU VM test harness**
|
|
43
|
+
- `npm run vm` — spins up real cloud VMs (Arch, Debian, Fedora) with Cockpit installed and your plugin mounted via virtfs. Used for end-to-end testing against a live Cockpit instance. See [VM Testing](docs/wiki/VM-Testing.md).
|
|
44
|
+
|
|
45
|
+
**Reusable CI/CD workflows**
|
|
46
|
+
- Lint, typecheck, test, and build on every push
|
|
47
|
+
- RPM, DEB, and Arch package build verification
|
|
48
|
+
- Semantic version bumping from conventional commits
|
|
49
|
+
- Automated release asset upload and AUR publishing
|
|
50
|
+
- GitHub Wiki sync from `docs/wiki/`
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @rxtx4816/cockpit-plugin-base-react
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Peer dependencies: `react >=19`, `react-dom >=19`, `i18next >=26`, `react-i18next >=17`
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// src/index.tsx
|
|
64
|
+
import "./i18n";
|
|
65
|
+
import "@rxtx4816/cockpit-plugin-base-react/dark-theme";
|
|
66
|
+
import { bootstrapPlugin } from "@rxtx4816/cockpit-plugin-base-react/bootstrap";
|
|
67
|
+
import App from "./App";
|
|
68
|
+
|
|
69
|
+
bootstrapPlugin(App);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For full setup guidance, config sharing, and workflow integration see the [wiki](docs/wiki/Home.md).
|
|
73
|
+
|
|
74
|
+
## Documentation
|
|
75
|
+
|
|
76
|
+
- [Getting Started](docs/wiki/Getting-Started.md)
|
|
77
|
+
- [Hooks](docs/wiki/Hooks.md)
|
|
78
|
+
- [Components](docs/wiki/Components.md)
|
|
79
|
+
- [Systemd Layer](docs/wiki/Systemd.md)
|
|
80
|
+
- [Testing](docs/wiki/Testing.md)
|
|
81
|
+
- [VM Testing](docs/wiki/VM-Testing.md)
|
|
82
|
+
- [CI/CD Workflows](docs/wiki/CI-CD.md)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT © 2026 RXTX4816
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# CI/CD Workflows
|
|
2
|
+
|
|
3
|
+
The package ships reusable GitHub Actions workflows that plugins call with `uses:`. This keeps CI logic in one place and lets all plugins inherit fixes and improvements automatically.
|
|
4
|
+
|
|
5
|
+
## Using the workflows
|
|
6
|
+
|
|
7
|
+
In your plugin's `.github/workflows/`:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
# .github/workflows/ci.yml
|
|
11
|
+
name: CI
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches: [main]
|
|
15
|
+
pull_request:
|
|
16
|
+
branches: [main]
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
ci:
|
|
20
|
+
uses: RXTX4816/cockpit-plugin-base-react/.github/workflows/ci-plugin.yml@main
|
|
21
|
+
with:
|
|
22
|
+
plugin-name: cockpit-caddy
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Available workflows
|
|
28
|
+
|
|
29
|
+
### ci-plugin.yml
|
|
30
|
+
|
|
31
|
+
Runs on every push and pull request. Steps: lint → typecheck → test → build → verify build output.
|
|
32
|
+
|
|
33
|
+
Required inputs:
|
|
34
|
+
- `plugin-name` — used to verify `src/main.js` and `src/main.css` exist after build
|
|
35
|
+
|
|
36
|
+
### pkg-ci-plugin.yml
|
|
37
|
+
|
|
38
|
+
Builds and verifies RPM, DEB, and Arch packages using a placeholder `0.0.0` version. Confirms that the packaging definitions (`.spec`, `debian/`, `PKGBUILD`) are valid before a real release.
|
|
39
|
+
|
|
40
|
+
Required inputs:
|
|
41
|
+
- `plugin-name`
|
|
42
|
+
- `spec-file` — path to the RPM spec file (e.g. `cockpit-caddy.spec`)
|
|
43
|
+
- `aur-pkgname` — AUR package name
|
|
44
|
+
|
|
45
|
+
### semantic-release-plugin.yml
|
|
46
|
+
|
|
47
|
+
Determines the next version from conventional commits since the last git tag and creates the tag. Does not build or publish anything — it only tags. The tag then triggers whatever release workflow you have listening on `push.tags`.
|
|
48
|
+
|
|
49
|
+
Version bump rules:
|
|
50
|
+
- `BREAKING CHANGE` anywhere in a commit body → major
|
|
51
|
+
- `feat:` prefix → minor
|
|
52
|
+
- anything else → patch
|
|
53
|
+
- no commits since last tag → skip (exits 0, no tag created)
|
|
54
|
+
|
|
55
|
+
Required secrets:
|
|
56
|
+
- `RELEASE_TOKEN` — a GitHub token with `contents: write` permission
|
|
57
|
+
|
|
58
|
+
Optional inputs:
|
|
59
|
+
- `initial_version` — version to use when no prior tag exists (default: `v1.0.0`)
|
|
60
|
+
|
|
61
|
+
### release-plugin.yml
|
|
62
|
+
|
|
63
|
+
Builds release assets (tarball + sha256, RPM, DEB) and uploads them to a GitHub release. Also updates the `PKGBUILD` and pushes to AUR.
|
|
64
|
+
|
|
65
|
+
Required inputs:
|
|
66
|
+
- `plugin-name`, `spec-file`, `aur-pkgname`
|
|
67
|
+
- `maintainer-name`, `maintainer-email`
|
|
68
|
+
|
|
69
|
+
Required secrets:
|
|
70
|
+
- `AUR_SSH_KEY` — SSH private key registered with the AUR account
|
|
71
|
+
- `RELEASE_TOKEN` — GitHub token with `contents: write`
|
|
72
|
+
|
|
73
|
+
### sync-wiki-plugin.yml
|
|
74
|
+
|
|
75
|
+
Copies all `.md` files from `docs/wiki/` in your plugin repo to the GitHub Wiki. Triggered by push to main.
|
|
76
|
+
|
|
77
|
+
No inputs or secrets required beyond the default `GITHUB_TOKEN`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Secrets reference
|
|
82
|
+
|
|
83
|
+
| Secret | Used by | Description |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `RELEASE_TOKEN` | semantic-release, release | GitHub PAT with `contents: write` |
|
|
86
|
+
| `AUR_SSH_KEY` | release | SSH private key for AUR pushes |
|
|
87
|
+
| `NPM_TOKEN` | (base package only) | npm automation token for publishing |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
All components are exported from the components entrypoint:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { ConfirmDialog, ErrorBoundary, HelpPopover, LogViewer, StatusBadge, ToastProvider } from "@rxtx4816/cockpit-plugin-base-react/components";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`ToastProvider` is mounted automatically by `bootstrapPlugin`. The others are available for use anywhere in your plugin.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ConfirmDialog
|
|
14
|
+
|
|
15
|
+
A PatternFly modal dialog driven by `useConfirmAction` state. Supports multi-step confirmation flows — for example, showing a warning first and requiring the user to type a resource name before proceeding.
|
|
16
|
+
|
|
17
|
+
Receives the `state` and `cancel` from `useConfirmAction` and renders the appropriate step content.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ErrorBoundary
|
|
22
|
+
|
|
23
|
+
A React error boundary that catches render errors anywhere in the component tree and displays a PatternFly alert with the error message and stack trace. Prevents the entire plugin from going blank on an unexpected error.
|
|
24
|
+
|
|
25
|
+
Mounted automatically by `bootstrapPlugin`, but can also be used to wrap specific subtrees.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## HelpPopover
|
|
30
|
+
|
|
31
|
+
A small PatternFly popover for contextual help. Renders a help icon button that opens a popover with a title and body text. Use it next to form fields or section headings to explain non-obvious behaviour.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## LogViewer
|
|
36
|
+
|
|
37
|
+
A scrollable, terminal-style log display that accepts an array of output lines (typically from `useAsyncStream`). Automatically scrolls to the bottom on new output. Used for displaying real-time command output or service journal entries.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## StatusBadge
|
|
42
|
+
|
|
43
|
+
A color-coded label for service or resource states. Maps state strings (e.g. `"active"`, `"failed"`, `"inactive"`, `"unknown"`) to PatternFly status colors. Used by `ServiceControl` and can be used standalone wherever you need to display a state.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ToastProvider
|
|
48
|
+
|
|
49
|
+
Global toast notification context. Wrap your app with `ToastProvider` (done automatically by `bootstrapPlugin`) and use the `useToast` hook to fire notifications from anywhere:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const { addToast } = useToast();
|
|
53
|
+
addToast({ title: "Saved", variant: "success" });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Toasts are displayed in the top-right corner and auto-dismiss after a configurable timeout.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @rxtx4816/cockpit-plugin-base-react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Peer dependencies required in your plugin:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install react react-dom i18next react-i18next
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Bootstrapping your plugin
|
|
18
|
+
|
|
19
|
+
Every Cockpit plugin needs an entry point that initialises i18n, the dark theme, and mounts React. This package handles all of it:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// src/index.tsx
|
|
23
|
+
import "./i18n";
|
|
24
|
+
import "@rxtx4816/cockpit-plugin-base-react/dark-theme";
|
|
25
|
+
import { bootstrapPlugin } from "@rxtx4816/cockpit-plugin-base-react/bootstrap";
|
|
26
|
+
import App from "./App";
|
|
27
|
+
|
|
28
|
+
bootstrapPlugin(App);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`bootstrapPlugin` wraps your app in an `ErrorBoundary` and a `ToastProvider`, then mounts it into the `#app` element that Cockpit expects.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## i18n setup
|
|
36
|
+
|
|
37
|
+
Create `src/i18n.ts` in your plugin:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { initCockpitI18n } from "@rxtx4816/cockpit-plugin-base-react/i18n";
|
|
41
|
+
|
|
42
|
+
initCockpitI18n();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This sets up i18next with Cockpit's locale loading conventions so `useTranslation()` works throughout your plugin.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Shared tooling config
|
|
50
|
+
|
|
51
|
+
Extend from the base configs so all plugins stay consistent.
|
|
52
|
+
|
|
53
|
+
**tsconfig.json**
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"extends": "@rxtx4816/cockpit-plugin-base-react/tsconfig.base.json",
|
|
57
|
+
"compilerOptions": {
|
|
58
|
+
"paths": {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**eslint.config.js**
|
|
64
|
+
```js
|
|
65
|
+
import { createEslintConfig } from "@rxtx4816/cockpit-plugin-base-react/eslint.config.base";
|
|
66
|
+
|
|
67
|
+
export default createEslintConfig();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Pass extra globals if your plugin uses custom Cockpit types:
|
|
71
|
+
```js
|
|
72
|
+
export default createEslintConfig({ CockpitHttpClient: "readonly" });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**vitest.config.ts**
|
|
76
|
+
```ts
|
|
77
|
+
import { defineConfig } from "@rxtx4816/cockpit-plugin-base-react/vitest.config.base";
|
|
78
|
+
|
|
79
|
+
export default defineConfig();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Dark theme
|
|
85
|
+
|
|
86
|
+
Importing the `dark-theme` side-effect module is all that's needed. It listens for three signals and keeps the `pf-v6-theme-dark` class on `<html>` in sync:
|
|
87
|
+
|
|
88
|
+
- `localStorage` key `shell:style` (values: `"light"`, `"dark"`, `"auto"`)
|
|
89
|
+
- The custom `cockpit-style` event dispatched by the Cockpit shell switcher
|
|
90
|
+
- The OS-level `prefers-color-scheme` media query
|
|
91
|
+
|
|
92
|
+
No configuration required.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# cockpit-plugin-base-react Wiki
|
|
2
|
+
|
|
3
|
+
This wiki covers everything you need to build, test, and ship Cockpit plugins using `@rxtx4816/cockpit-plugin-base-react`.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Getting Started](Getting-Started.md) — install, bootstrap, and first plugin setup
|
|
8
|
+
- [Hooks](Hooks.md) — async state, polling, streams, confirmation flows
|
|
9
|
+
- [Components](Components.md) — UI building blocks: toasts, dialogs, logs, badges
|
|
10
|
+
- [Systemd Layer](Systemd.md) — service status, control, and typed API wrappers
|
|
11
|
+
- [Testing](Testing.md) — unit testing with mocked Cockpit and HTTP client
|
|
12
|
+
- [VM Testing](VM-Testing.md) — end-to-end testing with real QEMU VMs
|
|
13
|
+
- [CI/CD Workflows](CI-CD.md) — reusable GitHub Actions for builds, packages, and releases
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Hooks
|
|
2
|
+
|
|
3
|
+
All hooks are exported from the main entrypoint:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { useAsyncAction, useAutoRefresh, useAsyncStream, useConfirmAction, usePollingFetch } from "@rxtx4816/cockpit-plugin-base-react";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## useAsyncAction
|
|
12
|
+
|
|
13
|
+
Wraps an async operation with `loading`, `error`, and `execute` state. Designed for buttons or forms that trigger backend calls.
|
|
14
|
+
|
|
15
|
+
The hook returns an object with:
|
|
16
|
+
- `execute(...args)` — triggers the operation
|
|
17
|
+
- `loading` — true while the operation is in progress
|
|
18
|
+
- `error` — the caught error if the operation failed, or null
|
|
19
|
+
- `reset()` — clears error state
|
|
20
|
+
|
|
21
|
+
Errors are caught automatically; unhandled promise rejections do not propagate to the component tree.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## useAutoRefresh
|
|
26
|
+
|
|
27
|
+
Runs a callback on a configurable interval. Returns a `refresh()` function for on-demand triggering and a `loading` flag.
|
|
28
|
+
|
|
29
|
+
Useful for periodically re-fetching data without managing `setInterval` lifecycle yourself. The interval is cleared on unmount.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## useAsyncStream
|
|
34
|
+
|
|
35
|
+
Consumes a Cockpit channel as a line-buffered async stream. Returns the accumulated output lines and an `error` state.
|
|
36
|
+
|
|
37
|
+
Used internally by `LogViewer` and useful anywhere you need to display or process real-time output from a spawned process.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## useConfirmAction
|
|
42
|
+
|
|
43
|
+
Manages a multi-step confirmation flow with typed state transitions. Returns:
|
|
44
|
+
|
|
45
|
+
- `state` — current step or `null` when idle
|
|
46
|
+
- `start(initialStep)` — opens the flow
|
|
47
|
+
- `next(step)` — advances to the next step
|
|
48
|
+
- `cancel()` — resets to idle
|
|
49
|
+
|
|
50
|
+
Pairs with `ConfirmDialog` to build destructive action flows (e.g. delete with a typed confirmation).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## usePollingFetch
|
|
55
|
+
|
|
56
|
+
Fetches a resource with automatic polling and returns `{ data, loading, error, refresh }`. The poll interval is configurable.
|
|
57
|
+
|
|
58
|
+
Handles the full lifecycle: initial fetch, polling, cleanup on unmount, and error recovery. Calling `refresh()` triggers an immediate re-fetch and resets the poll timer.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Systemd Layer
|
|
2
|
+
|
|
3
|
+
The systemd layer provides typed hooks, a control component, and low-level API wrappers for interacting with systemd services through Cockpit.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { useServiceStatus, ServiceControl } from "@rxtx4816/cockpit-plugin-base-react/systemd";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## useServiceStatus
|
|
12
|
+
|
|
13
|
+
Reactive hook that tracks the state of a systemd service. Polls via `systemctl is-active` and returns:
|
|
14
|
+
|
|
15
|
+
- `status` — one of `"active"`, `"inactive"`, `"failed"`, `"activating"`, `"deactivating"`, `"unknown"`
|
|
16
|
+
- `loading` — true on the initial fetch
|
|
17
|
+
- `error` — any error from the underlying `cockpit.spawn` call
|
|
18
|
+
- `refresh()` — manually re-check the service state
|
|
19
|
+
|
|
20
|
+
The hook automatically re-polls at a configurable interval, so your UI stays in sync without manual management.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ServiceControl
|
|
25
|
+
|
|
26
|
+
A self-contained component that renders start/stop/restart/enable/disable buttons for a named service. Internally uses `useServiceStatus` for the current state and the `api` helpers to dispatch commands.
|
|
27
|
+
|
|
28
|
+
Buttons are disabled while an operation is in progress. Status changes are reflected immediately via an optimistic state update, then confirmed by the next poll.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## API helpers
|
|
33
|
+
|
|
34
|
+
Low-level typed wrappers around `cockpit.spawn` for common systemctl operations:
|
|
35
|
+
|
|
36
|
+
- `startService(name)` — `systemctl start`
|
|
37
|
+
- `stopService(name)` — `systemctl stop`
|
|
38
|
+
- `restartService(name)` — `systemctl restart`
|
|
39
|
+
- `enableService(name)` — `systemctl enable`
|
|
40
|
+
- `disableService(name)` — `systemctl disable`
|
|
41
|
+
- `getServiceStatus(name)` — returns the current active state string
|
|
42
|
+
|
|
43
|
+
All functions return Promises and throw on non-zero exit codes.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
The package ships a Vitest setup file that configures jsdom and installs jest-dom matchers. Reference it from your plugin's `vitest.config.ts` (handled automatically when you extend the base config):
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineConfig } from "@rxtx4816/cockpit-plugin-base-react/vitest.config.base";
|
|
9
|
+
export default defineConfig();
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Test utilities
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { mockCockpit, mockHttpClient } from "@rxtx4816/cockpit-plugin-base-react/testing/helpers";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### mockCockpit
|
|
21
|
+
|
|
22
|
+
Returns an in-memory mock of the `cockpit` browser global. Stubs out `cockpit.spawn`, `cockpit.file`, `cockpit.http`, and channel creation so tests never attempt real system calls.
|
|
23
|
+
|
|
24
|
+
Set it up in your test file:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.stubGlobal("cockpit", mockCockpit());
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Individual spawn calls can be configured to return specific output or to reject, letting you test both success and error paths.
|
|
33
|
+
|
|
34
|
+
### mockHttpClient
|
|
35
|
+
|
|
36
|
+
Returns a mock of the Cockpit HTTP client with configurable per-path responses. Useful for testing components that call `cockpit.http().get(path)`.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const client = mockHttpClient({ "/api/status": '{"running": true}' });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`get`, `post`, and `request` are all `vi.fn()` instances, so you can assert call counts and arguments with standard Vitest matchers.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Running tests
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm test # single run
|
|
50
|
+
npm run test:watch # watch mode
|
|
51
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# VM Testing
|
|
2
|
+
|
|
3
|
+
The package ships a QEMU-based VM harness for end-to-end testing against a real Cockpit instance running on a real OS. It spins up cloud VMs (Arch, Debian, Fedora by default), installs Cockpit, and mounts your built plugin via virtfs — no packaging or installation step required.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Arch Linux:
|
|
8
|
+
```bash
|
|
9
|
+
sudo pacman -S qemu-full cloud-image-utils wget
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
KVM access is strongly recommended. Without it the VMs run without hardware acceleration and will be significantly slower.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
1. A base cloud image is downloaded once per distro and kept on disk
|
|
17
|
+
2. Each VM gets a thin overlay disk so the base image is never modified
|
|
18
|
+
3. cloud-init provisions the VM on first boot: creates a `test` user, installs Cockpit and any plugin-specific packages, and mounts your plugin's `src/` directory read-only into the Cockpit install path via 9p/virtfs
|
|
19
|
+
4. Changes to your built output (`src/main.js`, `src/main.css`) are immediately visible inside the VM without a restart
|
|
20
|
+
|
|
21
|
+
## Plugin configuration
|
|
22
|
+
|
|
23
|
+
Each plugin provides `scripts/test-vm.config.sh` to customise the harness:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
PLUGIN_NAME="cockpit-caddy"
|
|
27
|
+
MOUNT_TAG="cockpit_caddy"
|
|
28
|
+
INSTALL_PATH="/usr/share/cockpit/cockpit-caddy"
|
|
29
|
+
|
|
30
|
+
extra_packages() {
|
|
31
|
+
echo "caddy"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
extra_runcmd() {
|
|
35
|
+
echo " - systemctl enable --now caddy"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Add to your plugin's `package.json`:
|
|
42
|
+
```json
|
|
43
|
+
"vm": "node_modules/@rxtx4816/cockpit-plugin-base-react/scripts/test-vm.sh"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run build # build the plugin first
|
|
50
|
+
npm run vm download arch # download base image (once)
|
|
51
|
+
npm run vm start arch # start the VM
|
|
52
|
+
npm run vm wait arch # block until cloud-init finishes (~2 min first boot)
|
|
53
|
+
# open https://localhost:9090 — login: test / test
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After the initial boot, subsequent starts are fast (no re-provisioning unless you `clean`).
|
|
57
|
+
|
|
58
|
+
## All commands
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `download [vm\|all]` | Download base cloud images |
|
|
63
|
+
| `build` | Run `npm run build` |
|
|
64
|
+
| `start [vm ...]` | Start VM(s) in background |
|
|
65
|
+
| `wait <vm>` | Block until cloud-init completes |
|
|
66
|
+
| `stop [vm ...]` | Stop VM(s) |
|
|
67
|
+
| `status` | Show all VMs with ports and running state |
|
|
68
|
+
| `ssh <vm>` | Open an SSH session into the VM |
|
|
69
|
+
| `logs <vm>` | Tail the VM serial console |
|
|
70
|
+
| `clean [vm ...]` | Wipe disk and cloud-init state (base image kept) |
|
|
71
|
+
| `rebuild [vm ...]` | `clean` + `start` in one step |
|
|
72
|
+
| `reset [vm ...]` | Remove all VM files including base image |
|
|
73
|
+
|
|
74
|
+
## Ports
|
|
75
|
+
|
|
76
|
+
By default the VMs are assigned sequential ports starting from:
|
|
77
|
+
- Cockpit: `9090`, `9091`, `9092` (arch, debian, fedora)
|
|
78
|
+
- SSH: `2220`, `2221`, `2222`
|
|
79
|
+
|
|
80
|
+
These can be changed in your `test-vm.config.sh` via `SSH_BASE` and `COCKPIT_BASE`.
|
|
81
|
+
|
|
82
|
+
## Live reload workflow
|
|
83
|
+
|
|
84
|
+
Because the plugin is mounted via virtfs, you can iterate quickly:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run watch # rebuild on source changes
|
|
88
|
+
npm run vm start arch
|
|
89
|
+
npm run vm wait arch
|
|
90
|
+
# reload the browser tab after each rebuild — no VM restart needed
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Environment overrides
|
|
94
|
+
|
|
95
|
+
| Variable | Default | Description |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `VM_MEM` | `1024` | Memory per VM in MB |
|
|
98
|
+
| `VM_CPUS` | `2` | vCPU count |
|
|
99
|
+
| `VM_DISK_SIZE` | `12G` | Overlay disk size |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rxtx4816/cockpit-plugin-base-react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Shared infrastructure for Cockpit plugins: i18n, dark theme, test setup, config presets, CI/CD workflows, and QEMU VM harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "RXTX4816",
|
|
@@ -60,20 +60,24 @@
|
|
|
60
60
|
"default": "./eslint.config.base.js"
|
|
61
61
|
},
|
|
62
62
|
"./vitest.config.base": {
|
|
63
|
-
"default": "./vitest.config.base.
|
|
63
|
+
"default": "./vitest.config.base.js"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"files": [
|
|
67
|
+
"LICENSE",
|
|
68
|
+
"README.md",
|
|
69
|
+
"docs/",
|
|
67
70
|
"src/",
|
|
68
71
|
"scripts/",
|
|
69
72
|
"tsconfig.base.json",
|
|
70
73
|
"eslint.config.base.js",
|
|
71
|
-
"vitest.config.base.
|
|
74
|
+
"vitest.config.base.js"
|
|
72
75
|
],
|
|
73
76
|
"bin": {
|
|
74
77
|
"cockpit-test-vm": "./scripts/test-vm.sh"
|
|
75
78
|
},
|
|
76
79
|
"scripts": {
|
|
80
|
+
"lint": "eslint src/",
|
|
77
81
|
"typecheck": "tsc --noEmit",
|
|
78
82
|
"test": "vitest run",
|
|
79
83
|
"test:watch": "vitest"
|
package/src/cockpit.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Ambient type declarations for the Cockpit browser global.
|
|
2
2
|
// Superset covering cockpit-caddy and cockpit-compose usage patterns.
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
|
3
4
|
/// <reference path="./css.d.ts" />
|
|
4
5
|
|
|
5
6
|
declare interface CockpitProcess extends Promise<string> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
2
|
-
import { vi, describe, it, expect, beforeEach
|
|
2
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
3
3
|
import { useServiceStatus } from "./useServiceStatus";
|
|
4
4
|
|
|
5
5
|
const mockSpawn = vi.fn();
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import { defineConfig } from "vitest/config";
|
|
2
|
-
import type { TestUserConfig } from "vitest/config";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export function createVitestConfig(overrides: TestConfig = {}) {
|
|
3
|
+
export function createVitestConfig(overrides = {}) {
|
|
7
4
|
const { coverage: coverageOverrides, setupFiles: extraSetupFiles, ...rest } = overrides;
|
|
8
5
|
|
|
9
6
|
return defineConfig({
|
|
10
7
|
server: {
|
|
11
|
-
// Allow Vite's dev server to serve files from symlinked file: packages
|
|
12
|
-
// that live outside the consuming project's root directory.
|
|
13
8
|
fs: { allow: [".."] },
|
|
14
9
|
},
|
|
15
10
|
resolve: {
|
|
16
|
-
// Deduplicate packages that must be singletons when cockpit-plugin-base-react
|
|
17
|
-
// is installed as a file: link (symlink) — without this, the linked
|
|
18
|
-
// package resolves these from its own node_modules and React / i18next
|
|
19
|
-
// end up with two separate instances, breaking hooks and translations.
|
|
20
11
|
dedupe: ["react", "react-dom", "i18next", "react-i18next", "@patternfly/react-core", "@patternfly/react-icons"],
|
|
21
12
|
},
|
|
22
13
|
test: {
|