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:
commit
5f9f15842b
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
  
|
||||
|
||||
## 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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
export declare function BuildGuide(): React.JSX.Element;
|
||||
//# sourceMappingURL=BuildGuide.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
export declare function Configuration(): React.JSX.Element;
|
||||
//# sourceMappingURL=Configuration.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
export declare function ExportImage(): React.JSX.Element;
|
||||
//# sourceMappingURL=ExportImage.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
export declare function ImportImage(): React.JSX.Element;
|
||||
//# sourceMappingURL=ImportImage.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
export declare function KataModule(): React.JSX.Element;
|
||||
//# sourceMappingURL=KataModule.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
export declare function KernelDetail(): React.JSX.Element;
|
||||
//# sourceMappingURL=KernelDetail.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import React from "react";
|
||||
export declare function MetricsDashboard(): React.JSX.Element;
|
||||
//# sourceMappingURL=MetricsDashboard.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,eAAe,CAAC"}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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.
|
||||
*/
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
import { type ClassValue } from "clsx";
|
||||
export declare function cn(...inputs: ClassValue[]): string;
|
||||
//# sourceMappingURL=utils.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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:;"
|
||||
}
|
||||
|
|
@ -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:;"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} · {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
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue