diff --git a/app/(main)/instance/_hooks/backups.ts b/app/(main)/instance/_hooks/backups.ts index 798472c..afe5c26 100644 --- a/app/(main)/instance/_hooks/backups.ts +++ b/app/(main)/instance/_hooks/backups.ts @@ -1,4 +1,4 @@ -import useSWR, { mutate } from 'swr'; +import useSWR from 'swr'; import { jsonFetcher } from '../../../_lib/fetcher'; import { Backup } from '../../_components/backups'; import { StandardResponse } from '../../../_lib/response'; @@ -10,28 +10,69 @@ import { } from '../_lib/backups'; export function useBackups(instanceName: string) { - const { data, error, isLoading } = useSWR>( - `/1.0/instances/${instanceName}/backups?recursion=1`, + const backupKey = `/1.0/instances/${instanceName}/backups?recursion=1`; + + const { data, error, isLoading, mutate } = useSWR< + StandardResponse + >( + backupKey, (url: string) => jsonFetcher(url), ); + const revalidateBackups = async (withDelay?: number) => { + await mutate(); + if (withDelay) { + setTimeout(() => mutate(), withDelay); + } + }; + const createBackup = async ( name?: string, instanceOnly?: boolean, optimizedStorage?: boolean, ) => { await apiCreateBackup(instanceName, name, instanceOnly, optimizedStorage); - await mutate(`/1.0/instances/${instanceName}/backups?recursion=1`); + await revalidateBackups(1000); }; const deleteBackup = async (backupName: string) => { - await apiDeleteBackup(instanceName, backupName); - await mutate(`/1.0/instances/${instanceName}/backups?recursion=1`); + const removeBackup = ( + current?: StandardResponse, + ): StandardResponse => { + if (!current) { + return { + type: 'sync', + status: 'Success', + status_code: 200, + metadata: [], + }; + } + return { + ...current, + metadata: current.metadata.filter( + (backup) => backup.name !== backupName, + ), + }; + }; + + await mutate( + async (current?: StandardResponse) => { + await apiDeleteBackup(instanceName, backupName); + return removeBackup(current); + }, + { + optimisticData: removeBackup, + rollbackOnError: true, + revalidate: true, + }, + ); + + setTimeout(() => mutate(), 1000); }; const renameBackup = async (oldName: string, newName: string) => { await apiRenameBackup(instanceName, oldName, newName); - await mutate(`/1.0/instances/${instanceName}/backups?recursion=1`); + await revalidateBackups(1000); }; const downloadBackup = (backupName: string) => { diff --git a/app/_context/events.tsx b/app/_context/events.tsx index 3cfca3b..06661ae 100644 --- a/app/_context/events.tsx +++ b/app/_context/events.tsx @@ -2,6 +2,7 @@ import React, { createContext, useEffect, useState, useRef } from 'react'; import { toast } from 'sonner'; +import { mutate } from 'swr'; type EventType = 'operation' | 'logging' | 'lifecycle'; @@ -151,6 +152,29 @@ export function EventEmitterProvider({ }); activeOperations.current.delete(toastId); } + + // Perform mutations on affected resources only for terminal states + if (['Success', 'Failure', 'Cancelled'].includes(op.status) && op.resources) { + const uniqueResources = new Set(); + Object.values(op.resources).forEach((resourceList) => { + resourceList.forEach((resource) => { + uniqueResources.add(resource); + + // Also mutate the parent collection if applicable (simple heuristic). + // The threshold `parts.length > 3` assumes paths follow the pattern `/version/collection/resource` + // (e.g., `/1.0/instances/foo`), where the first part is empty due to the leading slash. + // Thus, length > 3 means there is a resource within a collection, and we can derive the parent collection path. + // Normalize path to handle trailing slashes and query params + const normalizedResource = resource.replace(/\/$/, '').split('?')[0]; + const parts = normalizedResource.split('/'); + if (parts.length > 3) { + const collection = parts.slice(0, parts.length - 1).join('/'); + uniqueResources.add(collection); + } + }); + }); + uniqueResources.forEach((path) => mutate(path)); + } }; connect();