Better support for Talos ISO downloads.
This commit is contained in:
167
BUILDING_WILD_APP.md
Normal file
167
BUILDING_WILD_APP.md
Normal file
@@ -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.
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ export function AppSidebar() {
|
|||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
|
|
||||||
<SidebarMenuSubItem>
|
{/* <SidebarMenuSubItem>
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton asChild>
|
||||||
<NavLink to={`/instances/${instanceId}/dhcp`}>
|
<NavLink to={`/instances/${instanceId}/dhcp`}>
|
||||||
<div className="p-1 rounded-md">
|
<div className="p-1 rounded-md">
|
||||||
@@ -173,7 +173,7 @@ export function AppSidebar() {
|
|||||||
<span className="truncate">PXE</span>
|
<span className="truncate">PXE</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem> */}
|
||||||
|
|
||||||
<SidebarMenuSubItem>
|
<SidebarMenuSubItem>
|
||||||
<SidebarMenuSubButton asChild>
|
<SidebarMenuSubButton asChild>
|
||||||
|
|||||||
301
src/router/pages/AssetsIsoPage.tsx
Normal file
301
src/router/pages/AssetsIsoPage.tsx
Normal file
@@ -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<string | null>(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 (
|
||||||
|
<Badge variant="warning" className="flex items-center gap-1">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
Downloading
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloaded) {
|
||||||
|
return (
|
||||||
|
<Badge variant="success" className="flex items-center gap-1">
|
||||||
|
<CheckCircle className="h-3 w-3" />
|
||||||
|
Available
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant="secondary" className="flex items-center gap-1">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
Missing
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex justify-between text-sm text-muted-foreground mb-1">
|
||||||
|
<span>Downloading...</span>
|
||||||
|
<span>{percentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-muted rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-primary h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="border-b">
|
||||||
|
<div className="container mx-auto px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link to="/" className="flex items-center gap-2">
|
||||||
|
<CloudLightning className="h-6 w-6 text-primary" />
|
||||||
|
<span className="text-lg font-bold">Wild Cloud</span>
|
||||||
|
</Link>
|
||||||
|
<span className="text-muted-foreground">/</span>
|
||||||
|
<span className="text-sm font-medium">ISO Management</span>
|
||||||
|
</div>
|
||||||
|
<Link to="/">
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Educational Intro Section */}
|
||||||
|
<Card className="p-6 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||||
|
<BookOpen className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-semibold text-purple-900 dark:text-purple-100 mb-2">
|
||||||
|
What is a Bootable ISO?
|
||||||
|
</h3>
|
||||||
|
<p className="text-purple-800 dark:text-purple-200 mb-3 leading-relaxed">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p className="text-purple-700 dark:text-purple-300 mb-4 text-sm">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
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.talos.dev/latest/talos-guides/install/bare-metal-platforms/digital-rebar/', '_blank')}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
Learn about creating bootable USB drives
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Usb className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<CardTitle>ISO Management</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Download Talos ISO images for creating bootable USB drives
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{error ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">Error Loading Assets</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">{(error as Error).message}</p>
|
||||||
|
<Button onClick={() => window.location.reload()}>Reload Page</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Version Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Talos Version</label>
|
||||||
|
<select
|
||||||
|
value={selectedVersion}
|
||||||
|
onChange={(e) => setSelectedVersion(e.target.value)}
|
||||||
|
className="w-full md:w-64 px-3 py-2 border rounded-lg bg-background"
|
||||||
|
>
|
||||||
|
<option value="v1.8.0">v1.8.0 (Latest)</option>
|
||||||
|
<option value="v1.7.6">v1.7.6</option>
|
||||||
|
<option value="v1.7.5">v1.7.5</option>
|
||||||
|
<option value="v1.6.7">v1.6.7</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ISO Asset */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-4">ISO Image</h4>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : !isoAsset ? (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<Disc className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">No ISO Available</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
Download a Talos ISO to get started with USB boot.
|
||||||
|
</p>
|
||||||
|
<Button onClick={handleDownload} disabled={downloadAsset.isPending}>
|
||||||
|
{downloadAsset.isPending ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
|
) : (
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Download ISO
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2 bg-muted rounded-lg">
|
||||||
|
<Disc className="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h5 className="font-medium capitalize">Talos ISO</h5>
|
||||||
|
{getStatusBadge(isoAsset.downloaded, statusData?.downloading)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground space-y-1">
|
||||||
|
{schematic?.version && <div>Version: {schematic.version}</div>}
|
||||||
|
{isoAsset.size && <div>Size: {(isoAsset.size / 1024 / 1024).toFixed(2)} MB</div>}
|
||||||
|
{isoAsset.path && (
|
||||||
|
<div className="font-mono text-xs truncate">{isoAsset.path}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{getDownloadProgress()}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!isoAsset.downloaded && !statusData?.downloading && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={downloadAsset.isPending}
|
||||||
|
>
|
||||||
|
{downloadAsset.isPending ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="h-4 w-4 mr-1" />
|
||||||
|
Download
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isoAsset.downloaded && schematicId && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = assetsApi.getAssetUrl(schematicId, 'iso');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-1" />
|
||||||
|
Download to Computer
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions Card */}
|
||||||
|
<Card className="p-6 bg-muted/50">
|
||||||
|
<h4 className="font-medium mb-3 flex items-center gap-2">
|
||||||
|
<Usb className="h-5 w-5" />
|
||||||
|
Next Steps
|
||||||
|
</h4>
|
||||||
|
<ol className="space-y-2 text-sm text-muted-foreground list-decimal list-inside">
|
||||||
|
<li>Download the ISO image above</li>
|
||||||
|
<li>Download a USB writing tool (e.g., Balena Etcher, Rufus, or dd)</li>
|
||||||
|
<li>Write the ISO to a USB drive (minimum 2GB)</li>
|
||||||
|
<li>Boot your target computer from the USB drive</li>
|
||||||
|
<li>Follow the Talos installation process</li>
|
||||||
|
</ol>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
299
src/router/pages/AssetsPxePage.tsx
Normal file
299
src/router/pages/AssetsPxePage.tsx
Normal file
@@ -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 (
|
||||||
|
<Badge variant="default" className="flex items-center gap-1">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
Downloading
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloaded) {
|
||||||
|
return (
|
||||||
|
<Badge variant="success" className="flex items-center gap-1">
|
||||||
|
<CheckCircle className="h-3 w-3" />
|
||||||
|
Available
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant="secondary" className="flex items-center gap-1">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
Missing
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAssetIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'kernel':
|
||||||
|
return <FileArchive className="h-5 w-5 text-blue-500" />;
|
||||||
|
case 'initramfs':
|
||||||
|
return <FileArchive className="h-5 w-5 text-green-500" />;
|
||||||
|
default:
|
||||||
|
return <FileArchive className="h-5 w-5 text-muted-foreground" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex justify-between text-sm text-muted-foreground mb-1">
|
||||||
|
<span>Downloading...</span>
|
||||||
|
<span>{percentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-muted rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-primary h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAssetDownloading = (assetType: AssetType) => {
|
||||||
|
return statusData?.progress?.[assetType]?.status === 'downloading';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="border-b">
|
||||||
|
<div className="container mx-auto px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link to="/" className="flex items-center gap-2">
|
||||||
|
<CloudLightning className="h-6 w-6 text-primary" />
|
||||||
|
<span className="text-lg font-bold">Wild Cloud</span>
|
||||||
|
</Link>
|
||||||
|
<span className="text-muted-foreground">/</span>
|
||||||
|
<span className="text-sm font-medium">PXE Management</span>
|
||||||
|
</div>
|
||||||
|
<Link to="/">
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Educational Intro Section */}
|
||||||
|
<Card className="p-6 bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-950/20 dark:to-amber-950/20 border-orange-200 dark:border-orange-800">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="p-3 bg-orange-100 dark:bg-orange-900/30 rounded-lg">
|
||||||
|
<BookOpen className="h-6 w-6 text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-semibold text-orange-900 dark:text-orange-100 mb-2">
|
||||||
|
What is PXE Boot?
|
||||||
|
</h3>
|
||||||
|
<p className="text-orange-800 dark:text-orange-200 mb-3 leading-relaxed">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p className="text-orange-700 dark:text-orange-300 mb-4 text-sm">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-orange-700 border-orange-300 hover:bg-orange-100 dark:text-orange-300 dark:border-orange-700 dark:hover:bg-orange-900/20"
|
||||||
|
onClick={() => window.open('https://www.talos.dev/latest/talos-guides/install/bare-metal-platforms/pxe/', '_blank')}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
Learn more about network booting
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<HardDrive className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<CardTitle>PXE Configuration</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage PXE boot assets and network boot configuration
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{error ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">Error Loading Assets</h3>
|
||||||
|
<p className="text-muted-foreground mb-4">{(error as Error).message}</p>
|
||||||
|
<Button onClick={() => window.location.reload()}>Reload Page</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Version Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Talos Version</label>
|
||||||
|
<select
|
||||||
|
value={selectedVersion}
|
||||||
|
onChange={(e) => setSelectedVersion(e.target.value)}
|
||||||
|
className="w-full md:w-64 px-3 py-2 border rounded-lg bg-background"
|
||||||
|
>
|
||||||
|
<option value="v1.8.0">v1.8.0 (Latest)</option>
|
||||||
|
<option value="v1.7.6">v1.7.6</option>
|
||||||
|
<option value="v1.7.5">v1.7.5</option>
|
||||||
|
<option value="v1.6.7">v1.6.7</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assets List */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-4">Boot Assets</h4>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{pxeAssets.map((asset) => (
|
||||||
|
<Card key={asset.type} className="p-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2 bg-muted rounded-lg">{getAssetIcon(asset.type)}</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h5 className="font-medium capitalize">{asset.type}</h5>
|
||||||
|
{getStatusBadge(asset.downloaded, isAssetDownloading(asset.type as AssetType))}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground space-y-1">
|
||||||
|
{schematic?.version && <div>Version: {schematic.version}</div>}
|
||||||
|
{asset.size && <div>Size: {(asset.size / 1024 / 1024).toFixed(2)} MB</div>}
|
||||||
|
{asset.path && (
|
||||||
|
<div className="font-mono text-xs truncate">{asset.path}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{getDownloadProgress(asset.type as AssetType)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!asset.downloaded && !isAssetDownloading(asset.type as AssetType) && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDownload(asset.type as AssetType)}
|
||||||
|
disabled={downloadAsset.isPending}
|
||||||
|
>
|
||||||
|
{downloadAsset.isPending ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="h-4 w-4 mr-1" />
|
||||||
|
Download
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Download All Button */}
|
||||||
|
{pxeAssets.length > 0 && pxeAssets.some((a) => !a.downloaded) && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleDownloadAll}
|
||||||
|
disabled={downloadAsset.isPending || statusData?.downloading}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download All Missing Assets
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,87 +4,97 @@ import { Button } from '../../components/ui/button';
|
|||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
Trash2,
|
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
Disc,
|
Disc,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
|
||||||
Usb,
|
Usb,
|
||||||
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { usePxeAssets, useDownloadPxeAsset, useDeletePxeAsset } from '../../services/api/hooks/usePxeAssets';
|
import { useAssetList, useDownloadAsset, useDeleteAsset } from '../../services/api/hooks/useAssets';
|
||||||
import { useInstanceContext } from '../../hooks';
|
import { assetsApi } from '../../services/api/assets';
|
||||||
import type { PxeAssetType } from '../../services/api/types/pxe';
|
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() {
|
export function IsoPage() {
|
||||||
const { currentInstance } = useInstanceContext();
|
const { data, isLoading, error, refetch } = useAssetList();
|
||||||
const { data, isLoading, error } = usePxeAssets(currentInstance);
|
const downloadAsset = useDownloadAsset();
|
||||||
const downloadAsset = useDownloadPxeAsset();
|
const deleteAsset = useDeleteAsset();
|
||||||
const deleteAsset = useDeletePxeAsset();
|
|
||||||
const [downloadingType, setDownloadingType] = useState<string | null>(null);
|
|
||||||
const [selectedVersion, setSelectedVersion] = useState('v1.8.0');
|
|
||||||
|
|
||||||
// Filter to show only ISO assets
|
const [schematicId, setSchematicId] = useState('');
|
||||||
const isoAssets = data?.assets.filter((asset) => asset.type === 'iso') || [];
|
const [selectedVersion, setSelectedVersion] = useState('v1.11.2');
|
||||||
|
const [selectedPlatform, setSelectedPlatform] = useState<Platform>('amd64');
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
|
||||||
const handleDownload = async (type: PxeAssetType) => {
|
const handleDownload = async () => {
|
||||||
if (!currentInstance) return;
|
if (!schematicId) {
|
||||||
|
alert('Please enter a schematic ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setDownloadingType(type);
|
setIsDownloading(true);
|
||||||
try {
|
try {
|
||||||
const url = `https://github.com/siderolabs/talos/releases/download/${selectedVersion}/metal-amd64.iso`;
|
|
||||||
await downloadAsset.mutateAsync({
|
await downloadAsset.mutateAsync({
|
||||||
instanceName: currentInstance,
|
schematicId,
|
||||||
request: { type, version: selectedVersion, url },
|
request: {
|
||||||
|
version: selectedVersion,
|
||||||
|
platform: selectedPlatform,
|
||||||
|
assets: ['iso']
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
// Refresh the list after download
|
||||||
|
await refetch();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Download failed:', err);
|
console.error('Download failed:', err);
|
||||||
|
alert(`Download failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||||
} finally {
|
} finally {
|
||||||
setDownloadingType(null);
|
setIsDownloading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (type: PxeAssetType) => {
|
const handleDelete = async (schematicIdToDelete: string) => {
|
||||||
if (!currentInstance) return;
|
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 });
|
try {
|
||||||
};
|
await deleteAsset.mutateAsync(schematicIdToDelete);
|
||||||
|
await refetch();
|
||||||
const getStatusBadge = (status?: string) => {
|
} catch (err) {
|
||||||
const statusValue = status || 'missing';
|
console.error('Delete failed:', err);
|
||||||
const variants: Record<string, 'secondary' | 'success' | 'destructive' | 'warning'> = {
|
alert(`Delete failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||||
available: 'success',
|
|
||||||
missing: 'secondary',
|
|
||||||
downloading: 'warning',
|
|
||||||
error: 'destructive',
|
|
||||||
};
|
|
||||||
|
|
||||||
const icons: Record<string, React.ReactNode> = {
|
|
||||||
available: <CheckCircle className="h-3 w-3" />,
|
|
||||||
missing: <AlertCircle className="h-3 w-3" />,
|
|
||||||
downloading: <Loader2 className="h-3 w-3 animate-spin" />,
|
|
||||||
error: <XCircle className="h-3 w-3" />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge variant={variants[statusValue] || 'secondary'} className="flex items-center gap-1">
|
|
||||||
{icons[statusValue]}
|
|
||||||
{statusValue.charAt(0).toUpperCase() + statusValue.slice(1)}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAssetIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'iso':
|
|
||||||
return <Disc className="h-5 w-5 text-primary" />;
|
|
||||||
default:
|
|
||||||
return <Disc className="h-5 w-5" />;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Educational Intro Section */}
|
{/* Educational Intro Section */}
|
||||||
@@ -111,180 +121,230 @@ export function IsoPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
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"
|
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')}
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-4 w-4 mr-2" />
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
Learn about creating bootable USB drives
|
Download Balena Etcher
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Download New ISO Section */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="p-2 bg-primary/10 rounded-lg">
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
<Usb className="h-6 w-6 text-primary" />
|
<Disc className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CardTitle>ISO Management</CardTitle>
|
<CardTitle>Download Talos ISO</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Download Talos ISO images for creating bootable USB drives
|
Specify the schematic ID, version, and platform to download a Talos ISO image
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Schematic ID Input */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">
|
||||||
|
Schematic ID
|
||||||
|
<span className="text-muted-foreground ml-2">(64-character hex string)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={schematicId}
|
||||||
|
onChange={(e) => setSchematicId(e.target.value)}
|
||||||
|
placeholder="e.g., 434a0300db532066f1098e05ac068159371d00f0aba0a3103a0e826e83825c82"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg bg-background font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Get your schematic ID from the{' '}
|
||||||
|
<a
|
||||||
|
href="https://factory.talos.dev"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Talos Image Factory
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Version Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Talos Version</label>
|
||||||
|
<select
|
||||||
|
value={selectedVersion}
|
||||||
|
onChange={(e) => setSelectedVersion(e.target.value)}
|
||||||
|
className="w-full md:w-64 px-3 py-2 border rounded-lg bg-background"
|
||||||
|
>
|
||||||
|
<option value="v1.11.2">v1.11.2</option>
|
||||||
|
<option value="v1.11.1">v1.11.1</option>
|
||||||
|
<option value="v1.11.0">v1.11.0</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Platform Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Platform</label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="platform"
|
||||||
|
value="amd64"
|
||||||
|
checked={selectedPlatform === 'amd64'}
|
||||||
|
onChange={(e) => setSelectedPlatform(e.target.value as Platform)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>amd64 (Intel/AMD 64-bit)</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="platform"
|
||||||
|
value="arm64"
|
||||||
|
checked={selectedPlatform === 'arm64'}
|
||||||
|
onChange={(e) => setSelectedPlatform(e.target.value as Platform)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>arm64 (ARM 64-bit)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Download Button */}
|
||||||
|
<Button
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={isDownloading || !schematicId}
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
|
Downloading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download ISO
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Downloaded ISOs Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Downloaded ISO Images</CardTitle>
|
||||||
|
<CardDescription>Available ISO images on Wild Central</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!currentInstance ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Usb className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
<h3 className="text-lg font-medium mb-2">No Instance Selected</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Please select or create an instance to manage ISO images.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-medium mb-2">Error Loading ISO</h3>
|
<h3 className="text-lg font-medium mb-2">Error Loading ISOs</h3>
|
||||||
<p className="text-muted-foreground mb-4">{(error as Error).message}</p>
|
<p className="text-muted-foreground mb-4">{(error as Error).message}</p>
|
||||||
<Button onClick={() => window.location.reload()}>Reload Page</Button>
|
<Button onClick={() => refetch()}>Retry</Button>
|
||||||
|
</div>
|
||||||
|
) : isoAssets.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Disc className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">No ISOs Downloaded</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Download a Talos ISO using the form above to get started.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-3">
|
||||||
{/* Version Selection */}
|
{isoAssets.map((asset: any) => {
|
||||||
<div>
|
const version = extractVersionFromPath(asset.path || '');
|
||||||
<label className="text-sm font-medium mb-2 block">Talos Version</label>
|
const platform = extractPlatformFromPath(asset.path || '');
|
||||||
<select
|
return (
|
||||||
value={selectedVersion}
|
<Card key={asset.schematic_id} className="p-4">
|
||||||
onChange={(e) => setSelectedVersion(e.target.value)}
|
<div className="flex items-center gap-4">
|
||||||
className="w-full md:w-64 px-3 py-2 border rounded-lg bg-background"
|
<div className="p-2 bg-muted rounded-lg">
|
||||||
>
|
<Disc className="h-5 w-5 text-primary" />
|
||||||
<option value="v1.8.0">v1.8.0 (Latest)</option>
|
</div>
|
||||||
<option value="v1.7.6">v1.7.6</option>
|
<div className="flex-1">
|
||||||
<option value="v1.7.5">v1.7.5</option>
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<option value="v1.6.7">v1.6.7</option>
|
<h5 className="font-medium">Talos ISO</h5>
|
||||||
</select>
|
<Badge variant="outline">{version}</Badge>
|
||||||
</div>
|
<Badge variant="outline" className="uppercase">{platform}</Badge>
|
||||||
|
{asset.downloaded ? (
|
||||||
{/* ISO Asset */}
|
<Badge variant="success" className="flex items-center gap-1">
|
||||||
<div>
|
<CheckCircle className="h-3 w-3" />
|
||||||
<h4 className="font-medium mb-4">ISO Image</h4>
|
Downloaded
|
||||||
{isLoading ? (
|
</Badge>
|
||||||
<div className="flex items-center justify-center py-8">
|
) : (
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
<Badge variant="secondary" className="flex items-center gap-1">
|
||||||
</div>
|
<AlertCircle className="h-3 w-3" />
|
||||||
) : isoAssets.length === 0 ? (
|
Missing
|
||||||
<Card className="p-8 text-center">
|
</Badge>
|
||||||
<Disc className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
)}
|
||||||
<h3 className="text-lg font-medium mb-2">No ISO Available</h3>
|
|
||||||
<p className="text-muted-foreground mb-4">
|
|
||||||
Download a Talos ISO to get started with USB boot.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => handleDownload('iso')} disabled={downloadAsset.isPending}>
|
|
||||||
{downloadAsset.isPending ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
||||||
) : (
|
|
||||||
<Download className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
Download ISO
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{isoAssets.map((asset) => (
|
|
||||||
<Card key={asset.type} className="p-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="p-2 bg-muted rounded-lg">{getAssetIcon(asset.type)}</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h5 className="font-medium capitalize">Talos ISO</h5>
|
|
||||||
{getStatusBadge(asset.status)}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground space-y-1">
|
|
||||||
{asset.version && <div>Version: {asset.version}</div>}
|
|
||||||
{asset.size && <div>Size: {asset.size}</div>}
|
|
||||||
{asset.path && (
|
|
||||||
<div className="font-mono text-xs truncate">{asset.path}</div>
|
|
||||||
)}
|
|
||||||
{asset.error && (
|
|
||||||
<div className="text-red-500">{asset.error}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{asset.status !== 'available' && asset.status !== 'downloading' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDownload(asset.type as PxeAssetType)}
|
|
||||||
disabled={
|
|
||||||
downloadAsset.isPending || downloadingType === asset.type
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{downloadingType === asset.type ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Download className="h-4 w-4 mr-1" />
|
|
||||||
Download
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{asset.status === 'available' && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
// Download the ISO file from Wild Central to user's computer
|
|
||||||
if (asset.path && currentInstance) {
|
|
||||||
window.location.href = `/api/v1/instances/${currentInstance}/pxe/assets/iso`;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4 mr-1" />
|
|
||||||
Download to Computer
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => handleDelete(asset.type as PxeAssetType)}
|
|
||||||
disabled={deleteAsset.isPending}
|
|
||||||
>
|
|
||||||
{deleteAsset.isPending ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className="text-sm text-muted-foreground space-y-1">
|
||||||
))}
|
<div className="font-mono text-xs truncate">
|
||||||
</div>
|
Schematic: {asset.schematic_id}
|
||||||
)}
|
</div>
|
||||||
</div>
|
{asset.size && (
|
||||||
|
<div>Size: {(asset.size / 1024 / 1024).toFixed(2)} MB</div>
|
||||||
{/* Instructions Card */}
|
)}
|
||||||
<Card className="p-6 bg-muted/50">
|
</div>
|
||||||
<h4 className="font-medium mb-3 flex items-center gap-2">
|
</div>
|
||||||
<Usb className="h-5 w-5" />
|
{asset.downloaded && (
|
||||||
Next Steps
|
<div className="flex gap-2">
|
||||||
</h4>
|
<Button
|
||||||
<ol className="space-y-2 text-sm text-muted-foreground list-decimal list-inside">
|
size="sm"
|
||||||
<li>Download the ISO image above</li>
|
variant="outline"
|
||||||
<li>Download a USB writing tool (e.g., Balena Etcher, Rufus, or dd)</li>
|
onClick={() => {
|
||||||
<li>Write the ISO to a USB drive (minimum 2GB)</li>
|
window.location.href = assetsApi.getAssetUrl(asset.schematic_id, 'iso');
|
||||||
<li>Boot your target computer from the USB drive</li>
|
}}
|
||||||
<li>Follow the Talos installation process</li>
|
>
|
||||||
</ol>
|
<Download className="h-4 w-4 mr-1" />
|
||||||
</Card>
|
Download to Computer
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
onClick={() => handleDelete(asset.schematic_id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Instructions Card */}
|
||||||
|
<Card className="p-6 bg-muted/50">
|
||||||
|
<h4 className="font-medium mb-3 flex items-center gap-2">
|
||||||
|
<Usb className="h-5 w-5" />
|
||||||
|
Next Steps
|
||||||
|
</h4>
|
||||||
|
<ol className="space-y-2 text-sm text-muted-foreground list-decimal list-inside">
|
||||||
|
<li>Get your schematic ID from Talos Image Factory</li>
|
||||||
|
<li>Download the ISO image using the form above</li>
|
||||||
|
<li>Download a USB writing tool (e.g., Balena Etcher, Rufus, or dd)</li>
|
||||||
|
<li>Write the ISO to a USB drive (minimum 2GB)</li>
|
||||||
|
<li>Boot your target computer from the USB drive</li>
|
||||||
|
<li>Follow the Talos installation process</li>
|
||||||
|
</ol>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate, Link } from 'react-router';
|
||||||
import { useInstanceContext } from '../../hooks/useInstanceContext';
|
import { useInstanceContext } from '../../hooks/useInstanceContext';
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../../components/ui/card';
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Server } from 'lucide-react';
|
import { Server, Usb, HardDrive, CloudLightning } from 'lucide-react';
|
||||||
|
|
||||||
export function LandingPage() {
|
export function LandingPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -16,25 +16,70 @@ export function LandingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||||
<Card className="w-full max-w-md">
|
<div className="container max-w-4xl px-4">
|
||||||
<CardHeader className="text-center">
|
<div className="text-center mb-8">
|
||||||
<CardTitle className="text-2xl">Wild Cloud</CardTitle>
|
<div className="flex items-center justify-center gap-3 mb-4">
|
||||||
<CardDescription>
|
<CloudLightning className="h-12 w-12 text-primary" />
|
||||||
Select an instance to manage your cloud infrastructure
|
<h1 className="text-4xl font-bold">Wild Cloud</h1>
|
||||||
</CardDescription>
|
</div>
|
||||||
</CardHeader>
|
<p className="text-lg text-muted-foreground">
|
||||||
<CardContent className="space-y-4">
|
Manage your cloud infrastructure with ease
|
||||||
<Button
|
</p>
|
||||||
onClick={handleSelectInstance}
|
</div>
|
||||||
className="w-full"
|
|
||||||
size="lg"
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
>
|
<Card>
|
||||||
<Server className="mr-2 h-5 w-5" />
|
<CardHeader className="text-center">
|
||||||
{currentInstance ? `Continue to ${currentInstance}` : 'Go to Default Instance'}
|
<CardTitle className="text-xl">Cloud Instance</CardTitle>
|
||||||
</Button>
|
<CardDescription>
|
||||||
</CardContent>
|
Manage your Wild Cloud instance
|
||||||
</Card>
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Button
|
||||||
|
onClick={handleSelectInstance}
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Server className="mr-2 h-5 w-5" />
|
||||||
|
{currentInstance ? `Continue to ${currentInstance}` : 'Go to Default Instance'}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle className="text-xl">Boot Assets</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Download Talos installation media
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<Link to="/iso" className="block">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Usb className="mr-2 h-5 w-5" />
|
||||||
|
ISO / USB Boot
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/pxe" className="block">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<HardDrive className="mr-2 h-5 w-5" />
|
||||||
|
PXE Network Boot
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,23 @@ import { InfrastructurePage } from './pages/InfrastructurePage';
|
|||||||
import { ClusterPage } from './pages/ClusterPage';
|
import { ClusterPage } from './pages/ClusterPage';
|
||||||
import { AppsPage } from './pages/AppsPage';
|
import { AppsPage } from './pages/AppsPage';
|
||||||
import { AdvancedPage } from './pages/AdvancedPage';
|
import { AdvancedPage } from './pages/AdvancedPage';
|
||||||
|
import { AssetsIsoPage } from './pages/AssetsIsoPage';
|
||||||
|
import { AssetsPxePage } from './pages/AssetsPxePage';
|
||||||
|
|
||||||
export const routes: RouteObject[] = [
|
export const routes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <LandingPage />,
|
element: <LandingPage />,
|
||||||
},
|
},
|
||||||
|
// Centralized asset routes (not under instance context)
|
||||||
|
{
|
||||||
|
path: '/iso',
|
||||||
|
element: <AssetsIsoPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/pxe',
|
||||||
|
element: <AssetsPxePage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/instances/:instanceId',
|
path: '/instances/:instanceId',
|
||||||
element: <InstanceLayout />,
|
element: <InstanceLayout />,
|
||||||
|
|||||||
42
src/services/api/assets.ts
Normal file
42
src/services/api/assets.ts
Normal file
@@ -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<AssetListResponse> => {
|
||||||
|
const response = await apiClient.get('/api/v1/assets');
|
||||||
|
return response as AssetListResponse;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get schematic details
|
||||||
|
get: async (schematicId: string): Promise<Schematic> => {
|
||||||
|
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<AssetStatusResponse> => {
|
||||||
|
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 };
|
||||||
|
},
|
||||||
|
};
|
||||||
58
src/services/api/hooks/useAssets.ts
Normal file
58
src/services/api/hooks/useAssets.ts
Normal file
@@ -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'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export { operationsApi } from './operations';
|
|||||||
export { dnsmasqApi } from './dnsmasq';
|
export { dnsmasqApi } from './dnsmasq';
|
||||||
export { utilitiesApi } from './utilities';
|
export { utilitiesApi } from './utilities';
|
||||||
export { pxeApi } from './pxe';
|
export { pxeApi } from './pxe';
|
||||||
|
export { assetsApi } from './assets';
|
||||||
|
|
||||||
// React Query hooks
|
// React Query hooks
|
||||||
export { useInstance, useInstanceOperations, useInstanceClusterHealth } from './hooks/useInstance';
|
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 { useClusterHealth, useClusterStatus, useClusterNodes } from './hooks/useCluster';
|
||||||
export { useDashboardToken, useClusterVersions, useNodeIPs, useControlPlaneIP, useCopySecret } from './hooks/useUtilities';
|
export { useDashboardToken, useClusterVersions, useNodeIPs, useControlPlaneIP, useCopySecret } from './hooks/useUtilities';
|
||||||
export { usePxeAssets, useDownloadPxeAsset, useDeletePxeAsset } from './hooks/usePxeAssets';
|
export { usePxeAssets, useDownloadPxeAsset, useDeletePxeAsset } from './hooks/usePxeAssets';
|
||||||
|
export { useAssetList, useAsset, useAssetStatus, useDownloadAsset } from './hooks/useAssets';
|
||||||
|
|||||||
38
src/services/api/types/asset.ts
Normal file
38
src/services/api/types/asset.ts
Normal file
@@ -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<string, Asset>;
|
||||||
|
complete: boolean;
|
||||||
|
}
|
||||||
@@ -7,3 +7,4 @@ export * from './cluster';
|
|||||||
export * from './app';
|
export * from './app';
|
||||||
export * from './service';
|
export * from './service';
|
||||||
export * from './pxe';
|
export * from './pxe';
|
||||||
|
export * from './asset';
|
||||||
|
|||||||
Reference in New Issue
Block a user