fix(realtime): sync instance/workspace list state without manual refresh
- Invalidate workspace list cache after assigning servers/users so server/user counts on workspace cards update immediately. - Invalidate node/VM caches after workspace server/user assignment so the Nodes page reflects visibility changes right away. - Soft-delete the cached VM/LXC row and broadcast vm_deleted the moment a VM/container is destroyed, instead of waiting for the 10s background sync_vm_cache tick. - Invalidate the VM list on the frontend when a deploy task completes, since the backend already persists the new instance to cache synchronously before marking the task done.
This commit is contained in:
@@ -951,6 +951,13 @@ async def delete_vm(
|
||||
# Удаляем VM
|
||||
result = client.delete_vm(node, vmid)
|
||||
if result:
|
||||
# Помечаем запись в локальном кэше удалённой сразу, не дожидаясь
|
||||
# фонового sync_vm_cache (тикает раз в 10с) — иначе VM ещё
|
||||
# какое-то время висит в списке после реального удаления.
|
||||
try:
|
||||
soft_delete_vm_instance(db, server_id, vmid)
|
||||
except Exception as _sde:
|
||||
logger.warning(f"Failed to soft-delete cached VM {vmid}: {_sde}")
|
||||
# Освобождаем IP в IPAM (если есть)
|
||||
try:
|
||||
ipam = IPAMService(db)
|
||||
@@ -1003,6 +1010,11 @@ async def delete_vm(
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} deleted VM {vmid} on {server.name}")
|
||||
try:
|
||||
from ...websocket_manager import broadcast_event
|
||||
broadcast_event("vm_deleted", server_id=server_id, vmid=vmid, node=node, name=vm_name, vm_type="qemu")
|
||||
except Exception as _we:
|
||||
logger.warning(f"Failed to broadcast vm_deleted for VM {vmid}: {_we}")
|
||||
return JSONResponse(content={
|
||||
"status": "success",
|
||||
"message": f"VM {vmid} удалена",
|
||||
@@ -1312,6 +1324,13 @@ async def delete_container(
|
||||
# Удаляем контейнер
|
||||
result = client.delete_container(node, vmid)
|
||||
if result:
|
||||
# Помечаем запись в локальном кэше удалённой сразу, не дожидаясь
|
||||
# фонового sync_vm_cache (тикает раз в 10с) — иначе контейнер ещё
|
||||
# какое-то время висит в списке после реального удаления.
|
||||
try:
|
||||
soft_delete_vm_instance(db, server_id, vmid)
|
||||
except Exception as _sde:
|
||||
logger.warning(f"Failed to soft-delete cached container {vmid}: {_sde}")
|
||||
# Освобождаем IP в IPAM (если есть)
|
||||
try:
|
||||
ipam = IPAMService(db)
|
||||
@@ -1359,6 +1378,11 @@ async def delete_container(
|
||||
)
|
||||
|
||||
logger.info(f"User {current_user.username} deleted container {vmid} on {server.name}")
|
||||
try:
|
||||
from ...websocket_manager import broadcast_event
|
||||
broadcast_event("vm_deleted", server_id=server_id, vmid=vmid, node=node, name=container_name, vm_type="lxc")
|
||||
except Exception as _we:
|
||||
logger.warning(f"Failed to broadcast vm_deleted for container {vmid}: {_we}")
|
||||
return JSONResponse(content={
|
||||
"status": "success",
|
||||
"message": f"Контейнер {vmid} удалён",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -80,7 +81,7 @@ import {
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { StatusDot } from '@/components/shared/status-dot';
|
||||
import { ColumnFilter, multiSelectFilter } from '@/components/shared/column-filter';
|
||||
import { useVirtualMachines, useBulkOperation, usePowerAction, useVMStatusSync, useInstancesMetricsSync, useBulkTasksSync } from '@/hooks/use-instances';
|
||||
import { useVirtualMachines, useBulkOperation, usePowerAction, useVMStatusSync, useInstancesMetricsSync, useBulkTasksSync, vmKeys } from '@/hooks/use-instances';
|
||||
import { useServers } from '@/hooks/use-nodes';
|
||||
import { useProfile } from '@/hooks/use-settings';
|
||||
import { formatBytes, vmTypeLabel, formatUptime, formatVmConfig } from '@/lib/format';
|
||||
@@ -291,6 +292,7 @@ export default function InstancesPage() {
|
||||
}, [t]);
|
||||
|
||||
// ── Deploy task polling ──────────────────────────────────────────
|
||||
const qc = useQueryClient();
|
||||
const { tasks: deployTasks, updateTask, removeTask } = useDeployTasksStore();
|
||||
const activeTasks = deployTasks.filter((t) => t.status === 'pending' || t.status === 'running');
|
||||
|
||||
@@ -318,6 +320,10 @@ export default function InstancesPage() {
|
||||
{ description: data.error_message, duration: 15000 },
|
||||
);
|
||||
}
|
||||
// Новая VM уже сохранена в кэше бэкендом — подтягиваем список сразу,
|
||||
// не дожидаясь фонового vm_created от 10-секундного sync_vm_cache.
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
setTimeout(() => removeTask(task.id), 5000);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import type { Workspace } from '@/types';
|
||||
import { nodeKeys } from './use-nodes';
|
||||
import { vmKeys } from './use-instances';
|
||||
|
||||
export const workspaceKeys = {
|
||||
all: ['workspaces'] as const,
|
||||
@@ -61,7 +63,14 @@ export function useUpdateWorkspaceServers() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ids }: { id: number; ids: number[] }) =>
|
||||
apiClient.post<WorkspaceDetail>(`/api/workspaces/${id}/servers`, { ids }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['workspace'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.all });
|
||||
qc.invalidateQueries({ queryKey: ['workspace'] });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.topology });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,6 +79,13 @@ export function useUpdateWorkspaceUsers() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ids }: { id: number; ids: number[] }) =>
|
||||
apiClient.post<WorkspaceDetail>(`/api/workspaces/${id}/users`, { ids }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['workspace'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.all });
|
||||
qc.invalidateQueries({ queryKey: ['workspace'] });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.topology });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user