diff --git a/docs/managing-instances.md b/docs/managing-instances.md index d67c0c8..be5e768 100644 --- a/docs/managing-instances.md +++ b/docs/managing-instances.md @@ -42,6 +42,7 @@ Each instance is displayed as a card showing: ![Create Instance Screenshot](images/create_instance.png) 1. Click the **"Create Instance"** button on the dashboard +2. *Optional*: Click **"Import"** in the dialog header to load a previously exported configuration 2. Enter a unique **Name** for your instance (only required field) 3. **Select Target Node**: Choose which node to deploy the instance to from the dropdown 4. **Choose Backend Type**: @@ -219,6 +220,12 @@ curl -X PUT http://localhost:8080/api/v1/instances/{name} \ Configuration changes require restarting the instance to take effect. +## Export Instance + +**Via Web UI** +1. Click the **"More actions"** button (three dots) on an instance card +2. Click **"Export"** to download the instance configuration as a JSON file + ## View Logs **Via Web UI** diff --git a/pkg/backends/backend.go b/pkg/backends/backend.go index db18920..022e778 100644 --- a/pkg/backends/backend.go +++ b/pkg/backends/backend.go @@ -93,6 +93,8 @@ func (o *Options) MarshalJSON() ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to marshal backend options: %w", err) } + // Create a new map to avoid concurrent map writes + aux.BackendOptions = make(map[string]any) if err := json.Unmarshal(optionsData, &aux.BackendOptions); err != nil { return nil, fmt.Errorf("failed to unmarshal backend options to map: %w", err) } diff --git a/webui/src/components/BackendFormField.tsx b/webui/src/components/BackendFormField.tsx index e66fedd..bb49fc1 100644 --- a/webui/src/components/BackendFormField.tsx +++ b/webui/src/components/BackendFormField.tsx @@ -12,13 +12,13 @@ interface BackendFormFieldProps { const BackendFormField: React.FC = ({ fieldKey, value, onChange }) => { // Get configuration for basic fields, or use field name for advanced fields - const config = basicBackendFieldsConfig[fieldKey as string] || { label: fieldKey } + const config = basicBackendFieldsConfig[fieldKey] || { label: fieldKey } // Get type from Zod schema const fieldType = getBackendFieldType(fieldKey) const handleChange = (newValue: string | number | boolean | string[] | undefined) => { - onChange(fieldKey as string, newValue) + onChange(fieldKey, newValue) } const renderField = () => { diff --git a/webui/src/components/InstanceCard.tsx b/webui/src/components/InstanceCard.tsx index a867dd3..657f1ab 100644 --- a/webui/src/components/InstanceCard.tsx +++ b/webui/src/components/InstanceCard.tsx @@ -2,12 +2,13 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { Instance } from "@/types/instance"; -import { Edit, FileText, Play, Square, Trash2, MoreHorizontal } from "lucide-react"; +import { Edit, FileText, Play, Square, Trash2, MoreHorizontal, Download } from "lucide-react"; import LogsDialog from "@/components/LogDialog"; import HealthBadge from "@/components/HealthBadge"; import BackendBadge from "@/components/BackendBadge"; import { useState } from "react"; import { useInstanceHealth } from "@/hooks/useInstanceHealth"; +import { instancesApi } from "@/lib/api"; interface InstanceCardProps { instance: Instance; @@ -52,6 +53,40 @@ function InstanceCard({ setIsLogsOpen(true); }; + const handleExport = () => { + void (async () => { + try { + // Fetch the most up-to-date instance data from the backend + const instanceData = await instancesApi.get(instance.name); + + // Remove docker_enabled as it's a computed field, not persisted to disk + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { docker_enabled, ...persistedData } = instanceData; + + // Convert to JSON string with pretty formatting (matching backend format) + const jsonString = JSON.stringify(persistedData, null, 2); + + // Create a blob and download link + const blob = new Blob([jsonString], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `${instance.name}.json`; + + // Trigger download + document.body.appendChild(link); + link.click(); + + // Cleanup + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + console.error("Failed to export instance:", error); + alert(`Failed to export instance: ${error instanceof Error ? error.message : "Unknown error"}`); + } + })(); + }; + const running = instance.status === "running"; return ( @@ -131,6 +166,18 @@ function InstanceCard({ Logs + + + )} + +
diff --git a/webui/src/components/InstanceList.tsx b/webui/src/components/InstanceList.tsx index fd258c5..7529b6a 100644 --- a/webui/src/components/InstanceList.tsx +++ b/webui/src/components/InstanceList.tsx @@ -56,9 +56,9 @@ function InstanceList({ editInstance }: InstanceListProps) { { void startInstance(instance.name) }} + stopInstance={() => { void stopInstance(instance.name) }} + deleteInstance={() => { void deleteInstance(instance.name) }} editInstance={editInstance} /> ))} diff --git a/webui/src/components/ParseCommandDialog.tsx b/webui/src/components/ParseCommandDialog.tsx index ba5075d..92482e5 100644 --- a/webui/src/components/ParseCommandDialog.tsx +++ b/webui/src/components/ParseCommandDialog.tsx @@ -54,7 +54,7 @@ const ParseCommandDialog: React.FC = ({ options = await backendsApi.vllm.parseCommand(command); break; default: - throw new Error(`Unsupported backend type: ${backendType}`); + throw new Error(`Unsupported backend type: ${String(backendType)}`); } onParsed(options);