A Cockpit module for managing Kata Containers — lightweight VM-based container sandboxes that provide hardware-level isolation for container workloads.

This commit is contained in:
Jeremy Anderson 2026-07-21 21:30:46 -04:00
commit 5f9f15842b
209 changed files with 38069 additions and 0 deletions

190
LICENSE Executable file
View File

@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to 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 discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024-2026 Cockpit Kata Containers Contributors
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.

460
README.md Executable file
View File

@ -0,0 +1,460 @@
# Cockpit Kata Containers
A [Cockpit](https://cockpit-project.org/) module for managing [Kata Containers](https://katacontainers.io/) — lightweight VM-based container sandboxes that provide hardware-level isolation for container workloads.
![Cockpit Kata Containers](https://img.shields.io/badge/cockpit-286%2B-blue) ![Kata](https://img.shields.io/badge/kata--runtime-2.5%2B-green) ![License](https://img.shields.io/badge/license-Apache--2.0-orange)
## What It Does
Cockpit Kata Containers brings the same usability-first approach as Cockpit's built-in **machines** module (libvirt VM management) to Kata Containers. It provides a unified interface for:
- **Sandbox Management** — List, inspect, start, stop, and restart Kata sandboxes across all VMM backends (QEMU, Cloud Hypervisor, Firecracker, Dragonball)
- **Image Import** — Import rootfs tarballs, initrd images, and **QCrows** self-describing VM container archives with a multi-step validation wizard that checks for kata-agent, init systems, resolv.conf, size limits, format compliance, and archive integrity
- **Image Export** — Export QCrows images to **qcow2** disk images, **ISO** for live USB media, or **PXE** for network boot, directly from the Cockpit UI with format-specific configuration options
- **Kernel Management** — Inspect guest kernel configs, validate Kata-required options, review boot parameters, and regenerate initrd with environment-aware VMM module selection
- **Initrd Regeneration** — Rebuild initrd with precise awareness of the VMM transport layer, kernel compression support, and rootfs init style — unlike `dracut` or `mkinitramfs`, produces an initrd where kata-agent IS the final process
- **PXE Push** — Deploy QCrows images to TFTP servers for network boot with a one-click button, automatic dnsmasq configuration, and squashfs rootfs packing
- **Build Guide** — Step-by-step walkthrough for building compatible custom Kata container images (rootfs and initrd), with copyable commands and progress tracking
- **Runtime Configuration** — View and edit `configuration.toml` files for each Kata runtime profile with live TOML editing and structured/raw view toggle
- **Metrics Dashboard** — Real-time resource utilization from kata-monitor's Prometheus endpoint with per-sandbox CPU, memory, and network breakdowns
## Architecture
```
cockpit-kata-containers/
├── README.md # This file
├── quickstart.md # Quick-start installation guide
├── installer.sh # Automated OS-aware installer
├── LICENSE # Apache-2.0
├── Caddyfile # Reverse proxy config for standalone mode
├── qcrows-spec.md # QCrows format specification
├── qcrows-pack # CLI tool to create .qcrows archives
├── qcrows-verify # CLI tool to verify .qcrows integrity
├── qcrows-inspect # CLI tool to read .qcrows metadata
├── qcrows-export # CLI tool to export .qcrows to qcow2, ISO, or PXE
├── qcrows-initrd-regen # CLI tool to regenerate initrd (env-aware)
├── cockpit-kata/ # Production Cockpit module (webpack build)
│ ├── manifest.json # Cockpit package manifest
│ ├── webpack.config.js # Bundler config — outputs to dist/
│ ├── package.json # Build dependencies (React 18, webpack 5)
│ ├── tsconfig.json # TypeScript config
│ ├── cockpit.d.ts # Type declarations for cockpit.js APIs
│ └── src/
│ └── index.tsx # Entry point — mounts KataModule
├── src/ # Next.js prototype + shared components
│ ├── app/
│ │ ├── page.tsx # Renders KataModule
│ │ ├── layout.tsx # Dark theme root layout
│ │ ├── globals.css # Tailwind CSS base
│ │ └── api/kata/
│ │ ├── validate/route.ts # Pre-upload validation endpoint
│ │ └── upload/route.ts # File upload with server-side checks
│ ├── components/kata/
│ │ ├── KataModule.tsx # Main shell — sidebar + top bar + routing
│ │ ├── SandboxList.tsx # Sandbox table with summary cards
│ │ ├── SandboxDetail.tsx # Per-sandbox detail with 4 tabs
│ │ ├── ImportImage.tsx # 4-step import wizard + QCrows kernel validation
│ │ ├── ExportImage.tsx # Export to qcow2/ISO/PXE with format-specific config
│ │ ├── KernelDetail.tsx # Kernel config viewer + boot params + initrd regen
│ │ ├── BuildGuide.tsx # Rootfs/initrd build instructions
│ │ ├── Configuration.tsx # Runtime config viewer + TOML editor
│ │ └── MetricsDashboard.tsx # Aggregate + per-sandbox metrics
│ ├── lib/
│ │ ├── kata-mock.ts # Mock data layer + TypeScript interfaces
│ │ ├── backend/
│ │ │ ├── adapter.ts # Auto-detect cockpit vs mock backend
│ │ │ └── cockpit-api.ts # Real cockpit.spawn/http/file calls
│ │ └── utils.ts # cn() utility, shared helpers
│ └── hooks/
│ ├── use-toast.ts # Toast notifications
│ └── use-mobile.ts # Responsive breakpoint hook
├── package.json # Next.js project config
├── tsconfig.json # Root TypeScript config
├── tailwind.config.ts # Tailwind CSS config
├── postcss.config.mjs # PostCSS config
├── next.config.ts # Next.js config
├── eslint.config.mjs # ESLint config
└── components.json # shadcn/ui component registry
```
## Two Deployment Modes
### Mode 1: Cockpit Module (Production)
The `cockpit-kata/` directory builds into a self-contained Cockpit package that drops into `/usr/share/cockpit/kata/`. It uses:
- `cockpit.spawn()` — runs `kata-runtime`, `crictl`, `kata-ctl` on the host
- `cockpit.http()` — queries `kata-monitor` on `127.0.0.1:8090` for Prometheus metrics
- `cockpit.file()` — reads/writes `configuration.toml` with atomic TOML round-tripping
Build and install:
```bash
cd cockpit-kata
npm install
npm run build
sudo cp -r dist/ /usr/share/cockpit/kata/
```
### Mode 2: Standalone Web App (Development/Demo)
The Next.js prototype runs independently with mock data, no Kata or Cockpit installation required:
```bash
npm install
npm run dev
# Open http://localhost:3000
```
The backend adapter (`src/lib/backend/adapter.ts`) auto-detects whether `cockpit` is available in the global scope and routes API calls to either the real Cockpit backend or the mock data layer. This means the same UI components work in both modes without modification.
## QCrows — Self-Describing VM Container Images
QCrows (pronounced *cue-crows*) is a self-describing container image format designed for VM-based container runtimes like Kata Containers. A `.qcrows` file bundles everything needed to deploy a VM container into a single, verifiable archive.
### What's in a QCrows Archive
| Component | Required | Description |
|-----------|----------|-------------|
| `metadata.toml` | Yes | Image manifest — name, version, arch, hypervisor compat, kernel info, agent version |
| `menu.toml` | Yes | Cockpit UI menu entry — label, category, init system, workload tags |
| `hashes.sha256` | Yes | SHA-256 integrity verification for all files |
| `rootfs.tar.*` | Yes | Guest OS filesystem (tar.gz, tar.xz, or tar.zst) |
| `initrd.*` | One of | Initial ramdisk (cpio-gzip, cpio-lz4, cpio-xz, or cramfs) |
| `kernel/` | **REQUIRED** | Guest kernel binary + config — eliminates kernel-rootfs mismatch |
| `boot-params.conf` | Optional | Per-image kernel command line parameters |
| `build.toml` | Recommended | Build provenance — build system, flags, source hashes |
| `spec.md` | Optional | Build guide for reproducing or modifying the image |
### Build System Support
The `qcrows-pack` tool auto-detects and supports:
| Build System | Package Manager | Typical Use |
|-------------|----------------|-------------|
| Gentoo | Portage | Full control, hardened builds |
| Source Mage (Sorcery) | cast/dispel | Source-based, spell system |
| Buildroot | make + kconfig | Embedded, cross-compilation |
| Lunar Linux | lin/lrm | Source-based, rolling |
| LEDE / OpenWrt | opkg + make | Routers, embedded, IoT |
| BTC | custom | Niche / custom pipeline |
### Creating a QCrows Image
```bash
# From individual build outputs
qcrows-pack \
--rootfs ./output/rootfs.tar.gz \
--initrd ./output/initrd.img \
--kernel ./output/bzImage \
--kernel-config ./output/.config \
--name "alpine-3.20-kata" --version "3.20.1" \
--build-system buildroot \
--hypervisors "qemu,cloud-hypervisor" \
-o alpine-3.20-kata.qcrows
# From a Buildroot output directory (auto-discovery)
qcrows-pack --from-dir ./buildroot/output/ \
--name "buildroot-kata" --build-system buildroot \
-o buildroot-kata.qcrows
# Verify before importing
qcrows-verify alpine-3.20-kata.qcrows
# Inspect metadata without extraction
qcrows-inspect alpine-3.20-kata.qcrows
# Export to qcow2 for QEMU direct boot
qcrows-export --format qcow2 alpine-3.20-kata.qcrows -o alpine-3.20-kata.qcow2
# Export to bootable ISO for live USB
qcrows-export --format iso alpine-3.20-kata.qcrows -o alpine-live.iso
dd if=alpine-live.iso of=/dev/sdX bs=4M status=progress && sync
```
### Importing in Cockpit
1. Click **Import Image** in the sidebar
2. Select **QCrows Image** as the import mode
3. Drop a `.qcrows` file — the module auto-detects the format
4. The 11-step validator runs: extract → verify hashes → parse metadata → parse menu → check compatibility → validate kernel binary → validate kernel config → validate rootfs → validate initrd → validate boot-params → register
5. Review the parsed metadata (name, version, arch, kernel, hypervisors, agent) and the Cockpit menu entry preview
6. Click **Import QCrows Image**
See [qcrows-spec.md](qcrows-spec.md) for the complete format specification.
### Exporting QCrows Images
The **Export** view in the Cockpit UI and the `qcrows-export` CLI tool convert `.qcrows` archives into bootable formats:
**From the Cockpit UI:** Click **Export** in the sidebar → select a QCrows image → choose target format (qcow2, ISO, or PXE) → configure options → click **Export**. The UI provides format-specific options (EFI boot, disk size, partition labels, server IP), a progress indicator, and a usage hint on completion.
**From the CLI:**
**qcow2** — QEMU disk image with partitioned layout, rootfs installed, kernel in `/boot`, and bootloader configured:
```bash
# Auto-sized qcow2 with MBR boot
qcrows-export --format qcow2 image.qcrows -o disk.qcow2
# 2GB disk with EFI partition layout
qcrows-export --format qcow2 --disk-size 2G --efi image.qcrows -o disk-efi.qcow2
# Boot the result
qemu-system-x86_64 -m 1G -smp 2 -drive file=disk.qcow2,format=qcow2 -enable-kvm
```
**ISO** — Bootable live ISO for USB media, including a `/kata/` directory for host-side deployment:
```bash
# MBR-bootable (hybrid — dd-able to USB)
qcrows-export --format iso image.qcrows -o live.iso
# EFI-bootable
qcrows-export --format iso --efi image.qcrows -o live-efi.iso
# Write to USB
dd if=live.iso of=/dev/sdX bs=4M status=progress && sync
```
**PXE** — Push the image to a TFTP directory for network boot via dnsmasq:
```bash
# Deploy to TFTP with PXE configuration
qcrows-export --format pxe image.qcrows --server-ip 192.168.1.1
# Or use the "Push via PXE" button in the Import view
```
| Format | Boot Mode | Use Case | Prerequisites | UI Access |
|--------|-----------|----------|---------------|-----------|
| qcow2 | MBR/extlinux or EFI/GRUB | Standalone QEMU VM | qemu-img, parted, mkfs.ext4 | Export view |
| iso | MBR/isolinux or EFI/GRUB | Live USB, Kata deployment from media | xorriso, syslinux | Export view |
| pxe | pxelinux + dnsmasq | Network boot, bulk provisioning | dnsmasq, squashfs-tools | Export view + Import view PXE button |
### Initrd Regeneration
The `qcrows-initrd-regen` tool rebuilds the initrd component of a QCrows bundle with **environment awareness**. Unlike `dracut` or `mkinitramfs`, it produces an initrd where kata-agent IS the final process (no pivot_root) and includes ONLY the kernel modules required by the detected VMM transport layer.
**Auto-detection pipeline:**
1. **VMM** — Reads `kata-runtime` symlink and `configuration.toml` to determine active hypervisor
2. **Compression** — Inspects kernel `.config` for `CONFIG_RD_LZ4/GZIP/XZ/ZSTD`, selects fastest supported
3. **Init style** — Examines rootfs for `/sbin/init` (systemd), `/sbin/openrc-init` (OpenRC), or neither (busybox)
4. **kata-agent** — Scans rootfs for the agent binary at standard paths
5. **Modules** — Selects kernel modules from the VMM-specific matrix (QEMU: virtio-pci, CLH: virtio-mmio, Firecracker: virtio-mmio minimal, Dragonball: vsock-direct)
```bash
# Auto-detect everything from the host environment
qcrows-initrd-regen alpine-3.20-kata.qcrows
# Target QEMU with lz4 compression
qcrows-initrd-regen alpine.qcrows --vmm qemu --compress lz4
# Update the bundle in-place
qcrows-initrd-regen alpine.qcrows --in-place --vmm cloud-hypervisor
# Or use the "Regen Initrd" button in the Kernel Management view
```
## Features in Detail
### Sandbox Management
The sandbox list view mirrors the Cockpit machines module's VM list pattern:
- Summary cards at the top (Total Sandboxes, Running, Total vCPU, Total Memory)
- Searchable, filterable table with status badges, VMM type, and boot time
- Action dropdown per sandbox (Start / Stop / Restart / Console)
- Click-through to detail view with breadcrumb navigation back
The detail view provides:
- **Overview tab** — Identity (name, namespace, UID, runtime class) and Runtime (VMM, kernel, agent, vCPU, memory)
- **Metrics tab** — CPU/memory usage with sparklines, VMM API call counts, network I/O
- **Network tab** — vsock connection details for kata-agent communication
- **Console tab** — Text terminal to the kata-agent inside the guest VM
### Image Import
A 4-step wizard guides users through importing Kata container images:
1. **Select** — Choose image type (Rootfs Tarball or Initrd Image) and drag-and-drop the file
2. **Configure** — Set image name and hypervisor compatibility (QEMU / CLH / Firecracker / Dragonball)
3. **Validate** — Automatic checks for format compliance, required files (init, kata-agent, resolv.conf), size limits, and security concerns
4. **Complete** — Import progress with stage messages and validation summary
Validation rules include:
| Rule | Applies To | Severity |
|------|-----------|----------|
| Valid tar archive | rootfs | error |
| Init system present | rootfs | error |
| kata-agent binary | rootfs | error |
| resolv.conf present | rootfs | warning |
| Valid initrd format | initrd | error |
| Size limits | both | warning |
| No setuid binaries | rootfs | warning |
| Kernel modules | rootfs | warning |
### Build Guide
Step-by-step instructions for creating compatible custom images:
**Rootfs (6 steps):** Choose distro → Install kata-agent → Configure init → Set up networking → Optimize size → Package as tar.gz
**Initrd (5 steps):** Choose base → Install kata-agent → Configure init → Optimize → Package as cpio archive
Each step includes copyable command blocks, important notes, and warnings. A comparison card explains when to use rootfs vs initrd.
### Kernel Management
The **Kernel** view provides a dedicated interface for inspecting guest kernels bundled in QCrows images:
- **Image selector** — Sidebar listing all imported QCrows images with kernel version, format, and size at a glance
- **Kernel overview cards** — Version, format (vmlinuz/vmlinux), size, and config validation status
- **Hypervisor compatibility** — Matrix showing which VMMs support the kernel's format (vmlinuz for QEMU/x86_64, vmlinux for CLH/Firecracker/Dragonball)
- **Kernel config viewer** — Required Kata options (VSOCKETS, VIRTIO, DEVTMPFS) with pass/fail status, recommended options with coverage metrics, total config statistics (built-in vs module count)
- **Boot parameters** — Kernel command line from `boot-params.conf`, parameter breakdown, and security warnings for dangerous options (e.g., `module.sig_enforce=0`)
- **Initrd Regeneration** — Rebuild initrd with VMM-aware module selection, configurable compression, init style, and in-place bundle update
- **Build provenance** — Compiler, defconfig, build ID, and import file paths
The kernel view is especially valuable for source-based distro builds where the kernel is compiled alongside the rootfs — it provides assurance that the kernel config has all required Kata options enabled before deployment.
### Configuration
View and edit Kata runtime configuration files (`configuration.toml`) for each VMM profile:
- Config selector sidebar listing all profiles (kata-qemu, kata-clh, kata-fc, kata-dragonball)
- Structured view showing paths, default resources, and boot mode
- Raw TOML editor with save/cancel and unsaved changes detection
- Discard confirmation dialog when navigating away from unsaved edits
### Metrics Dashboard
Real-time monitoring powered by `kata-monitor`'s Prometheus endpoint:
- Aggregate stats with sparklines (CPU, Memory, Network RX/TX)
- Per-sandbox metric cards with progress bars
- Empty state with setup instructions when kata-monitor is not running
## Backend Adapter Pattern
The module uses a unified API layer that works in both modes:
```
UI Components
adapter.ts ──→ isCockpitAvailable() ?
│ │
▼ YES ▼ NO
cockpit-api.ts kata-mock.ts
│ │
▼ ▼
cockpit.spawn() simulateDelay()
cockpit.http() MOCK_SANDBOXES
cockpit.file() MOCK_METRICS
```
All functions return the same TypeScript types (`Sandbox`, `ImportedImage`, `RuntimeConfig`, `SandboxMetrics`), so UI components never need to know which backend is active.
## Prerequisites
### For Cockpit Module (Production)
| Component | Minimum Version | Purpose |
|-----------|----------------|---------|
| Cockpit | 286+ | Web management framework |
| kata-runtime | 2.5+ | Kata Containers runtime |
| containerd | 1.6+ | CRI container runtime |
| crictl | 1.25+ | CRI CLI tool |
| kata-monitor | (optional) | Prometheus metrics endpoint |
| KVM | enabled | Hardware virtualization |
### For Standalone Mode (Development)
| Component | Minimum Version | Purpose |
|-----------|----------------|---------|
| Node.js | 18+ | JavaScript runtime |
| npm or bun | latest | Package manager |
## Quick Install
The fastest way to get running:
```bash
# Clone and enter the project
git clone <repo-url> cockpit-kata-containers
cd cockpit-kata-containers
# Run the automated installer
chmod +x installer.sh
sudo ./installer.sh
```
The installer detects your OS, installs all dependencies, installs QCrows CLI tools to `/usr/local/bin/`, builds the Cockpit module, and starts required services. See [quickstart.md](quickstart.md) for the manual approach.
## Development
### Standalone Mode (Mock Data)
```bash
npm install
npm run dev
# → http://localhost:3000
```
### Cockpit Module Development
```bash
cd cockpit-kata
npm install
npm run watch # Auto-rebuild on changes
npm run devel-install # Symlink to ~/.local/share/cockpit/kata
```
Then open Cockpit in your browser and navigate to the "Kata Containers" tool.
### Building for Production
```bash
cd cockpit-kata
npm run build
sudo cp -r dist/ /usr/share/cockpit/kata/
# Verify installation
cockpit-bridge --packages | grep kata
```
## CSP Requirements
The `manifest.json` includes a Content Security Policy that allows:
- `connect-src 'self' http://127.0.0.1:8090 ws:` — kata-monitor HTTP and WebSocket
- `style-src 'self' 'unsafe-inline'` — React inline styles
- `img-src 'self' data:` — inline SVG icons and data URIs
If kata-monitor binds to a different address, update the CSP in `cockpit-kata/manifest.json`.
## Comparison with Cockpit Machines Module
| Aspect | Machines Module | Kata Module |
|--------|----------------|-------------|
| Object model | Persistent libvirt VMs | Ephemeral Kata sandboxes (per-pod lifecycle) |
| Creation flow | In-module wizard (disk, RAM, OS) | Via Kubernetes RuntimeClass |
| Import feature | Disk images (qcow2, raw) | Rootfs tarballs, initrd images, QCrows bundles |
| Export feature | — | qcow2, ISO, PXE (UI + CLI) |
| Initrd regen | — | Environment-aware VMM module selection |
| Config format | libvirt XML | TOML (configuration.toml) |
| Console access | VNC (graphical) | vsock text terminal |
| Metrics source | libvirt API | kata-monitor Prometheus endpoint |
| List view | VM table + status | Sandbox table + status (same pattern) |
| Action menus | Start/Stop/Restart/Console | Start/Stop/Restart/Console (same pattern) |
| Status badges | Color-coded running/stopped | Color-coded running/stopped/error |
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/my-feature`)
3. Commit your changes (`git commit -am 'Add my feature'`)
4. Push to the branch (`git push origin feature/my-feature`)
5. Open a Pull Request
## License
Apache-2.0 — see [LICENSE](LICENSE) for details.

93
cockpit-kata/README.md Executable file
View File

@ -0,0 +1,93 @@
# Cockpit Kata Containers Module — Build & Install
## Quick Start
```bash
# Install dependencies
npm install
# Build for production
npm run build
# Install for local Cockpit development (symlinks to ~/.local/share/cockpit/kata)
npm run devel-install
# Watch for changes during development
npm run watch
```
## Production Install
```bash
# Build
npm run build
# Copy the dist/ directory to the system Cockpit package directory
sudo cp -r dist/ /usr/share/cockpit/kata/
# Verify Cockpit sees the package
cockpit-bridge --packages | grep kata
```
## Architecture
```
cockpit-kata/
├── manifest.json # Cockpit package manifest (menu placement, CSP, version requirements)
├── webpack.config.js # Build config — bundles React app into single JS file
├── package.json # npm build scripts and dependencies
├── tsconfig.json # TypeScript config
├── cockpit.d.ts # Type declarations for cockpit.js APIs
├── src/
│ ├── index.tsx # Entry point — mounts KataModule into DOM
│ ├── KataModule.tsx # Main component (copied from Next.js prototype)
│ ├── backend/
│ │ ├── adapter.ts # Backend adapter — selects cockpit vs mock mode
│ │ ├── cockpit-api.ts # Real cockpit.spawn() / cockpit.http() / cockpit.file() calls
│ │ └── mock-api.ts # Mock fallback for standalone development
│ ├── components/ # UI components (same as Next.js prototype)
│ └── types.ts # Shared TypeScript interfaces
└── dist/ # Build output — files Cockpit actually serves
├── index.js # Bundled React app
├── index.js.map # Source map for debugging
└── manifest.json # Copied from root
```
## Backend Adapter Pattern
The module uses a two-mode backend:
1. **Cockpit mode** (production): `cockpit.spawn()` calls `kata-runtime` / `kata-ctl` / `crictl`,
`cockpit.http()` talks to `kata-monitor` on `127.0.0.1:8090`,
`cockpit.file()` watches/edits `configuration.toml`
2. **Mock mode** (standalone dev): Same TypeScript interfaces, but backed by
simulated data and delays. This is what the Next.js prototype uses.
The adapter in `src/backend/adapter.ts` auto-detects whether `cockpit` is available
in the global scope and selects the appropriate implementation.
## CSP Requirements
The `manifest.json` includes a `content-security-policy` that allows:
- `connect-src 'self' http://127.0.0.1:8090 ws:` — for kata-monitor HTTP and WebSocket
- `style-src 'self' 'unsafe-inline'` — for CSS-in-JS (React inline styles)
- `img-src 'self' data:` — for inline SVG icons and data URIs
If kata-monitor binds to a different address, update the CSP in manifest.json.
## Integration with Cockpit Machines Module
The Kata module follows the same UX patterns as Cockpit's machines module:
- List view → detail view navigation
- Action dropdown menus (start/stop/restart/console)
- Breadcrumb-based navigation back to list
- Card-based summary stats at the top of list views
Key divergences from the machines module:
- Kata sandboxes are ephemeral (per-pod lifecycle), not persistent VMs
- Creation happens via Kubernetes RuntimeClass, not an in-module wizard
- Import is for rootfs/initrd images, not disk images
- Config is TOML, not libvirt XML
- Console is vsock-based text only, not VNC

18
cockpit-kata/build.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Build script for the cockpit-kata Cockpit module
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "[cockpit-kata] Building production bundle..."
./node_modules/.bin/webpack --mode production
if [[ -f dist/index.js ]]; then
SIZE=$(du -sh dist/ | cut -f1)
echo "[cockpit-kata] Build successful: ${SIZE}"
echo "[cockpit-kata] Output: dist/index.js + dist/index.css"
else
echo "[cockpit-kata] ERROR: Build failed — dist/index.js not found"
exit 1
fi

109
cockpit-kata/cockpit.d.ts vendored Executable file
View File

@ -0,0 +1,109 @@
/**
* cockpit.js type declarations for TypeScript
*
* In a real Cockpit deployment, cockpit.js is provided globally by
* cockpit-bridge at /usr/share/cockpit/cockpit.js. These declarations
* let TypeScript compile without installing the full cockpit package.
*/
declare module "cockpit" {
// ─── spawn ────────────────────────────────────────────────────────
// cockpit.spawn(command, options?) → Process
// Runs a local command and returns a Promise-like Process stream.
export function spawn(
command: string[],
options?: SpawnOptions
): Process;
export interface SpawnOptions {
superuser?: "try" | "require" | null;
err?: "message" | "ignore" | "out";
environ?: string[];
directory?: string;
pty?: boolean;
binary?: boolean;
}
export interface Process extends Promise<string> {
stream(callback: (data: string) => void): Process;
input(data: string, stream?: boolean): void;
close(reason?: string): void;
then(onFulfilled?: (output: string) => void, onRejected?: (error: SpawnError) => void): Process;
catch(onRejected: (error: SpawnError) => void): Process;
}
export interface SpawnError extends Error {
problem?: string;
exit_status?: number;
exit_signal?: string;
}
// ─── file ─────────────────────────────────────────────────────────
// cockpit.file(path, options?) → FileHandle
export function file(path: string, options?: FileOptions): FileHandle;
export interface FileOptions {
binary?: boolean;
superuser?: "try" | "require" | null;
}
export interface FileHandle {
read(): Promise<string | null>;
replace(content: string): Promise<void>;
modify(callback: (oldContent: string | null) => string | null): Promise<void>;
watch(callback: (content: string | null) => void, options?: { read?: boolean }): void;
close(): void;
}
// ─── http ─────────────────────────────────────────────────────────
// cockpit.http(options) → HttpChannel
export function http(options: HttpOptions): HttpChannel;
export interface HttpOptions {
port?: number;
path?: string;
address?: string;
superuser?: "try" | "require" | null;
tls?: Record<string, unknown>;
}
export interface HttpChannel {
get(path: string, headers?: Record<string, string>): HttpRequest;
post(path: string, body: string, headers?: Record<string, string>): HttpRequest;
}
export interface HttpRequest extends Promise<string> {
then(onFulfilled?: (data: string) => void, onRejected?: (error: Error) => void): HttpRequest;
catch(onRejected: (error: Error) => void): HttpRequest;
}
// ─── dbus ─────────────────────────────────────────────────────────
export function dbus(name: string, options?: DbusOptions): DbusClient;
export interface DbusOptions {
bus?: "system" | "session";
superuser?: "try" | "require" | null;
track?: boolean;
}
export interface DbusClient {
proxy(interfaceName: string, path: string): DbusProxy;
call(path: string, interfaceName: string, method: string, args: unknown[]): Promise<unknown[]>;
}
export interface DbusProxy {
wait(): Promise<void>;
[key: string]: unknown;
}
// ─── User info ────────────────────────────────────────────────────
export function user(): UserInfo;
export interface UserInfo {
id: number;
name: string;
full_name: string;
groups: string[];
home: string;
shell: string;
}
}

View File

@ -0,0 +1,3 @@
import React from "react";
export declare function BuildGuide(): React.JSX.Element;
//# sourceMappingURL=BuildGuide.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"BuildGuide.d.ts","sourceRoot":"","sources":["../../../src/components/kata/BuildGuide.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAwQxC,wBAAgB,UAAU,sBA2TzB"}

View File

@ -0,0 +1,3 @@
import React from "react";
export declare function Configuration(): React.JSX.Element;
//# sourceMappingURL=Configuration.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"Configuration.d.ts","sourceRoot":"","sources":["../../../src/components/kata/Configuration.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAyHhE,wBAAgB,aAAa,sBA6f5B"}

View File

@ -0,0 +1,3 @@
import React from "react";
export declare function ExportImage(): React.JSX.Element;
//# sourceMappingURL=ExportImage.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"ExportImage.d.ts","sourceRoot":"","sources":["../../../src/components/kata/ExportImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAyDnD,wBAAgB,WAAW,sBA6a1B"}

View File

@ -0,0 +1,3 @@
import React from "react";
export declare function ImportImage(): React.JSX.Element;
//# sourceMappingURL=ImportImage.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"ImportImage.d.ts","sourceRoot":"","sources":["../../../src/components/kata/ImportImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAwD7D,wBAAgB,WAAW,sBA28C1B"}

View File

@ -0,0 +1,3 @@
import React from "react";
export declare function KataModule(): React.JSX.Element;
//# sourceMappingURL=KataModule.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"KataModule.d.ts","sourceRoot":"","sources":["../../../src/components/kata/KataModule.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAwCxC,wBAAgB,UAAU,sBA6JzB"}

View File

@ -0,0 +1,3 @@
import React from "react";
export declare function KernelDetail(): React.JSX.Element;
//# sourceMappingURL=KernelDetail.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"KernelDetail.d.ts","sourceRoot":"","sources":["../../../src/components/kata/KernelDetail.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAwCnD,wBAAgB,YAAY,sBAqF3B"}

View File

@ -0,0 +1,3 @@
import React from "react";
export declare function MetricsDashboard(): React.JSX.Element;
//# sourceMappingURL=MetricsDashboard.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"MetricsDashboard.d.ts","sourceRoot":"","sources":["../../../src/components/kata/MetricsDashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAwBnD,wBAAgB,gBAAgB,sBA2U/B"}

View File

@ -0,0 +1,9 @@
import React from "react";
import { type Sandbox } from "@/lib/kata-mock";
interface SandboxDetailProps {
sandbox: Sandbox;
onBack: () => void;
}
export declare function SandboxDetail({ sandbox, onBack }: SandboxDetailProps): React.JSX.Element;
export {};
//# sourceMappingURL=SandboxDetail.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"SandboxDetail.d.ts","sourceRoot":"","sources":["../../../src/components/kata/SandboxDetail.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAsBnD,OAAO,EAAuB,KAAK,OAAO,EAAuB,MAAM,iBAAiB,CAAC;AAEzF,UAAU,kBAAkB;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,kBAAkB,qBAySpE"}

View File

@ -0,0 +1,8 @@
import React from "react";
import { type Sandbox } from "@/lib/kata-mock";
interface SandboxListProps {
onSelectSandbox: (sandbox: Sandbox) => void;
}
export declare function SandboxList({ onSelectSandbox }: SandboxListProps): React.JSX.Element;
export {};
//# sourceMappingURL=SandboxList.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"SandboxList.d.ts","sourceRoot":"","sources":["../../../src/components/kata/SandboxList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAkCnD,OAAO,EAAkB,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAgB/D,UAAU,gBAAgB;IACxB,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C;AAED,wBAAgB,WAAW,CAAC,EAAE,eAAe,EAAE,EAAE,gBAAgB,qBAiQhE"}

View File

@ -0,0 +1,8 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
declare function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>): React.JSX.Element;
declare function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>): React.JSX.Element;
declare function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>): React.JSX.Element;
declare function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>): React.JSX.Element;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
//# sourceMappingURL=accordion.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"accordion.d.ts","sourceRoot":"","sources":["../../../src/components/ui/accordion.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,kBAAkB,MAAM,2BAA2B,CAAA;AAK/D,iBAAS,SAAS,CAAC,EACjB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,IAAI,CAAC,qBAEtD;AAED,iBAAS,aAAa,CAAC,EACrB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,IAAI,CAAC,qBAQtD;AAED,iBAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,qBAgBzD;AAED,iBAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,qBAUzD;AAED,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAA"}

