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';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR from 'swr';
|
||||||
import NodeList from './NodeList';
|
import NodeList from './NodeList';
|
||||||
import VMList from './VMList';
|
import VMList from './VMList';
|
||||||
import LXCList from './LXCList';
|
import LXCList from './LXCList';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { SWRConfig } from 'swr';
|
|
||||||
import ConsolePanel from './ConsolePanel';
|
import ConsolePanel from './ConsolePanel';
|
||||||
|
|
||||||
const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000);
|
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 }>}
|
* @returns {Promise<{ data: *, error: string | null }>}
|
||||||
*/
|
*/
|
||||||
async function fetchData(url) {
|
async function fetchData(url) {
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(url);
|
const res = await axios.get(url);
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -34,24 +31,36 @@ export default function Dashboard() {
|
|||||||
const [activeTab, setActiveTab] = useState('nodes');
|
const [activeTab, setActiveTab] = useState('nodes');
|
||||||
const [activeConsole, setActiveConsole] = useState(null);
|
const [activeConsole, setActiveConsole] = useState(null);
|
||||||
const [powerActionStatus, setPowerActionStatus] = 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) => {
|
const handlePowerAction = async (node, vmid, type, action) => {
|
||||||
setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)}ing ${type === 'qemu' ? 'VM' : 'LXC'} ${vmid}...`, type: 'pending' });
|
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 {
|
try {
|
||||||
await axios.post('/api/power', { node, vmid, type, action });
|
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' });
|
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) {
|
} 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' });
|
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,
|
refreshInterval: POLL_INTERVAL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use optimistic data when available, fall back to SWR data
|
||||||
const nodes = nodesData?.data ?? [];
|
const nodes = nodesData?.data ?? [];
|
||||||
const vms = vmsData?.data ?? [];
|
const vms = vmsOptimistic ?? (vmsData?.data ?? []);
|
||||||
const lxc = lxcData?.data ?? [];
|
const lxc = lxcOptimistic ?? (lxcData?.data ?? []);
|
||||||
const summary = summaryData?.data ?? null;
|
const summary = summaryData?.data ?? null;
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
|||||||
Reference in New Issue
Block a user