193 lines
6.4 KiB
TypeScript
193 lines
6.4 KiB
TypeScript
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
import { Database, X, RefreshCw } from "lucide-react";
|
|
import { useState } from "react";
|
|
import {
|
|
listBackupSchedulesOptions,
|
|
listSnapshotsOptions,
|
|
refreshSnapshotsMutation,
|
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
|
import { SnapshotsTable } from "~/client/components/snapshots-table";
|
|
import { Button } from "~/client/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
|
import { Input } from "~/client/components/ui/input";
|
|
import { Table, TableBody, TableCell, TableRow } from "~/client/components/ui/table";
|
|
import type { BackupSchedule, Repository, Snapshot } from "~/client/lib/types";
|
|
import { toast } from "sonner";
|
|
|
|
type Props = {
|
|
repository: Repository;
|
|
initialSnapshots?: Snapshot[];
|
|
initialBackupSchedules?: BackupSchedule[];
|
|
};
|
|
|
|
export const RepositorySnapshotsTabContent = ({ repository, initialSnapshots, initialBackupSchedules }: Props) => {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
const { data, isPending, failureReason } = useQuery({
|
|
...listSnapshotsOptions({ path: { shortId: repository.shortId } }),
|
|
initialData: initialSnapshots,
|
|
});
|
|
|
|
const schedules = useQuery({
|
|
...listBackupSchedulesOptions(),
|
|
initialData: initialBackupSchedules,
|
|
});
|
|
|
|
const refreshMutation = useMutation({
|
|
...refreshSnapshotsMutation(),
|
|
onSuccess: (data) => {
|
|
toast.success(`Snapshot cache refreshed. Found ${data.count} snapshots.`);
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to refresh snapshots: ${error.message}`);
|
|
},
|
|
});
|
|
|
|
const handleRefresh = () => {
|
|
refreshMutation.mutate({ path: { shortId: repository.shortId } });
|
|
};
|
|
|
|
const snapshots = data ?? [];
|
|
|
|
const filteredSnapshots = snapshots.filter((snapshot: Snapshot) => {
|
|
if (!searchQuery) return true;
|
|
const searchLower = searchQuery.toLowerCase();
|
|
|
|
const backup = schedules.data?.find((b) => snapshot.tags.includes(b.shortId));
|
|
|
|
return (
|
|
snapshot.short_id.toLowerCase().includes(searchLower) ||
|
|
snapshot.paths.some((path) => path.toLowerCase().includes(searchLower)) ||
|
|
backup?.name?.toLowerCase().includes(searchLower) ||
|
|
backup?.volume?.name?.toLowerCase().includes(searchLower)
|
|
);
|
|
});
|
|
|
|
const hasNoFilteredSnapshots = !filteredSnapshots.length;
|
|
|
|
if (repository.status === "error") {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center text-center py-12">
|
|
<Database className="mb-4 h-12 w-12 text-destructive" />
|
|
<p className="text-destructive font-semibold">Repository error</p>
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
This repository is in an error state and cannot be accessed.
|
|
</p>
|
|
{repository.lastError && (
|
|
<div className="mt-4 w-full max-w-md bg-destructive/10 border border-destructive/20 rounded-md p-3">
|
|
<p className="text-sm text-destructive wrap-break-word">{repository.lastError}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (failureReason) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center text-center py-12">
|
|
<Database className="mb-4 h-12 w-12 text-destructive" />
|
|
<p className="text-destructive font-semibold">Failed to Load Snapshots</p>
|
|
<p className="text-sm text-muted-foreground mt-2">{failureReason.message}</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (isPending) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
<p className="text-muted-foreground">Loading snapshots</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!snapshots.length) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center text-center py-16 px-4">
|
|
<div className="relative mb-8">
|
|
<div className="absolute inset-0 animate-pulse">
|
|
<div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" />
|
|
</div>
|
|
<div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-linear-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
|
|
<Database className="w-16 h-16 text-primary/70" strokeWidth={1.5} />
|
|
</div>
|
|
</div>
|
|
<div className="max-w-md space-y-3">
|
|
<h3 className="text-2xl font-semibold text-foreground">No snapshots yet</h3>
|
|
<p className="text-muted-foreground text-sm">
|
|
Snapshots are point-in-time backups of your data. Create your first backup to see it here.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className="p-0 gap-0">
|
|
<CardHeader className="p-4 bg-card-header">
|
|
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-4 justify-between">
|
|
<div className="flex-1">
|
|
<CardTitle>Snapshots</CardTitle>
|
|
<CardDescription className="mt-1">
|
|
Backup snapshots stored in this repository. Total: {snapshots.length}
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex gap-2 items-center">
|
|
<Input
|
|
className="w-full lg:w-60"
|
|
placeholder="Search snapshots..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
<Button
|
|
onClick={handleRefresh}
|
|
variant="outline"
|
|
disabled={refreshMutation.isPending}
|
|
title="Refresh snapshot list"
|
|
>
|
|
<RefreshCw className={refreshMutation.isPending ? "h-4 w-4 animate-spin" : "h-4 w-4"} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
{hasNoFilteredSnapshots ? (
|
|
<Table className="border-t">
|
|
<TableBody>
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center py-12">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<p className="text-muted-foreground">No snapshots match your search.</p>
|
|
<Button onClick={() => setSearchQuery("")} variant="outline" size="sm">
|
|
<X className="h-4 w-4 mr-2" />
|
|
Clear search
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
) : (
|
|
<SnapshotsTable
|
|
snapshots={filteredSnapshots}
|
|
repositoryId={repository.shortId}
|
|
backups={schedules.data ?? []}
|
|
listSnapshotsQueryOptions={{ path: { shortId: repository.shortId } }}
|
|
/>
|
|
)}
|
|
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-between border-t">
|
|
<span>
|
|
{hasNoFilteredSnapshots
|
|
? "No snapshots match filters."
|
|
: `Showing ${filteredSnapshots.length} of ${snapshots.length}`}
|
|
</span>
|
|
</div>
|
|
</Card>
|
|
);
|
|
};
|