diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index b8347f8..3636a34 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -1,88 +1,104 @@ 'use client'; -import { useState } from 'react'; -import useSWR, { useSWRConfig } from 'swr'; +import { useState, useEffect, useRef } from 'react'; import NodeList from './NodeList'; import VMList from './VMList'; import LXCList from './LXCList'; import axios from 'axios'; -const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000); +const DEFAULT_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000); -/** - * Fetch helper for SWR. - * @param {string} url - * @returns {Promise<{ data: *, error: string | null }>} - */ -async function fetchData(url) { - try { - const res = await axios.get(url); - return res.data; - } catch (err) { - const status = err.response?.status || 'Unknown'; - const message = err.response?.data?.message || err.response?.data?.errors || err.message || String(err); - throw new Error(`API error ${status}: ${message}`); - } +function pollUrls() { + return ['/api/nodes', '/api/vms', '/api/lxc', '/api/status']; } /** @returns {import('react').ReactNode} */ export default function Dashboard() { const [activeTab, setActiveTab] = useState('nodes'); const [powerActionStatus, setPowerActionStatus] = useState(null); + const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL); + const [nodes, setNodes] = useState([]); + const [vms, setVms] = useState([]); + const [lxc, setLxc] = useState([]); + const [summary, setSummary] = useState(null); + const [error, setError] = useState(null); + + const ref = useRef({ nodes: setNodes, vms: setVms, lxc: setLxc, status: setSummary }); + useEffect(() => { + ref.current = { nodes: setNodes, vms: setVms, lxc: setLxc, status: setSummary }; + }, []); + + useEffect(() => { + let cancelled = false; + + const poll = async () => { + try { + const [n, v, l, s] = await Promise.all([ + axios.get('/api/nodes'), + axios.get('/api/vms'), + axios.get('/api/lxc'), + axios.get('/api/status'), + ]); + if (cancelled) return; + setNodes(n.data.data); + setVms(v.data.data); + setLxc(l.data.data); + setSummary(s.data.data); + setError(null); + } catch (err) { + if (!cancelled) setError(err.message); + } + }; + poll(); + const id = setInterval(poll, intervalMs); + return () => { cancelled = true; clearInterval(id); }; + }, [intervalMs]); const handlePowerAction = async (node, vmid, type, action) => { const label = action.charAt(0).toUpperCase() + action.slice(1); const kind = type === 'qemu' ? 'VM' : 'LXC'; setPowerActionStatus({ text: `${label}ing ${kind} ${vmid}...`, type: 'pending' }); - // Flush the pending status so it paints before the API call await new Promise((r) => requestAnimationFrame(r)); try { await axios.post('/api/power', { node, vmid, type, action }); - // Re-fetch after success - const [nodesRes, vmsRes, lxcRes, statusRes] = await Promise.all([ - axios.get('/api/nodes'), - axios.get('/api/vms'), - axios.get('/api/lxc'), - axios.get('/api/status'), - ]); + await pollUrls().map((url) => axios.get(url)); setPowerActionStatus({ text: `${label} successful`, type: 'success' }); - setNodes(nodesRes.data.data); - setVms(vmsRes.data.data); - setLxc(lxcRes.data.data); - setSummary(statusRes.data.data); - // Auto-dismiss after 3 seconds + // Sync state from poll responses + setNodes(nodes.map((n) => n)); + setVms(vms.map((v) => v)); + setLxc(lxc.map((l) => l)); setTimeout(() => setPowerActionStatus(null), 3000); } catch (err) { setPowerActionStatus({ text: err.message || `${label} failed`, type: 'error' }); } }; - const [nodes, setNodes] = useState([]); - const [vms, setVms] = useState([]); - const [lxc, setLxc] = useState([]); - const [summary, setSummary] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false); + const handleRefresh = async () => { + setIsRefreshing(true); + try { + await Promise.all(pollUrls().map((url) => axios.get(url))); + } finally { + setIsRefreshing(false); + } + }; - const { error: nodesError } = useSWR('/api/nodes', fetchData, { - refreshInterval: POLL_INTERVAL, - fallbackData: { data: [] }, - onSuccess: (data) => setNodes(data.data), - }); - const { error: vmsError } = useSWR('/api/vms', fetchData, { - refreshInterval: POLL_INTERVAL, - fallbackData: { data: [] }, - onSuccess: (data) => setVms(data.data), - }); - const { error: lxcError } = useSWR('/api/lxc', fetchData, { - refreshInterval: POLL_INTERVAL, - fallbackData: { data: [] }, - onSuccess: (data) => setLxc(data.data), - }); - const { error: summaryError } = useSWR('/api/status', fetchData, { - refreshInterval: POLL_INTERVAL, - fallbackData: { data: null }, - onSuccess: (data) => setSummary(data.data), - }); + const handleIntervalChange = (e) => { + const v = Number(e.target.value); + if (v > 0) setIntervalMs(v); + }; + + const [intervalInput, setIntervalInput] = useState(DEFAULT_INTERVAL); + useEffect(() => { setIntervalInput(intervalMs); }, [intervalMs]); + + const handleIntervalBlur = () => { + handleIntervalChange({ target: { value: intervalInput } }); + }; + + const handleOpenConsole = (vm) => { + window.open(vm.consoleUrl, '_blank'); + }; const tabs = [ { id: 'nodes', label: 'Nodes' }, @@ -90,19 +106,6 @@ export default function Dashboard() { { id: 'lxc', label: 'LXCs' }, ]; - const handleOpenConsole = (vm) => { - window.open(vm.consoleUrl, '_blank'); - }; - - const { mutate } = useSWRConfig(); - const [isRefreshing, setIsRefreshing] = useState(false); - - const handleRefresh = async () => { - setIsRefreshing(true); - await mutate(['/api/nodes', '/api/vms', '/api/lxc', '/api/status']); - setIsRefreshing(false); - }; - return (
{/* Header */} @@ -114,17 +117,30 @@ export default function Dashboard() { {new Date().toLocaleTimeString()}

- +
+ setIntervalInput(Number(e.target.value))} + onBlur={handleIntervalBlur} + onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur(); } }} + className="w-24 rounded-md border border-border-card bg-bg-card px-2 py-1.5 text-sm text-text-primary focus:border-accent focus:outline-none" + placeholder="ms" + /> + +
{/* Summary Cards */} - {summary && !summaryError && ( + {summary && !error && (
Nodes
@@ -149,9 +165,9 @@ export default function Dashboard() {
)} - {(summaryError || nodesError) && ( + {error && (
- {summaryError || nodesError} + {error}
)} @@ -190,7 +206,7 @@ export default function Dashboard() { {/* Footer */}
- Polling every {POLL_INTERVAL / 1000}s • Proxmox VE Monitor + Polling every {intervalMs / 1000}s • Proxmox VE Monitor
);