All checks were successful
Test / frontend-tests (pull_request) Successful in 1m44s
Test / frontend-typecheck (pull_request) Successful in 1m57s
PR Review Automation / review (pull_request) Successful in 4m19s
Test / rust-fmt-check (pull_request) Successful in 12m57s
Test / rust-clippy (pull_request) Successful in 14m41s
Test / rust-tests (pull_request) Successful in 16m43s
- Replace hardcoded dummy data in VMs, Containers, Storage, Backup, and Firewall pages with live API calls; show empty-state UI when no clusters are configured - Add list_proxmox_containers backend command (LXC via cluster/resources) and register it in the Tauri handler and frontend proxmoxClient.ts - Fix add_proxmox_cluster to store credentials without requiring a live Proxmox connection; persist username in DB (migration 034); update list/get queries to read username column from new schema - Replace alert() in RemotesPage with toast.error() + rethrow so errors surface correctly in Tauri WebView - Replace tauri-plugin-updater with direct Gitea HTTP API call for update checks; use tauri-plugin-opener for browser launch; Updater UI now shows current/latest version and release notes - Add gogs.tftsr.com to CSP connect-src - Fix all 74 pre-existing ESLint no-explicit-any warnings in proxmoxClient.ts; remove stale eslint-disable directive in ACLPage.tsx - All checks pass: cargo fmt, clippy -D warnings, 411 Rust tests, tsc --noEmit, eslint --max-warnings 0, 386 frontend tests
120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Button } from '@/components/ui/index';
|
|
import { Input } from '@/components/ui/index';
|
|
import { RefreshCw } from 'lucide-react';
|
|
import { BackupJobList } from '@/components/Proxmox';
|
|
import { listProxmoxClusters, listProxmoxBackupJobs } from '@/lib/proxmoxClient';
|
|
import type { ClusterInfo } from '@/lib/domain';
|
|
import { toast } from 'sonner';
|
|
|
|
export function ProxmoxBackupPage() {
|
|
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
|
|
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
|
|
const [nodeInputValue, setNodeInputValue] = useState('localhost');
|
|
const [nodeId, setNodeId] = useState('localhost');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const [jobs, setJobs] = useState<any[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
listProxmoxClusters()
|
|
.then((cls) => {
|
|
setClusters(cls);
|
|
if (cls.length > 0) setSelectedClusterId(cls[0].id);
|
|
})
|
|
.catch((err) => {
|
|
console.error('Failed to load clusters:', err);
|
|
toast.error('Failed to load clusters');
|
|
});
|
|
}, []);
|
|
|
|
const loadJobs = useCallback(async (clusterId: string, nId: string) => {
|
|
if (!clusterId) return;
|
|
setIsLoading(true);
|
|
try {
|
|
const data = await listProxmoxBackupJobs(clusterId, nId);
|
|
setJobs(data);
|
|
} catch (err) {
|
|
console.error('Failed to load backup jobs:', err);
|
|
toast.error('Failed to load backup jobs');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (selectedClusterId) loadJobs(selectedClusterId, nodeId);
|
|
}, [selectedClusterId, nodeId, loadJobs]);
|
|
|
|
const applyNodeId = () => {
|
|
setNodeId(nodeInputValue.trim() || 'localhost');
|
|
};
|
|
|
|
if (clusters.length === 0 && !isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Backup Jobs</h1>
|
|
<p className="text-muted-foreground">Manage Proxmox Backup Server jobs</p>
|
|
</div>
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p>No Proxmox clusters configured.</p>
|
|
<p className="text-sm mt-1">Add a remote connection first.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Backup Jobs</h1>
|
|
<p className="text-muted-foreground">Manage Proxmox Backup Server jobs</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
{clusters.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted-foreground">Cluster:</span>
|
|
<select
|
|
className="text-sm border rounded px-2 py-1 bg-background"
|
|
value={selectedClusterId}
|
|
onChange={(e) => setSelectedClusterId(e.target.value)}
|
|
>
|
|
{clusters.map((c) => (
|
|
<option key={c.id} value={c.id}>{c.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted-foreground">Node:</span>
|
|
<Input
|
|
className="w-36 h-8 text-sm"
|
|
value={nodeInputValue}
|
|
onChange={(e) => setNodeInputValue(e.target.value)}
|
|
onKeyDown={(e) => { if (e.key === 'Enter') applyNodeId(); }}
|
|
placeholder="localhost"
|
|
/>
|
|
<Button variant="outline" size="sm" onClick={applyNodeId}>Apply</Button>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => loadJobs(selectedClusterId, nodeId)}
|
|
>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
<BackupJobList
|
|
jobs={jobs}
|
|
onRefresh={() => loadJobs(selectedClusterId, nodeId)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|