fix: rewrite power action handler — instant status feedback + reliable state
- Status message always shows immediately on click (pending -> success/error) - Auto-dismiss after 3 seconds on success - Use fallbackData to prevent lists going empty during fetch - Direct state sync on success to keep summary counts in sync Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -31,58 +31,59 @@ 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);
|
|
||||||
|
|
||||||
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' });
|
const label = action.charAt(0).toUpperCase() + action.slice(1);
|
||||||
|
const kind = type === 'qemu' ? 'VM' : 'LXC';
|
||||||
// Optimistic UI update — change happens instantly
|
setPowerActionStatus({ text: `${label}ing ${kind} ${vmid}...`, type: 'pending' });
|
||||||
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 });
|
||||||
setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)} successful`, type: 'success' });
|
// Re-fetch after success
|
||||||
// Clear optimistic after successful API call — next refetch will reconcile
|
const [nodesRes, vmsRes, lxcRes, statusRes] = await Promise.all([
|
||||||
setTimeout(() => {
|
axios.get('/api/nodes'),
|
||||||
setVmsOptimistic(null);
|
axios.get('/api/vms'),
|
||||||
setLxcOptimistic(null);
|
axios.get('/api/lxc'),
|
||||||
}, 5000);
|
axios.get('/api/status'),
|
||||||
|
]);
|
||||||
|
setPowerActionStatus({ text: `${label} successful`, type: 'success' });
|
||||||
|
// Update local state so summary counts stay in sync until poll
|
||||||
|
setNodes(nodesRes.data.data);
|
||||||
|
setVms(vmsRes.data.data);
|
||||||
|
setLxc(lxcRes.data.data);
|
||||||
|
setSummary(statusRes.data.data);
|
||||||
|
// Auto-dismiss after 3 seconds
|
||||||
|
setTimeout(() => setPowerActionStatus(null), 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Revert optimistic update on failure
|
setPowerActionStatus({ text: err.message || `${label} failed`, type: 'error' });
|
||||||
setVmsOptimistic(null);
|
|
||||||
setLxcOptimistic(null);
|
|
||||||
setPowerActionStatus({ text: err.message || `${action.charAt(0).toUpperCase() + action.slice(1)} failed`, type: 'error' });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: nodesData, error: nodesError } = useSWR('/api/nodes', fetchData, {
|
const [nodes, setNodes] = useState([]);
|
||||||
refreshInterval: POLL_INTERVAL,
|
const [vms, setVms] = useState([]);
|
||||||
});
|
const [lxc, setLxc] = useState([]);
|
||||||
const { data: vmsData, error: vmsError } = useSWR('/api/vms', fetchData, {
|
const [summary, setSummary] = useState(null);
|
||||||
refreshInterval: POLL_INTERVAL,
|
|
||||||
});
|
|
||||||
const { data: lxcData, error: lxcError } = useSWR('/api/lxc', fetchData, {
|
|
||||||
refreshInterval: POLL_INTERVAL,
|
|
||||||
});
|
|
||||||
const { data: summaryData, error: summaryError } = useSWR('/api/status', fetchData, {
|
|
||||||
refreshInterval: POLL_INTERVAL,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use optimistic data when available, fall back to SWR data
|
const { error: nodesError } = useSWR('/api/nodes', fetchData, {
|
||||||
const nodes = nodesData?.data ?? [];
|
refreshInterval: POLL_INTERVAL,
|
||||||
const vms = vmsOptimistic ?? (vmsData?.data ?? []);
|
fallbackData: { data: [] },
|
||||||
const lxc = lxcOptimistic ?? (lxcData?.data ?? []);
|
onSuccess: (data) => setNodes(data.data),
|
||||||
const summary = summaryData?.data ?? null;
|
});
|
||||||
|
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 tabs = [
|
const tabs = [
|
||||||
{ id: 'nodes', label: 'Nodes' },
|
{ id: 'nodes', label: 'Nodes' },
|
||||||
|
|||||||
Reference in New Issue
Block a user