refactor: render cached data directly if available during ssr
This commit is contained in:
parent
12454ee401
commit
70352e9f91
6 changed files with 84 additions and 29 deletions
|
|
@ -51,6 +51,7 @@ type Props = {
|
||||||
repos: Repository[];
|
repos: Repository[];
|
||||||
scheduleNotifs: ScheduleNotification[];
|
scheduleNotifs: ScheduleNotification[];
|
||||||
mirrors: ScheduleMirror[];
|
mirrors: ScheduleMirror[];
|
||||||
|
snapshots?: Snapshot[];
|
||||||
};
|
};
|
||||||
scheduleId: string;
|
scheduleId: string;
|
||||||
initialSnapshotId?: string;
|
initialSnapshotId?: string;
|
||||||
|
|
@ -78,6 +79,7 @@ export function ScheduleDetailsPage(props: Props) {
|
||||||
failureReason,
|
failureReason,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
...listSnapshotsOptions({ path: { shortId: schedule.repository.shortId }, query: { backupId: schedule.shortId } }),
|
...listSnapshotsOptions({ path: { shortId: schedule.repository.shortId }, query: { backupId: schedule.shortId } }),
|
||||||
|
initialData: loaderData.snapshots,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateSchedule = useMutation({
|
const updateSchedule = useMutation({
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,17 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui
|
||||||
import { RepositoryInfoTabContent } from "../tabs/info";
|
import { RepositoryInfoTabContent } from "../tabs/info";
|
||||||
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import type { BackupSchedule, Snapshot } from "~/client/lib/types";
|
||||||
|
|
||||||
export default function RepositoryDetailsPage({ repositoryId }: { repositoryId: string }) {
|
export default function RepositoryDetailsPage({
|
||||||
|
repositoryId,
|
||||||
|
initialSnapshots,
|
||||||
|
initialBackupSchedules,
|
||||||
|
}: {
|
||||||
|
repositoryId: string;
|
||||||
|
initialSnapshots?: Snapshot[];
|
||||||
|
initialBackupSchedules?: BackupSchedule[];
|
||||||
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId/" });
|
const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId/" });
|
||||||
const activeTab = tab || "info";
|
const activeTab = tab || "info";
|
||||||
|
|
@ -27,7 +36,11 @@ export default function RepositoryDetailsPage({ repositoryId }: { repositoryId:
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="snapshots">
|
<TabsContent value="snapshots">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<RepositorySnapshotsTabContent repository={data} />
|
<RepositorySnapshotsTabContent
|
||||||
|
repository={data}
|
||||||
|
initialSnapshots={initialSnapshots}
|
||||||
|
initialBackupSchedules={initialBackupSchedules}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,26 @@ import { Button } from "~/client/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
import { Table, TableBody, TableCell, TableRow } from "~/client/components/ui/table";
|
import { Table, TableBody, TableCell, TableRow } from "~/client/components/ui/table";
|
||||||
import type { Repository, Snapshot } from "~/client/lib/types";
|
import type { BackupSchedule, Repository, Snapshot } from "~/client/lib/types";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
|
initialSnapshots?: Snapshot[];
|
||||||
|
initialBackupSchedules?: BackupSchedule[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
export const RepositorySnapshotsTabContent = ({ repository, initialSnapshots, initialBackupSchedules }: Props) => {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
const { data, isFetching, failureReason } = useQuery({
|
const { data, isPending, failureReason } = useQuery({
|
||||||
...listSnapshotsOptions({ path: { shortId: repository.shortId } }),
|
...listSnapshotsOptions({ path: { shortId: repository.shortId } }),
|
||||||
initialData: [],
|
initialData: initialSnapshots,
|
||||||
});
|
});
|
||||||
|
|
||||||
const schedules = useQuery({
|
const schedules = useQuery({
|
||||||
...listBackupSchedulesOptions(),
|
...listBackupSchedulesOptions(),
|
||||||
|
initialData: initialBackupSchedules,
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshMutation = useMutation({
|
const refreshMutation = useMutation({
|
||||||
|
|
@ -44,7 +47,9 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||||
refreshMutation.mutate({ path: { shortId: repository.shortId } });
|
refreshMutation.mutate({ path: { shortId: repository.shortId } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredSnapshots = data.filter((snapshot: Snapshot) => {
|
const snapshots = data ?? [];
|
||||||
|
|
||||||
|
const filteredSnapshots = snapshots.filter((snapshot: Snapshot) => {
|
||||||
if (!searchQuery) return true;
|
if (!searchQuery) return true;
|
||||||
const searchLower = searchQuery.toLowerCase();
|
const searchLower = searchQuery.toLowerCase();
|
||||||
|
|
||||||
|
|
@ -58,7 +63,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNoFilteredSnapshots = !filteredSnapshots?.length;
|
const hasNoFilteredSnapshots = !filteredSnapshots.length;
|
||||||
|
|
||||||
if (repository.status === "error") {
|
if (repository.status === "error") {
|
||||||
return (
|
return (
|
||||||
|
|
@ -91,7 +96,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFetching && !data.length) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex items-center justify-center py-12">
|
<CardContent className="flex items-center justify-center py-12">
|
||||||
|
|
@ -101,7 +106,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.length) {
|
if (!snapshots.length) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center text-center py-16 px-4">
|
<CardContent className="flex flex-col items-center justify-center text-center py-16 px-4">
|
||||||
|
|
@ -131,7 +136,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CardTitle>Snapshots</CardTitle>
|
<CardTitle>Snapshots</CardTitle>
|
||||||
<CardDescription className="mt-1">
|
<CardDescription className="mt-1">
|
||||||
Backup snapshots stored in this repository. Total: {data.length}
|
Backup snapshots stored in this repository. Total: {snapshots.length}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|
@ -180,7 +185,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||||
<span>
|
<span>
|
||||||
{hasNoFilteredSnapshots
|
{hasNoFilteredSnapshots
|
||||||
? "No snapshots match filters."
|
? "No snapshots match filters."
|
||||||
: `Showing ${filteredSnapshots.length} of ${data.length}`}
|
: `Showing ${filteredSnapshots.length} of ${snapshots.length}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
listSnapshotsOptions,
|
listSnapshotsOptions,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { ScheduleDetailsPage } from "~/client/modules/backups/routes/backup-details";
|
import { ScheduleDetailsPage } from "~/client/modules/backups/routes/backup-details";
|
||||||
|
import { prefetchOrSkip } from "~/utils/prefetch";
|
||||||
|
|
||||||
export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
|
export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -24,14 +25,21 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
|
||||||
context.queryClient.ensureQueryData({ ...getScheduleMirrorsOptions({ path: { shortId: backupId } }) }),
|
context.queryClient.ensureQueryData({ ...getScheduleMirrorsOptions({ path: { shortId: backupId } }) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
void context.queryClient.prefetchQuery({
|
const snapshotOptions = listSnapshotsOptions({
|
||||||
...listSnapshotsOptions({
|
path: { shortId: schedule.repository.shortId },
|
||||||
path: { shortId: schedule.repository.shortId },
|
query: { backupId: schedule.shortId },
|
||||||
query: { backupId: schedule.shortId },
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { schedule, notifs, repos, scheduleNotifs, mirrors };
|
await prefetchOrSkip(context.queryClient, snapshotOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
schedule,
|
||||||
|
notifs,
|
||||||
|
repos,
|
||||||
|
scheduleNotifs,
|
||||||
|
mirrors,
|
||||||
|
snapshots: context.queryClient.getQueryData(snapshotOptions.queryKey),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
staticData: {
|
staticData: {
|
||||||
breadcrumb: (match) => [
|
breadcrumb: (match) => [
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,26 @@ import {
|
||||||
listSnapshotsOptions,
|
listSnapshotsOptions,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import RepositoryDetailsPage from "~/client/modules/repositories/routes/repository-details";
|
import RepositoryDetailsPage from "~/client/modules/repositories/routes/repository-details";
|
||||||
|
import { prefetchOrSkip } from "~/utils/prefetch";
|
||||||
|
|
||||||
export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/")({
|
export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
errorComponent: (e) => <div>{e.error.message}</div>,
|
errorComponent: (e) => <div>{e.error.message}</div>,
|
||||||
loader: async ({ params, context }) => {
|
loader: async ({ params, context }) => {
|
||||||
void context.queryClient.prefetchQuery({
|
const snapshotOptions = listSnapshotsOptions({ path: { shortId: params.repositoryId } });
|
||||||
...listSnapshotsOptions({ path: { shortId: params.repositoryId } }),
|
const schedulesOptions = listBackupSchedulesOptions();
|
||||||
});
|
|
||||||
void context.queryClient.prefetchQuery({
|
|
||||||
...listBackupSchedulesOptions(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await context.queryClient.ensureQueryData({
|
const [res] = await Promise.all([
|
||||||
...getRepositoryOptions({ path: { shortId: params.repositoryId } }),
|
context.queryClient.ensureQueryData(getRepositoryOptions({ path: { shortId: params.repositoryId } })),
|
||||||
});
|
prefetchOrSkip(context.queryClient, snapshotOptions),
|
||||||
|
prefetchOrSkip(context.queryClient, schedulesOptions),
|
||||||
|
]);
|
||||||
|
|
||||||
return res;
|
return {
|
||||||
|
...res,
|
||||||
|
snapshots: context.queryClient.getQueryData(snapshotOptions.queryKey),
|
||||||
|
backupSchedules: context.queryClient.getQueryData(schedulesOptions.queryKey),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
validateSearch: type({ tab: "string?" }),
|
validateSearch: type({ tab: "string?" }),
|
||||||
staticData: {
|
staticData: {
|
||||||
|
|
@ -44,6 +47,13 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/")
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { repositoryId } = Route.useParams();
|
const { repositoryId } = Route.useParams();
|
||||||
|
const { snapshots, backupSchedules } = Route.useLoaderData();
|
||||||
|
|
||||||
return <RepositoryDetailsPage repositoryId={repositoryId} />;
|
return (
|
||||||
|
<RepositoryDetailsPage
|
||||||
|
repositoryId={repositoryId}
|
||||||
|
initialSnapshots={snapshots}
|
||||||
|
initialBackupSchedules={backupSchedules}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
app/utils/prefetch.ts
Normal file
17
app/utils/prefetch.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { FetchQueryOptions, QueryClient, QueryKey } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export async function prefetchOrSkip<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = Error,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey,
|
||||||
|
>(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||||
|
timeoutMs = 150,
|
||||||
|
): Promise<void> {
|
||||||
|
await Promise.race([
|
||||||
|
queryClient.prefetchQuery(options),
|
||||||
|
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue