diff --git a/src-tauri/src/commands/proxmox.rs b/src-tauri/src/commands/proxmox.rs index 90dd8eb4..b915aa9f 100644 --- a/src-tauri/src/commands/proxmox.rs +++ b/src-tauri/src/commands/proxmox.rs @@ -14,6 +14,22 @@ pub struct ClusterConnection { pub port: u16, } +/// Cluster info enriched with live connection health status +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClusterInfoWithHealth { + pub id: String, + pub name: String, + pub cluster_type: ClusterType, + pub url: String, + pub port: u16, + pub username: String, + pub created_at: String, + pub updated_at: String, + /// True if an active client object exists in the in-memory connection pool + pub connected: bool, +} + /// Add a Proxmox cluster #[tauri::command] pub async fn add_proxmox_cluster( @@ -119,10 +135,12 @@ pub async fn remove_proxmox_cluster(id: String, state: State<'_, AppState>) -> R Ok(()) } -/// List all Proxmox clusters +/// List all Proxmox clusters, annotated with live connection health #[tauri::command] -pub async fn list_proxmox_clusters(state: State<'_, AppState>) -> Result, String> { - let clusters = { +pub async fn list_proxmox_clusters( + state: State<'_, AppState>, +) -> Result, String> { + let db_clusters = { let db = state .db .lock() @@ -154,11 +172,31 @@ pub async fn list_proxmox_clusters(state: State<'_, AppState>) -> Result, _>>() - .map_err(|e| e.to_string()) + .collect::, _>>() + .map_err(|e| e.to_string())? }; - clusters + // Annotate each cluster with whether a live client exists in the connection pool + let live_clients = state.proxmox_clusters.lock().await; + let result = db_clusters + .into_iter() + .map(|c| { + let connected = live_clients.contains_key(&c.id); + ClusterInfoWithHealth { + id: c.id, + name: c.name, + cluster_type: c.cluster_type, + url: c.url, + port: c.port, + username: c.username, + created_at: c.created_at, + updated_at: c.updated_at, + connected, + } + }) + .collect(); + + Ok(result) } /// Get a specific Proxmox cluster diff --git a/src/lib/proxmoxClient.ts b/src/lib/proxmoxClient.ts index a9a8c178..b9ea54ad 100644 --- a/src/lib/proxmoxClient.ts +++ b/src/lib/proxmoxClient.ts @@ -877,10 +877,11 @@ export const listNetworkInterfaces = async ( // ─── Cluster Views (typed) ──────────────────────────────────────────────────── export interface ClusterView { - id: string; + view_id: string; name: string; - includes?: string[]; - excludes?: string[]; + description?: string; + layout?: string; + enabled?: boolean; } /** @@ -895,13 +896,15 @@ export const listClusterViews = async ( /** * Create a cluster view * @param clusterId - Cluster identifier - * @param config - View configuration + * @param viewId - View identifier + * @param name - View display name */ export const createClusterView = async ( clusterId: string, - config: Partial + viewId: string, + name: string ): Promise => - invoke("create_cluster_view", { clusterId, config }); + invoke("create_cluster_view", { clusterId, viewId, name }); /** * Delete a cluster view diff --git a/src/pages/Proxmox/NetworkPage.tsx b/src/pages/Proxmox/NetworkPage.tsx index b86a3e44..ac93bdde 100644 --- a/src/pages/Proxmox/NetworkPage.tsx +++ b/src/pages/Proxmox/NetworkPage.tsx @@ -1,43 +1,118 @@ -import React from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Button } from '@/components/ui/index'; -import { RefreshCw } from 'lucide-react'; +import { Badge } from '@/components/ui/index'; +import { RefreshCw, Network } from 'lucide-react'; +import { listNetworkInterfaces, listProxmoxClusters, NetworkInterface } from '@/lib/proxmoxClient'; export function ProxmoxNetworkPage() { + const [interfaces, setInterfaces] = useState([]); + const [clusterId, setClusterId] = useState(''); + const [nodeId] = useState('localhost'); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadInterfaces = useCallback(async (cId: string, nId: string) => { + if (!cId) return; + setLoading(true); + setError(null); + try { + const ifaces = await listNetworkInterfaces(cId, nId); + setInterfaces(ifaces); + } catch (e) { + setError(String(e)); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + listProxmoxClusters() + .then((cls) => { + if (cls.length > 0) { + setClusterId(cls[0].id); + void loadInterfaces(cls[0].id, nodeId); + } + }) + .catch(console.error); + }, [loadInterfaces, nodeId]); + return (

Network

-

Configure network interfaces and bridges

-
-
- +

Network interfaces and bridges