View File

@ -0,0 +1,10 @@
import * as React from "react";
import { type VariantProps } from "class-variance-authority";
declare const badgeVariants: (props?: ({
variant?: "default" | "destructive" | "outline" | "secondary" | null | undefined;
} & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
asChild?: boolean;
}): React.JSX.Element;
export { Badge, badgeVariants };
//# sourceMappingURL=badge.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"badge.d.ts","sourceRoot":"","sources":["../../../src/components/ui/badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,aAAa;;mFAmBlB,CAAA;AAED,iBAAS,KAAK,CAAC,EACb,SAAS,EACT,OAAO,EACP,OAAe,EACf,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,GAC7B,YAAY,CAAC,OAAO,aAAa,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,qBAU3D;AAED,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAA"}

View File

@ -0,0 +1,11 @@
import * as React from "react";
import { type VariantProps } from "class-variance-authority";
declare const buttonVariants: (props?: ({
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
} & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}): React.JSX.Element;
export { Button, buttonVariants };
//# sourceMappingURL=button.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/ui/button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;mFA6BnB,CAAA;AAED,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAe,EACf,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAC/B,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,qBAUF;AAED,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}

View File

@ -0,0 +1,10 @@
import * as React from "react";
declare function Card({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function CardTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function CardDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
//# sourceMappingURL=card.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../src/components/ui/card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iBAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWjE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQtE;AAED,iBAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQ5E;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQxE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQvE;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,GACZ,CAAA"}

View File

@ -0,0 +1,5 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
declare function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>): React.JSX.Element;
export { Checkbox };
//# sourceMappingURL=checkbox.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"checkbox.d.ts","sourceRoot":"","sources":["../../../src/components/ui/checkbox.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAA;AAK7D,iBAAS,QAAQ,CAAC,EAChB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,IAAI,CAAC,qBAkBrD;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}

View File

@ -0,0 +1,16 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
declare function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>): React.JSX.Element;
declare function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>): React.JSX.Element;
declare function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>): React.JSX.Element;
declare function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>): React.JSX.Element;
declare function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>): React.JSX.Element;
declare function DialogContent({ className, children, showCloseButton, ...props }: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}): React.JSX.Element;
declare function DialogHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function DialogFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
declare function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>): React.JSX.Element;
declare function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>): React.JSX.Element;
export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, };
//# sourceMappingURL=dialog.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../../../src/components/ui/dialog.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AAKzD,iBAAS,MAAM,CAAC,EACd,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,qBAEnD;AAED,iBAAS,aAAa,CAAC,EACrB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,qBAEtD;AAED,iBAAS,YAAY,CAAC,EACpB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,MAAM,CAAC,qBAErD;AAED,iBAAS,WAAW,CAAC,EACnB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,qBAEpD;AAED,iBAAS,aAAa,CAAC,EACrB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,qBAWtD;AAED,iBAAS,aAAa,CAAC,EACrB,SAAS,EACT,QAAQ,EACR,eAAsB,EACtB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,GAAG;IACxD,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,qBAyBA;AAED,iBAAS,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQzE;AAED,iBAAS,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWzE;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,qBAQpD;AAED,iBAAS,iBAAiB,CAAC,EACzB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,WAAW,CAAC,qBAQ1D;AAED,OAAO,EACL,MAAM,EACN,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,GACd,CAAA"}

View File

@ -0,0 +1,26 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
declare function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>): React.JSX.Element;
declare function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>): React.JSX.Element;
declare function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>): React.JSX.Element;
declare function DropdownMenuContent({ className, sideOffset, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>): React.JSX.Element;
declare function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>): React.JSX.Element;
declare function DropdownMenuItem({ className, inset, variant, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}): React.JSX.Element;
declare function DropdownMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>): React.JSX.Element;
declare function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>): React.JSX.Element;
declare function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>): React.JSX.Element;
declare function DropdownMenuLabel({ className, inset, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}): React.JSX.Element;
declare function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>): React.JSX.Element;
declare function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
declare function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>): React.JSX.Element;
declare function DropdownMenuSubTrigger({ className, inset, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}): React.JSX.Element;
declare function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>): React.JSX.Element;
export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, };
//# sourceMappingURL=dropdown-menu.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"dropdown-menu.d.ts","sourceRoot":"","sources":["../../../src/components/ui/dropdown-menu.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,qBAAqB,MAAM,+BAA+B,CAAA;AAKtE,iBAAS,YAAY,CAAC,EACpB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,IAAI,CAAC,qBAEzD;AAED,iBAAS,kBAAkB,CAAC,EAC1B,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,MAAM,CAAC,qBAI3D;AAED,iBAAS,mBAAmB,CAAC,EAC3B,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,OAAO,CAAC,qBAO5D;AAED,iBAAS,mBAAmB,CAAC,EAC3B,SAAS,EACT,UAAc,EACd,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,OAAO,CAAC,qBAc5D;AAED,iBAAS,iBAAiB,CAAC,EACzB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,KAAK,CAAC,qBAI1D;AAED,iBAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,KAAK,EACL,OAAmB,EACnB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,IAAI,CAAC,GAAG;IAC3D,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,SAAS,GAAG,aAAa,CAAA;CACpC,qBAaA;AAED,iBAAS,wBAAwB,CAAC,EAChC,SAAS,EACT,QAAQ,EACR,OAAO,EACP,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,YAAY,CAAC,qBAmBjE;AAED,iBAAS,sBAAsB,CAAC,EAC9B,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,UAAU,CAAC,qBAO/D;AAED,iBAAS,qBAAqB,CAAC,EAC7B,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,SAAS,CAAC,qBAkB9D;AAED,iBAAS,iBAAiB,CAAC,EACzB,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAC5D,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,qBAYA;AAED,iBAAS,qBAAqB,CAAC,EAC7B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,SAAS,CAAC,qBAQ9D;AAED,iBAAS,oBAAoB,CAAC,EAC5B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,qBAW9B;AAED,iBAAS,eAAe,CAAC,EACvB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,GAAG,CAAC,qBAExD;AAED,iBAAS,sBAAsB,CAAC,EAC9B,SAAS,EACT,KAAK,EACL,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,UAAU,CAAC,GAAG;IACjE,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,qBAeA;AAED,iBAAS,sBAAsB,CAAC,EAC9B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,UAAU,CAAC,qBAW/D;AAED,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,sBAAsB,GACvB,CAAA"}

View File

@ -0,0 +1,4 @@
import * as React from "react";
declare function Input({ className, type, ...props }: React.ComponentProps<"input">): React.JSX.Element;
export { Input };
//# sourceMappingURL=input.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../../src/components/ui/input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iBAAS,KAAK,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,qBAc1E;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}

View File

@ -0,0 +1,5 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
declare function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>): React.JSX.Element;
export { Label };
//# sourceMappingURL=label.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"label.d.ts","sourceRoot":"","sources":["../../../src/components/ui/label.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAA;AAIvD,iBAAS,KAAK,CAAC,EACb,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,IAAI,CAAC,qBAWlD;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}

View File

@ -0,0 +1,5 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
declare function Progress({ className, value, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>): React.JSX.Element;
export { Progress };
//# sourceMappingURL=progress.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../../src/components/ui/progress.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAA;AAI7D,iBAAS,QAAQ,CAAC,EAChB,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,IAAI,CAAC,qBAiBrD;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}

View File

@ -0,0 +1,6 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
declare function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>): React.JSX.Element;
declare function ScrollBar({ className, orientation, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>): React.JSX.Element;
export { ScrollArea, ScrollBar };
//# sourceMappingURL=scroll-area.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"scroll-area.d.ts","sourceRoot":"","sources":["../../../src/components/ui/scroll-area.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,6BAA6B,CAAA;AAIlE,iBAAS,UAAU,CAAC,EAClB,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,mBAAmB,CAAC,IAAI,CAAC,qBAiBvD;AAED,iBAAS,SAAS,CAAC,EACjB,SAAS,EACT,WAAwB,EACxB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,mBAAmB,CAAC,mBAAmB,CAAC,qBAqBtE;AAED,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,CAAA"}

View File

@ -0,0 +1,16 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
declare function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>): React.JSX.Element;
declare function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>): React.JSX.Element;
declare function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>): React.JSX.Element;
declare function SelectTrigger({ className, size, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}): React.JSX.Element;
declare function SelectContent({ className, children, position, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>): React.JSX.Element;
declare function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>): React.JSX.Element;
declare function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>): React.JSX.Element;
declare function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>): React.JSX.Element;
declare function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>): React.JSX.Element;
declare function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>): React.JSX.Element;
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
//# sourceMappingURL=select.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/components/ui/select.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AAKzD,iBAAS,MAAM,CAAC,EACd,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,qBAEnD;AAED,iBAAS,WAAW,CAAC,EACnB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,qBAEpD;AAED,iBAAS,WAAW,CAAC,EACnB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,qBAEpD;AAED,iBAAS,aAAa,CAAC,EACrB,SAAS,EACT,IAAgB,EAChB,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,GAAG;IACxD,IAAI,CAAC,EAAE,IAAI,GAAG,SAAS,CAAA;CACxB,qBAiBA;AAED,iBAAS,aAAa,CAAC,EACrB,SAAS,EACT,QAAQ,EACR,QAAmB,EACnB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,qBA4BtD;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,qBAQpD;AAED,iBAAS,UAAU,CAAC,EAClB,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,qBAkBnD;AAED,iBAAS,eAAe,CAAC,EACvB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,SAAS,CAAC,qBAQxD;AAED,iBAAS,oBAAoB,CAAC,EAC5B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,cAAc,CAAC,qBAa7D;AAED,iBAAS,sBAAsB,CAAC,EAC9B,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,gBAAgB,CAAC,qBAa/D;AAED,OAAO,EACL,MAAM,EACN,aAAa,EACb,WAAW,EACX,UAAU,EACV,WAAW,EACX,sBAAsB,EACtB,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,WAAW,GACZ,CAAA"}

View File

@ -0,0 +1,5 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
declare function Separator({ className, orientation, decorative, ...props }: React.ComponentProps<typeof SeparatorPrimitive.Root>): React.JSX.Element;
export { Separator };
//# sourceMappingURL=separator.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"separator.d.ts","sourceRoot":"","sources":["../../../src/components/ui/separator.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,kBAAkB,MAAM,2BAA2B,CAAA;AAI/D,iBAAS,SAAS,CAAC,EACjB,SAAS,EACT,WAA0B,EAC1B,UAAiB,EACjB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,IAAI,CAAC,qBAatD;AAED,OAAO,EAAE,SAAS,EAAE,CAAA"}

View File

@ -0,0 +1,5 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
declare function Slider({ className, defaultValue, value, min, max, ...props }: React.ComponentProps<typeof SliderPrimitive.Root>): React.JSX.Element;
export { Slider };
//# sourceMappingURL=slider.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../../../src/components/ui/slider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AAIzD,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,YAAY,EACZ,KAAK,EACL,GAAO,EACP,GAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,qBA8CnD;AAED,OAAO,EAAE,MAAM,EAAE,CAAA"}

View File

@ -0,0 +1,5 @@
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
declare function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>): React.JSX.Element;
export { Switch };
//# sourceMappingURL=switch.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../../src/components/ui/switch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AAIzD,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,qBAkBnD;AAED,OAAO,EAAE,MAAM,EAAE,CAAA"}

View File

@ -0,0 +1,11 @@
import * as React from "react";
declare function Table({ className, ...props }: React.ComponentProps<"table">): React.JSX.Element;
declare function TableHeader({ className, ...props }: React.ComponentProps<"thead">): React.JSX.Element;
declare function TableBody({ className, ...props }: React.ComponentProps<"tbody">): React.JSX.Element;
declare function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">): React.JSX.Element;
declare function TableRow({ className, ...props }: React.ComponentProps<"tr">): React.JSX.Element;
declare function TableHead({ className, ...props }: React.ComponentProps<"th">): React.JSX.Element;
declare function TableCell({ className, ...props }: React.ComponentProps<"td">): React.JSX.Element;
declare function TableCaption({ className, ...props }: React.ComponentProps<"caption">): React.JSX.Element;
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, };
//# sourceMappingURL=table.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../src/components/ui/table.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iBAAS,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,qBAapE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,qBAQ1E;AAED,iBAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,qBAQxE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,qBAW1E;AAED,iBAAS,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,qBAWpE;AAED,iBAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,qBAWrE;AAED,iBAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,qBAWrE;AAED,iBAAS,YAAY,CAAC,EACpB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,qBAQjC;AAED,OAAO,EACL,KAAK,EACL,WAAW,EACX,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,YAAY,GACb,CAAA"}

View File

@ -0,0 +1,8 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
declare function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): React.JSX.Element;
declare function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>): React.JSX.Element;
declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
declare function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>): React.JSX.Element;
export { Tabs, TabsList, TabsTrigger, TabsContent };
//# sourceMappingURL=tabs.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../src/components/ui/tabs.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,aAAa,MAAM,sBAAsB,CAAA;AAIrD,iBAAS,IAAI,CAAC,EACZ,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,aAAa,CAAC,IAAI,CAAC,qBAQjD;AAED,iBAAS,QAAQ,CAAC,EAChB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,aAAa,CAAC,IAAI,CAAC,qBAWjD;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,aAAa,CAAC,OAAO,CAAC,qBAWpD;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,aAAa,CAAC,OAAO,CAAC,qBAQpD;AAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,CAAA"}

