refactor: stylistic changes

This commit is contained in:
Nicolas Meienberger 2026-04-16 20:54:49 +02:00
parent da7e5a7b38
commit 42cecbe868
No known key found for this signature in database
2 changed files with 78 additions and 35 deletions

View file

@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { Copy, Plus, RefreshCw, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
@ -22,7 +22,6 @@ import {
getScheduleMirrorsOptions,
getMirrorCompatibilityOptions,
getMirrorSyncStatusOptions,
getMirrorSyncStatusQueryKey,
updateScheduleMirrorsMutation,
syncMirrorMutation,
} from "~/client/api-client/@tanstack/react-query.gen";
@ -32,10 +31,10 @@ import { RepositoryIcon } from "~/client/components/repository-icon";
import { StatusDot } from "~/client/components/status-dot";
import { ByteSize } from "~/client/components/bytes-size";
import { useServerEvents } from "~/client/hooks/use-server-events";
import { format, formatDistanceToNow } from "date-fns";
import { cn } from "~/client/lib/utils";
import type { GetScheduleMirrorsResponse } from "~/client/api-client";
import { Link } from "@tanstack/react-router";
import { useTimeFormat } from "~/client/lib/datetime";
type Props = {
scheduleShortId: string;
@ -69,14 +68,29 @@ const buildAssignments = (mirrors: GetScheduleMirrorsResponse) =>
);
export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, repositories, initialData }: Props) => {
const queryClient = useQueryClient();
const { formatDateTime, formatTimeAgo } = useTimeFormat();
const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(() => buildAssignments(initialData));
const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false);
const [syncDialogMirrorId, setSyncDialogMirrorId] = useState<string | null>(null);
const [selectedSnapshotIds, setSelectedSnapshotIds] = useState<Set<string>>(new Set());
const [syncDialogOpen, setSyncDialogOpen] = useState(false);
const { addEventListener } = useServerEvents();
const closeSyncDialog = () => {
setSyncDialogOpen(false);
setTimeout(() => {
setSyncDialogMirrorId(null);
setSelectedSnapshotIds(new Set());
}, 300);
};
const openSyncDialog = (mirrorShortId: string) => {
setSyncDialogOpen(true);
setSelectedSnapshotIds(new Set());
setSyncDialogMirrorId(mirrorShortId);
};
const { data: currentMirrors } = useSuspenseQuery({
...getScheduleMirrorsOptions({ path: { shortId: scheduleShortId } }),
});
@ -106,17 +120,11 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
const { data: syncStatus, isLoading: isSyncStatusLoading } = useQuery({
...getMirrorSyncStatusOptions({
path: { shortId: scheduleShortId, mirrorShortId: syncDialogMirrorId! },
path: { shortId: scheduleShortId, mirrorShortId: syncDialogMirrorId ?? "" },
}),
enabled: syncDialogMirrorId !== null,
});
useEffect(() => {
if (syncStatus?.missingSnapshots) {
setSelectedSnapshotIds(new Set(syncStatus.missingSnapshots.map((s) => s.short_id)));
}
}, [syncStatus]);
const toggleSnapshotSelection = (shortId: string) => {
setSelectedSnapshotIds((prev) => {
const next = new Set(prev);
@ -142,7 +150,7 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
...syncMirrorMutation(),
onSuccess: () => {
toast.success("Full sync started");
setSyncDialogMirrorId(null);
closeSyncDialog();
},
onError: (error) => {
toast.error("Failed to start sync", {
@ -195,11 +203,6 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
});
return next;
});
queryClient.removeQueries({
queryKey: getMirrorSyncStatusQueryKey({
path: { shortId: scheduleShortId, mirrorShortId: event.repositoryId },
}),
});
},
{ signal: abortController.signal },
);
@ -289,7 +292,7 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
return "Syncing...";
}
if (assignment.lastCopyAt) {
return formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true });
return formatTimeAgo(assignment.lastCopyAt);
}
return "Never";
};
@ -307,6 +310,8 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
return "Never copied";
};
const selectedMirrorRepo = repositories.find((r) => r.shortId === syncDialogMirrorId);
return (
<Card>
<CardHeader>
@ -443,14 +448,14 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
<Button
variant="ghost"
size="icon"
onClick={() => setSyncDialogMirrorId(repository.shortId)}
onClick={() => openSyncDialog(repository.shortId)}
disabled={isSyncing(assignment) || hasChanges}
className="h-8 w-8 text-muted-foreground hover:text-foreground"
>
<RefreshCw className={cn("h-4 w-4", isSyncing(assignment) && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>Sync all snapshots</TooltipContent>
<TooltipContent>Sync more snapshots</TooltipContent>
</Tooltip>
<Button
variant="ghost"
@ -481,22 +486,16 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
</div>
)}
<Dialog open={syncDialogMirrorId !== null} onOpenChange={(open) => !open && setSyncDialogMirrorId(null)}>
<Dialog open={syncDialogOpen} onOpenChange={(open) => !open && closeSyncDialog()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Sync snapshots</DialogTitle>
<DialogDescription>
{syncDialogMirrorId &&
(() => {
const mirrorRepo = repositories.find((r) => r.shortId === syncDialogMirrorId);
return mirrorRepo
? `Sync all missing snapshots to "${mirrorRepo.name}"`
: "Sync all missing snapshots to mirror";
})()}
{`Sync missing snapshots to ${selectedMirrorRepo?.name || "mirror repository"}.`}
</DialogDescription>
</DialogHeader>
{isSyncStatusLoading ? (
{isSyncStatusLoading && !syncStatus ? (
<div className="py-6 text-center text-muted-foreground text-sm">Loading snapshot status...</div>
) : syncStatus && syncStatus.missingSnapshots.length === 0 ? (
<div className="py-6 text-center text-muted-foreground text-sm">
@ -536,9 +535,7 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
/>
</TableCell>
<TableCell className="font-mono text-xs">{snapshot.short_id}</TableCell>
<TableCell className="text-sm">
{format(new Date(snapshot.time), "yyyy-MM-dd HH:mm")}
</TableCell>
<TableCell className="text-sm">{formatDateTime(new Date(snapshot.time))}</TableCell>
<TableCell className="text-right text-sm">
<ByteSize bytes={snapshot.size} base={1024} />
</TableCell>
@ -551,7 +548,7 @@ export const ScheduleMirrorsConfig = ({ scheduleShortId, primaryRepositoryId, re
) : null}
<DialogFooter>
<Button variant="outline" onClick={() => setSyncDialogMirrorId(null)}>
<Button variant="outline" onClick={closeSyncDialog}>
Cancel
</Button>
<Button

View file

@ -1,4 +1,5 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import waitForExpect from "wait-for-expect";
import { backupsService } from "../backups.service";
import { createTestVolume } from "~/test/helpers/volume";
import { createTestBackupSchedule } from "~/test/helpers/backup";
@ -148,8 +149,7 @@ describe("getMirrorSyncStatus", () => {
describe("syncMirror", () => {
test("should trigger sync and return success", async () => {
const { mockCopy } = setup();
const copyMock = mockCopy();
setup();
const volume = await createTestVolume();
const repository = await createTestRepository();
const mirrorRepository = await createTestRepository();
@ -185,6 +185,52 @@ describe("syncMirror", () => {
).rejects.toThrow("Mirror is already syncing");
});
test("should reject concurrent sync requests once a sync has started", async () => {
const { mockCopy } = setup();
const copyMock = mockCopy();
let releaseCopy: (() => void) | undefined;
const copyStarted = new Promise<void>((resolve) => {
copyMock.mockImplementation(
() =>
new Promise((copyResolve) => {
releaseCopy = () => copyResolve({ success: true, output: "" });
resolve();
}),
);
});
const volume = await createTestVolume();
const repository = await createTestRepository();
const mirrorRepository = await createTestRepository();
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
});
await createTestBackupScheduleMirror(schedule.id, mirrorRepository.id);
await expect(
backupsService.syncMirror(schedule.shortId, mirrorRepository.shortId as ShortId, ["snap1"]),
).resolves.toEqual({ success: true });
await copyStarted;
await waitForExpect(async () => {
const mirrors = await backupsService.getMirrors(schedule.shortId);
expect(mirrors[0]?.lastCopyStatus).toBe("in_progress");
});
await expect(
backupsService.syncMirror(schedule.shortId, mirrorRepository.shortId as ShortId, ["snap1"]),
).rejects.toThrow("Mirror is already syncing");
releaseCopy?.();
await waitForExpect(async () => {
const mirrors = await backupsService.getMirrors(schedule.shortId);
expect(mirrors[0]?.lastCopyStatus).toBe("success");
});
});
test("should throw if mirror is not configured for the schedule", async () => {
setup();
const volume = await createTestVolume();