Skip to content
55 changes: 48 additions & 7 deletions app/(main)/instance/_hooks/backups.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,28 +10,69 @@ import {
} from '../_lib/backups';

export function useBackups(instanceName: string) {
const { data, error, isLoading } = useSWR<StandardResponse<Backup[]>>(
`/1.0/instances/${instanceName}/backups?recursion=1`,
const backupKey = `/1.0/instances/${instanceName}/backups?recursion=1`;

const { data, error, isLoading, mutate } = useSWR<
StandardResponse<Backup[]>
>(
backupKey,
(url: string) => jsonFetcher<Backup[]>(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<Backup[]>,
): StandardResponse<Backup[]> => {
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<Backup[]>) => {
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) => {
Expand Down
24 changes: 24 additions & 0 deletions app/_context/events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<string>();
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();
Expand Down