View File

@ -0,0 +1,4 @@
import * as React from "react";
declare function Textarea({ className, ...props }: React.ComponentProps<"textarea">): React.JSX.Element;
export { Textarea };
//# sourceMappingURL=textarea.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"textarea.d.ts","sourceRoot":"","sources":["../../../src/components/ui/textarea.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iBAAS,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,qBAW1E;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}

View File

@ -0,0 +1,16 @@
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { type VariantProps } from "class-variance-authority";
declare const ToastProvider: React.FC<ToastPrimitives.ToastProviderProps>;
declare const ToastViewport: React.ForwardRefExoticComponent<Omit<ToastPrimitives.ToastViewportProps & React.RefAttributes<HTMLOListElement>, "ref"> & React.RefAttributes<HTMLOListElement>>;
declare const Toast: React.ForwardRefExoticComponent<Omit<ToastPrimitives.ToastProps & React.RefAttributes<HTMLLIElement>, "ref"> & VariantProps<(props?: ({
variant?: "default" | "destructive" | null | undefined;
} & import("class-variance-authority/dist/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLLIElement>>;
declare const ToastAction: React.ForwardRefExoticComponent<Omit<ToastPrimitives.ToastActionProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
declare const ToastClose: React.ForwardRefExoticComponent<Omit<ToastPrimitives.ToastCloseProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
declare const ToastTitle: React.ForwardRefExoticComponent<Omit<ToastPrimitives.ToastTitleProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
declare const ToastDescription: React.ForwardRefExoticComponent<Omit<ToastPrimitives.ToastDescriptionProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export { type ToastProps, type ToastActionElement, ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction, };
//# sourceMappingURL=toast.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../../src/components/ui/toast.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAKjE,QAAA,MAAM,aAAa,8CAA2B,CAAA;AAE9C,QAAA,MAAM,aAAa,kKAYjB,CAAA;AAmBF,QAAA,MAAM,KAAK;;0HAYT,CAAA;AAGF,QAAA,MAAM,WAAW,kKAYf,CAAA;AAGF,QAAA,MAAM,UAAU,iKAed,CAAA;AAGF,QAAA,MAAM,UAAU,2JASd,CAAA;AAGF,QAAA,MAAM,gBAAgB,iKASpB,CAAA;AAGF,KAAK,UAAU,GAAG,KAAK,CAAC,wBAAwB,CAAC,OAAO,KAAK,CAAC,CAAA;AAE9D,KAAK,kBAAkB,GAAG,KAAK,CAAC,YAAY,CAAC,OAAO,WAAW,CAAC,CAAA;AAEhE,OAAO,EACL,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,aAAa,EACb,aAAa,EACb,KAAK,EACL,UAAU,EACV,gBAAgB,EAChB,UAAU,EACV,WAAW,GACZ,CAAA"}

45
cockpit-kata/dist/hooks/use-toast.d.ts vendored Normal file
View File

@ -0,0 +1,45 @@
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
declare const actionTypes: {
readonly ADD_TOAST: "ADD_TOAST";
readonly UPDATE_TOAST: "UPDATE_TOAST";
readonly DISMISS_TOAST: "DISMISS_TOAST";
readonly REMOVE_TOAST: "REMOVE_TOAST";
};
type ActionType = typeof actionTypes;
type Action = {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
} | {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
} | {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
} | {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
export declare const reducer: (state: State, action: Action) => State;
type Toast = Omit<ToasterToast, "id">;
declare function toast({ ...props }: Toast): {
id: string;
dismiss: () => void;
update: (props: ToasterToast) => void;
};
declare function useToast(): {
toast: typeof toast;
dismiss: (toastId?: string) => void;
toasts: ToasterToast[];
};
export { useToast, toast };
//# sourceMappingURL=use-toast.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"use-toast.d.ts","sourceRoot":"","sources":["../../src/hooks/use-toast.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EACV,kBAAkB,EAClB,UAAU,EACX,MAAM,uBAAuB,CAAA;AAK9B,KAAK,YAAY,GAAG,UAAU,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,MAAM,CAAC,EAAE,kBAAkB,CAAA;CAC5B,CAAA;AAED,QAAA,MAAM,WAAW;;;;;CAKP,CAAA;AASV,KAAK,UAAU,GAAG,OAAO,WAAW,CAAA;AAEpC,KAAK,MAAM,GACP;IACA,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;IAC7B,KAAK,EAAE,YAAY,CAAA;CACpB,GACC;IACA,IAAI,EAAE,UAAU,CAAC,cAAc,CAAC,CAAA;IAChC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;CAC7B,GACC;IACA,IAAI,EAAE,UAAU,CAAC,eAAe,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAA;CAC7B,GACC;IACA,IAAI,EAAE,UAAU,CAAC,cAAc,CAAC,CAAA;IAChC,OAAO,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAA;CAC7B,CAAA;AAEH,UAAU,KAAK;IACb,MAAM,EAAE,YAAY,EAAE,CAAA;CACvB;AAoBD,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,EAAE,QAAQ,MAAM,KAAG,KAqDtD,CAAA;AAaD,KAAK,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAA;AAErC,iBAAS,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK;;;oBAGT,YAAY;EAwBpC;AAED,iBAAS,QAAQ;;wBAgBO,MAAM;YAtIpB,YAAY,EAAE;EAwIvB;AAED,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA"}

2661
cockpit-kata/dist/index.css vendored Normal file

File diff suppressed because it is too large Load Diff

1
cockpit-kata/dist/index.css.map vendored Normal file

File diff suppressed because one or more lines are too long

13
cockpit-kata/dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
/**
* Entry point for the Cockpit module build.
*
* This file imports the KataModule component and mounts it into
* the #kata-app container. In a real Cockpit deployment, this is
* the file referenced by manifest.json tools.index.
*
* The webpack config bundles this + all dependencies into dist/index.js
* and dist/index.css. Cockpit loads them automatically when the user
* navigates to the "Kata Containers" tool.
*/
import "./globals.css";
//# sourceMappingURL=index.d.ts.map

1
cockpit-kata/dist/index.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,eAAe,CAAC"}

3
cockpit-kata/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

46
cockpit-kata/dist/index.js.LICENSE.txt vendored Normal file
View File

@ -0,0 +1,46 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license lucide-react v0.525.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/

1
cockpit-kata/dist/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,45 @@
/**
* Backend adapter auto-detects cockpit availability and routes API calls
* to either the real cockpit backend or the mock data layer.
*
* UI components import from this module only they never import
* cockpit-api.ts or kata-mock.ts directly.
*/
import type { Sandbox, SandboxMetrics, ImportedImage, RuntimeConfig, QCrowsMetadata, PXEConfig, PXEPushResult, PXEStatus, InitrdRegenConfig, InitrdRegenResult, ExportConfig, ExportResult } from "@/lib/kata-mock";
import { watchRuntimeConfig } from "./cockpit-api";
export type BackendMode = "cockpit" | "mock";
export declare function getBackendMode(): BackendMode;
export declare function isCockpitMode(): boolean;
export declare function fetchSandboxes(): Promise<Sandbox[]>;
export declare function fetchSandboxById(id: string): Promise<Sandbox | undefined>;
export declare function fetchImportedImages(): Promise<ImportedImage[]>;
export declare function fetchRuntimeConfigs(): Promise<RuntimeConfig[]>;
export declare function fetchSandboxMetrics(sandboxId: string): Promise<SandboxMetrics | undefined>;
export declare function validateImage(file: File, type: "rootfs" | "initrd"): Promise<{
valid: boolean;
errors: string[];
warnings: string[];
}>;
export declare function importImageAction(name: string, type: "rootfs" | "initrd", file: File, hypervisorCompat: string[]): Promise<ImportedImage>;
export interface QCrowsImportResult {
kernelPath: string;
kernelConfigPath: string;
rootfsPath: string;
initrdPath: string | null;
bootParamsAppended: boolean;
configUpdated: string | null;
}
export declare function importQCrowsImage(metadata: QCrowsMetadata, stagingDir: string): Promise<QCrowsImportResult>;
export declare function saveRuntimeConfig(configPath: string, newContent: string): Promise<void>;
export declare function modifyRuntimeConfig(configPath: string, modifier: (oldContent: string | null) => string | null): Promise<void>;
export { watchRuntimeConfig };
export declare function runHostCheck(): Promise<{
canRun: boolean;
messages: string[];
}>;
export type { PXEStatus } from "@/lib/kata-mock";
export declare function checkPXEStatus(): Promise<PXEStatus>;
export declare function pushViaPXE(image: import("@/lib/kata-mock").QCrowsImage, config: PXEConfig): Promise<PXEPushResult>;
export declare function regenInitrd(image: import("@/lib/kata-mock").QCrowsImage, config: Partial<InitrdRegenConfig>): Promise<InitrdRegenResult>;
export declare function exportQCrowsImage(image: import("@/lib/kata-mock").QCrowsImage, config: ExportConfig): Promise<ExportResult>;
//# sourceMappingURL=adapter.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/lib/backend/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,SAAS,EACT,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACb,MAAM,iBAAiB,CAAC;AAgBzB,OAAO,EASL,kBAAkB,EAKnB,MAAM,eAAe,CAAC;AAIvB,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAE7C,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAYD,wBAAsB,cAAc,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAEzD;AAED,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAK/E;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAGpE;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAEpE;AAED,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAKhG;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,QAAQ,GAAG,QAAQ,GACxB,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAGnE;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,IAAI,EAAE,IAAI,EACV,gBAAgB,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC,aAAa,CAAC,CAGxB;AAID,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,cAAc,EACxB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,CAAC,CAgB7B;AAID,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,GAAG,IAAI,GACrD,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAI9B,wBAAsB,YAAY,IAAI,OAAO,CAAC;IAC5C,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC,CAiBD;AAID,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,wBAAsB,cAAc,IAAI,OAAO,CAAC,SAAS,CAAC,CAEzD;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,OAAO,iBAAiB,EAAE,WAAW,EAC5C,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,aAAa,CAAC,CAKxB;AAID,wBAAsB,WAAW,CAC/B,KAAK,EAAE,OAAO,iBAAiB,EAAE,WAAW,EAC5C,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAID,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,OAAO,iBAAiB,EAAE,WAAW,EAC5C,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAKvB"}

View File

@ -0,0 +1,47 @@
/**
* Cockpit API adapter real backend for production Cockpit deployment
*
* Uses cockpit.spawn() for CLI commands, cockpit.http() for kata-monitor,
* and cockpit.file() for configuration.toml access.
*
* All functions return the same TypeScript types as the mock layer,
* so the UI components don't need to know which backend is active.
*/
import type { Sandbox, SandboxMetrics, ImportedImage, RuntimeConfig, QCrowsMetadata, QCrowsImage, PXEConfig, PXEPushResult, PXEStatus, InitrdRegenConfig, InitrdRegenResult, ExportConfig, ExportResult } from "@/lib/kata-mock";
declare global {
interface Window {
cockpit?: typeof import("cockpit");
}
}
export declare function isCockpitAvailable(): boolean;
export declare function fetchSandboxesCockpit(): Promise<Sandbox[]>;
export declare function fetchSandboxMetricsCockpit(sandboxId: string): Promise<SandboxMetrics | undefined>;
export declare function fetchRuntimeConfigsCockpit(): Promise<RuntimeConfig[]>;
export declare function saveRuntimeConfigCockpit(configPath: string, newContent: string): Promise<void>;
export declare function modifyRuntimeConfigCockpit(configPath: string, modifier: (oldContent: string | null) => string | null): Promise<void>;
export declare function watchRuntimeConfig(configPath: string, callback: (content: string | null) => void): () => void;
export declare function validateImageCockpit(filePath: string, type: "rootfs" | "initrd"): Promise<{
valid: boolean;
errors: string[];
warnings: string[];
}>;
export declare function importImageCockpit(sourcePath: string, name: string, type: "rootfs" | "initrd", hypervisorCompat: string[], targetDir?: string): Promise<ImportedImage>;
export declare function deleteImageCockpit(imagePath: string): Promise<void>;
export declare function runHostCheck(): Promise<{
canRun: boolean;
messages: string[];
}>;
export interface QCrowsImportResult {
kernelPath: string;
kernelConfigPath: string;
rootfsPath: string;
initrdPath: string | null;
bootParamsAppended: boolean;
configUpdated: string | null;
}
export declare function importQCrowsImageCockpit(metadata: QCrowsMetadata, stagingDir: string): Promise<QCrowsImportResult>;
export declare function checkPXEStatusCockpit(): Promise<PXEStatus>;
export declare function pushViaPXECockpit(image: QCrowsImage, config: PXEConfig): Promise<PXEPushResult>;
export declare function regenInitrdCockpit(qcrowsPath: string, config: Partial<InitrdRegenConfig>): Promise<InitrdRegenResult>;
export declare function exportQCrowsCockpit(image: QCrowsImage, config: ExportConfig): Promise<ExportResult>;
//# sourceMappingURL=cockpit-api.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"cockpit-api.d.ts","sourceRoot":"","sources":["../../../src/lib/backend/cockpit-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,aAAa,EACb,aAAa,EAEb,cAAc,EACd,WAAW,EACX,SAAS,EACT,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EAEb,MAAM,iBAAiB,CAAC;AAIzB,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,OAAO,CAAC,EAAE,cAAc,SAAS,CAAC,CAAC;KACpC;CACF;AAMD,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAID,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAgFhE;AAgBD,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAerC;AAkCD,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAwB3E;AAmCD,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAsB,0BAA0B,CAC9C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,GAAG,IAAI,GACrD,OAAO,CAAC,IAAI,CAAC,CAUf;AAID,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GACzC,MAAM,IAAI,CASZ;AAID,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,QAAQ,GAAG,QAAQ,GACxB,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAsEnE;AAID,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,gBAAgB,EAAE,MAAM,EAAE,EAC1B,SAAS,SAAgC,GACxC,OAAO,CAAC,aAAa,CAAC,CAoCxB;AAID,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQzE;AAID,wBAAsB,YAAY,IAAI,OAAO,CAAC;IAC5C,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC,CAoBD;AAID,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,cAAc,EACxB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,CAAC,CAqH7B;AAID,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,SAAS,CAAC,CAsBhE;AAED,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,aAAa,CAAC,CAwHxB;AAID,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,GACjC,OAAO,CAAC,iBAAiB,CAAC,CA6D5B;AAID,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,CAoEvB"}

288
cockpit-kata/dist/lib/kata-mock.d.ts vendored Normal file
View File

@ -0,0 +1,288 @@
/**
* Mock data layer for Kata Containers Cockpit Module
* Simulates kata-runtime, crictl, kata-monitor, and configuration.toml outputs
* In a real Cockpit module, these would be cockpit.spawn() / cockpit.http() calls
*/
export interface Sandbox {
id: string;
name: string;
namespace: string;
uid: string;
runtimeClass: string;
vmm: "qemu" | "cloud-hypervisor" | "firecracker" | "dragonball";
status: "running" | "stopped" | "creating" | "error";
guestKernel: string;
agentConnected: boolean;
createdAt: string;
cpuCores: number;
memoryMB: number;
bootTimeMs: number;
vsockPort: number;
pods: PodInfo[];
}
export interface PodInfo {
name: string;
namespace: string;
uid: string;
runtimeClass: string;
}
export interface ImportedImage {
id: string;
name: string;
type: "rootfs" | "initrd";
path: string;
sizeMB: number;
createdAt: string;
kernelVersion?: string;
agentVersion?: string;
hypervisorCompat: string[];
validated: boolean;
validationErrors?: string[];
}
export interface RuntimeConfig {
id: string;
name: string;
hypervisor: string;
configPath: string;
active: boolean;
kernelPath: string;
initrdPath: string;
imagePath: string;
defaultVCPUs: number;
defaultMemoryMB: number;
defaultBootCpuWeight: number;
}
export interface QCrowsKernelConfig {
requiredOptions: Record<string, boolean>;
optionalOptions: Record<string, boolean>;
totalOptions: number;
moduleCount: number;
builtInCount: number;
}
export interface QCrowsBootParams {
params: string[];
source: string;
}
export interface QCrowsMetadata {
formatVersion: string;
name: string;
version: string;
description: string;
arch: string;
os: string;
createdAt: string;
hypervisors: string[];
kataRuntimeMin: string;
minVcpus: number;
minMemoryMB: number;
recommendedVcpus: number;
recommendedMemoryMB: number;
kernelVersion: string;
kernelIncluded: boolean;
kernelPath: string;
kernelFormat: "vmlinuz" | "vmlinux";
kernelConfigPath: string;
kernelSizeMB: number;
kernelConfigValid: boolean;
initrdIncluded: boolean;
initrdType: string;
initrdPath: string;
rootfsType: string;
rootfsPath: string;
rootfsSizeMB: number;
agentName: string;
agentVersion: string;
bootParamsIncluded: boolean;
bootParamsPath: string;
}
export interface QCrowsMenuEntry {
label: string;
category: "minimal" | "server" | "development" | "embedded" | "hardened" | "custom";
icon: string;
priority: number;
initSystem: string;
packageCount: number;
shell: string;
workloads: string[];
environments: string[];
}
export declare const KATA_REQUIRED_KERNEL_OPTIONS: Record<string, string>;
export declare const KATA_RECOMMENDED_KERNEL_OPTIONS: Record<string, string>;
export declare const HYPERVISOR_KERNEL_FORMAT: Record<string, {
format: "vmlinuz" | "vmlinux";
arches: string[];
}>;
export interface QCrowsBuildInfo {
system: string;
timestamp: string;
hostArch: string;
targetArch: string;
reproducible: boolean;
buildId: string;
kernelCompiler?: string;
kernelDefconfig?: string;
}
export interface QCrowsImage {
id: string;
filename: string;
metadata: QCrowsMetadata;
menu: QCrowsMenuEntry;
build?: QCrowsBuildInfo;
kernelConfig?: QCrowsKernelConfig;
bootParams?: QCrowsBootParams;
hashVerified: boolean;
importedAt: string;
}
export interface MetricPoint {
timestamp: number;
value: number;
}
export interface SandboxMetrics {
sandboxId: string;
cpuUsagePercent: number;
memoryUsageMB: number;
memoryLimitMB: number;
networkRxBytes: number;
networkTxBytes: number;
vmmApiCalls: number;
vmmApiErrors: number;
historyCpu: MetricPoint[];
historyMemory: MetricPoint[];
}
export interface PXEConfig {
tftpDir: string;
serverIP: string;
subnetPrefix: string;
subnetMask: string;
rangeStart: string;
rangeEnd: string;
gateway: string;
dns: string;
label: string;
defaultBoot: boolean;
appendCmdline: string;
}
export declare const PXE_DEFAULTS: PXEConfig;
export interface PXEPushResult {
success: boolean;
tftpDir: string;
pxeConfigPath: string;
kernelDest: string;
initrdDest: string | null;
rootfsDest: string | null;
messages: string[];
errors: string[];
}
export interface PXEStatus {
dnsmasqRunning: boolean;
tftpDirExists: boolean;
tftpDirWritable: boolean;
existingEntries: string[];
}
export interface InitrdRegenConfig {
vmm: "qemu" | "cloud-hypervisor" | "firecracker" | "dragonball";
format: "cpio-gzip" | "cpio-lz4" | "cpio-xz" | "cpio-zstd" | "cramfs";
initStyle: "systemd" | "openrc" | "busybox";
extraModules: string[];
excludeModules: string[];
inPlace: boolean;
agentPath: string;
stripLocales: boolean;
stripDocs: boolean;
stripMan: boolean;
}
export interface InitrdRegenResult {
success: boolean;
output: string;
sizeMB: number;
vmm: string;
format: string;
initStyle: string;
kernelVersion: string;
moduleCount: number;
messages: string[];
errors: string[];
}
export declare const INITRD_REGEN_DEFAULTS: Partial<InitrdRegenConfig>;
export declare const VMM_MODULE_MATRIX: Record<string, {
required: string[];
optional: string[];
}>;
export declare const CORE_INITRD_MODULES: string[];
export declare const COMPRESS_PRIORITY: {
algo: string;
configKey: string;
bootSpeed: string;
}[];
export declare const MOCK_SANDBOXES: Sandbox[];
export declare const MOCK_IMPORTED_IMAGES: ImportedImage[];
export declare const MOCK_RUNTIME_CONFIGS: RuntimeConfig[];
export declare const MOCK_METRICS: Record<string, SandboxMetrics>;
export declare const MOCK_QCROWS_IMAGES: QCrowsImage[];
export declare const QCROWS_VALIDATION_STEPS: {
id: string;
label: string;
description: string;
}[];
export declare function fetchQCrowsImages(): Promise<QCrowsImage[]>;
export declare function validateQCrowsImage(_file: File): Promise<{
valid: boolean;
errors: string[];
warnings: string[];
metadata?: QCrowsMetadata;
menu?: QCrowsMenuEntry;
}>;
export interface ValidationRule {
id: string;
label: string;
description: string;
appliesTo: ("rootfs" | "initrd")[];
severity: "error" | "warning";
check: string;
}
export declare const IMPORT_VALIDATION_RULES: ValidationRule[];
export declare function simulateDelay(ms?: number): Promise<void>;
export declare function fetchSandboxes(): Promise<Sandbox[]>;
export declare function fetchSandboxById(id: string): Promise<Sandbox | undefined>;
export declare function fetchImportedImages(): Promise<ImportedImage[]>;
export declare function fetchRuntimeConfigs(): Promise<RuntimeConfig[]>;
export declare function fetchSandboxMetrics(sandboxId: string): Promise<SandboxMetrics | undefined>;
export declare function validateImage(file: File, type: "rootfs" | "initrd"): Promise<{
valid: boolean;
errors: string[];
warnings: string[];
}>;
export declare function importImage(name: string, type: "rootfs" | "initrd", file: File, hypervisorCompat: string[]): Promise<ImportedImage>;
export declare function checkPXEStatus(): Promise<PXEStatus>;
export declare function pushViaPXE(image: QCrowsImage, config: PXEConfig): Promise<PXEPushResult>;
export declare function regenInitrd(image: QCrowsImage, config: Partial<InitrdRegenConfig>): Promise<InitrdRegenResult>;
export type ExportFormat = "qcow2" | "iso" | "pxe";
export interface ExportConfig {
format: ExportFormat;
outputPath: string;
diskSize: string;
efi: boolean;
bootLabel: string;
rootLabel: string;
serverIP: string;
appendCmdline: string;
}
export declare const EXPORT_DEFAULTS: ExportConfig;
export declare const EXPORT_FORMAT_OPTIONS: Array<{
id: ExportFormat;
label: string;
description: string;
icon: string;
prerequisites: string[];
}>;
export interface ExportResult {
success: boolean;
format: ExportFormat;
outputPath: string;
sizeMB: number;
duration: number;
messages: string[];
errors: string[];
}
export declare function exportQCrows(image: QCrowsImage, config: ExportConfig): Promise<ExportResult>;
//# sourceMappingURL=kata-mock.d.ts.map

File diff suppressed because one or more lines are too long

3
cockpit-kata/dist/lib/utils.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import { type ClassValue } from "clsx";
export declare function cn(...inputs: ClassValue[]): string;
//# sourceMappingURL=utils.d.ts.map

1
cockpit-kata/dist/lib/utils.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG5C,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}

16
cockpit-kata/dist/manifest.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"name": "kata",
"requires": {
"cockpit": "286"
},
"tools": {
"index": {
"label": "Kata Containers",
"priority": 1
}
},
"preload": [
"index"
],
"content-security-policy": "default-src 'self'; connect-src 'self' http://127.0.0.1:8090 ws:; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
}

16
cockpit-kata/manifest.json Executable file
View File

@ -0,0 +1,16 @@
{
"name": "kata",
"requires": {
"cockpit": "286"
},
"tools": {
"index": {
"label": "Kata Containers",
"priority": 1
}
},
"preload": [
"index"
],
"content-security-policy": "default-src 'self'; connect-src 'self' http://127.0.0.1:8090 ws:; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
}

4440
cockpit-kata/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
cockpit-kata/package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "cockpit-kata",
"version": "0.5.0",
"description": "Cockpit module for managing Kata Containers \u2014 sandboxes, image import, build guides, runtime configuration, QCrows format",
"main": "dist/index.js",
"scripts": {
"build": "webpack --mode production",
"watch": "webpack --watch --mode development",
"devel-install": "mkdir -p ~/.local/share/cockpit/kata && ln -sfn $(pwd) ~/.local/share/cockpit/kata",
"devel-uninstall": "rm -f ~/.local/share/cockpit/kata",
"lint": "eslint src/",
"test": "echo 'No tests yet \u2014 see bots/ directory for Cockpit test framework integration'"
},
"dependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0",
"lucide-react": "^0.525.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^3.3.1",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-toast": "^1.2.14"
},
"devDependencies": {
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"copy-webpack-plugin": "^12.0.0",
"css-loader": "^7.1.0",
"mini-css-extract-plugin": "^2.9.0",
"postcss": "^8.4.38",
"postcss-loader": "^8.1.0",
"style-loader": "^4.0.0",
"tailwindcss": "^3.4.4",
"ts-loader": "^9.5.0",
"typescript": "^5.5.0",
"webpack": "^5.92.0",
"webpack-cli": "^5.1.0"
},
"license": "Apache-2.0"
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,35 @@
/**
* Cockpit.js stub for development builds.
*
* In a real Cockpit deployment, cockpit.js is provided globally by
* cockpit-bridge at /usr/share/cockpit/cockpit.js. This stub provides
* enough functionality for local development and testing.
*/
const cockpit = {
spawn: (command, options) => {
const output = JSON.stringify({ command, options });
return Promise.resolve(output);
},
file: (path, options) => ({
read: () => Promise.resolve(null),
replace: () => Promise.resolve(),
modify: () => Promise.resolve(),
watch: () => {},
close: () => {},
}),
http: (options) => ({
get: (path) => Promise.resolve(""),
post: (path, body) => Promise.resolve(""),
}),
user: () => ({
id: 0,
name: "root",
full_name: "Root",
groups: ["root", "wheel"],
home: "/root",
shell: "/bin/bash",
}),
};
module.exports = cockpit;

View File

@ -0,0 +1,582 @@
"use client";
import React, { useState } from "react";
import { cn } from "@/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import {
BookOpen,
Package,
FileArchive,
CheckCircle2,
Circle,
ChevronRight,
Terminal,
Copy,
AlertTriangle,
Lightbulb,
ArrowRight,
} from "lucide-react";
type GuideType = "rootfs" | "initrd";
interface BuildStep {
title: string;
description: string;
commands: string[];
notes?: string;
warnings?: string;
}
const ROOTFS_STEPS: BuildStep[] = [
{
title: "Choose a base distribution",
description:
"Select a minimal Linux distribution as the rootfs base. Alpine, Debian minimal, and Ubuntu cloud images are common choices. The key requirement is that the distribution can boot inside a VM with a custom kernel and that you can install the kata-agent into it.",
commands: [
"# Option A: Alpine Linux (smallest, ~5MiB rootfs)",
"mkdir rootfs && cd rootfs",
"docker export $(docker create alpine:3.18) | tar -xf -",
"",
"# Option B: Debian minimal (~80MiB rootfs)",
"debootstrap --variant=minbase bookworm rootfs http://deb.debian.org/debian",
"",
"# Option C: Ubuntu cloud (~120MiB rootfs)",
"cloud-localds rootfs.img ubuntu-22.04-server-cloudimg-amd64.img",
],
notes: "Alpine produces the smallest images but has musl libc; some workloads may need glibc. Debian/Ubuntu provide glibc but are larger.",
},
{
title: "Install the kata-agent",
description:
"The kata-agent is the in-guest process that communicates with the host-side runtime over vsock. Without it, the host cannot manage containers inside the VM. The agent must be installed at /usr/bin/kata-agent (or /usr/local/bin/kata-agent) and a systemd service must be enabled.",
commands: [
"# Download the kata-agent binary for your architecture",
"KATA_VERSION=2.5.0",
"wget -O rootfs/usr/bin/kata-agent \\",
' "https://github.com/kata-containers/kata-containers/releases/download/${KATA_VERSION}/kata-agent"',
"chmod +x rootfs/usr/bin/kata-agent",
"",
"# Install the systemd service unit",
"cat > rootfs/etc/systemd/system/kata-containers.target << 'EOF'",
"[Unit]",
"Description=kata containers target",
"Requires=multi-user.target",
"After=multi-user.target",
"EOF",
"",
"cat > rootfs/etc/systemd/system/kata-agent.service << 'EOF'",
"[Unit]",
"Description=kata-agent",
"Requires=kata-containers.target",
"After=kata-containers.target",
"",
"[Service]",
"ExecStart=/usr/bin/kata-agent",
"Restart=always",
"RestartSec=1",
"",
"[Install]",
"WantedBy=kata-containers.target",
"EOF",
"",
"# Enable the agent service (chroot into the rootfs)",
"chroot rootfs /bin/bash -c 'systemctl enable kata-agent.service'",
],
warnings: "The agent version must match the host-side runtime version. Mismatched versions may cause vsock communication failures or missing API methods.",
},
{
title: "Configure the init system",
description:
"Kata VMs boot with a custom kernel that expects /sbin/init to exist. For systemd-based rootfs images, this is typically a symlink already. For Alpine (OpenRC), ensure /sbin/init points to the correct init binary. The kata-agent service must start after the base init system brings up the minimum required services.",
commands: [
"# Verify /sbin/init exists and is executable",
"ls -la rootfs/sbin/init",
"",
"# For systemd-based systems, verify the default target",
"chroot rootfs /bin/bash -c 'systemctl get-default'",
"",
"# For OpenRC (Alpine), ensure init points to /sbin/openrc-init",
"ln -sf /sbin/openrc-init rootfs/sbin/init 2>/dev/null || true",
],
},
{
title: "Configure networking and DNS",
description:
"The guest VM needs working DNS resolution for container image pulls and outbound connectivity. Kata configures a virtual network interface via virtio-net, but DNS configuration must be present in the rootfs.",
commands: [
"# Ensure /etc/resolv.conf exists",
"echo 'nameserver 8.8.8.8' > rootfs/etc/resolv.conf",
"",
"# For systemd-resolved setups, create the symlink",
"ln -sf /run/systemd/resolve/stub-resolv.conf rootfs/etc/resolv.conf 2>/dev/null || true",
],
},
{
title: "Strip and optimize the rootfs",
description:
"Remove unnecessary packages, documentation, and caches to minimize image size. Smaller rootfs images boot faster and use less memory. A practical rootfs should be under 500MiB uncompressed; the compressed tarball should target under 200MiB.",
commands: [
"# Remove unnecessary packages (Debian/Ubuntu)",
"chroot rootfs apt-get purge -y \\",
" perl doc-debian manpages info \\",
" vim-tiny nano fdisk parted",
"chroot rootfs apt-get autoremove -y",
"chroot rootfs apt-get clean",
"",
"# Remove caches and documentation",
"rm -rf rootfs/usr/share/doc/*",
"rm -rf rootfs/usr/share/man/*",
"rm -rf rootfs/var/cache/apt/*",
"rm -rf rootfs/var/lib/apt/lists/*",
"",
"# Remove unnecessary files (all distros)",
"rm -rf rootfs/usr/share/info/*",
"rm -rf rootfs/usr/share/locale/*",
"find rootfs -name '*.pyc' -delete",
"find rootfs -name '__pycache__' -type d -exec rm -rf {} +",
],
},
{
title: "Package as tarball",
description:
"Create a gzip-compressed tar archive of the rootfs. This is the format that Kata's configuration.toml references as the 'image' path for hypervisors that use rootfs-based boot (Cloud Hypervisor, Firecracker). The tarball must be created from inside the rootfs directory so paths are relative.",
commands: [
"# Create the tarball (must be relative paths from inside rootfs)",
"cd rootfs",
"tar -czf ../kata-rootfs.tar.gz .",
"cd ..",
"",
"# Verify the tarball",
"tar -tzf kata-rootfs.tar.gz | head -20",
"ls -lh kata-rootfs.tar.gz",
"",
"# Move to kata-containers image directory",
"sudo mv kata-rootfs.tar.gz /usr/share/kata-containers/",
],
},
];
const INITRD_STEPS: BuildStep[] = [
{
title: "Compile the guest kernel",
description:
"Kata requires a custom guest kernel compiled with specific configuration options for VM isolation. The kernel must have virtio drivers, vsock support, and be configured for minimal boot time. Use the kata-containers kernel config as a starting point rather than building one from scratch.",
commands: [
"# Clone the kata-containers kernel configuration",
"git clone --depth 1 https://github.com/kata-containers/kata-containers",
"cd kata-containers/tools/packaging/kernel",
"",
"# Download and configure the kernel",
"./build-kernel.sh setup",
"./build-kernel.sh build",
"",
"# The resulting vmlinuz will be in:",
"ls -lh kata-vmlinuz.*",
],
notes: "The default kernel config disables unnecessary subsystems (sound, USB, Bluetooth) and enables virtio, vsock, and DAX for shared memory. For Firecracker, use the vmlinux (uncompressed) format; for QEMU/CLH, use vmlinuz (compressed).",
},
{
title: "Build the initramfs rootfs",
description:
"Create a minimal rootfs that will be embedded in the initrd image. This is similar to the rootfs tarball approach but much smaller — typically under 40MiB. The initramfs rootfs only needs the kata-agent, a minimal init, and essential utilities for boot. Alpine's package manager (apk) is ideal for building small initramfs images.",
commands: [
"# Create initramfs rootfs structure",
"mkdir -p initrd-rootfs/{bin,sbin,etc,proc,sys,dev,usr/bin,usr/sbin,lib,lib64}",
"",
"# Install Alpine base (minimal)",
"cd initrd-rootfs",
"apk -X http://dl-cdn.alpinelinux.org/alpine/v3.18/main \\",
" -X http://dl-cdn.alpinelinux.org/alpine/v3.18/community \\",
" -U --allow-untrusted --root . --initdb \\",
" add alpine-base kata-agent",
"",
"# Or install kata-agent manually",
"wget -O usr/bin/kata-agent \\",
' "https://github.com/kata-containers/kata-containers/releases/download/2.5.0/kata-agent"',
"chmod +x usr/bin/kata-agent",
],
},
{
title: "Configure init for initramfs boot",
description:
"The initrd boot path uses /init (not /sbin/init) as the entry point. Create an init script that mounts essential virtual filesystems and then execs the kata-agent. This is simpler than a full systemd setup because the initramfs environment is transient.",
commands: [
"# Create the /init entry point",
"cat > initrd-rootfs/init << 'INITEOF'",
"#!/bin/sh",
"mount -t proc proc /proc",
"mount -t sysfs sysfs /sys",
"mount -t devtmpfs devtmpfs /dev",
"mkdir -p /dev/pts /dev/shm",
"mount -t devpts devpts /dev/pts",
"mount -t tmpfs tmpfs /dev/shm",
"",
"# Start kata-agent",
"exec /usr/bin/kata-agent",
"INITEOF",
"chmod +x initrd-rootfs/init",
],
},
{
title: "Package as initrd image",
description:
"Create a cpio archive compressed with gzip (or lz4/xz for faster decompression). The cpio format is the standard initramfs format expected by the Linux kernel. The archive must be created from inside the rootfs directory with relative paths, just like the rootfs tarball.",
commands: [
"# Create the cpio archive",
"cd initrd-rootfs",
"find . | cpio -o -H newc | gzip -9 > ../kata-initrd.img",
"cd ..",
"",
"# For faster boot, use lz4 compression (if supported by kernel)",
"# find . | cpio -o -H newc | lz4 -l > ../kata-initrd.img",
"",
"# Verify the image",
"file kata-initrd.img",
"ls -lh kata-initrd.img",
"",
"# Move to kata-containers image directory",
"sudo mv kata-initrd.img /usr/share/kata-containers/",
],
notes: "gzip -9 gives best compression but slower decompression at boot. gzip -1 or lz4 trades compression ratio for faster boot times. For production, benchmark both on your target hardware.",
},
{
title: "Update configuration.toml",
description:
"Point the Kata runtime configuration to your new kernel and initrd/rootfs images. Each hypervisor profile has its own configuration.toml. After updating the config, restart containerd (or the CRI runtime) to pick up the changes.",
commands: [
"# Edit the relevant configuration.toml",
"sudo vim /usr/share/defaults/kata-containers/configuration-qemu.toml",
"",
"# For initrd-based boot, set these paths:",
"# kernel = \"/usr/share/kata-containers/kata-vmlinuz\"",
"# initrd = \"/usr/share/kata-containers/kata-initrd.img\"",
"",
"# For rootfs-based boot, set these paths:",
"# kernel = \"/usr/share/kata-containers/kata-vmlinuz\"",
"# image = \"/usr/share/kata-containers/kata-rootfs.tar.gz\"",
"",
"# Restart containerd to pick up config changes",
"sudo systemctl restart containerd",
],
},
];
export function BuildGuide() {
const [guideType, setGuideType] = useState<GuideType>("rootfs");
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
const steps = guideType === "rootfs" ? ROOTFS_STEPS : INITRD_STEPS;
const toggleStep = (idx: number) => {
setCompletedSteps((prev) => {
const next = new Set(prev);
if (next.has(idx)) next.delete(idx);
else next.add(idx);
return next;
});
};
const copyCommand = (cmd: string) => {
navigator.clipboard?.writeText(cmd);
};
const progress = (completedSteps.size / steps.length) * 100;
return (
<div className="p-6 space-y-6 max-w-4xl">
{/* Guide selector */}
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm text-slate-300">
<BookOpen className="h-4 w-4 text-emerald-400" />
<span className="font-medium">Image Build Guide</span>
</div>
<p className="text-xs text-slate-500">
Step-by-step instructions for building Kata-compatible rootfs tarballs
and initrd images. Follow each step in order and mark them complete as
you progress.
</p>
<div className="flex gap-2">
<button
onClick={() => {
setGuideType("rootfs");
setCompletedSteps(new Set());
}}
className={cn(
"flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm font-medium transition-colors",
guideType === "rootfs"
? "border-emerald-600 bg-emerald-900/20 text-emerald-300"
: "border-slate-700 bg-[#0a0f1a] text-slate-400 hover:border-slate-600"
)}
>
<Package className="h-4 w-4" />
Rootfs Tarball
</button>
<button
onClick={() => {
setGuideType("initrd");
setCompletedSteps(new Set());
}}
className={cn(
"flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm font-medium transition-colors",
guideType === "initrd"
? "border-cyan-600 bg-cyan-900/20 text-cyan-300"
: "border-slate-700 bg-[#0a0f1a] text-slate-400 hover:border-slate-600"
)}
>
<FileArchive className="h-4 w-4" />
Initrd Image
</button>
</div>
{/* Progress */}
<div className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Progress</span>
<span className="text-slate-400">
{completedSteps.size} / {steps.length} steps
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-800 overflow-hidden">
<div
className="h-full rounded-full bg-emerald-500 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
{/* Comparison info */}
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Lightbulb className="h-4 w-4 text-amber-400 shrink-0 mt-0.5" />
<div className="text-xs text-slate-400 space-y-1.5">
<p className="text-slate-300 font-medium">
{guideType === "rootfs" ? "Rootfs" : "Initrd"} vs{" "}
{guideType === "rootfs" ? "Initrd" : "Rootfs"} when to use which?
</p>
{guideType === "rootfs" ? (
<>
<p>
<strong className="text-slate-200">Rootfs tarballs</strong> are
used by Cloud Hypervisor and Firecracker backends. They provide a
full filesystem that is mounted as a block device, supporting
larger images with more packages. Boot time is typically 150-350ms.
</p>
<p>
<strong className="text-slate-200">Initrd images</strong> are
used by QEMU and Dragonball backends. They are loaded directly
into RAM by the kernel, providing faster boot for small images
but limited by available memory. Boot time is typically 100-200ms.
</p>
</>
) : (
<>
<p>
<strong className="text-slate-200">Initrd images</strong> are
loaded directly into RAM faster boot but size-constrained. Use
for QEMU and Dragonball backends where minimal footprint matters.
</p>
<p>
<strong className="text-slate-200">Rootfs tarballs</strong> are
mounted as block devices slower boot but no practical size limit.
Use for Cloud Hypervisor and Firecracker backends where you need
more packages in the guest.
</p>
</>
)}
</div>
</div>
</CardContent>
</Card>
{/* Steps */}
<div className="space-y-4">
{steps.map((step, idx) => {
const isComplete = completedSteps.has(idx);
return (
<Card
key={idx}
className={cn(
"bg-[#131c31] border transition-colors",
isComplete
? "border-emerald-700/30 bg-emerald-900/5"
: "border-slate-700/50"
)}
>
<CardContent className="p-4 space-y-3">
{/* Step header */}
<div className="flex items-start gap-3">
<button
onClick={() => toggleStep(idx)}
className="mt-0.5 shrink-0"
>
{isComplete ? (
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
) : (
<Circle className="h-5 w-5 text-slate-600 hover:text-slate-400 transition-colors" />
)}
</button>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Badge
variant="outline"
className="text-[9px] px-1.5 py-0 border-slate-600 text-slate-400"
>
Step {idx + 1}
</Badge>
<h3
className={cn(
"text-sm font-medium",
isComplete
? "text-emerald-300 line-through decoration-emerald-700"
: "text-slate-200"
)}
>
{step.title}
</h3>
</div>
<p className="text-xs text-slate-400 mt-1.5 leading-relaxed">
{step.description}
</p>
</div>
</div>
{/* Commands */}
<div className="ml-8 rounded-md bg-[#0a0f1a] border border-slate-700/30 overflow-hidden">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-slate-700/30">
<div className="flex items-center gap-1.5">
<Terminal className="h-3 w-3 text-slate-500" />
<span className="text-[10px] text-slate-500">Terminal</span>
</div>
<Button
variant="ghost"
size="sm"
className="h-5 text-[10px] text-slate-500 hover:text-slate-300 px-1.5"
onClick={() =>
copyCommand(step.commands.filter((c) => c && !c.startsWith("#")).join("\n"))
}
>
<Copy className="h-3 w-3 mr-1" />
Copy
</Button>
</div>
<div className="p-3 font-mono text-xs leading-relaxed overflow-x-auto">
{step.commands.map((cmd, i) => (
<div key={i} className="flex">
{cmd.startsWith("#") ? (
<span className="text-slate-600">{cmd}</span>
) : cmd === "" ? (
<br />
) : (
<span className="text-slate-300">{cmd}</span>
)}
</div>
))}
</div>
</div>
{/* Notes */}
{step.notes && (
<div className="ml-8 flex items-start gap-2 text-xs text-cyan-400 bg-cyan-900/10 rounded px-3 py-2">
<Lightbulb className="h-3.5 w-3.5 shrink-0 mt-0.5" />
<span>{step.notes}</span>
</div>
)}
{/* Warnings */}
{step.warnings && (
<div className="ml-8 flex items-start gap-2 text-xs text-amber-400 bg-amber-900/10 rounded px-3 py-2">
<AlertTriangle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
<span>{step.warnings}</span>
</div>
)}
</CardContent>
</Card>
);
})}
</div>
{/* Quick reference */}
<Card className="bg-[#131c31] border-slate-700/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-slate-200">
Quick Reference: File Requirements
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="text-xs font-medium text-emerald-400 flex items-center gap-1.5">
<Package className="h-3.5 w-3.5" />
Rootfs Tarball
</h4>
<ul className="text-[11px] text-slate-400 space-y-1">
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Format: gzip-compressed tar (.tar.gz)
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Must contain /sbin/init
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Must contain /usr/bin/kata-agent
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Must contain /etc/resolv.conf
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Recommended max: 2GiB compressed
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Used by: CLH, Firecracker
</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="text-xs font-medium text-cyan-400 flex items-center gap-1.5">
<FileArchive className="h-3.5 w-3.5" />
Initrd Image
</h4>
<ul className="text-[11px] text-slate-400 space-y-1">
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Format: gzip/lz4/xz cpio archive
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Must contain /init entry point
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Must contain /usr/bin/kata-agent
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Must mount proc/sys/dev in /init
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Recommended max: 512MiB
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 className="h-3 w-3 text-slate-500" />
Used by: QEMU, Dragonball
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,633 @@
"use client";
import React, { useState, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
Settings,
CheckCircle2,
XCircle,
Copy,
Loader2,
FileText,
Server,
Cpu,
HardDrive,
Edit3,
Save,
X,
RotateCcw,
AlertTriangle,
Eye,
Code,
} from "lucide-react";
import { fetchRuntimeConfigs, type RuntimeConfig } from "@/lib/kata-mock";
import { saveRuntimeConfig } from "@/lib/backend/adapter";
// ─── Mock TOML content generator ────────────────────────────────────────
// In cockpit mode, this would come from cockpit.file().read()
// In mock mode, we generate representative TOML from the RuntimeConfig
function generateMockToml(config: RuntimeConfig): string {
const hypervisorSection = config.hypervisor === "qemu"
? `[hypervisor.qemu]
path = "/usr/bin/qemu-system-x86_64"
kernel = "${config.kernelPath}"
${config.initrdPath ? `initrd = "${config.initrdPath}"` : `image = "${config.imagePath}"`}
machine_type = "pc"
default_vcpus = ${config.defaultVCPUs}
default_memory = ${config.defaultMemoryMB}
default_max_vcpus = 16
default_max_memory = 32768
disable_image = false
hot_plug_vfio = "root-port"
pcie_root_port = 0
firmware = ""
machine_accelerators = ""
${config.hypervisor === "qemu" ? `enable_vhost_user_store = true
vhost_user_store_path = "/var/run/kata-containers/vhost-user/"
file_backed_mem_root_dir = "/dev/shm/"
guest_hook_path = "/usr/share/oci/hooks/"` : ""}`
: config.hypervisor === "cloud-hypervisor"
? `[hypervisor.cloud-hypervisor]
path = "/usr/bin/cloud-hypervisor"
kernel = "${config.kernelPath}"
${config.imagePath ? `image = "${config.imagePath}"` : `initrd = "${config.initrdPath}"`}
default_vcpus = ${config.defaultVCPUs}
default_memory = ${config.defaultMemoryMB}
default_max_vcpus = 16
default_max_memory = 32768
disable_image = false
hot_plug_vfio = "root-port"
pcie_root_port = 0`
: config.hypervisor === "firecracker"
? `[hypervisor.firecracker]
path = "/usr/bin/jailer"
kernel = "${config.kernelPath}"
${config.imagePath ? `image = "${config.imagePath}"` : `initrd = "${config.initrdPath}"`}
default_vcpus = ${config.defaultVCPUs}
default_memory = ${config.defaultMemoryMB}
default_max_vcpus = 4
default_max_memory = 8192
disable_image = false
jailer_path = "/usr/bin/jailer"
firecracker_path = "/usr/bin/firecracker"`
: `[hypervisor.dragonball]
path = "/usr/bin/dragonball"
kernel = "${config.kernelPath}"
${config.initrdPath ? `initrd = "${config.initrdPath}"` : `image = "${config.imagePath}"`}
default_vcpus = ${config.defaultVCPUs}
default_memory = ${config.defaultMemoryMB}
default_max_vcpus = 16
default_max_memory = 32768`;
return `# Kata Containers Configuration
# Runtime: ${config.name}
# Hypervisor: ${config.hypervisor}
# Path: ${config.configPath}
#
# This file is in TOML format. Edit with care.
# Changes take effect after restarting containerd:
# sudo systemctl restart containerd
[runtime]
enable_debug = false
enable_tracing = false
disable_new_netns = false
disable_guest_seccomp = false
guest_hook_path = "/usr/share/oci/hooks/"
interception_network = false
${hypervisorSection}
[agent]
enable_tracing = false
trace_type = ""
trace_mode = ""
[netmon]
enable = false
`;
}
export function Configuration() {
const [configs, setConfigs] = useState<RuntimeConfig[]>([]);
const [loading, setLoading] = useState(true);
const [selectedConfig, setSelectedConfig] = useState<string | null>(null);
// Config editor state
const [editMode, setEditMode] = useState(false);
const [editContent, setEditContent] = useState("");
const [originalContent, setOriginalContent] = useState("");
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [saveSuccess, setSaveSuccess] = useState(false);
const [confirmDiscard, setConfirmDiscard] = useState(false);
// Live reload (cockpit.file().watch() equivalent)
const [liveContent, setLiveContent] = useState<string | null>(null);
const [viewTab, setViewTab] = useState<"structured" | "raw">("structured");
useEffect(() => {
let cancelled = false;
const load = async () => {
const data = await fetchRuntimeConfigs();
if (!cancelled) {
setConfigs(data);
setSelectedConfig(data[0]?.id || null);
setLoading(false);
}
};
load();
return () => { cancelled = true; };
}, []);
const activeConfig = configs.find((c) => c.id === selectedConfig);
// When active config changes, update live content
useEffect(() => {
if (activeConfig) {
const toml = generateMockToml(activeConfig);
setLiveContent(toml);
if (!editMode) {
setEditContent(toml);
setOriginalContent(toml);
}
}
}, [activeConfig, editMode]);
const handleEdit = () => {
if (!liveContent) return;
setEditContent(liveContent);
setOriginalContent(liveContent);
setEditMode(true);
setSaveError(null);
setSaveSuccess(false);
setViewTab("raw");
};
const handleSave = async () => {
if (!activeConfig) return;
setSaving(true);
setSaveError(null);
try {
await saveRuntimeConfig(activeConfig.configPath, editContent);
setSaveSuccess(true);
setOriginalContent(editContent);
setLiveContent(editContent);
setTimeout(() => {
setSaveSuccess(false);
setEditMode(false);
setViewTab("structured");
}, 1500);
} catch (err) {
setSaveError(err instanceof Error ? err.message : "Failed to save configuration");
} finally {
setSaving(false);
}
};
const handleCancel = () => {
if (editContent !== originalContent) {
setConfirmDiscard(true);
} else {
setEditMode(false);
setViewTab("structured");
}
};
const handleDiscardConfirm = () => {
setConfirmDiscard(false);
setEditMode(false);
setEditContent(originalContent);
setViewTab("structured");
};
const hasChanges = editContent !== originalContent;
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
</div>
);
}
return (
<div className="p-6 space-y-5">
<div className="flex items-center gap-2 text-sm text-slate-300">
<Settings className="h-4 w-4 text-emerald-400" />
<span className="font-medium">Runtime Configuration</span>
{editMode && (
<Badge variant="outline" className="text-[9px] px-1.5 py-0 border-amber-700/50 text-amber-400 ml-2">
Editing
</Badge>
)}
</div>
<p className="text-xs text-slate-500">
View and manage Kata runtime configuration profiles. Each profile
corresponds to a RuntimeClass in Kubernetes and defines the hypervisor
backend, guest kernel, rootfs/initrd paths, and resource defaults.
Configuration files are in TOML format at the paths shown below.
</p>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
{/* Config selector */}
<div className="space-y-2">
{configs.map((config) => (
<button
key={config.id}
onClick={() => {
if (editMode && hasChanges) {
setConfirmDiscard(true);
}
setSelectedConfig(config.id);
if (editMode) {
setEditMode(false);
setViewTab("structured");
}
}}
className={cn(
"w-full text-left p-3 rounded-lg border transition-colors",
selectedConfig === config.id
? "border-emerald-600/50 bg-emerald-900/10"
: "border-slate-700/30 bg-[#0a0f1a] hover:border-slate-600"
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<code className="text-xs font-mono text-slate-200">
{config.name}
</code>
{config.active ? (
<Badge
variant="outline"
className="text-[9px] px-1.5 py-0 border-emerald-700/50 text-emerald-400"
>
active
</Badge>
) : (
<Badge
variant="outline"
className="text-[9px] px-1.5 py-0 border-slate-600 text-slate-500"
>
inactive
</Badge>
)}
</div>
</div>
<p className="text-[11px] text-slate-500 mt-1">
Hypervisor: {config.hypervisor}
</p>
</button>
))}
</div>
{/* Config detail */}
{activeConfig && (
<div className="lg:col-span-2 space-y-4">
<Card className="bg-[#131c31] border-slate-700/50">
<CardHeader className="pb-3 pt-4 px-4">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
<FileText className="h-4 w-4 text-cyan-400" />
{activeConfig.name}
</CardTitle>
<div className="flex items-center gap-2">
{editMode ? (
<>
<Button
variant="ghost"
size="sm"
onClick={handleCancel}
className="h-7 text-xs text-slate-400 hover:text-slate-200"
>
<X className="h-3 w-3 mr-1" />
Cancel
</Button>
<Button
size="sm"
onClick={handleSave}
disabled={!hasChanges || saving}
className={cn(
"h-7 text-xs text-white",
hasChanges
? "bg-emerald-600 hover:bg-emerald-700"
: "bg-slate-700 cursor-not-allowed"
)}
>
{saving ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : saveSuccess ? (
<CheckCircle2 className="h-3 w-3 mr-1" />
) : (
<Save className="h-3 w-3 mr-1" />
)}
{saving ? "Saving..." : saveSuccess ? "Saved!" : "Save"}
</Button>
</>
) : (
<Button
variant="outline"
size="sm"
onClick={handleEdit}
className="h-7 text-xs border-slate-700 text-slate-400 hover:text-slate-200"
>
<Edit3 className="h-3 w-3 mr-1.5" />
Edit Config
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-4">
{/* Save error */}
{saveError && (
<div className="flex items-center gap-2 text-xs text-red-400 bg-red-900/10 rounded px-3 py-2">
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
{saveError}
</div>
)}
{/* Save success */}
{saveSuccess && (
<div className="flex items-center gap-2 text-xs text-emerald-400 bg-emerald-900/10 rounded px-3 py-2">
<CheckCircle2 className="h-3.5 w-3.5 shrink-0" />
Configuration saved. Restart containerd to apply changes.
</div>
)}
{editMode ? (
/* ─── TOML Editor ────────────────────────────────────── */
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Code className="h-3.5 w-3.5 text-amber-400" />
<span className="text-xs font-medium text-slate-300">
{activeConfig.configPath}
</span>
</div>
{hasChanges && (
<Badge variant="outline" className="text-[9px] px-1.5 py-0 border-amber-700/50 text-amber-400">
Unsaved changes
</Badge>
)}
</div>
<div className="rounded-md border border-slate-700/50 overflow-hidden">
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="w-full h-[500px] bg-[#0a0f1a] text-slate-200 font-mono text-xs p-4 resize-none focus:outline-none focus:ring-1 focus:ring-emerald-600/50 leading-relaxed"
spellCheck={false}
/>
</div>
<div className="flex items-center justify-between text-[10px] text-slate-600">
<span>
{editContent.split("\n").length} lines,{" "}
{editContent.length} chars
</span>
<div className="flex items-center gap-3">
<button
onClick={() => setEditContent(originalContent)}
className="flex items-center gap-1 hover:text-slate-400 transition-colors"
>
<RotateCcw className="h-3 w-3" />
Reset to original
</button>
<button
onClick={() => navigator.clipboard?.writeText(editContent)}
className="flex items-center gap-1 hover:text-slate-400 transition-colors"
>
<Copy className="h-3 w-3" />
Copy all
</button>
</div>
</div>
<div className="flex items-start gap-2 text-[11px] text-amber-400 bg-amber-900/10 rounded px-3 py-2">
<AlertTriangle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
<span>
Changes require <code className="text-amber-300 bg-amber-900/20 px-1 rounded">sudo systemctl restart containerd</code> to take effect.
Invalid TOML may prevent the runtime from starting.
</span>
</div>
</div>
) : (
/* ─── Structured View ─────────────────────────────────── */
<>
{/* View toggle */}
<Tabs value={viewTab} onValueChange={(v) => setViewTab(v as "structured" | "raw")}>
<TabsList className="bg-[#0a0f1a] border border-slate-700/50 h-7 p-0.5">
<TabsTrigger
value="structured"
className="text-[10px] data-[state=active]:bg-slate-700/60 data-[state=active]:text-white text-slate-400 h-6 px-2.5"
>
<Eye className="h-3 w-3 mr-1" />
Structured
</TabsTrigger>
<TabsTrigger
value="raw"
className="text-[10px] data-[state=active]:bg-slate-700/60 data-[state=active]:text-white text-slate-400 h-6 px-2.5"
>
<Code className="h-3 w-3 mr-1" />
Raw TOML
</TabsTrigger>
</TabsList>
<TabsContent value="structured" className="mt-4 space-y-4">
{/* Paths */}
<div className="space-y-2.5">
<h4 className="text-xs font-medium text-slate-400 uppercase tracking-wider">
Paths
</h4>
{[
{ label: "Configuration File", value: activeConfig.configPath },
{ label: "Guest Kernel", value: activeConfig.kernelPath },
{ label: "Initrd Image", value: activeConfig.initrdPath || "(none — using rootfs image)", dim: !activeConfig.initrdPath },
{ label: "Rootfs Image", value: activeConfig.imagePath || "(none — using initrd)", dim: !activeConfig.imagePath },
].map((item) => (
<div key={item.label} className="flex items-center justify-between text-xs">
<span className="text-slate-500">{item.label}</span>
<div className="flex items-center gap-1.5 max-w-[60%]">
<code className={cn("font-mono truncate", item.dim ? "text-slate-600 italic" : "text-slate-300")}>
{item.value}
</code>
{!item.dim && (
<button
onClick={() => navigator.clipboard?.writeText(item.value)}
className="text-slate-600 hover:text-slate-400 shrink-0"
>
<Copy className="h-3 w-3" />
</button>
)}
</div>
</div>
))}
</div>
<Separator className="bg-slate-700/30" />
{/* Resources */}
<div className="space-y-2.5">
<h4 className="text-xs font-medium text-slate-400 uppercase tracking-wider">
Default Resources
</h4>
<div className="grid grid-cols-3 gap-3">
<div className="bg-[#0a0f1a] rounded-lg p-3 text-center">
<Cpu className="h-4 w-4 text-cyan-400 mx-auto mb-1" />
<p className="text-lg font-semibold text-slate-200">{activeConfig.defaultVCPUs}</p>
<p className="text-[10px] text-slate-500">vCPU</p>
</div>
<div className="bg-[#0a0f1a] rounded-lg p-3 text-center">
<HardDrive className="h-4 w-4 text-violet-400 mx-auto mb-1" />
<p className="text-lg font-semibold text-slate-200">
{activeConfig.defaultMemoryMB >= 1024
? `${(activeConfig.defaultMemoryMB / 1024).toFixed(1)}G`
: `${activeConfig.defaultMemoryMB}M`}
</p>
<p className="text-[10px] text-slate-500">Memory</p>
</div>
<div className="bg-[#0a0f1a] rounded-lg p-3 text-center">
<Server className="h-4 w-4 text-amber-400 mx-auto mb-1" />
<p className="text-lg font-semibold text-slate-200 capitalize">{activeConfig.hypervisor}</p>
<p className="text-[10px] text-slate-500">VMM</p>
</div>
</div>
</div>
<Separator className="bg-slate-700/30" />
{/* Boot mode */}
<div className="space-y-2.5">
<h4 className="text-xs font-medium text-slate-400 uppercase tracking-wider">
Boot Mode
</h4>
<div className="flex items-center gap-3">
<Badge variant="outline" className={cn("text-xs px-2 py-0.5", activeConfig.initrdPath ? "border-cyan-700/50 text-cyan-400" : "border-slate-600 text-slate-500")}>
{activeConfig.initrdPath ? <CheckCircle2 className="h-3 w-3 mr-1" /> : <XCircle className="h-3 w-3 mr-1" />}
Initrd
</Badge>
<Badge variant="outline" className={cn("text-xs px-2 py-0.5", activeConfig.imagePath ? "border-emerald-700/50 text-emerald-400" : "border-slate-600 text-slate-500")}>
{activeConfig.imagePath ? <CheckCircle2 className="h-3 w-3 mr-1" /> : <XCircle className="h-3 w-3 mr-1" />}
Rootfs Image
</Badge>
</div>
<p className="text-[11px] text-slate-500">
{activeConfig.initrdPath
? "This profile boots using an initrd image loaded into RAM. Faster boot, but image size is constrained by guest memory."
: "This profile boots using a rootfs tarball mounted as a block device. Larger images supported, slightly slower boot."}
</p>
</div>
</TabsContent>
<TabsContent value="raw" className="mt-4">
<div className="rounded-md border border-slate-700/30 overflow-hidden">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-slate-700/30 bg-[#0a0f1a]">
<span className="text-[10px] text-slate-500 font-mono">{activeConfig.configPath}</span>
<Button
variant="ghost"
size="sm"
className="h-5 text-[10px] text-slate-500 hover:text-slate-300 px-1.5"
onClick={() => liveContent && navigator.clipboard?.writeText(liveContent)}
>
<Copy className="h-3 w-3 mr-1" />
Copy
</Button>
</div>
<pre className="p-4 font-mono text-xs text-slate-400 leading-relaxed overflow-auto max-h-[500px] bg-[#0a0f1a]">
{liveContent || "Loading..."}
</pre>
</div>
</TabsContent>
</Tabs>
</>
)}
</CardContent>
</Card>
{/* Machines module comparison */}
<Card className="bg-[#131c31] border-slate-700/50">
<CardHeader className="pb-3 pt-4 px-4">
<CardTitle className="text-sm font-medium text-slate-200">
Cockpit Machines Module Comparison
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-700/30">
<th className="text-left text-slate-500 font-medium py-2 pr-4">Aspect</th>
<th className="text-left text-slate-500 font-medium py-2 pr-4">Machines (libvirt)</th>
<th className="text-left text-emerald-500 font-medium py-2">Kata (this module)</th>
</tr>
</thead>
<tbody className="text-slate-300">
{[
["Backend", "libvirt via D-Bus", "kata-runtime / kata-ctl via cockpit.spawn()"],
["Object model", "virDomain (persistent + transient)", "Sandbox (ephemeral, per-pod lifecycle)"],
["List view", "VM name + state + VCPU/Memory", "Sandbox name + namespace + RuntimeClass + VMM"],
["Create flow", "Install from ISO / URL / PXE / import disk", "Created by K8s via RuntimeClass; import images only"],
["Import feature", "Import existing disk image (.qcow2, .raw)", "Import rootfs tarball + initrd image with validation"],
["Config editing", "libvirt XML editor (raw XML)", "TOML configuration.toml (structured view + raw editor)"],
["Metrics source", "libvirt domain stats API", "kata-monitor /metrics (Prometheus format)"],
["Console access", "VNC / serial via libvirt", "kata-agent vsock console (text only)"],
].map(([aspect, machines, kata], i) => (
<tr key={i} className="border-b border-slate-700/10">
<td className="py-2 pr-4 text-slate-400 font-medium">{aspect}</td>
<td className="py-2 pr-4">{machines}</td>
<td className="py-2 text-emerald-300">{kata}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
)}
</div>
{/* Discard changes confirmation */}
<Dialog open={confirmDiscard} onOpenChange={setConfirmDiscard}>
<DialogContent className="bg-[#131c31] border-slate-700 text-slate-200">
<DialogHeader>
<DialogTitle className="text-sm">Discard unsaved changes?</DialogTitle>
</DialogHeader>
<p className="text-xs text-slate-400">
You have unsaved changes to the configuration. Discarding will revert
to the last saved version. This cannot be undone.
</p>
<DialogFooter className="gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setConfirmDiscard(false)}
className="text-slate-400"
>
Keep Editing
</Button>
<Button
size="sm"
onClick={handleDiscardConfirm}
className="bg-red-600 hover:bg-red-700 text-white"
>
Discard Changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -0,0 +1,489 @@
"use client";
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
HardDrive,
Disc,
Radio,
Download,
Loader2,
CheckCircle2,
XCircle,
ChevronRight,
Package,
AlertTriangle,
} from "lucide-react";
import {
fetchQCrowsImages,
EXPORT_DEFAULTS,
EXPORT_FORMAT_OPTIONS,
type QCrowsImage,
type ExportConfig,
type ExportResult,
type ExportFormat,
} from "@/lib/kata-mock";
import { exportQCrowsImage } from "@/lib/backend/adapter";
const FORMAT_ICONS: Record<ExportFormat, React.ElementType> = {
qcow2: HardDrive,
iso: Disc,
pxe: Radio,
};
const FORMAT_COLORS: Record<ExportFormat, string> = {
qcow2: "text-blue-400",
iso: "text-amber-400",
pxe: "text-violet-400",
};
const DEFAULT_OUTPUT_PATHS: Record<ExportFormat, (slug: string) => string> = {
qcow2: (slug) => `${slug}.qcow2`,
iso: (slug) => `${slug}.iso`,
pxe: () => "/srv/tftp",
};
const USAGE_HINTS: Record<ExportFormat, (outputPath: string) => string> = {
qcow2: (p) => `qemu-system-x86_64 -m 1G -smp 2 -drive file=${p},format=qcow2 -enable-kvm`,
iso: (p) => `dd if=${p} of=/dev/sdX bs=4M status=progress && sync`,
pxe: () => `systemctl restart dnsmasq`,
};
export function ExportImage() {
const [images, setImages] = useState<QCrowsImage[]>([]);
const [loading, setLoading] = useState(true);
const [selectedImage, setSelectedImage] = useState<QCrowsImage | null>(null);
const [config, setConfig] = useState<ExportConfig>({ ...EXPORT_DEFAULTS });
const [exporting, setExporting] = useState(false);
const [result, setResult] = useState<ExportResult | null>(null);
const [fetchError, setFetchError] = useState<string | null>(null);
useEffect(() => {
fetchQCrowsImages()
.then((data) => {
setImages(data);
if (data.length > 0) setSelectedImage(data[0]);
})
.catch((err: unknown) => {
setFetchError(err instanceof Error ? err.message : "Failed to load images");
})
.finally(() => setLoading(false));
}, []);
const handleExport = async () => {
if (!selectedImage) return;
setExporting(true);
setResult(null);
try {
const res = await exportQCrowsImage(selectedImage, config);
setResult(res);
} catch {
setResult({
success: false,
format: config.format,
outputPath: config.outputPath,
sizeMB: 0,
duration: 0,
messages: [],
errors: ["Export failed unexpectedly"],
});
} finally {
setExporting(false);
}
};
const updateConfig = (patch: Partial<ExportConfig>) => {
setConfig((prev) => ({ ...prev, ...patch }));
setResult(null);
};
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-8 w-8 animate-spin text-slate-400" />
</div>
);
}
if (fetchError) {
return (
<div className="flex flex-col items-center justify-center h-full text-red-400">
<XCircle className="h-8 w-8 mb-3" />
<p className="text-sm">Failed to load images</p>
<p className="text-xs text-slate-500 mt-1">{fetchError}</p>
</div>
);
}
return (
<div className="flex h-full">
{/* Image selector sidebar */}
<div className="w-64 border-r border-slate-700/50 bg-[#0c1222] flex flex-col">
<div className="px-3 py-3 border-b border-slate-700/50">
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
QCrows Images
</h3>
</div>
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
{images.map((img) => (
<button
key={img.id}
onClick={() => {
setSelectedImage(img);
setResult(null);
}}
className={cn(
"flex flex-col w-full rounded-md px-3 py-2.5 text-left transition-colors",
selectedImage?.id === img.id
? "bg-slate-700/60 text-white"
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800/60"
)}
>
<span className="text-sm font-medium truncate">{img.metadata.name}</span>
<span className="text-[11px] text-slate-500 mt-0.5">
{img.metadata.kernelVersion} &middot; {img.metadata.arch}
</span>
</button>
))}
</div>
</ScrollArea>
</div>
{/* Main content */}
<div className="flex-1 overflow-auto p-6 space-y-6">
{selectedImage ? (
<>
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-slate-100">
Export {selectedImage.metadata.name}
</h2>
<p className="text-sm text-slate-400 mt-1">
Convert QCrows image to bootable format for deployment
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[11px]">
v{selectedImage.metadata.version}
</Badge>
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[11px]">
{selectedImage.metadata.arch}
</Badge>
</div>
</div>
{/* Image summary card */}
<Card className="bg-slate-800/40 border-slate-700/50">
<CardContent className="p-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-slate-500 text-xs">Kernel</span>
<p className="text-slate-200 font-medium">{selectedImage.metadata.kernelVersion}</p>
</div>
<div>
<span className="text-slate-500 text-xs">Rootfs</span>
<p className="text-slate-200 font-medium">{selectedImage.metadata.rootfsSizeMB}MB</p>
</div>
<div>
<span className="text-slate-500 text-xs">Initrd</span>
<p className="text-slate-200 font-medium">
{selectedImage.metadata.initrdIncluded ? selectedImage.metadata.initrdType : "None"}
</p>
</div>
<div>
<span className="text-slate-500 text-xs">Hypervisors</span>
<p className="text-slate-200 font-medium">
{selectedImage.metadata.hypervisors.join(", ")}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Format selector */}
<div>
<h3 className="text-sm font-medium text-slate-300 mb-3">Target Format</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{EXPORT_FORMAT_OPTIONS.map((fmt) => {
const Icon = FORMAT_ICONS[fmt.id];
const isActive = config.format === fmt.id;
return (
<button
key={fmt.id}
onClick={() => updateConfig({ format: fmt.id })}
className={cn(
"flex flex-col items-start rounded-lg border p-4 text-left transition-all",
isActive
? "border-blue-500/60 bg-blue-500/10 ring-1 ring-blue-500/30"
: "border-slate-700/50 bg-slate-800/30 hover:border-slate-600 hover:bg-slate-800/50"
)}
>
<div className="flex items-center gap-2 mb-2">
<Icon className={cn("h-5 w-5", isActive ? FORMAT_COLORS[fmt.id] : "text-slate-500")} />
<span className={cn("text-sm font-medium", isActive ? "text-slate-100" : "text-slate-300")}>
{fmt.label}
</span>
</div>
<p className="text-xs text-slate-500 leading-relaxed mb-2">{fmt.description}</p>
<div className="flex flex-wrap gap-1">
{fmt.prerequisites.map((pkg) => (
<Badge key={pkg} variant="outline" className="text-[9px] px-1.5 py-0 border-slate-700 text-slate-500">
{pkg}
</Badge>
))}
</div>
</button>
);
})}
</div>
</div>
{/* Configuration section */}
<Card className="bg-slate-800/40 border-slate-700/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-slate-300">
Export Configuration
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Output path */}
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-xs text-slate-400 text-right">Output Path</Label>
<div className="col-span-3">
<Input
placeholder={
DEFAULT_OUTPUT_PATHS[config.format](
selectedImage.metadata.name.replace(/[^a-zA-Z0-9-]/g, "-")
)
}
value={config.outputPath}
onChange={(e) => updateConfig({ outputPath: e.target.value })}
className="h-8 text-sm bg-slate-900/50 border-slate-700"
/>
</div>
</div>
{/* Format-specific options */}
{config.format === "qcow2" && (
<>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-xs text-slate-400 text-right">Disk Size</Label>
<div className="col-span-3">
<Input
placeholder="Auto (1.3x rootfs + 512MB)"
value={config.diskSize}
onChange={(e) => updateConfig({ diskSize: e.target.value })}
className="h-8 text-sm bg-slate-900/50 border-slate-700"
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-xs text-slate-400 text-right">EFI Boot</Label>
<div className="col-span-3 flex items-center gap-2">
<Switch
checked={config.efi}
onCheckedChange={(checked) => updateConfig({ efi: checked })}
/>
<span className="text-xs text-slate-500">
{config.efi ? "GPT + ESP partition layout" : "MBR partition layout"}
</span>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-xs text-slate-400 text-right">Boot Label</Label>
<div className="col-span-3">
<Input
value={config.bootLabel}
onChange={(e) => updateConfig({ bootLabel: e.target.value })}
className="h-8 text-sm bg-slate-900/50 border-slate-700 w-32"
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-xs text-slate-400 text-right">Root Label</Label>
<div className="col-span-3">
<Input
value={config.rootLabel}
onChange={(e) => updateConfig({ rootLabel: e.target.value })}
className="h-8 text-sm bg-slate-900/50 border-slate-700 w-32"
/>
</div>
</div>
</>
)}
{config.format === "iso" && (
<>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-xs text-slate-400 text-right">EFI Boot</Label>
<div className="col-span-3 flex items-center gap-2">
<Switch
checked={config.efi}
onCheckedChange={(checked) => updateConfig({ efi: checked })}
/>
<span className="text-xs text-slate-500">
{config.efi ? "GRUB EFI + isolinux" : "isolinux only (hybrid MBR)"}
</span>
</div>
</div>
</>
)}
{config.format === "pxe" && (
<>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-xs text-slate-400 text-right">Server IP</Label>
<div className="col-span-3">
<Input
value={config.serverIP}
onChange={(e) => updateConfig({ serverIP: e.target.value })}
className="h-8 text-sm bg-slate-900/50 border-slate-700 w-48"
/>
</div>
</div>
</>
)}
{/* Common: append cmdline */}
<div className="grid grid-cols-4 items-start gap-4">
<Label className="text-xs text-slate-400 text-right pt-1">Append Cmdline</Label>
<div className="col-span-3">
<Textarea
placeholder="Extra kernel command-line parameters (e.g. console=ttyS0,115200)"
value={config.appendCmdline}
onChange={(e) => updateConfig({ appendCmdline: e.target.value })}
className="text-xs bg-slate-900/50 border-slate-700 min-h-[60px]"
rows={2}
/>
</div>
</div>
</CardContent>
</Card>
{/* Export button */}
<div className="flex items-center gap-3">
<Button
onClick={handleExport}
disabled={exporting}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{exporting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Exporting...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Export as {config.format.toUpperCase()}
</>
)}
</Button>
{result && !result.success && (
<Button
variant="outline"
onClick={() => setResult(null)}
className="text-slate-400 border-slate-700"
>
Dismiss
</Button>
)}
</div>
{/* Result display */}
{result && (
<Card className={cn(
"border",
result.success ? "border-emerald-700/50 bg-emerald-900/10" : "border-red-700/50 bg-red-900/10"
)}>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
{result.success ? (
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
) : (
<XCircle className="h-5 w-5 text-red-400" />
)}
<CardTitle className="text-sm font-medium">
{result.success ? "Export Complete" : "Export Failed"}
</CardTitle>
{result.success && (
<Badge variant="outline" className="border-emerald-700 text-emerald-400 text-[10px] ml-2">
{result.duration}s
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-3">
{result.success && (
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-slate-500 text-xs">Output</span>
<p className="text-slate-200 font-mono text-xs">{result.outputPath}</p>
</div>
<div>
<span className="text-slate-500 text-xs">Size</span>
<p className="text-slate-200">{result.sizeMB}MB</p>
</div>
<div>
<span className="text-slate-500 text-xs">Format</span>
<p className="text-slate-200 uppercase">{result.format}</p>
</div>
</div>
)}
{result.messages.length > 0 && (
<div className="space-y-1">
{result.messages.map((msg, i) => (
<div key={i} className="flex items-start gap-2 text-xs">
<ChevronRight className="h-3 w-3 text-slate-600 mt-0.5 shrink-0" />
<span className="text-slate-400">{msg}</span>
</div>
))}
</div>
)}
{result.errors.length > 0 && (
<div className="space-y-1">
{result.errors.map((err, i) => (
<div key={i} className="flex items-start gap-2 text-xs">
<XCircle className="h-3 w-3 text-red-500 mt-0.5 shrink-0" />
<span className="text-red-400">{err}</span>
</div>
))}
</div>
)}
{/* Usage hints — format-specific */}
{result.success && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<p className="text-xs text-slate-500 mb-1">Next step:</p>
<code className="text-xs text-slate-300 bg-slate-900/50 px-2 py-1 rounded block">
{USAGE_HINTS[result.format](result.outputPath)}
</code>
</div>
)}
</CardContent>
</Card>
)}
</>
) : (
<div className="flex flex-col items-center justify-center h-full text-slate-500">
<Package className="h-12 w-12 mb-3 text-slate-700" />
<p className="text-sm">No QCrows images imported</p>
<p className="text-xs mt-1">Import a QCrows image first, then export to a bootable format</p>
</div>
)}
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,200 @@
"use client";
import React, { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import {
Boxes,
Upload,
BookOpen,
Settings,
Activity,
Server,
Cpu,
Download,
ChevronLeft,
Menu,
} from "lucide-react";
import { SandboxList } from "./SandboxList";
import { SandboxDetail } from "./SandboxDetail";
import { ImportImage } from "./ImportImage";
import { BuildGuide } from "./BuildGuide";
import { Configuration } from "./Configuration";
import { MetricsDashboard } from "./MetricsDashboard";
import { KernelDetail } from "./KernelDetail";
import { ExportImage } from "./ExportImage";
import type { Sandbox } from "@/lib/kata-mock";
type View = "sandboxes" | "import" | "kernel" | "export" | "guide" | "configuration" | "metrics";
const NAV_ITEMS: { id: View; label: string; icon: React.ElementType }[] = [
{ id: "sandboxes", label: "Sandboxes", icon: Boxes },
{ id: "import", label: "Import Image", icon: Upload },
{ id: "kernel", label: "Kernel", icon: Cpu },
{ id: "export", label: "Export", icon: Download },
{ id: "guide", label: "Build Guide", icon: BookOpen },
{ id: "configuration", label: "Configuration", icon: Settings },
{ id: "metrics", label: "Metrics", icon: Activity },
];
export function KataModule() {
const [activeView, setActiveView] = useState<View>("sandboxes");
const [selectedSandbox, setSelectedSandbox] = useState<Sandbox | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const handleSelectSandbox = (sandbox: Sandbox) => {
setSelectedSandbox(sandbox);
setActiveView("sandboxes");
};
const handleBackToList = () => {
setSelectedSandbox(null);
};
const handleNavigate = (view: View) => {
setActiveView(view);
if (view !== "sandboxes") {
setSelectedSandbox(null);
}
};
return (
<div className="flex h-screen bg-[#0f172a] text-slate-100 overflow-hidden">
{/* Sidebar */}
<aside
className={cn(
"flex flex-col border-r border-slate-700/50 bg-[#0c1222] transition-all duration-200",
sidebarCollapsed ? "w-16" : "w-56"
)}
>
{/* Header */}
<div className="flex items-center gap-2 px-3 h-14 border-b border-slate-700/50">
<Server className="h-5 w-5 text-emerald-400 shrink-0" />
{!sidebarCollapsed && (
<span className="font-semibold text-sm tracking-tight truncate">
Kata Containers
</span>
)}
</div>
{/* Nav Items */}
<ScrollArea className="flex-1 py-2">
<nav className="space-y-0.5 px-2">
{NAV_ITEMS.map((item) => {
const Icon = item.icon;
const isActive =
item.id === activeView &&
(item.id !== "sandboxes" || !selectedSandbox);
return (
<button
key={item.id}
onClick={() => handleNavigate(item.id)}
className={cn(
"flex items-center gap-2.5 w-full rounded-md px-2.5 py-2 text-sm font-medium transition-colors",
isActive
? "bg-slate-700/60 text-white"
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800/60"
)}
>
<Icon className="h-4 w-4 shrink-0" />
{!sidebarCollapsed && <span className="truncate">{item.label}</span>}
</button>
);
})}
</nav>
</ScrollArea>
{/* Collapse toggle */}
<div className="border-t border-slate-700/50 p-2">
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="w-full justify-center text-slate-400 hover:text-slate-200"
>
{sidebarCollapsed ? (
<Menu className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col min-w-0">
{/* Top bar */}
<header className="flex items-center justify-between h-14 px-6 border-b border-slate-700/50 bg-[#0f172a]">
<div className="flex items-center gap-3">
{selectedSandbox && activeView === "sandboxes" ? (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleBackToList}
className="text-slate-400 hover:text-white h-7 px-2"
>
Sandboxes
</Button>
<span className="text-slate-600">/</span>
<span className="text-sm font-medium text-slate-200">
{selectedSandbox.name}
</span>
<Badge
variant="outline"
className={cn(
"text-[10px] px-1.5 py-0",
selectedSandbox.status === "running"
? "border-emerald-600 text-emerald-400"
: selectedSandbox.status === "error"
? "border-red-600 text-red-400"
: "border-slate-600 text-slate-400"
)}
>
{selectedSandbox.status}
</Badge>
</div>
) : (
<h1 className="text-sm font-semibold text-slate-200">
{NAV_ITEMS.find((n) => n.id === activeView)?.label}
</h1>
)}
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-emerald-700 text-emerald-400 text-[10px]">
kata-runtime 2.5
</Badge>
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[10px]">
node: worker-01
</Badge>
</div>
</header>
{/* Content area */}
<div className="flex-1 overflow-auto">
{activeView === "sandboxes" && (
<>
{selectedSandbox ? (
<SandboxDetail
sandbox={selectedSandbox}
onBack={handleBackToList}
/>
) : (
<SandboxList onSelectSandbox={handleSelectSandbox} />
)}
</>
)}
{activeView === "import" && <ImportImage />}
{activeView === "kernel" && <KernelDetail />}
{activeView === "export" && <ExportImage />}
{activeView === "guide" && <BuildGuide />}
{activeView === "configuration" && <Configuration />}
{activeView === "metrics" && <MetricsDashboard />}
</div>
</main>
</div>
);
}

View File

@ -0,0 +1,957 @@
"use client";
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Cpu,
CheckCircle2,
XCircle,
AlertTriangle,
FileText,
Terminal,
Info,
Settings,
Download,
ChevronDown,
ChevronRight,
ExternalLink,
RefreshCw,
} from "lucide-react";
import {
fetchQCrowsImages,
KATA_REQUIRED_KERNEL_OPTIONS,
KATA_RECOMMENDED_KERNEL_OPTIONS,
HYPERVISOR_KERNEL_FORMAT,
VMM_MODULE_MATRIX,
COMPRESS_PRIORITY,
INITRD_REGEN_DEFAULTS,
type QCrowsImage,
type QCrowsKernelConfig,
type QCrowsBootParams,
type InitrdRegenConfig,
type InitrdRegenResult,
} from "@/lib/kata-mock";
import { regenInitrd } from "@/lib/backend/adapter";
export function KernelDetail() {
const [images, setImages] = useState<QCrowsImage[]>([]);
const [loading, setLoading] = useState(true);
const [selectedImage, setSelectedImage] = useState<QCrowsImage | null>(null);
useEffect(() => {
fetchQCrowsImages().then((data) => {
setImages(data);
if (data.length > 0) setSelectedImage(data[0]);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-pulse text-slate-400">Loading kernel images...</div>
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Cpu className="h-5 w-5 text-emerald-400" />
Kernel Management
</h2>
<p className="text-sm text-slate-400 mt-1">
Inspect guest kernels bundled in QCrows images config options, boot parameters, and hypervisor compatibility
</p>
</div>
<Badge variant="outline" className="border-emerald-700 text-emerald-400 text-[10px]">
QCrows v0.2 Kernel REQUIRED
</Badge>
</div>
<div className="grid grid-cols-12 gap-6">
{/* Image selector sidebar */}
<div className="col-span-3">
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-xs font-medium text-slate-400 uppercase tracking-wider">
QCrows Images
</CardTitle>
</CardHeader>
<CardContent className="px-2 pb-3">
<div className="space-y-1">
{images.map((img) => (
<button
key={img.id}
onClick={() => setSelectedImage(img)}
className={cn(
"w-full text-left rounded-md px-3 py-2.5 text-sm transition-colors",
selectedImage?.id === img.id
? "bg-slate-700/60 text-white"
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800/60"
)}
>
<div className="font-medium truncate">{img.menu.label}</div>
<div className="text-[11px] text-slate-500 mt-0.5 flex items-center gap-2">
<span>{img.metadata.kernelVersion}</span>
<span className="text-slate-600">|</span>
<span>{img.metadata.kernelFormat}</span>
<span className="text-slate-600">|</span>
<span>{img.metadata.kernelSizeMB}MB</span>
</div>
</button>
))}
</div>
</CardContent>
</Card>
</div>
{/* Main kernel detail area */}
<div className="col-span-9 space-y-5">
{selectedImage && (
<KernelDetailView image={selectedImage} />
)}
</div>
</div>
</div>
);
}
function KernelDetailView({ image }: { image: QCrowsImage }) {
const meta = image.metadata;
const kConfig = image.kernelConfig;
const bootParams = image.bootParams;
const [regenOpen, setRegenOpen] = useState(false);
const [regenConfig, setRegenConfig] = useState<Partial<InitrdRegenConfig>>(INITRD_REGEN_DEFAULTS);
const [regening, setRegening] = useState(false);
const [regenResult, setRegenResult] = useState<InitrdRegenResult | null>(null);
const executeRegen = async () => {
setRegening(true);
setRegenResult(null);
try {
const result = await regenInitrd(image, regenConfig);
setRegenResult(result);
} catch {
setRegenResult({
success: false, output: "", sizeMB: 0, vmm: regenConfig.vmm || "qemu",
format: regenConfig.format || "cpio-gzip", initStyle: regenConfig.initStyle || "busybox",
kernelVersion: meta.kernelVersion, moduleCount: 0,
messages: [], errors: ["Initrd regeneration failed unexpectedly"],
});
} finally {
setRegening(false);
}
};
return (
<>
{/* Kernel Overview Cards */}
<div className="grid grid-cols-4 gap-4">
<KernelStatCard
label="Kernel Version"
value={meta.kernelVersion}
icon={<Cpu className="h-4 w-4" />}
color="emerald"
/>
<KernelStatCard
label="Format"
value={meta.kernelFormat === "vmlinuz" ? "vmlinuz (bzImage)" : "vmlinux (ELF)"}
icon={<FileText className="h-4 w-4" />}
color="blue"
/>
<KernelStatCard
label="Size"
value={`${meta.kernelSizeMB} MB`}
icon={<Download className="h-4 w-4" />}
color="amber"
/>
<KernelStatCard
label="Config Valid"
value={meta.kernelConfigValid ? "Pass" : "Fail"}
icon={meta.kernelConfigValid ? <CheckCircle2 className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
color={meta.kernelConfigValid ? "emerald" : "red"}
/>
</div>
{/* Hypervisor Compatibility */}
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
<Settings className="h-4 w-4 text-blue-400" />
Hypervisor Kernel Format Compatibility
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<div className="grid grid-cols-2 gap-3">
{meta.hypervisors.map((hyp) => {
const requirement = HYPERVISOR_KERNEL_FORMAT[hyp];
const matches = requirement && requirement.format === meta.kernelFormat;
const archMatch = requirement && requirement.arches.includes(meta.arch);
return (
<div
key={hyp}
className={cn(
"flex items-center justify-between rounded-md border px-3 py-2",
matches && archMatch
? "border-emerald-700/50 bg-emerald-900/10"
: "border-amber-700/50 bg-amber-900/10"
)}
>
<div>
<div className="text-sm font-medium text-slate-200">{hyp}</div>
<div className="text-[11px] text-slate-400">
Requires: {requirement?.format || "vmlinux"} ({requirement?.arches.join(", ") || "any"})
</div>
</div>
<div className="flex items-center gap-1.5">
{matches && archMatch ? (
<>
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
<span className="text-[11px] text-emerald-400 font-medium">Compatible</span>
</>
) : (
<>
<AlertTriangle className="h-4 w-4 text-amber-400" />
<span className="text-[11px] text-amber-400 font-medium">Format mismatch</span>
</>
)}
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
{/* Initrd Regen Action */}
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-sm font-medium text-slate-200 flex items-center justify-between">
<div className="flex items-center gap-2">
<RefreshCw className="h-4 w-4 text-violet-400" />
Initrd Regeneration
</div>
<Button
size="sm"
onClick={() => { setRegenOpen(true); setRegenResult(null); }}
className="bg-violet-700 hover:bg-violet-600 text-white text-xs h-7"
>
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
Regen Initrd
</Button>
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<div className="text-xs text-slate-400 space-y-1.5">
<p>
Rebuild the initrd component with environment-aware module selection. Unlike <code className="text-slate-300 bg-slate-800 px-1 rounded">dracut</code> or{" "}
<code className="text-slate-300 bg-slate-800 px-1 rounded">mkinitramfs</code>, this produces an initrd where kata-agent IS the final process
(no pivot_root) and includes ONLY the modules required by the detected VMM.
</p>
<div className="flex items-center gap-4 mt-2 text-slate-500">
<span>Auto-detects: VMM, compression, init style, kata-agent</span>
<span>|</span>
<span>Module matrix: {Object.keys(VMM_MODULE_MATRIX).length} VMMs</span>
</div>
</div>
{/* Regen Dialog */}
{regenOpen && !regenResult && (
<div className="mt-4 rounded-lg border border-slate-700/50 bg-[#0a0f1a] p-4 space-y-4">
<div className="text-sm font-medium text-slate-200">Configure Initrd Regeneration</div>
<div className="grid grid-cols-2 gap-4">
{/* VMM Selection */}
<div className="space-y-1.5">
<label className="text-xs text-slate-400">Target VMM</label>
<select
value={regenConfig.vmm || ""}
onChange={(e) => setRegenConfig((c) => ({ ...c, vmm: e.target.value as InitrdRegenConfig["vmm"] || undefined }))}
className="w-full rounded-md border border-slate-700 bg-[#111827] text-sm text-slate-200 px-3 py-1.5"
>
<option value="">Auto-detect</option>
<option value="qemu">QEMU (virtio-pci)</option>
<option value="cloud-hypervisor">Cloud Hypervisor (virtio-mmio)</option>
<option value="firecracker">Firecracker (virtio-mmio minimal)</option>
<option value="dragonball">Dragonball (vsock-direct)</option>
</select>
</div>
{/* Compression */}
<div className="space-y-1.5">
<label className="text-xs text-slate-400">Compression</label>
<select
value={regenConfig.format || ""}
onChange={(e) => setRegenConfig((c) => ({ ...c, format: e.target.value as InitrdRegenConfig["format"] || undefined }))}
className="w-full rounded-md border border-slate-700 bg-[#111827] text-sm text-slate-200 px-3 py-1.5"
>
<option value="">Auto-detect from kernel config</option>
{COMPRESS_PRIORITY.map((c) => (
<option key={c.algo} value={c.algo}>
{c.algo.replace("cpio-", "").toUpperCase()} ({c.bootSpeed})
</option>
))}
<option value="cramfs">CRAMFS (read-only, minimal)</option>
</select>
</div>
{/* Init Style */}
<div className="space-y-1.5">
<label className="text-xs text-slate-400">Init Style</label>
<select
value={regenConfig.initStyle || ""}
onChange={(e) => setRegenConfig((c) => ({ ...c, initStyle: e.target.value as InitrdRegenConfig["initStyle"] || undefined }))}
className="w-full rounded-md border border-slate-700 bg-[#111827] text-sm text-slate-200 px-3 py-1.5"
>
<option value="">Auto-detect from rootfs</option>
<option value="busybox">Busybox (kata-agent as PID 1)</option>
<option value="systemd">systemd (via kata-containers.target)</option>
<option value="openrc">OpenRC (via init script)</option>
</select>
</div>
{/* In-place toggle */}
<div className="space-y-1.5">
<label className="text-xs text-slate-400">Output Mode</label>
<div className="flex items-center gap-3 mt-1">
<button
onClick={() => setRegenConfig((c) => ({ ...c, inPlace: true }))}
className={cn(
"px-3 py-1.5 rounded-md text-xs font-medium border transition-colors",
regenConfig.inPlace
? "border-violet-600 bg-violet-900/20 text-violet-300"
: "border-slate-700 bg-[#111827] text-slate-400 hover:border-slate-600"
)}
>
In-place (update bundle)
</button>
<button
onClick={() => setRegenConfig((c) => ({ ...c, inPlace: false }))}
className={cn(
"px-3 py-1.5 rounded-md text-xs font-medium border transition-colors",
!regenConfig.inPlace
? "border-violet-600 bg-violet-900/20 text-violet-300"
: "border-slate-700 bg-[#111827] text-slate-400 hover:border-slate-600"
)}
>
Separate file
</button>
</div>
</div>
</div>
{/* Module preview for selected VMM */}
{regenConfig.vmm && VMM_MODULE_MATRIX[regenConfig.vmm] && (
<div className="text-xs text-slate-400 bg-[#111827] rounded-md p-3">
<div className="font-medium text-slate-300 mb-1.5">
Modules for {regenConfig.vmm}:
</div>
<div className="flex flex-wrap gap-1.5">
{VMM_MODULE_MATRIX[regenConfig.vmm].required.map((m) => (
<Badge key={m} variant="outline" className="text-[9px] border-emerald-700 text-emerald-400 px-1.5 py-0">
{m}
</Badge>
))}
{VMM_MODULE_MATRIX[regenConfig.vmm].optional.map((m) => (
<Badge key={m} variant="outline" className="text-[9px] border-slate-600 text-slate-500 px-1.5 py-0">
{m}?
</Badge>
))}
</div>
</div>
)}
<div className="flex items-center gap-3 pt-2">
<Button
size="sm"
onClick={executeRegen}
disabled={regening}
className="bg-violet-700 hover:bg-violet-600 text-white text-xs h-8"
>
{regening ? (
<>
<RefreshCw className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Regenerating...
</>
) : (
<>
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
Regenerate Initrd
</>
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setRegenOpen(false)}
className="text-xs h-8 text-slate-400"
>
Cancel
</Button>
</div>
</div>
)}
{/* Regen Result */}
{regenResult && (
<div className={cn(
"mt-4 rounded-lg border p-4 space-y-3",
regenResult.success
? "border-emerald-700/50 bg-emerald-900/10"
: "border-red-700/50 bg-red-900/10"
)}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{regenResult.success ? (
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
) : (
<XCircle className="h-5 w-5 text-red-400" />
)}
<span className={cn("text-sm font-medium", regenResult.success ? "text-emerald-300" : "text-red-300")}>
{regenResult.success ? "Initrd regenerated successfully" : "Initrd regeneration failed"}
</span>
</div>
<Button size="sm" variant="ghost" onClick={() => { setRegenResult(null); setRegenOpen(false); }} className="text-xs text-slate-400 h-6">
Close
</Button>
</div>
{regenResult.success && (
<div className="grid grid-cols-3 gap-3">
<div className="bg-[#0a0f1a] rounded-md p-2.5">
<div className="text-[10px] text-slate-500 uppercase">Output</div>
<div className="text-sm text-slate-200 font-mono mt-0.5">{regenResult.output}</div>
</div>
<div className="bg-[#0a0f1a] rounded-md p-2.5">
<div className="text-[10px] text-slate-500 uppercase">Size</div>
<div className="text-sm text-slate-200 mt-0.5">{regenResult.sizeMB} MB</div>
</div>
<div className="bg-[#0a0f1a] rounded-md p-2.5">
<div className="text-[10px] text-slate-500 uppercase">Modules</div>
<div className="text-sm text-slate-200 mt-0.5">{regenResult.moduleCount} included</div>
</div>
</div>
)}
{regenResult.messages.length > 0 && (
<div className="bg-[#0a0f1a] rounded-md p-3 max-h-40 overflow-y-auto">
<div className="text-[10px] text-slate-500 uppercase mb-1.5">Pipeline Log</div>
{regenResult.messages.map((msg, i) => (
<div key={i} className="text-xs text-slate-400 font-mono py-0.5">{msg}</div>
))}
</div>
)}
{regenResult.errors.length > 0 && (
<div className="bg-red-950/30 rounded-md p-3">
<div className="text-[10px] text-red-400 uppercase mb-1.5">Errors</div>
{regenResult.errors.map((err, i) => (
<div key={i} className="text-xs text-red-300 font-mono py-0.5">{err}</div>
))}
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Tabs: Config Options / Boot Params / Build Info */}
<Tabs defaultValue="config" className="space-y-4">
<TabsList className="bg-[#111827] border border-slate-700/50">
<TabsTrigger value="config" className="data-[state=active]:bg-slate-700/60">
Kernel Config
</TabsTrigger>
<TabsTrigger value="boot-params" className="data-[state=active]:bg-slate-700/60">
Boot Parameters
</TabsTrigger>
<TabsTrigger value="build" className="data-[state=active]:bg-slate-700/60">
Build Info
</TabsTrigger>
</TabsList>
<TabsContent value="config">
<KernelConfigPanel config={kConfig} kernelVersion={meta.kernelVersion} />
</TabsContent>
<TabsContent value="boot-params">
<BootParamsPanel bootParams={bootParams} kernelVersion={meta.kernelVersion} />
</TabsContent>
<TabsContent value="build">
<BuildInfoPanel image={image} />
</TabsContent>
</Tabs>
</>
);
}
function KernelStatCard({
label,
value,
icon,
color,
}: {
label: string;
value: string;
icon: React.ReactNode;
color: "emerald" | "blue" | "amber" | "red";
}) {
const colorMap = {
emerald: "text-emerald-400",
blue: "text-blue-400",
amber: "text-amber-400",
red: "text-red-400",
};
return (
<Card className="bg-[#111827] border-slate-700/50">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<span className={colorMap[color]}>{icon}</span>
<span className="text-[11px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
</div>
<div className="text-lg font-semibold text-slate-100">{value}</div>
</CardContent>
</Card>
);
}
function KernelConfigPanel({
config,
kernelVersion,
}: {
config?: QCrowsKernelConfig;
kernelVersion: string;
}) {
const [showRecommended, setShowRecommended] = useState(false);
if (!config) {
return (
<Card className="bg-[#111827] border-slate-700/50">
<CardContent className="p-6 text-center text-slate-400">
No kernel config data available for this image.
</CardContent>
</Card>
);
}
const requiredPass = Object.values(config.requiredOptions).filter(Boolean).length;
const requiredTotal = Object.keys(config.requiredOptions).length;
const optionalPass = Object.values(config.optionalOptions).filter(Boolean).length;
const optionalTotal = Object.keys(config.optionalOptions).length;
const allRequiredPass = requiredPass === requiredTotal;
return (
<div className="space-y-4">
{/* Summary cards */}
<div className="grid grid-cols-3 gap-4">
<Card className="bg-[#111827] border-slate-700/50">
<CardContent className="p-4">
<div className="text-[11px] text-slate-400 uppercase tracking-wider mb-2">Required Options</div>
<div className="flex items-center gap-2">
<span className={cn("text-xl font-bold", allRequiredPass ? "text-emerald-400" : "text-red-400")}>
{requiredPass}/{requiredTotal}
</span>
{allRequiredPass ? (
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
) : (
<XCircle className="h-5 w-5 text-red-400" />
)}
</div>
<Progress
value={(requiredPass / requiredTotal) * 100}
className={cn("mt-2 h-1.5", allRequiredPass ? "[&>div]:bg-emerald-500" : "[&>div]:bg-red-500")}
/>
</CardContent>
</Card>
<Card className="bg-[#111827] border-slate-700/50">
<CardContent className="p-4">
<div className="text-[11px] text-slate-400 uppercase tracking-wider mb-2">Recommended Options</div>
<div className="flex items-center gap-2">
<span className="text-xl font-bold text-blue-400">{optionalPass}/{optionalTotal}</span>
</div>
<Progress
value={(optionalPass / optionalTotal) * 100}
className="mt-2 h-1.5 [&>div]:bg-blue-500"
/>
</CardContent>
</Card>
<Card className="bg-[#111827] border-slate-700/50">
<CardContent className="p-4">
<div className="text-[11px] text-slate-400 uppercase tracking-wider mb-2">Config Statistics</div>
<div className="text-sm space-y-1">
<div className="flex justify-between text-slate-300">
<span>Total options</span>
<span className="font-mono">{config.totalOptions.toLocaleString()}</span>
</div>
<div className="flex justify-between text-slate-300">
<span>Built-in</span>
<span className="font-mono text-emerald-400">{config.builtInCount.toLocaleString()}</span>
</div>
<div className="flex justify-between text-slate-300">
<span>Modules</span>
<span className="font-mono text-blue-400">{config.moduleCount.toLocaleString()}</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Required options detail */}
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-amber-400" />
Required Kata Kernel Options
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
{Object.entries(config.requiredOptions).map(([opt, enabled]) => {
const desc = KATA_REQUIRED_KERNEL_OPTIONS[opt] || "";
return (
<div
key={opt}
className={cn(
"flex items-center justify-between rounded-md border px-3 py-2",
enabled
? "border-emerald-700/30 bg-emerald-900/5"
: "border-red-700/30 bg-red-900/5"
)}
>
<div className="flex items-center gap-3">
{enabled ? (
<CheckCircle2 className="h-4 w-4 text-emerald-400 shrink-0" />
) : (
<XCircle className="h-4 w-4 text-red-400 shrink-0" />
)}
<code className="text-sm font-mono text-slate-200">{opt}</code>
</div>
<div className="flex items-center gap-3">
<span className="text-[11px] text-slate-400">{desc}</span>
<Badge
variant="outline"
className={cn(
"text-[9px] px-1.5 py-0",
enabled ? "border-emerald-600 text-emerald-400" : "border-red-600 text-red-400"
)}
>
{enabled ? "y" : "MISSING"}
</Badge>
</div>
</div>
);
})}
</CardContent>
</Card>
{/* Recommended options (collapsible) */}
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader
className="py-3 px-4 cursor-pointer"
onClick={() => setShowRecommended(!showRecommended)}
>
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
<Info className="h-4 w-4 text-blue-400" />
Recommended Kata Kernel Options
<Badge variant="outline" className="border-blue-700 text-blue-400 text-[9px] px-1.5 py-0 ml-auto mr-2">
{optionalPass}/{optionalTotal}
</Badge>
{showRecommended ? (
<ChevronDown className="h-4 w-4 text-slate-400" />
) : (
<ChevronRight className="h-4 w-4 text-slate-400" />
)}
</CardTitle>
</CardHeader>
{showRecommended && (
<CardContent className="px-4 pb-4 space-y-2">
{Object.entries(config.optionalOptions).map(([opt, enabled]) => {
const desc = KATA_RECOMMENDED_KERNEL_OPTIONS[opt] || "";
return (
<div
key={opt}
className={cn(
"flex items-center justify-between rounded-md border px-3 py-2",
enabled
? "border-blue-700/20 bg-blue-900/5"
: "border-slate-700/30 bg-slate-900/20"
)}
>
<div className="flex items-center gap-3">
{enabled ? (
<CheckCircle2 className="h-4 w-4 text-blue-400 shrink-0" />
) : (
<XCircle className="h-4 w-4 text-slate-500 shrink-0" />
)}
<code className="text-sm font-mono text-slate-200">{opt}</code>
</div>
<div className="flex items-center gap-3">
<span className="text-[11px] text-slate-400">{desc}</span>
<Badge
variant="outline"
className={cn(
"text-[9px] px-1.5 py-0",
enabled ? "border-blue-600 text-blue-400" : "border-slate-600 text-slate-500"
)}
>
{enabled ? "y" : "n/m"}
</Badge>
</div>
</div>
);
})}
</CardContent>
)}
</Card>
</div>
);
}
function BootParamsPanel({
bootParams,
kernelVersion,
}: {
bootParams?: QCrowsBootParams;
kernelVersion: string;
}) {
if (!bootParams) {
return (
<Card className="bg-[#111827] border-slate-700/50">
<CardContent className="p-6 text-center text-slate-400">
No boot parameters data available for this image.
</CardContent>
</Card>
);
}
const dangerousParams = [
"module.sig_enforce=0",
"nokaslr",
"nopti",
"nosmap",
"nosmep",
];
const warnings = bootParams.params.filter((p) =>
dangerousParams.some((d) => p.includes(d.replace("=", "").replace("=0", "")))
);
return (
<div className="space-y-4">
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
<Terminal className="h-4 w-4 text-emerald-400" />
Kernel Command Line
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<div className="rounded-md bg-[#0a0f1a] border border-slate-700/50 p-4 font-mono text-sm text-slate-200 break-all">
<span className="text-slate-500">BOOT_IMAGE=/kernel/vmlinuz</span>{" "}
{bootParams.params.map((param, i) => (
<span key={i}>
{i > 0 && " "}
<span
className={cn(
dangerousParams.some((d) => param.includes(d.split("=")[0]))
? "text-amber-400 underline decoration-amber-600 decoration-wavy"
: "text-emerald-300"
)}
>
{param}
</span>
</span>
))}
</div>
<div className="mt-3 flex items-center gap-2">
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[10px]">
Source: {bootParams.source}
</Badge>
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[10px]">
{bootParams.params.length} parameters
</Badge>
</div>
</CardContent>
</Card>
{/* Parameter breakdown */}
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-sm font-medium text-slate-200">Parameter Breakdown</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
{bootParams.params.map((param, i) => {
const isDangerous = dangerousParams.some((d) => param.includes(d.split("=")[0]));
const [key, ...valParts] = param.split("=");
const val = valParts.join("=");
return (
<div
key={i}
className={cn(
"flex items-center gap-3 rounded-md border px-3 py-2",
isDangerous
? "border-amber-700/30 bg-amber-900/5"
: "border-slate-700/30"
)}
>
{isDangerous ? (
<AlertTriangle className="h-4 w-4 text-amber-400 shrink-0" />
) : (
<CheckCircle2 className="h-4 w-4 text-emerald-400 shrink-0" />
)}
<code className="text-sm font-mono text-slate-200">{key}</code>
{val && (
<>
<span className="text-slate-500">=</span>
<code className="text-sm font-mono text-blue-300">{val}</code>
</>
)}
{isDangerous && (
<Badge variant="outline" className="border-amber-600 text-amber-400 text-[9px] px-1.5 py-0 ml-auto">
Security concern
</Badge>
)}
</div>
);
})}
</CardContent>
</Card>
{warnings.length > 0 && (
<Card className="bg-[#111827] border-amber-700/30">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-amber-400 shrink-0 mt-0.5" />
<div>
<div className="text-sm font-medium text-amber-300">Security Warnings</div>
<div className="text-sm text-slate-400 mt-1">
The following parameters may reduce security isolation in the guest VM:
</div>
<ul className="mt-2 space-y-1">
{warnings.map((w, i) => (
<li key={i} className="text-sm text-amber-300 font-mono">{w}</li>
))}
</ul>
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
}
function BuildInfoPanel({ image }: { image: QCrowsImage }) {
const build = image.build;
const meta = image.metadata;
if (!build) {
return (
<Card className="bg-[#111827] border-slate-700/50">
<CardContent className="p-6 text-center text-slate-400">
No build information available for this image.
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
<Info className="h-4 w-4 text-blue-400" />
Kernel Build Provenance
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<div className="grid grid-cols-2 gap-x-8 gap-y-3">
<BuildInfoRow label="Build System" value={build.system} />
<BuildInfoRow label="Build ID" value={build.buildId || "N/A"} mono />
<BuildInfoRow label="Host Arch" value={build.hostArch} />
<BuildInfoRow label="Target Arch" value={build.targetArch} />
<BuildInfoRow label="Kernel Compiler" value={build.kernelCompiler || "N/A"} />
<BuildInfoRow label="Kernel Defconfig" value={build.kernelDefconfig || "N/A"} mono />
<BuildInfoRow label="Reproducible" value={build.reproducible ? "Yes" : "No"} />
<BuildInfoRow label="Built At" value={build.timestamp} />
</div>
</CardContent>
</Card>
{/* Kernel file paths after import */}
<Card className="bg-[#111827] border-slate-700/50">
<CardHeader className="py-3 px-4">
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
<ExternalLink className="h-4 w-4 text-slate-400" />
Import Paths (after registration)
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
<PathRow
label="Kernel binary"
path={`/usr/share/kata-containers/vmlinuz-${meta.kernelVersion}.container`}
exists={meta.kernelIncluded}
/>
<PathRow
label="Kernel config"
path={`/usr/share/kata-containers/config-${meta.kernelVersion}.container`}
exists={meta.kernelConfigValid}
/>
{meta.initrdIncluded && (
<PathRow
label="Initrd"
path={`/usr/share/kata-containers/kata-${meta.name}-initrd.img`}
exists={true}
/>
)}
<PathRow
label="Rootfs"
path={`/usr/share/kata-containers/kata-${meta.name}-rootfs${meta.rootfsType === "tar-gzip" ? ".tar.gz" : meta.rootfsType === "tar-xz" ? ".tar.xz" : ".tar.zst"}`}
exists={true}
/>
{meta.bootParamsIncluded && (
<PathRow
label="Boot params"
path="(appended to kernel_params in configuration.toml)"
exists={true}
/>
)}
</CardContent>
</Card>
</div>
);
}
function BuildInfoRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div className="flex items-center justify-between">
<span className="text-sm text-slate-400">{label}</span>
<span className={cn("text-sm text-slate-200", mono && "font-mono")}>{value}</span>
</div>
);
}
function PathRow({ label, path, exists }: { label: string; path: string; exists: boolean }) {
return (
<div className="flex items-center gap-3 text-sm">
{exists ? (
<CheckCircle2 className="h-4 w-4 text-emerald-400 shrink-0" />
) : (
<XCircle className="h-4 w-4 text-red-400 shrink-0" />
)}
<span className="text-slate-400 w-28 shrink-0">{label}</span>
<code className="text-slate-200 font-mono text-xs break-all">{path}</code>
</div>
);
}

View File

@ -0,0 +1,358 @@
"use client";
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import {
Activity,
Cpu,
HardDrive,
Wifi,
Loader2,
AlertTriangle,
Server,
RefreshCw,
Radio,
} from "lucide-react";
import {
fetchSandboxes,
fetchSandboxMetrics,
type Sandbox,
type SandboxMetrics,
} from "@/lib/kata-mock";
export function MetricsDashboard() {
const [sandboxes, setSandboxes] = useState<Sandbox[]>([]);
const [metricsMap, setMetricsMap] = useState<Record<string, SandboxMetrics>>({});
const [loading, setLoading] = useState(true);
const [kataMonitorRunning, setKataMonitorRunning] = useState(true);
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
const loadData = React.useCallback(async () => {
setLoading(true);
const sbxData = await fetchSandboxes();
setSandboxes(sbxData);
const running = sbxData.filter((s) => s.status === "running");
const metricsResults: Record<string, SandboxMetrics> = {};
for (const sbx of running) {
const m = await fetchSandboxMetrics(sbx.id);
if (m) metricsResults[sbx.id] = m;
}
setMetricsMap(metricsResults);
setLastRefresh(new Date());
setLoading(false);
}, []);
useEffect(() => {
let cancelled = false;
const init = async () => {
if (cancelled) return;
await loadData();
};
init();
return () => { cancelled = true; };
}, [loadData]);
const formatBytes = (bytes: number) => {
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
return `${(bytes / 1024).toFixed(1)} KiB`;
};
const runningSandboxes = sandboxes.filter((s) => s.status === "running");
// Aggregate metrics
const totalCpu = runningSandboxes.reduce((sum, s) => {
const m = metricsMap[s.id];
return sum + (m?.cpuUsagePercent || 0);
}, 0);
const totalMemory = runningSandboxes.reduce((sum, s) => {
const m = metricsMap[s.id];
return sum + (m?.memoryUsageMB || 0);
}, 0);
const totalRx = runningSandboxes.reduce((sum, s) => {
const m = metricsMap[s.id];
return sum + (m?.networkRxBytes || 0);
}, 0);
const totalTx = runningSandboxes.reduce((sum, s) => {
const m = metricsMap[s.id];
return sum + (m?.networkTxBytes || 0);
}, 0);
// Simple sparkline component
const Sparkline = ({ data, color }: { data: { value: number }[]; color: string }) => {
if (data.length === 0) return null;
const max = Math.max(...data.map((d) => d.value));
const min = Math.min(...data.map((d) => d.value));
const range = max - min || 1;
const width = 120;
const height = 28;
const points = data
.map((d, i) => {
const x = (i / (data.length - 1)) * width;
const y = height - ((d.value - min) / range) * (height - 4) - 2;
return `${x},${y}`;
})
.join(" ");
return (
<svg width={width} height={height} className="inline-block">
<polyline
points={points}
fill="none"
stroke={color}
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
);
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
</div>
);
}
// Empty state: kata-monitor not running
if (!kataMonitorRunning) {
return (
<div className="p-6 space-y-5">
<div className="flex items-center gap-2 text-sm text-slate-300">
<Activity className="h-4 w-4 text-emerald-400" />
<span className="font-medium">Metrics Dashboard</span>
</div>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-8 flex flex-col items-center justify-center">
<Radio className="h-12 w-12 text-slate-600 mb-4" />
<h3 className="text-lg font-medium text-slate-300 mb-2">
kata-monitor is not running
</h3>
<p className="text-sm text-slate-500 text-center max-w-md mb-6">
The kata-monitor daemon provides Prometheus-format metrics for
running sandboxes. It is opt-in and must be started explicitly
before metrics data is available.
</p>
<div className="bg-[#0a0f1a] rounded-md p-4 font-mono text-xs text-slate-400 border border-slate-700/30 max-w-lg w-full">
<p className="text-slate-500"># Start kata-monitor as a standalone daemon</p>
<p>kata-monitor --listen-address 127.0.0.1:8090</p>
<p className="mt-2 text-slate-500"># Or deploy as a Kubernetes DaemonSet</p>
<p>kubectl apply -f kata-monitor-daemonset.yaml</p>
</div>
<Button
variant="outline"
className="mt-4 border-slate-700 text-slate-400"
onClick={() => setKataMonitorRunning(true)}
>
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
Check Again
</Button>
</CardContent>
</Card>
</div>
);
}
return (
<div className="p-6 space-y-5">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-slate-300">
<Activity className="h-4 w-4 text-emerald-400" />
<span className="font-medium">Metrics Dashboard</span>
</div>
<div className="flex items-center gap-3">
<Badge variant="outline" className="text-[10px] px-1.5 py-0 border-emerald-700/50 text-emerald-400">
<Radio className="h-2.5 w-2.5 mr-1" />
kata-monitor live
</Badge>
<span className="text-[10px] text-slate-600">
Last refresh: {lastRefresh.toLocaleTimeString()}
</span>
<Button
variant="ghost"
size="sm"
onClick={loadData}
className="h-7 w-7 p-0 text-slate-400 hover:text-white"
>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/* Aggregate stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{
label: "Total CPU Usage",
value: `${totalCpu.toFixed(1)}%`,
icon: Cpu,
color: "text-cyan-400",
sparkData: Object.values(metricsMap).map((m) => ({
value: m.cpuUsagePercent,
})),
sparkColor: "#22d3ee",
},
{
label: "Total Memory",
value:
totalMemory >= 1024
? `${(totalMemory / 1024).toFixed(1)} GiB`
: `${totalMemory.toFixed(0)} MiB`,
icon: HardDrive,
color: "text-violet-400",
sparkData: Object.values(metricsMap).map((m) => ({
value: m.memoryUsageMB,
})),
sparkColor: "#a78bfa",
},
{
label: "Network RX",
value: formatBytes(totalRx),
icon: Wifi,
color: "text-emerald-400",
sparkData: [],
sparkColor: "#34d399",
},
{
label: "Network TX",
value: formatBytes(totalTx),
icon: Wifi,
color: "text-blue-400",
sparkData: [],
sparkColor: "#60a5fa",
},
].map((stat) => (
<Card
key={stat.label}
className="bg-[#131c31] border-slate-700/50"
>
<CardContent className="p-4 flex items-center gap-3">
<div
className={cn(
"h-9 w-9 rounded-lg flex items-center justify-center bg-slate-800/80 shrink-0",
stat.color
)}
>
<stat.icon className="h-4.5 w-4.5" />
</div>
<div className="min-w-0">
<p className="text-xs text-slate-500">{stat.label}</p>
<p className="text-lg font-semibold text-slate-100">
{stat.value}
</p>
{stat.sparkData.length > 0 && (
<Sparkline
data={stat.sparkData}
color={stat.sparkColor}
/>
)}
</div>
</CardContent>
</Card>
))}
</div>
{/* Per-sandbox metrics */}
<div className="space-y-3">
<h3 className="text-xs font-medium text-slate-400 uppercase tracking-wider">
Per-Sandbox Metrics
</h3>
{runningSandboxes.length === 0 ? (
<div className="text-center py-8 text-slate-500">
<Server className="h-8 w-8 mx-auto mb-2 opacity-30" />
<p className="text-xs">No running sandboxes</p>
</div>
) : (
runningSandboxes.map((sbx) => {
const m = metricsMap[sbx.id];
if (!m) return null;
return (
<Card
key={sbx.id}
className="bg-[#131c31] border-slate-700/50"
>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Server className="h-3.5 w-3.5 text-amber-400" />
<span className="text-sm font-medium text-slate-200">
{sbx.name}
</span>
<code className="text-[10px] text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded">
{sbx.runtimeClass}
</code>
</div>
{m.vmmApiErrors > 0 && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 border-red-700/50 text-red-400"
>
<AlertTriangle className="h-3 w-3 mr-1" />
{m.vmmApiErrors} API errors
</Badge>
)}
</div>
<div className="grid grid-cols-4 gap-4">
<div>
<p className="text-[10px] text-slate-500 mb-1">CPU</p>
<div className="flex items-center gap-2">
<p className="text-sm font-semibold text-cyan-400">
{m.cpuUsagePercent.toFixed(1)}%
</p>
<div className="flex-1 h-1.5 rounded-full bg-slate-800 overflow-hidden">
<div
className="h-full rounded-full bg-cyan-500"
style={{
width: `${Math.min(100, m.cpuUsagePercent)}%`,
}}
/>
</div>
</div>
</div>
<div>
<p className="text-[10px] text-slate-500 mb-1">Memory</p>
<p className="text-sm font-semibold text-violet-400">
{m.memoryUsageMB}{" "}
<span className="text-slate-600 font-normal">
/ {m.memoryLimitMB} MiB
</span>
</p>
</div>
<div>
<p className="text-[10px] text-slate-500 mb-1">VMM Calls</p>
<p className="text-sm font-semibold text-amber-400">
{m.vmmApiCalls.toLocaleString()}
</p>
</div>
<div>
<p className="text-[10px] text-slate-500 mb-1">Network</p>
<p className="text-xs text-slate-300">
<span className="text-emerald-400">
{formatBytes(m.networkRxBytes)}
</span>{" "}
RX /{" "}
<span className="text-blue-400">
{formatBytes(m.networkTxBytes)}
</span>{" "}
TX
</p>
</div>
</div>
</CardContent>
</Card>
);
})
)}
</div>
</div>
);
}

View File

@ -0,0 +1,329 @@
"use client";
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
ArrowLeft,
Cpu,
HardDrive,
Wifi,
Clock,
Server,
Shield,
Activity,
Terminal,
Copy,
CheckCircle2,
XCircle,
AlertTriangle,
} from "lucide-react";
import { fetchSandboxMetrics, type Sandbox, type SandboxMetrics } from "@/lib/kata-mock";
interface SandboxDetailProps {
sandbox: Sandbox;
onBack: () => void;
}
export function SandboxDetail({ sandbox, onBack }: SandboxDetailProps) {
const [metrics, setMetrics] = useState<SandboxMetrics | undefined>();
const [loadingMetrics, setLoadingMetrics] = useState(true);
useEffect(() => {
let cancelled = false;
const loadMetrics = async () => {
if (sandbox.status === "running") {
const m = await fetchSandboxMetrics(sandbox.id);
if (!cancelled) {
setMetrics(m);
setLoadingMetrics(false);
}
} else {
if (!cancelled) {
setLoadingMetrics(false);
}
}
};
loadMetrics();
return () => { cancelled = true; };
}, [sandbox.id, sandbox.status]);
const formatBytes = (bytes: number) => {
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
return `${(bytes / 1024).toFixed(1)} KiB`;
};
return (
<div className="p-6 space-y-5">
{/* Overview cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4 flex items-center gap-3">
<div className="h-9 w-9 rounded-lg flex items-center justify-center bg-slate-800/80 text-cyan-400">
<Cpu className="h-4.5 w-4.5" />
</div>
<div>
<p className="text-xs text-slate-500">vCPU</p>
<p className="text-lg font-semibold text-slate-100">{sandbox.cpuCores}</p>
</div>
</CardContent>
</Card>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4 flex items-center gap-3">
<div className="h-9 w-9 rounded-lg flex items-center justify-center bg-slate-800/80 text-violet-400">
<HardDrive className="h-4.5 w-4.5" />
</div>
<div>
<p className="text-xs text-slate-500">Memory</p>
<p className="text-lg font-semibold text-slate-100">
{sandbox.memoryMB >= 1024 ? `${(sandbox.memoryMB / 1024).toFixed(1)} GiB` : `${sandbox.memoryMB} MiB`}
</p>
</div>
</CardContent>
</Card>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4 flex items-center gap-3">
<div className="h-9 w-9 rounded-lg flex items-center justify-center bg-slate-800/80 text-amber-400">
<Clock className="h-4.5 w-4.5" />
</div>
<div>
<p className="text-xs text-slate-500">Boot Time</p>
<p className="text-lg font-semibold text-slate-100">
{sandbox.bootTimeMs > 0 ? `${sandbox.bootTimeMs}ms` : "--"}
</p>
</div>
</CardContent>
</Card>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4 flex items-center gap-3">
<div className="h-9 w-9 rounded-lg flex items-center justify-center bg-slate-800/80 text-emerald-400">
<Shield className="h-4.5 w-4.5" />
</div>
<div>
<p className="text-xs text-slate-500">Agent</p>
<p className="text-lg font-semibold flex items-center gap-1.5">
{sandbox.agentConnected ? (
<><CheckCircle2 className="h-4 w-4 text-emerald-400" /> <span className="text-emerald-400">Connected</span></>
) : (
<><XCircle className="h-4 w-4 text-red-400" /> <span className="text-red-400">Offline</span></>
)}
</p>
</div>
</CardContent>
</Card>
</div>
{/* Tabs */}
<Tabs defaultValue="overview" className="space-y-4">
<TabsList className="bg-[#131c31] border border-slate-700/50 h-9 p-0.5">
<TabsTrigger
value="overview"
className="text-xs data-[state=active]:bg-slate-700/60 data-[state=active]:text-white text-slate-400 h-8 px-3"
>
Overview
</TabsTrigger>
<TabsTrigger
value="metrics"
className="text-xs data-[state=active]:bg-slate-700/60 data-[state=active]:text-white text-slate-400 h-8 px-3"
>
Metrics
</TabsTrigger>
<TabsTrigger
value="network"
className="text-xs data-[state=active]:bg-slate-700/60 data-[state=active]:text-white text-slate-400 h-8 px-3"
>
Network
</TabsTrigger>
<TabsTrigger
value="console"
className="text-xs data-[state=active]:bg-slate-700/60 data-[state=active]:text-white text-slate-400 h-8 px-3"
>
Console
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Identity */}
<Card className="bg-[#131c31] border-slate-700/50">
<CardHeader className="pb-3 pt-4 px-4">
<CardTitle className="text-sm font-medium text-slate-300">
Identity
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2.5">
{[
["Sandbox ID", sandbox.id],
["UID", sandbox.uid],
["Name", sandbox.name],
["Namespace", sandbox.namespace],
["RuntimeClass", sandbox.runtimeClass],
].map(([label, value]) => (
<div key={label} className="flex items-center justify-between text-xs">
<span className="text-slate-500">{label}</span>
<div className="flex items-center gap-1.5">
<code className="text-slate-300 font-mono">{value}</code>
<button
onClick={() => navigator.clipboard?.writeText(value)}
className="text-slate-600 hover:text-slate-400 transition-colors"
>
<Copy className="h-3 w-3" />
</button>
</div>
</div>
))}
</CardContent>
</Card>
{/* Runtime */}
<Card className="bg-[#131c31] border-slate-700/50">
<CardHeader className="pb-3 pt-4 px-4">
<CardTitle className="text-sm font-medium text-slate-300">
Runtime
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2.5">
{[
["VMM Backend", sandbox.vmm],
["Guest Kernel", sandbox.guestKernel],
["vsock Port", sandbox.vsockPort > 0 ? String(sandbox.vsockPort) : "N/A"],
["Created", new Date(sandbox.createdAt).toLocaleString()],
["Status", sandbox.status],
].map(([label, value]) => (
<div key={label} className="flex items-center justify-between text-xs">
<span className="text-slate-500">{label}</span>
<span className="text-slate-300">{value}</span>
</div>
))}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="metrics" className="space-y-4">
{sandbox.status !== "running" ? (
<div className="flex flex-col items-center justify-center py-12 text-slate-500">
<Activity className="h-10 w-10 mb-3 opacity-30" />
<p className="text-sm">Sandbox is not running</p>
<p className="text-xs mt-1">Start the sandbox to view metrics</p>
</div>
) : loadingMetrics ? (
<div className="flex items-center justify-center py-12">
<div className="h-6 w-6 border-2 border-emerald-400 border-t-transparent rounded-full animate-spin" />
</div>
) : metrics ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4">
<p className="text-xs text-slate-500 mb-1">CPU Usage</p>
<p className="text-2xl font-bold text-cyan-400">{metrics.cpuUsagePercent.toFixed(1)}%</p>
<div className="mt-2 h-1.5 rounded-full bg-slate-800 overflow-hidden">
<div
className="h-full rounded-full bg-cyan-500 transition-all"
style={{ width: `${Math.min(100, metrics.cpuUsagePercent)}%` }}
/>
</div>
</CardContent>
</Card>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4">
<p className="text-xs text-slate-500 mb-1">Memory</p>
<p className="text-2xl font-bold text-violet-400">
{metrics.memoryUsageMB} <span className="text-sm font-normal text-slate-500">/ {metrics.memoryLimitMB} MiB</span>
</p>
<div className="mt-2 h-1.5 rounded-full bg-slate-800 overflow-hidden">
<div
className="h-full rounded-full bg-violet-500 transition-all"
style={{ width: `${(metrics.memoryUsageMB / metrics.memoryLimitMB) * 100}%` }}
/>
</div>
</CardContent>
</Card>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4">
<p className="text-xs text-slate-500 mb-1">VMM API Calls</p>
<p className="text-2xl font-bold text-amber-400">{metrics.vmmApiCalls.toLocaleString()}</p>
{metrics.vmmApiErrors > 0 && (
<p className="text-xs text-red-400 mt-1 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{metrics.vmmApiErrors} errors
</p>
)}
</CardContent>
</Card>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4">
<p className="text-xs text-slate-500 mb-1">Network RX</p>
<p className="text-xl font-bold text-emerald-400">{formatBytes(metrics.networkRxBytes)}</p>
</CardContent>
</Card>
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4">
<p className="text-xs text-slate-500 mb-1">Network TX</p>
<p className="text-xl font-bold text-blue-400">{formatBytes(metrics.networkTxBytes)}</p>
</CardContent>
</Card>
</div>
) : null}
</TabsContent>
<TabsContent value="network" className="space-y-4">
<Card className="bg-[#131c31] border-slate-700/50">
<CardHeader className="pb-3 pt-4 px-4">
<CardTitle className="text-sm font-medium text-slate-300">
vsock Connection
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2.5">
{[
["Transport", "vsock"],
["Port", sandbox.vsockPort > 0 ? String(sandbox.vsockPort) : "N/A"],
["Agent URL", sandbox.agentConnected ? `vsock://${sandbox.vsockPort}` : "disconnected"],
["CID", "3"],
].map(([label, value]) => (
<div key={label} className="flex items-center justify-between text-xs">
<span className="text-slate-500">{label}</span>
<code className="text-slate-300 font-mono">{value}</code>
</div>
))}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="console" className="space-y-4">
<Card className="bg-[#131c31] border-slate-700/50">
<CardContent className="p-4">
<div className="rounded-md bg-[#0a0f1a] border border-slate-700/30 p-4 font-mono text-xs text-slate-400 h-64 overflow-auto">
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-slate-700/30">
<Terminal className="h-3.5 w-3.5 text-emerald-400" />
<span className="text-slate-500">kata-agent console</span>
</div>
{sandbox.agentConnected ? (
<div className="space-y-1">
<p><span className="text-emerald-400">$</span> kata-agent --version</p>
<p>kata-agent version 2.5.0</p>
<p className="mt-2"><span className="text-emerald-400">$</span> cat /proc/version</p>
<p>Linux version 6.1.62 (kata-containers) (gcc (GCC) 12.3.0) #1 SMP</p>
<p className="mt-2"><span className="text-emerald-400">$</span> uptime</p>
<p>{new Date().toLocaleTimeString()} up 2h 47m, 0 users, load average: 0.23, 0.18, 0.15</p>
<p className="mt-1"><span className="text-emerald-400">$</span> <span className="animate-pulse">_</span></p>
</div>
) : (
<p className="text-slate-600">Agent is not connected. Start the sandbox to access the console.</p>
)}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,314 @@
"use client";
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Search,
Plus,
MoreVertical,
Play,
Square,
RotateCcw,
Terminal,
Boxes,
Cpu,
HardDrive,
Zap,
AlertTriangle,
} from "lucide-react";
import { fetchSandboxes, type Sandbox } from "@/lib/kata-mock";
const VMM_ICONS: Record<string, string> = {
qemu: "Q",
"cloud-hypervisor": "CLH",
firecracker: "FC",
dragonball: "DB",
};
const STATUS_COLORS: Record<string, string> = {
running: "bg-emerald-500/20 text-emerald-400 border-emerald-700",
stopped: "bg-slate-500/20 text-slate-400 border-slate-600",
creating: "bg-amber-500/20 text-amber-400 border-amber-700",
error: "bg-red-500/20 text-red-400 border-red-700",
};
interface SandboxListProps {
onSelectSandbox: (sandbox: Sandbox) => void;
}
export function SandboxList({ onSelectSandbox }: SandboxListProps) {
const [sandboxes, setSandboxes] = useState<Sandbox[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
useEffect(() => {
fetchSandboxes().then((data) => {
setSandboxes(data);
setLoading(false);
});
}, []);
const filtered = sandboxes.filter(
(s) =>
s.name.toLowerCase().includes(search.toLowerCase()) ||
s.namespace.toLowerCase().includes(search.toLowerCase()) ||
s.runtimeClass.toLowerCase().includes(search.toLowerCase())
);
const runningCount = sandboxes.filter((s) => s.status === "running").length;
const totalCount = sandboxes.length;
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-3">
<div className="h-8 w-8 border-2 border-emerald-400 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-slate-400">Loading sandboxes...</span>
</div>
</div>
);
}
return (
<div className="p-6 space-y-5">
{/* Summary cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{
label: "Total Sandboxes",
value: totalCount,
icon: Boxes,
color: "text-slate-300",
},
{
label: "Running",
value: runningCount,
icon: Play,
color: "text-emerald-400",
},
{
label: "Total vCPU",
value: sandboxes
.filter((s) => s.status === "running")
.reduce((sum, s) => sum + s.cpuCores, 0),
icon: Cpu,
color: "text-cyan-400",
},
{
label: "Total Memory",
value: `${(
sandboxes
.filter((s) => s.status === "running")
.reduce((sum, s) => sum + s.memoryMB, 0) / 1024
).toFixed(1)} GiB`,
icon: HardDrive,
color: "text-violet-400",
},
].map((stat) => (
<Card
key={stat.label}
className="bg-[#131c31] border-slate-700/50"
>
<CardContent className="p-4 flex items-center gap-3">
<div
className={cn(
"h-9 w-9 rounded-lg flex items-center justify-center bg-slate-800/80",
stat.color
)}
>
<stat.icon className="h-4.5 w-4.5" />
</div>
<div>
<p className="text-xs text-slate-500">{stat.label}</p>
<p className="text-lg font-semibold text-slate-100">
{stat.value}
</p>
</div>
</CardContent>
</Card>
))}
</div>
{/* Search + actions bar */}
<div className="flex items-center gap-3">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input
placeholder="Filter sandboxes by name, namespace, or runtime..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9 bg-[#131c31] border-slate-700/50 text-slate-200 placeholder:text-slate-600 h-9"
/>
</div>
<Button
size="sm"
className="bg-emerald-600 hover:bg-emerald-700 text-white h-9"
>
<Plus className="h-3.5 w-3.5 mr-1.5" />
Create Sandbox
</Button>
</div>
{/* Table */}
{filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
<Boxes className="h-12 w-12 mb-3 opacity-30" />
<p className="text-sm font-medium">No sandboxes found</p>
<p className="text-xs mt-1">
{search
? "Try adjusting your search filter"
: "Create a sandbox or check that kata-runtime is configured"}
</p>
</div>
) : (
<div className="rounded-lg border border-slate-700/50 overflow-hidden">
<Table>
<TableHeader>
<TableRow className="border-slate-700/50 hover:bg-transparent">
<TableHead className="text-slate-400 text-xs font-medium">
Name
</TableHead>
<TableHead className="text-slate-400 text-xs font-medium">
Namespace
</TableHead>
<TableHead className="text-slate-400 text-xs font-medium">
Runtime
</TableHead>
<TableHead className="text-slate-400 text-xs font-medium">
VMM
</TableHead>
<TableHead className="text-slate-400 text-xs font-medium">
Status
</TableHead>
<TableHead className="text-slate-400 text-xs font-medium">
vCPU / Memory
</TableHead>
<TableHead className="text-slate-400 text-xs font-medium">
Boot
</TableHead>
<TableHead className="text-slate-400 text-xs font-medium w-10" />
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((sandbox) => (
<TableRow
key={sandbox.id}
onClick={() => onSelectSandbox(sandbox)}
className="border-slate-700/30 cursor-pointer hover:bg-slate-800/40 transition-colors"
>
<TableCell className="font-medium text-slate-200 text-sm">
<div className="flex items-center gap-2">
<Zap className="h-3.5 w-3.5 text-amber-400 shrink-0" />
{sandbox.name}
</div>
</TableCell>
<TableCell className="text-slate-400 text-sm">
{sandbox.namespace}
</TableCell>
<TableCell className="text-sm">
<code className="text-xs bg-slate-800 px-1.5 py-0.5 rounded text-cyan-300">
{sandbox.runtimeClass}
</code>
</TableCell>
<TableCell className="text-sm">
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 border-slate-600 text-slate-300"
>
{VMM_ICONS[sandbox.vmm] || sandbox.vmm}
</Badge>
</TableCell>
<TableCell className="text-sm">
<Badge
variant="outline"
className={cn(
"text-[10px] px-1.5 py-0",
STATUS_COLORS[sandbox.status]
)}
>
{sandbox.status === "error" && (
<AlertTriangle className="h-3 w-3 mr-1" />
)}
{sandbox.status}
</Badge>
</TableCell>
<TableCell className="text-slate-300 text-sm">
{sandbox.cpuCores} / {sandbox.memoryMB >= 1024 ? `${(sandbox.memoryMB / 1024).toFixed(1)}G` : `${sandbox.memoryMB}M`}
</TableCell>
<TableCell className="text-slate-400 text-sm">
{sandbox.bootTimeMs > 0 ? (
<span className="flex items-center gap-1">
{sandbox.bootTimeMs}ms
{sandbox.bootTimeMs < 150 && (
<span className="text-emerald-400 text-[10px]">fast</span>
)}
</span>
) : (
<span className="text-slate-600">--</span>
)}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-slate-500 hover:text-slate-300"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-[#131c31] border-slate-700"
>
{sandbox.status === "running" ? (
<DropdownMenuItem className="text-slate-300 focus:bg-slate-800">
<Square className="h-3.5 w-3.5 mr-2" />
Stop
</DropdownMenuItem>
) : (
<DropdownMenuItem className="text-slate-300 focus:bg-slate-800">
<Play className="h-3.5 w-3.5 mr-2" />
Start
</DropdownMenuItem>
)}
<DropdownMenuItem className="text-slate-300 focus:bg-slate-800">
<RotateCcw className="h-3.5 w-3.5 mr-2" />
Restart
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300 focus:bg-slate-800">
<Terminal className="h-3.5 w-3.5 mr-2" />
Console
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,65 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,31 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,142 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,256 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

Some files were not shown because too many files have changed in this diff Show More