+
-
- - - Network Interfaces - - -
Network interface configuration coming soon
-
-
+ {error && ( +
+ {error} +
+ )} - - - Bridges - - -
Bridge configuration coming soon
-
-
-
+ + + Network Interfaces + + + {loading ? ( +
Loading...
+ ) : interfaces.length === 0 ? ( +
+ {clusterId ? 'No network interfaces found.' : 'No cluster configured.'} +
+ ) : ( +
+ {interfaces.map((iface, i) => ( +
+ +
+
+ {iface.iface} + {iface.type} + + {iface.active ? 'Active' : 'Inactive'} + + {iface.autostart && ( + Autostart + )} +
+ {(iface.address || iface.gateway) && ( +
+ {iface.address && ( + + {iface.address} + {iface.netmask ? `/${iface.netmask}` : ''} + + )} + {iface.gateway && ( + gw {iface.gateway} + )} +
+ )} + {iface.comments && ( +
+ {iface.comments} +
+ )} +
+
+ ))} +
+ )} +
+
); } diff --git a/src/pages/Proxmox/TasksPage.tsx b/src/pages/Proxmox/TasksPage.tsx index 730d1ffa..b45dad8a 100644 --- a/src/pages/Proxmox/TasksPage.tsx +++ b/src/pages/Proxmox/TasksPage.tsx @@ -1,45 +1,142 @@ -import React from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; import { Button } from '@/components/ui/index'; +import { Badge } from '@/components/ui/index'; import { RefreshCw } from 'lucide-react'; -import { ClusterOperationsList } from '@/components/Proxmox'; +import { listClusterTasks, listProxmoxClusters, ClusterTask } from '@/lib/proxmoxClient'; + +function taskBadgeVariant(exitstatus?: string): 'default' | 'destructive' | 'secondary' { + if (!exitstatus) return 'secondary'; + return exitstatus === 'OK' ? 'default' : 'destructive'; +} + +function taskBadgeLabel(exitstatus?: string): string { + if (!exitstatus) return 'running'; + return exitstatus; +} + +function formatTimestamp(epoch: number): string { + if (!epoch) return '-'; + return new Date(epoch * 1000).toLocaleString(); +} export function ProxmoxTasksPage() { + const [tasks, setTasks] = useState([]); + const [clusterId, setClusterId] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadTasks = useCallback(async (cId: string) => { + if (!cId) return; + setLoading(true); + setError(null); + try { + const t = await listClusterTasks(cId, 100); + setTasks(t); + } catch (e) { + setError(String(e)); + console.error(e); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + listProxmoxClusters() + .then((cls) => { + if (cls.length > 0) { + setClusterId(cls[0].id); + void loadTasks(cls[0].id); + } + }) + .catch(console.error); + }, [loadTasks]); + + const runningCount = tasks.filter((t) => !t.exitstatus).length; + const completedCount = tasks.filter((t) => t.exitstatus === 'OK').length; + const failedCount = tasks.filter( + (t) => t.exitstatus && t.exitstatus !== 'OK' + ).length; + return (
-

Tasks & Operations

-

Monitor cluster operations and tasks

-
-
- +

Tasks

+

Cluster task log and operations

+
-
+ {error && ( +
+ {error} +
+ )} + +
- - Task Summary - - -
Task summary widget coming soon
+ +
{runningCount}
+
Running
+
+
+ + +
{completedCount}
+
Completed
+
+
+ + +
{failedCount}
+
Failed
- Cluster Operations + Task Log - {}} - /> + {loading ? ( +
Loading tasks...
+ ) : tasks.length === 0 ? ( +
+ {clusterId ? 'No tasks found.' : 'No cluster configured.'} +
+ ) : ( +
+ {tasks.map((t, i) => ( +
+ + {taskBadgeLabel(t.exitstatus)} + + {t.type} + {t.node} + {t.user} + + {formatTimestamp(t.starttime)} + + + {t.upid} + +
+ ))} +
+ )}
diff --git a/src/pages/Proxmox/ViewsPage.tsx b/src/pages/Proxmox/ViewsPage.tsx new file mode 100644 index 00000000..9cfbc86d --- /dev/null +++ b/src/pages/Proxmox/ViewsPage.tsx @@ -0,0 +1,158 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index'; +import { Button } from '@/components/ui/index'; +import { Plus, Trash2, Eye } from 'lucide-react'; +import { + listClusterViews, + createClusterView, + deleteClusterView, + listProxmoxClusters, + ClusterView, +} from '@/lib/proxmoxClient'; + +export function ProxmoxViewsPage() { + const [views, setViews] = useState([]); + const [clusterId, setClusterId] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [newViewName, setNewViewName] = useState(''); + const [error, setError] = useState(null); + const [deleting, setDeleting] = useState(null); + + const loadViews = useCallback(async (cId: string) => { + if (!cId) return; + setError(null); + try { + const v = await listClusterViews(cId); + setViews(v); + } catch (e) { + setError(String(e)); + } + }, []); + + useEffect(() => { + listProxmoxClusters() + .then((cls) => { + if (cls.length > 0) { + setClusterId(cls[0].id); + void loadViews(cls[0].id); + } + }) + .catch(console.error); + }, [loadViews]); + + const handleCreate = async () => { + const trimmed = newViewName.trim(); + if (!trimmed || !clusterId) return; + setError(null); + try { + // Generate a simple ID from the name (lowercase, hyphenated) + const viewId = trimmed.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + await createClusterView(clusterId, viewId, trimmed); + setNewViewName(''); + setShowCreate(false); + void loadViews(clusterId); + } catch (e) { + setError(String(e)); + } + }; + + const handleDelete = async (viewId: string) => { + if (!clusterId) return; + setDeleting(viewId); + setError(null); + try { + await deleteClusterView(clusterId, viewId); + void loadViews(clusterId); + } catch (e) { + setError(String(e)); + } finally { + setDeleting(null); + } + }; + + return ( +
+
+
+

Views

+

Custom resource views and dashboards

+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {showCreate && ( + + + Create View + + + setNewViewName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') void handleCreate(); + if (e.key === 'Escape') setShowCreate(false); + }} + autoFocus + /> + + + + + )} + + {views.length === 0 && !showCreate ? ( + + + {clusterId ? 'No custom views configured.' : 'No cluster configured.'} + + + ) : ( +
+ {views.map((v) => ( + + +
+ +
+ {v.name} + {v.description && ( +

{v.description}

+ )} +
+
+ +
+
+ ))} +
+ )} +
+ ); +}