From 331777c5fd6c52218acdd4729c8c40ba53a58818 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Sun, 12 Oct 2025 20:16:45 +0000 Subject: [PATCH] Better support for Talos ISO downloads. --- BUILDING_WILD_APP.md | 167 ++++++++++ src/components/AppSidebar.tsx | 4 +- src/router/pages/AssetsIsoPage.tsx | 301 ++++++++++++++++++ src/router/pages/AssetsPxePage.tsx | 299 ++++++++++++++++++ src/router/pages/IsoPage.tsx | 456 ++++++++++++++++------------ src/router/pages/LandingPage.tsx | 87 ++++-- src/router/routes.tsx | 11 + src/services/api/assets.ts | 42 +++ src/services/api/hooks/useAssets.ts | 58 ++++ src/services/api/index.ts | 2 + src/services/api/types/asset.ts | 38 +++ src/services/api/types/index.ts | 1 + 12 files changed, 1245 insertions(+), 221 deletions(-) create mode 100644 BUILDING_WILD_APP.md create mode 100644 src/router/pages/AssetsIsoPage.tsx create mode 100644 src/router/pages/AssetsPxePage.tsx create mode 100644 src/services/api/assets.ts create mode 100644 src/services/api/hooks/useAssets.ts create mode 100644 src/services/api/types/asset.ts diff --git a/BUILDING_WILD_APP.md b/BUILDING_WILD_APP.md new file mode 100644 index 0000000..6425359 --- /dev/null +++ b/BUILDING_WILD_APP.md @@ -0,0 +1,167 @@ +# Building Wild App + +This document describes the architecture and tooling used to build the Wild App, the web-based interface for managing Wild Cloud instances, hosted on Wild Central. + +## Principles + +- Stick with well known standards. +- Keep it simple. +- Use popular, well-maintained libraries. +- Use components wherever possible to avoid reinventing the wheel. +- Use TypeScript for type safety. + +### Tooling +## Dev Environment Requirements + +- Node.js 20+ +- pnpm for package management +- vite for build tooling +- React + TypeScript +- Tailwind CSS for styling +- shadcn/ui for ready-made components +- radix-ui for accessible components +- eslint for linting +- tsc for type checking +- vitest for unit tests + +#### Makefile commands + +- Build application: `make app-build` +- Run locally: `make app-run` +- Format code: `make app-fmt` +- Lint and typecheck: `make app-check` +- Test installation: `make app-test` + +### Scaffolding apps + +It is important to start an app with a good base structure to avoid difficult to debug config issues. + +This is a recommended setup. + +#### Install pnpm if necessary: + +```bash +curl -fsSL https://get.pnpm.io/install.sh | sh - +``` + +#### Install a React + Speedy Web Compiler (SWC) + TypeScript + TailwindCSS app with vite: + +``` +pnpm create vite@latest my-app -- --template react-swc-ts +``` + +#### Reconfigure to use shadcn/ui (radix + tailwind components) (see https://ui.shadcn.com/docs/installation/vite) + +##### Add tailwind. + +```bash +pnpm add tailwindcss @tailwindcss/vite +``` + +##### Replace everything in src/index.css with a tailwind import: + +```bash +echo "@import \"tailwindcss\";" > src/index.css +``` + +##### Edit tsconfig files + +The current version of Vite splits TypeScript configuration into three files, two of which need to be edited. Add the baseUrl and paths properties to the compilerOptions section of the tsconfig.json and tsconfig.app.json files: + +tsconfig.json + +```json +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +tsconfig.app.json + +```json +{ + "compilerOptions": { + // ... + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + } + // ... + } +} +``` + +##### Update vite.config.ts + +```bash +pnpm add -D @types/node +``` +Then edit vite.config.ts to include the node types: + +```ts +import path from "path" +import tailwindcss from "@tailwindcss/vite" +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}) +``` + +##### Run the cli + +```bash +pnpm dlx shadcn@latest init +``` + +##### Add components + +```bash +pnpm dlx shadcn@latest add button +pnpm dlx shadcn@latest add alert-dialog +``` + +You can then use components with `import { Button } from "@/components/ui/button"` + +### UI Principles + +- Use shadcn AppSideBar as the main navigation for the app: https://ui.shadcn.com/docs/components/sidebar +- Support light and dark mode with Tailwind's built-in dark mode support: https://tailwindcss.com/docs/dark-mode + +### App Layout + +- The sidebar let's you select which cloud instance you are curently managing from a dropdown. +- The sidebar is divided into Central, Cluster, and Apps. + - Central: Utilities for managing Wild Central itself. + - Cluster: Utilities for managing the current Wild Cloud instance. + - Managing nodes. + - Managing services. + - Apps: Managing the apps deployed on the current Wild Cloud instance. + - List of apps. + - App details. + - App logs. + - App metrics. + diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 072b088..3da76c1 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -153,7 +153,7 @@ export function AppSidebar() { - + {/*
@@ -173,7 +173,7 @@ export function AppSidebar() { PXE - + */} diff --git a/src/router/pages/AssetsIsoPage.tsx b/src/router/pages/AssetsIsoPage.tsx new file mode 100644 index 0000000..63b39f8 --- /dev/null +++ b/src/router/pages/AssetsIsoPage.tsx @@ -0,0 +1,301 @@ +import { useState } from 'react'; +import { Link } from 'react-router'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card'; +import { Button } from '../../components/ui/button'; +import { Badge } from '../../components/ui/badge'; +import { + Download, + AlertCircle, + Loader2, + Disc, + BookOpen, + ExternalLink, + CheckCircle, + XCircle, + Usb, + ArrowLeft, + CloudLightning, +} from 'lucide-react'; +import { useAssetList, useDownloadAsset, useAssetStatus } from '../../services/api/hooks/useAssets'; +import { assetsApi } from '../../services/api/assets'; +import type { AssetType } from '../../services/api/types/asset'; + +export function AssetsIsoPage() { + const { data, isLoading, error } = useAssetList(); + const downloadAsset = useDownloadAsset(); + const [selectedSchematicId, setSelectedSchematicId] = useState(null); + const [selectedVersion, setSelectedVersion] = useState('v1.8.0'); + const { data: statusData } = useAssetStatus(selectedSchematicId); + + // Select the first schematic by default if available + const schematic = data?.schematics?.[0] || null; + const schematicId = schematic?.schematic_id || null; + + // Get the ISO asset + const isoAsset = schematic?.assets.find((asset) => asset.type === 'iso'); + + const handleDownload = async () => { + if (!schematicId) return; + + try { + await downloadAsset.mutateAsync({ + schematicId, + request: { version: selectedVersion, assets: ['iso'] }, + }); + } catch (err) { + console.error('Download failed:', err); + } + }; + + const getStatusBadge = (downloaded: boolean, downloading?: boolean) => { + if (downloading) { + return ( + + + Downloading + + ); + } + + if (downloaded) { + return ( + + + Available + + ); + } + + return ( + + + Missing + + ); + }; + + const getDownloadProgress = () => { + if (!statusData?.progress?.iso) return null; + + const progress = statusData.progress.iso; + if (progress.status === 'downloading' && progress.bytes_downloaded && progress.total_bytes) { + const percentage = (progress.bytes_downloaded / progress.total_bytes) * 100; + return ( +
+
+ Downloading... + {percentage.toFixed(1)}% +
+
+
+
+
+ ); + } + + return null; + }; + + return ( +
+ {/* Header */} +
+
+
+
+ + + Wild Cloud + + / + ISO Management +
+ + + +
+
+
+ + {/* Main Content */} +
+
+ {/* Educational Intro Section */} + +
+
+ +
+
+

+ What is a Bootable ISO? +

+

+ A bootable ISO is a special disk image file that can be written to a USB drive or DVD to create + installation media. When you boot a computer from this USB drive, it can install or run an + operating system directly from the drive without needing anything pre-installed. +

+

+ This is perfect for setting up individual computers in your cloud infrastructure. Download the + Talos ISO here, write it to a USB drive using tools like Balena Etcher or Rufus, then boot + your computer from the USB to install Talos Linux. +

+ +
+
+
+ + + +
+
+ +
+
+ ISO Management + + Download Talos ISO images for creating bootable USB drives + +
+
+
+ + {error ? ( +
+ +

Error Loading Assets

+

{(error as Error).message}

+ +
+ ) : ( +
+ {/* Version Selection */} +
+ + +
+ + {/* ISO Asset */} +
+

ISO Image

+ {isLoading ? ( +
+ +
+ ) : !isoAsset ? ( + + +

No ISO Available

+

+ Download a Talos ISO to get started with USB boot. +

+ +
+ ) : ( + +
+
+ +
+
+
+
Talos ISO
+ {getStatusBadge(isoAsset.downloaded, statusData?.downloading)} +
+
+ {schematic?.version &&
Version: {schematic.version}
} + {isoAsset.size &&
Size: {(isoAsset.size / 1024 / 1024).toFixed(2)} MB
} + {isoAsset.path && ( +
{isoAsset.path}
+ )} +
+ {getDownloadProgress()} +
+
+ {!isoAsset.downloaded && !statusData?.downloading && ( + + )} + {isoAsset.downloaded && schematicId && ( + + )} +
+
+
+ )} +
+ + {/* Instructions Card */} + +

+ + Next Steps +

+
    +
  1. Download the ISO image above
  2. +
  3. Download a USB writing tool (e.g., Balena Etcher, Rufus, or dd)
  4. +
  5. Write the ISO to a USB drive (minimum 2GB)
  6. +
  7. Boot your target computer from the USB drive
  8. +
  9. Follow the Talos installation process
  10. +
+
+
+ )} +
+
+
+
+
+ ); +} diff --git a/src/router/pages/AssetsPxePage.tsx b/src/router/pages/AssetsPxePage.tsx new file mode 100644 index 0000000..405030f --- /dev/null +++ b/src/router/pages/AssetsPxePage.tsx @@ -0,0 +1,299 @@ +import { useState } from 'react'; +import { Link } from 'react-router'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../../components/ui/card'; +import { Button } from '../../components/ui/button'; +import { Badge } from '../../components/ui/badge'; +import { + HardDrive, + BookOpen, + ExternalLink, + Download, + Loader2, + CheckCircle, + AlertCircle, + FileArchive, + ArrowLeft, + CloudLightning, +} from 'lucide-react'; +import { useAssetList, useDownloadAsset, useAssetStatus } from '../../services/api/hooks/useAssets'; +import type { AssetType } from '../../services/api/types/asset'; + +export function AssetsPxePage() { + const { data, isLoading, error } = useAssetList(); + const downloadAsset = useDownloadAsset(); + const [selectedVersion, setSelectedVersion] = useState('v1.8.0'); + + // Select the first schematic by default if available + const schematic = data?.schematics?.[0] || null; + const schematicId = schematic?.schematic_id || null; + const { data: statusData } = useAssetStatus(schematicId); + + // Get PXE assets (kernel and initramfs) + const pxeAssets = schematic?.assets.filter((asset) => asset.type !== 'iso') || []; + + const handleDownload = async (assetType: AssetType) => { + if (!schematicId) return; + + try { + await downloadAsset.mutateAsync({ + schematicId, + request: { version: selectedVersion, assets: [assetType] }, + }); + } catch (err) { + console.error('Download failed:', err); + } + }; + + const handleDownloadAll = async () => { + if (!schematicId) return; + + try { + await downloadAsset.mutateAsync({ + schematicId, + request: { version: selectedVersion, assets: ['kernel', 'initramfs'] }, + }); + } catch (err) { + console.error('Download failed:', err); + } + }; + + const getStatusBadge = (downloaded: boolean, downloading?: boolean) => { + if (downloading) { + return ( + + + Downloading + + ); + } + + if (downloaded) { + return ( + + + Available + + ); + } + + return ( + + + Missing + + ); + }; + + const getAssetIcon = (type: string) => { + switch (type) { + case 'kernel': + return ; + case 'initramfs': + return ; + default: + return ; + } + }; + + const getDownloadProgress = (assetType: AssetType) => { + if (!statusData?.progress?.[assetType]) return null; + + const progress = statusData.progress[assetType]; + if (progress?.status === 'downloading' && progress.bytes_downloaded && progress.total_bytes) { + const percentage = (progress.bytes_downloaded / progress.total_bytes) * 100; + return ( +
+
+ Downloading... + {percentage.toFixed(1)}% +
+
+
+
+
+ ); + } + + return null; + }; + + const isAssetDownloading = (assetType: AssetType) => { + return statusData?.progress?.[assetType]?.status === 'downloading'; + }; + + return ( +
+ {/* Header */} +
+
+
+
+ + + Wild Cloud + + / + PXE Management +
+ + + +
+
+
+ + {/* Main Content */} +
+
+ {/* Educational Intro Section */} + +
+
+ +
+
+

+ What is PXE Boot? +

+

+ PXE (Preboot Execution Environment) is like having a "network installer" that can set + up computers without needing USB drives or DVDs. When you turn on a computer, instead + of booting from its hard drive, it can boot from the network and automatically install + an operating system or run diagnostics. +

+

+ This is especially useful for setting up multiple computers in your cloud + infrastructure. PXE can automatically install and configure the same operating system + on many machines, making it easy to expand your personal cloud. +

+ +
+
+
+ + + +
+
+ +
+
+ PXE Configuration + + Manage PXE boot assets and network boot configuration + +
+
+
+ + {error ? ( +
+ +

Error Loading Assets

+

{(error as Error).message}

+ +
+ ) : ( +
+ {/* Version Selection */} +
+ + +
+ + {/* Assets List */} +
+

Boot Assets

+ {isLoading ? ( +
+ +
+ ) : ( +
+ {pxeAssets.map((asset) => ( + +
+
{getAssetIcon(asset.type)}
+
+
+
{asset.type}
+ {getStatusBadge(asset.downloaded, isAssetDownloading(asset.type as AssetType))} +
+
+ {schematic?.version &&
Version: {schematic.version}
} + {asset.size &&
Size: {(asset.size / 1024 / 1024).toFixed(2)} MB
} + {asset.path && ( +
{asset.path}
+ )} +
+ {getDownloadProgress(asset.type as AssetType)} +
+
+ {!asset.downloaded && !isAssetDownloading(asset.type as AssetType) && ( + + )} +
+
+
+ ))} +
+ )} +
+ + {/* Download All Button */} + {pxeAssets.length > 0 && pxeAssets.some((a) => !a.downloaded) && ( +
+ +
+ )} +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/router/pages/IsoPage.tsx b/src/router/pages/IsoPage.tsx index 65dd394..bace905 100644 --- a/src/router/pages/IsoPage.tsx +++ b/src/router/pages/IsoPage.tsx @@ -4,87 +4,97 @@ import { Button } from '../../components/ui/button'; import { Badge } from '../../components/ui/badge'; import { Download, - Trash2, AlertCircle, Loader2, Disc, BookOpen, ExternalLink, CheckCircle, - XCircle, Usb, + Trash2, } from 'lucide-react'; -import { usePxeAssets, useDownloadPxeAsset, useDeletePxeAsset } from '../../services/api/hooks/usePxeAssets'; -import { useInstanceContext } from '../../hooks'; -import type { PxeAssetType } from '../../services/api/types/pxe'; +import { useAssetList, useDownloadAsset, useDeleteAsset } from '../../services/api/hooks/useAssets'; +import { assetsApi } from '../../services/api/assets'; +import type { Platform } from '../../services/api/types/asset'; + +// Helper function to extract version from ISO filename +// Filename format: talos-v1.11.2-metal-amd64.iso +function extractVersionFromPath(path: string): string { + const filename = path.split('/').pop() || ''; + const match = filename.match(/talos-(v\d+\.\d+\.\d+)-metal/); + return match ? match[1] : 'unknown'; +} + +// Helper function to extract platform from ISO filename +// Filename format: talos-v1.11.2-metal-amd64.iso +function extractPlatformFromPath(path: string): string { + const filename = path.split('/').pop() || ''; + const match = filename.match(/-(amd64|arm64)\.iso$/); + return match ? match[1] : 'unknown'; +} export function IsoPage() { - const { currentInstance } = useInstanceContext(); - const { data, isLoading, error } = usePxeAssets(currentInstance); - const downloadAsset = useDownloadPxeAsset(); - const deleteAsset = useDeletePxeAsset(); - const [downloadingType, setDownloadingType] = useState(null); - const [selectedVersion, setSelectedVersion] = useState('v1.8.0'); + const { data, isLoading, error, refetch } = useAssetList(); + const downloadAsset = useDownloadAsset(); + const deleteAsset = useDeleteAsset(); - // Filter to show only ISO assets - const isoAssets = data?.assets.filter((asset) => asset.type === 'iso') || []; + const [schematicId, setSchematicId] = useState(''); + const [selectedVersion, setSelectedVersion] = useState('v1.11.2'); + const [selectedPlatform, setSelectedPlatform] = useState('amd64'); + const [isDownloading, setIsDownloading] = useState(false); - const handleDownload = async (type: PxeAssetType) => { - if (!currentInstance) return; + const handleDownload = async () => { + if (!schematicId) { + alert('Please enter a schematic ID'); + return; + } - setDownloadingType(type); + setIsDownloading(true); try { - const url = `https://github.com/siderolabs/talos/releases/download/${selectedVersion}/metal-amd64.iso`; await downloadAsset.mutateAsync({ - instanceName: currentInstance, - request: { type, version: selectedVersion, url }, + schematicId, + request: { + version: selectedVersion, + platform: selectedPlatform, + assets: ['iso'] + }, }); + // Refresh the list after download + await refetch(); } catch (err) { console.error('Download failed:', err); + alert(`Download failed: ${err instanceof Error ? err.message : 'Unknown error'}`); } finally { - setDownloadingType(null); + setIsDownloading(false); } }; - const handleDelete = async (type: PxeAssetType) => { - if (!currentInstance) return; + const handleDelete = async (schematicIdToDelete: string) => { + if (!confirm('Are you sure you want to delete this schematic and all its assets? This action cannot be undone.')) { + return; + } - await deleteAsset.mutateAsync({ instanceName: currentInstance, type }); - }; - - const getStatusBadge = (status?: string) => { - const statusValue = status || 'missing'; - const variants: Record = { - available: 'success', - missing: 'secondary', - downloading: 'warning', - error: 'destructive', - }; - - const icons: Record = { - available: , - missing: , - downloading: , - error: , - }; - - return ( - - {icons[statusValue]} - {statusValue.charAt(0).toUpperCase() + statusValue.slice(1)} - - ); - }; - - const getAssetIcon = (type: string) => { - switch (type) { - case 'iso': - return ; - default: - return ; + try { + await deleteAsset.mutateAsync(schematicIdToDelete); + await refetch(); + } catch (err) { + console.error('Delete failed:', err); + alert(`Delete failed: ${err instanceof Error ? err.message : 'Unknown error'}`); } }; + // Find all ISO assets from all schematics (including multiple ISOs per schematic) + const isoAssets = data?.schematics + .flatMap(schematic => { + // Get ALL ISO assets for this schematic (not just the first one) + const isoAssetsForSchematic = schematic.assets.filter(asset => asset.type === 'iso'); + return isoAssetsForSchematic.map(isoAsset => ({ + ...isoAsset, + schematic_id: schematic.schematic_id, + version: schematic.version + })); + }) || []; + return (
{/* Educational Intro Section */} @@ -111,180 +121,230 @@ export function IsoPage() { variant="outline" size="sm" className="text-purple-700 border-purple-300 hover:bg-purple-100 dark:text-purple-300 dark:border-purple-700 dark:hover:bg-purple-900/20" + onClick={() => window.open('https://www.balena.io/etcher/', '_blank')} > - Learn about creating bootable USB drives + Download Balena Etcher
+ {/* Download New ISO Section */}
- +
- ISO Management + Download Talos ISO - Download Talos ISO images for creating bootable USB drives + Specify the schematic ID, version, and platform to download a Talos ISO image
+ + {/* Schematic ID Input */} +
+ + setSchematicId(e.target.value)} + placeholder="e.g., 434a0300db532066f1098e05ac068159371d00f0aba0a3103a0e826e83825c82" + className="w-full px-3 py-2 border rounded-lg bg-background font-mono text-sm" + /> +

+ Get your schematic ID from the{' '} + + Talos Image Factory + +

+
+ + {/* Version Selection */} +
+ + +
+ + {/* Platform Selection */} +
+ +
+ + +
+
+ + {/* Download Button */} + +
+
+ + {/* Downloaded ISOs Section */} + + + Downloaded ISO Images + Available ISO images on Wild Central + - {!currentInstance ? ( -
- -

No Instance Selected

-

- Please select or create an instance to manage ISO images. -

+ {isLoading ? ( +
+
) : error ? (
-

Error Loading ISO

+

Error Loading ISOs

{(error as Error).message}

- + +
+ ) : isoAssets.length === 0 ? ( +
+ +

No ISOs Downloaded

+

+ Download a Talos ISO using the form above to get started. +

) : ( -
- {/* Version Selection */} -
- - -
- - {/* ISO Asset */} -
-

ISO Image

- {isLoading ? ( -
- -
- ) : isoAssets.length === 0 ? ( - - -

No ISO Available

-

- Download a Talos ISO to get started with USB boot. -

- -
- ) : ( -
- {isoAssets.map((asset) => ( - -
-
{getAssetIcon(asset.type)}
-
-
-
Talos ISO
- {getStatusBadge(asset.status)} -
-
- {asset.version &&
Version: {asset.version}
} - {asset.size &&
Size: {asset.size}
} - {asset.path && ( -
{asset.path}
- )} - {asset.error && ( -
{asset.error}
- )} -
-
-
- {asset.status !== 'available' && asset.status !== 'downloading' && ( - - )} - {asset.status === 'available' && ( - <> - - - - )} -
+
+ {isoAssets.map((asset: any) => { + const version = extractVersionFromPath(asset.path || ''); + const platform = extractPlatformFromPath(asset.path || ''); + return ( + +
+
+ +
+
+
+
Talos ISO
+ {version} + {platform} + {asset.downloaded ? ( + + + Downloaded + + ) : ( + + + Missing + + )}
- - ))} -
- )} -
- - {/* Instructions Card */} - -

- - Next Steps -

-
    -
  1. Download the ISO image above
  2. -
  3. Download a USB writing tool (e.g., Balena Etcher, Rufus, or dd)
  4. -
  5. Write the ISO to a USB drive (minimum 2GB)
  6. -
  7. Boot your target computer from the USB drive
  8. -
  9. Follow the Talos installation process
  10. -
-
+
+
+ Schematic: {asset.schematic_id} +
+ {asset.size && ( +
Size: {(asset.size / 1024 / 1024).toFixed(2)} MB
+ )} +
+
+ {asset.downloaded && ( +
+ + +
+ )} +
+
+ ); + })}
)} + + {/* Instructions Card */} + +

+ + Next Steps +

+
    +
  1. Get your schematic ID from Talos Image Factory
  2. +
  3. Download the ISO image using the form above
  4. +
  5. Download a USB writing tool (e.g., Balena Etcher, Rufus, or dd)
  6. +
  7. Write the ISO to a USB drive (minimum 2GB)
  8. +
  9. Boot your target computer from the USB drive
  10. +
  11. Follow the Talos installation process
  12. +
+
); } diff --git a/src/router/pages/LandingPage.tsx b/src/router/pages/LandingPage.tsx index f7a8aed..7656aef 100644 --- a/src/router/pages/LandingPage.tsx +++ b/src/router/pages/LandingPage.tsx @@ -1,8 +1,8 @@ -import { useNavigate } from 'react-router'; +import { useNavigate, Link } from 'react-router'; import { useInstanceContext } from '../../hooks/useInstanceContext'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; -import { Server } from 'lucide-react'; +import { Server, Usb, HardDrive, CloudLightning } from 'lucide-react'; export function LandingPage() { const navigate = useNavigate(); @@ -16,25 +16,70 @@ export function LandingPage() { }; return ( -
- - - Wild Cloud - - Select an instance to manage your cloud infrastructure - - - - - - +
+
+
+
+ +

Wild Cloud

+
+

+ Manage your cloud infrastructure with ease +

+
+ +
+ + + Cloud Instance + + Manage your Wild Cloud instance + + + + + + + + + + Boot Assets + + Download Talos installation media + + + + + + + + + + + +
+
); } diff --git a/src/router/routes.tsx b/src/router/routes.tsx index cfd7ca5..83af516 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -20,12 +20,23 @@ import { InfrastructurePage } from './pages/InfrastructurePage'; import { ClusterPage } from './pages/ClusterPage'; import { AppsPage } from './pages/AppsPage'; import { AdvancedPage } from './pages/AdvancedPage'; +import { AssetsIsoPage } from './pages/AssetsIsoPage'; +import { AssetsPxePage } from './pages/AssetsPxePage'; export const routes: RouteObject[] = [ { path: '/', element: , }, + // Centralized asset routes (not under instance context) + { + path: '/iso', + element: , + }, + { + path: '/pxe', + element: , + }, { path: '/instances/:instanceId', element: , diff --git a/src/services/api/assets.ts b/src/services/api/assets.ts new file mode 100644 index 0000000..3bc202f --- /dev/null +++ b/src/services/api/assets.ts @@ -0,0 +1,42 @@ +import { apiClient } from './client'; +import type { AssetListResponse, Schematic, DownloadAssetRequest, AssetStatusResponse } from './types/asset'; + +// Get API base URL +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5055'; + +export const assetsApi = { + // List all schematics + list: async (): Promise => { + const response = await apiClient.get('/api/v1/assets'); + return response as AssetListResponse; + }, + + // Get schematic details + get: async (schematicId: string): Promise => { + const response = await apiClient.get(`/api/v1/assets/${schematicId}`); + return response as Schematic; + }, + + // Download assets for a schematic + download: async (schematicId: string, request: DownloadAssetRequest): Promise<{ message: string }> => { + const response = await apiClient.post(`/api/v1/assets/${schematicId}/download`, request); + return response as { message: string }; + }, + + // Get download status + status: async (schematicId: string): Promise => { + const response = await apiClient.get(`/api/v1/assets/${schematicId}/status`); + return response as AssetStatusResponse; + }, + + // Get download URL for an asset (includes base URL for direct download) + getAssetUrl: (schematicId: string, assetType: 'kernel' | 'initramfs' | 'iso'): string => { + return `${API_BASE_URL}/api/v1/assets/${schematicId}/pxe/${assetType}`; + }, + + // Delete a schematic and all its assets + delete: async (schematicId: string): Promise<{ message: string }> => { + const response = await apiClient.delete(`/api/v1/assets/${schematicId}`); + return response as { message: string }; + }, +}; diff --git a/src/services/api/hooks/useAssets.ts b/src/services/api/hooks/useAssets.ts new file mode 100644 index 0000000..ce16456 --- /dev/null +++ b/src/services/api/hooks/useAssets.ts @@ -0,0 +1,58 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { assetsApi } from '../assets'; +import type { DownloadAssetRequest } from '../types/asset'; + +export function useAssetList() { + return useQuery({ + queryKey: ['assets'], + queryFn: assetsApi.list, + }); +} + +export function useAsset(schematicId: string | null | undefined) { + return useQuery({ + queryKey: ['assets', schematicId], + queryFn: () => assetsApi.get(schematicId!), + enabled: !!schematicId, + }); +} + +export function useAssetStatus(schematicId: string | null | undefined) { + return useQuery({ + queryKey: ['assets', schematicId, 'status'], + queryFn: () => assetsApi.status(schematicId!), + enabled: !!schematicId, + refetchInterval: (query) => { + const data = query.state.data; + // Poll every 2 seconds if downloading + return data?.downloading ? 2000 : false; + }, + }); +} + +export function useDownloadAsset() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ schematicId, request }: { schematicId: string; request: DownloadAssetRequest }) => + assetsApi.download(schematicId, request), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['assets'] }); + queryClient.invalidateQueries({ queryKey: ['assets', variables.schematicId] }); + queryClient.invalidateQueries({ queryKey: ['assets', variables.schematicId, 'status'] }); + }, + }); +} + +export function useDeleteAsset() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (schematicId: string) => assetsApi.delete(schematicId), + onSuccess: (_, schematicId) => { + queryClient.invalidateQueries({ queryKey: ['assets'] }); + queryClient.invalidateQueries({ queryKey: ['assets', schematicId] }); + queryClient.invalidateQueries({ queryKey: ['assets', schematicId, 'status'] }); + }, + }); +} diff --git a/src/services/api/index.ts b/src/services/api/index.ts index 564cd15..2ff7786 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -10,6 +10,7 @@ export { operationsApi } from './operations'; export { dnsmasqApi } from './dnsmasq'; export { utilitiesApi } from './utilities'; export { pxeApi } from './pxe'; +export { assetsApi } from './assets'; // React Query hooks export { useInstance, useInstanceOperations, useInstanceClusterHealth } from './hooks/useInstance'; @@ -17,3 +18,4 @@ export { useOperations, useOperation, useCancelOperation } from './hooks/useOper export { useClusterHealth, useClusterStatus, useClusterNodes } from './hooks/useCluster'; export { useDashboardToken, useClusterVersions, useNodeIPs, useControlPlaneIP, useCopySecret } from './hooks/useUtilities'; export { usePxeAssets, useDownloadPxeAsset, useDeletePxeAsset } from './hooks/usePxeAssets'; +export { useAssetList, useAsset, useAssetStatus, useDownloadAsset } from './hooks/useAssets'; diff --git a/src/services/api/types/asset.ts b/src/services/api/types/asset.ts new file mode 100644 index 0000000..ec895ad --- /dev/null +++ b/src/services/api/types/asset.ts @@ -0,0 +1,38 @@ +export type AssetType = 'kernel' | 'initramfs' | 'iso'; +export type Platform = 'amd64' | 'arm64'; + +// Simplified Asset interface matching backend +export interface Asset { + type: string; + path: string; + size: number; + sha256: string; + downloaded: boolean; +} + +// Schematic representation matching backend +export interface Schematic { + schematic_id: string; + version: string; + path: string; + assets: Asset[]; +} + +export interface AssetListResponse { + schematics: Schematic[]; +} + +export interface DownloadAssetRequest { + version: string; + platform?: Platform; + assets?: AssetType[]; + force?: boolean; +} + +// Simplified status response matching backend +export interface AssetStatusResponse { + schematic_id: string; + version: string; + assets: Record; + complete: boolean; +} diff --git a/src/services/api/types/index.ts b/src/services/api/types/index.ts index 5e23c8b..5549482 100644 --- a/src/services/api/types/index.ts +++ b/src/services/api/types/index.ts @@ -7,3 +7,4 @@ export * from './cluster'; export * from './app'; export * from './service'; export * from './pxe'; +export * from './asset';