fix: instant power action refresh with optimistic UI update
Update VM/LXC cards immediately on click instead of waiting for API response or polling interval. On failure, revert the optimistic update. Background polling reconciles after 5 seconds. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
import NodeList from './NodeList';
|
||||
import VMList from './VMList';
|
||||
import LXCList from './LXCList';
|
||||
import axios from 'axios';
|
||||
import { SWRConfig } from 'swr';
|
||||
import ConsolePanel from './ConsolePanel';
|
||||
|
||||
const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000);
|
||||
@@ -17,8 +16,6 @@ const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000)
|
||||
* @returns {Promise<{ data: *, error: string | null }>}
|
||||
*/
|
||||
async function fetchData(url) {
|
||||
|
||||
|
||||
try {
|
||||
const res = await axios.get(url);
|
||||
return res.data;
|
||||
@@ -34,24 +31,36 @@ export default function Dashboard() {
|
||||
const [activeTab, setActiveTab] = useState('nodes');
|
||||
const [activeConsole, setActiveConsole] = useState(null);
|
||||
const [powerActionStatus, setPowerActionStatus] = useState(null);
|
||||
// Optimistic local state — updated before API response
|
||||
const [vmsOptimistic, setVmsOptimistic] = useState(null);
|
||||
const [lxcOptimistic, setLxcOptimistic] = useState(null);
|
||||
|
||||
/**
|
||||
* @param {string} node
|
||||
* @param {number} vmid
|
||||
* @param {string} type
|
||||
* @param {string} action
|
||||
*/
|
||||
const handlePowerAction = async (node, vmid, type, action) => {
|
||||
setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)}ing ${type === 'qemu' ? 'VM' : 'LXC'} ${vmid}...`, type: 'pending' });
|
||||
|
||||
// Optimistic UI update — change happens instantly
|
||||
if (type === 'qemu') {
|
||||
setVmsOptimistic((prev) =>
|
||||
(prev ?? []).map((vm) => vm.vmid === vmid ? { ...vm, status: action === 'stop' || action === 'restart' ? 'stopped' : 'running' } : vm),
|
||||
);
|
||||
} else {
|
||||
setLxcOptimistic((prev) =>
|
||||
(prev ?? []).map((lxc) => lxc.vmid === vmid ? { ...lxc, status: action === 'stop' || action === 'restart' ? 'stopped' : 'running' } : lxc),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post('/api/power', { node, vmid, type, action });
|
||||
// Immediately refetch all data instead of waiting for polling interval
|
||||
await mutate('/api/nodes');
|
||||
await mutate('/api/vms');
|
||||
await mutate('/api/lxc');
|
||||
await mutate('/api/status');
|
||||
setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)} successful`, type: 'success' });
|
||||
// Clear optimistic after successful API call — next refetch will reconcile
|
||||
setTimeout(() => {
|
||||
setVmsOptimistic(null);
|
||||
setLxcOptimistic(null);
|
||||
}, 5000);
|
||||
} catch (err) {
|
||||
// Revert optimistic update on failure
|
||||
setVmsOptimistic(null);
|
||||
setLxcOptimistic(null);
|
||||
setPowerActionStatus({ text: err.message || `${action.charAt(0).toUpperCase() + action.slice(1)} failed`, type: 'error' });
|
||||
}
|
||||
};
|
||||
@@ -69,9 +78,10 @@ export default function Dashboard() {
|
||||
refreshInterval: POLL_INTERVAL,
|
||||
});
|
||||
|
||||
// Use optimistic data when available, fall back to SWR data
|
||||
const nodes = nodesData?.data ?? [];
|
||||
const vms = vmsData?.data ?? [];
|
||||
const lxc = lxcData?.data ?? [];
|
||||
const vms = vmsOptimistic ?? (vmsData?.data ?? []);
|
||||
const lxc = lxcOptimistic ?? (lxcData?.data ?? []);
|
||||
const summary = summaryData?.data ?? null;
|
||||
|
||||
const tabs = [
|
||||
|
||||
Reference in New Issue
Block a user