refactor: improve perf by calling api in parallel in clientLoaders (#312)

* refactor: improve perf by calling api in parallel in clientLoaders

* fix: dependent api calls
This commit is contained in:
Nico 2026-01-05 21:10:00 +01:00 committed by Nicolas Meienberger
parent 13428224e8
commit f0e956783a
7 changed files with 59 additions and 25 deletions

View file

@ -21,11 +21,13 @@ import { StatusDot } from "~/client/components/status-dot";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { Link } from "react-router"; import { Link } from "react-router";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import type { GetScheduleMirrorsResponse } from "~/client/api-client";
type Props = { type Props = {
scheduleId: number; scheduleId: number;
primaryRepositoryId: string; primaryRepositoryId: string;
repositories: Repository[]; repositories: Repository[];
initialData: GetScheduleMirrorsResponse;
}; };
type MirrorAssignment = { type MirrorAssignment = {
@ -36,13 +38,14 @@ type MirrorAssignment = {
lastCopyError: string | null; lastCopyError: string | null;
}; };
export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories }: Props) => { export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => {
const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(new Map()); const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(new Map());
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false);
const { data: currentMirrors } = useQuery({ const { data: currentMirrors } = useQuery({
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }), ...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
initialData,
}); });
const { data: compatibility } = useQuery({ const { data: compatibility } = useQuery({

View file

@ -14,10 +14,12 @@ import {
} from "~/client/api-client/@tanstack/react-query.gen"; } from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/client/lib/errors"; import { parseError } from "~/client/lib/errors";
import type { NotificationDestination } from "~/client/lib/types"; import type { NotificationDestination } from "~/client/lib/types";
import type { GetScheduleNotificationsResponse } from "~/client/api-client";
type Props = { type Props = {
scheduleId: number; scheduleId: number;
destinations: NotificationDestination[]; destinations: NotificationDestination[];
initialData: GetScheduleNotificationsResponse;
}; };
type NotificationAssignment = { type NotificationAssignment = {
@ -28,13 +30,14 @@ type NotificationAssignment = {
notifyOnFailure: boolean; notifyOnFailure: boolean;
}; };
export const ScheduleNotificationsConfig = ({ scheduleId, destinations }: Props) => { export const ScheduleNotificationsConfig = ({ scheduleId, destinations, initialData }: Props) => {
const [assignments, setAssignments] = useState<Map<number, NotificationAssignment>>(new Map()); const [assignments, setAssignments] = useState<Map<number, NotificationAssignment>>(new Map());
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false);
const { data: currentAssignments } = useQuery({ const { data: currentAssignments } = useQuery({
...getScheduleNotificationsOptions({ path: { scheduleId: scheduleId.toString() } }), ...getScheduleNotificationsOptions({ path: { scheduleId: scheduleId.toString() } }),
initialData,
}); });
const updateNotifications = useMutation({ const updateNotifications = useMutation({

View file

@ -30,7 +30,13 @@ import { ScheduleSummary } from "../components/schedule-summary";
import type { Route } from "./+types/backup-details"; import type { Route } from "./+types/backup-details";
import { SnapshotFileBrowser } from "../components/snapshot-file-browser"; import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
import { SnapshotTimeline } from "../components/snapshot-timeline"; import { SnapshotTimeline } from "../components/snapshot-timeline";
import { getBackupSchedule, listNotificationDestinations, listRepositories } from "~/client/api-client"; import {
getBackupSchedule,
getScheduleMirrors,
getScheduleNotifications,
listNotificationDestinations,
listRepositories,
} from "~/client/api-client";
import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config"; import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config";
import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config"; import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
@ -53,13 +59,23 @@ export function meta(_: Route.MetaArgs) {
} }
export const clientLoader = async ({ params }: Route.LoaderArgs) => { export const clientLoader = async ({ params }: Route.LoaderArgs) => {
const schedule = await getBackupSchedule({ path: { scheduleId: params.id } }); const [schedule, notifs, repos, scheduleNotifs, mirrors] = await Promise.all([
const notifs = await listNotificationDestinations(); getBackupSchedule({ path: { scheduleId: params.id } }),
const repos = await listRepositories(); listNotificationDestinations(),
listRepositories(),
getScheduleNotifications({ path: { scheduleId: params.id } }),
getScheduleMirrors({ path: { scheduleId: params.id } }),
]);
if (!schedule.data) return redirect("/backups"); if (!schedule.data) return redirect("/backups");
return { schedule: schedule.data, notifs: notifs.data, repos: repos.data }; return {
schedule: schedule.data,
notifs: notifs.data,
repos: repos.data,
scheduleNotifs: scheduleNotifs.data,
scheduleMirrors: mirrors.data,
};
}; };
export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) { export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) {
@ -240,13 +256,18 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
schedule={schedule} schedule={schedule}
/> />
<div className={cn({ hidden: !loaderData.notifs?.length })}> <div className={cn({ hidden: !loaderData.notifs?.length })}>
<ScheduleNotificationsConfig scheduleId={schedule.id} destinations={loaderData.notifs ?? []} /> <ScheduleNotificationsConfig
scheduleId={schedule.id}
destinations={loaderData.notifs ?? []}
initialData={loaderData.scheduleNotifs ?? []}
/>
</div> </div>
<div className={cn({ hidden: !loaderData.repos?.length || loaderData.repos.length < 2 })}> <div className={cn({ hidden: !loaderData.repos?.length || loaderData.repos.length < 2 })}>
<ScheduleMirrorsConfig <ScheduleMirrorsConfig
scheduleId={schedule.id} scheduleId={schedule.id}
primaryRepositoryId={schedule.repositoryId} primaryRepositoryId={schedule.repositoryId}
repositories={loaderData.repos ?? []} repositories={loaderData.repos ?? []}
initialData={loaderData.scheduleMirrors ?? []}
/> />
</div> </div>
<SnapshotTimeline <SnapshotTimeline

View file

@ -33,8 +33,7 @@ export function meta(_: Route.MetaArgs) {
} }
export const clientLoader = async () => { export const clientLoader = async () => {
const volumes = await listVolumes(); const [volumes, repositories] = await Promise.all([listVolumes(), listRepositories()]);
const repositories = await listRepositories();
if (volumes.data && repositories.data) return { volumes: volumes.data, repositories: repositories.data }; if (volumes.data && repositories.data) return { volumes: volumes.data, repositories: repositories.data };
return { volumes: [], repositories: [] }; return { volumes: [], repositories: [] };

View file

@ -24,15 +24,20 @@ export function meta({ params }: Route.MetaArgs) {
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const schedule = await getBackupSchedule({ path: { scheduleId: params.id } }); const schedule = await getBackupSchedule({ path: { scheduleId: params.id } });
if (!schedule.data) return redirect("/backups"); if (!schedule.data) return redirect("/backups");
const repositoryId = schedule.data.repository.id; const [snapshot, repository] = await Promise.all([
const snapshot = await getSnapshotDetails({ getSnapshotDetails({
path: { id: repositoryId, snapshotId: params.snapshotId }, path: {
}); id: schedule.data.repositoryId,
if (!snapshot.data) return redirect(`/backups/${params.id}`); snapshotId: params.snapshotId,
},
}),
getRepository({ path: { id: schedule.data.repositoryId } }),
]);
const repository = await getRepository({ path: { id: repositoryId } }); if (!snapshot.data) return redirect(`/backups/${params.id}`);
if (!repository.data) return redirect(`/backups/${params.id}`); if (!repository.data) return redirect(`/backups/${params.id}`);
return { return {

View file

@ -23,12 +23,12 @@ export function meta({ params }: Route.MetaArgs) {
} }
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const snapshot = await getSnapshotDetails({ const [snapshot, repository] = await Promise.all([
path: { id: params.id, snapshotId: params.snapshotId }, getSnapshotDetails({ path: { id: params.id, snapshotId: params.snapshotId } }),
}); getRepository({ path: { id: params.id } }),
if (!snapshot.data) return redirect("/repositories"); ]);
const repository = await getRepository({ path: { id: params.id } }); if (!snapshot.data) return redirect("/repositories");
if (!repository.data) return redirect(`/repositories`); if (!repository.data) return redirect(`/repositories`);
return { snapshot: snapshot.data, id: params.id, repository: repository.data, snapshotId: params.snapshotId }; return { snapshot: snapshot.data, id: params.id, repository: repository.data, snapshotId: params.snapshotId };

View file

@ -26,11 +26,14 @@ export function meta({ params }: Route.MetaArgs) {
} }
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const snapshot = getSnapshotDetails({ const [snapshot, repository] = await Promise.all([
path: { id: params.id, snapshotId: params.snapshotId }, getSnapshotDetails({
}); path: { id: params.id, snapshotId: params.snapshotId },
}),
getRepository({ path: { id: params.id } }),
]);
const repository = await getRepository({ path: { id: params.id } }); if (!snapshot.data) return redirect(`/repositories/${params.id}`);
if (!repository.data) return redirect("/repositories"); if (!repository.data) return redirect("/repositories");
return { snapshot: snapshot, repository: repository.data }; return { snapshot: snapshot, repository: repository.data };