From 6e6becec3bed268a79907a12d0c5e70ae15573b0 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Thu, 13 Nov 2025 22:28:53 +0100 Subject: [PATCH] refactor(breadcrumbs): use handler & match pattern --- app/client/components/app-breadcrumb.tsx | 38 ++++++-- app/client/lib/breadcrumbs.ts | 86 ------------------- .../modules/backups/routes/backup-details.tsx | 7 ++ app/client/modules/backups/routes/backups.tsx | 4 + .../modules/backups/routes/create-backup.tsx | 4 + .../repositories/routes/repositories.tsx | 4 + .../routes/repository-details.tsx | 7 ++ .../repositories/routes/snapshot-details.tsx | 8 ++ .../modules/settings/routes/settings.tsx | 4 + .../modules/volumes/routes/volume-details.tsx | 4 + app/client/modules/volumes/routes/volumes.tsx | 4 + app/client/routes/root.tsx | 8 +- app/server/core/scheduler.ts | 8 ++ app/server/modules/lifecycle/startup.ts | 1 + 14 files changed, 89 insertions(+), 98 deletions(-) delete mode 100644 app/client/lib/breadcrumbs.ts diff --git a/app/client/components/app-breadcrumb.tsx b/app/client/components/app-breadcrumb.tsx index 5918651..b6c32b2 100644 --- a/app/client/components/app-breadcrumb.tsx +++ b/app/client/components/app-breadcrumb.tsx @@ -1,4 +1,4 @@ -import { Link } from "react-router"; +import { Link, useMatches, type UIMatch } from "react-router"; import { Breadcrumb, BreadcrumbItem, @@ -7,14 +7,38 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "~/client/components/ui/breadcrumb"; -import { useBreadcrumbs } from "~/client/lib/breadcrumbs"; + +export interface BreadcrumbItemData { + label: string; + href?: string; +} + +interface RouteHandle { + breadcrumb?: (match: UIMatch) => BreadcrumbItemData[] | null; +} export function AppBreadcrumb() { - const breadcrumbs = useBreadcrumbs(); + const matches = useMatches(); + + // Find the last match with a breadcrumb handler + const lastMatchWithBreadcrumb = [...matches].reverse().find((match) => { + const handle = match.handle as RouteHandle | undefined; + return handle?.breadcrumb; + }); + + if (!lastMatchWithBreadcrumb) { + return null; + } + + const handle = lastMatchWithBreadcrumb.handle as RouteHandle; + const breadcrumbs = handle.breadcrumb?.(lastMatchWithBreadcrumb); + + if (!breadcrumbs || breadcrumbs.length === 0) { + return null; + } return ( - {breadcrumbs.map((breadcrumb, index) => { const isLast = index === breadcrumbs.length - 1; @@ -22,14 +46,12 @@ export function AppBreadcrumb() { return (
- {isLast || breadcrumb.isCurrentPage ? ( + {isLast || !breadcrumb.href ? ( {breadcrumb.label} - ) : breadcrumb.href ? ( + ) : ( {breadcrumb.label} - ) : ( - {breadcrumb.label} )} {!isLast && } diff --git a/app/client/lib/breadcrumbs.ts b/app/client/lib/breadcrumbs.ts deleted file mode 100644 index 7411203..0000000 --- a/app/client/lib/breadcrumbs.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { useLocation, useParams } from "react-router"; - -export interface BreadcrumbItem { - label: string; - href?: string; - isCurrentPage?: boolean; -} - -/** - * Generates breadcrumb items based on the current route - * @param pathname - Current pathname from useLocation - * @param params - Route parameters from useParams - * @returns Array of breadcrumb items - */ -export function generateBreadcrumbs(pathname: string, params: Record): BreadcrumbItem[] { - const breadcrumbs: BreadcrumbItem[] = []; - - if (pathname.startsWith("/repositories")) { - breadcrumbs.push({ - label: "Repositories", - href: "/repositories", - isCurrentPage: pathname === "/repositories", - }); - - if (pathname.startsWith("/repositories/") && params.name) { - const isSnapshotPage = !!params.snapshotId; - - breadcrumbs.push({ - label: params.name, - href: isSnapshotPage ? `/repositories/${params.name}` : undefined, - isCurrentPage: !isSnapshotPage, - }); - - if (isSnapshotPage && params.snapshotId) { - breadcrumbs.push({ - label: params.snapshotId, - isCurrentPage: true, - }); - } - } - - return breadcrumbs; - } - - if (pathname.startsWith("/backups")) { - breadcrumbs.push({ - label: "Backups", - href: "/backups", - isCurrentPage: pathname === "/backups", - }); - - if (pathname.startsWith("/backups/") && params.id) { - breadcrumbs.push({ - label: `Schedule #${params.id}`, - isCurrentPage: true, - }); - } - - return breadcrumbs; - } - - breadcrumbs.push({ - label: "Volumes", - href: "/volumes", - isCurrentPage: pathname === "/volumes", - }); - - if (pathname.startsWith("/volumes/") && params.name) { - breadcrumbs.push({ - label: params.name, - isCurrentPage: true, - }); - } - - return breadcrumbs; -} - -/** - * Hook to get breadcrumb data for the current route - */ -export function useBreadcrumbs(): BreadcrumbItem[] { - const location = useLocation(); - const params = useParams(); - - return generateBreadcrumbs(location.pathname, params); -} diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index fd2b053..a6311a3 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -20,6 +20,13 @@ import { SnapshotFileBrowser } from "../components/snapshot-file-browser"; import { SnapshotTimeline } from "../components/snapshot-timeline"; import { getBackupSchedule } from "~/client/api-client"; +export const handle = { + breadcrumb: (match: Route.MetaArgs) => [ + { label: "Backups", href: "/backups" }, + { label: `Schedule #${match.params.id}` }, + ], +}; + export function meta(_: Route.MetaArgs) { return [ { title: "Backup Job Details" }, diff --git a/app/client/modules/backups/routes/backups.tsx b/app/client/modules/backups/routes/backups.tsx index 8337201..3908fb3 100644 --- a/app/client/modules/backups/routes/backups.tsx +++ b/app/client/modules/backups/routes/backups.tsx @@ -9,6 +9,10 @@ import type { Route } from "./+types/backups"; import { listBackupSchedules } from "~/client/api-client"; import { listBackupSchedulesOptions } from "~/client/api-client/@tanstack/react-query.gen"; +export const handle = { + breadcrumb: () => [{ label: "Backups" }], +}; + export function meta(_: Route.MetaArgs) { return [ { title: "Backup Jobs" }, diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx index eab8668..fee33ce 100644 --- a/app/client/modules/backups/routes/create-backup.tsx +++ b/app/client/modules/backups/routes/create-backup.tsx @@ -18,6 +18,10 @@ import { CreateScheduleForm, type BackupScheduleFormValues } from "../components import type { Route } from "./+types/create-backup"; import { listRepositories, listVolumes } from "~/client/api-client"; +export const handle = { + breadcrumb: () => [{ label: "Backups", href: "/backups" }, { label: "Create" }], +}; + export function meta(_: Route.MetaArgs) { return [ { title: "Create Backup Job" }, diff --git a/app/client/modules/repositories/routes/repositories.tsx b/app/client/modules/repositories/routes/repositories.tsx index 39e3e56..be2485e 100644 --- a/app/client/modules/repositories/routes/repositories.tsx +++ b/app/client/modules/repositories/routes/repositories.tsx @@ -15,6 +15,10 @@ import type { Route } from "./+types/repositories"; import { cn } from "~/client/lib/utils"; import { EmptyState } from "~/client/components/empty-state"; +export const handle = { + breadcrumb: () => [{ label: "Repositories" }], +}; + export function meta(_: Route.MetaArgs) { return [ { title: "Repositories" }, diff --git a/app/client/modules/repositories/routes/repository-details.tsx b/app/client/modules/repositories/routes/repository-details.tsx index 941b511..c18d517 100644 --- a/app/client/modules/repositories/routes/repository-details.tsx +++ b/app/client/modules/repositories/routes/repository-details.tsx @@ -27,6 +27,13 @@ import { RepositoryInfoTabContent } from "../tabs/info"; import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; import { Loader2 } from "lucide-react"; +export const handle = { + breadcrumb: (match: Route.MetaArgs) => [ + { label: "Repositories", href: "/repositories" }, + { label: match.params.name }, + ], +}; + export function meta({ params }: Route.MetaArgs) { return [ { title: params.name }, diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index d607ac5..50e49b7 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -7,6 +7,14 @@ import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapsho import { getSnapshotDetails } from "~/client/api-client"; import type { Route } from "./+types/snapshot-details"; +export const handle = { + breadcrumb: (match: Route.MetaArgs) => [ + { label: "Repositories", href: "/repositories" }, + { label: match.params.name, href: `/repositories/${match.params.name}` }, + { label: match.params.snapshotId }, + ], +}; + export function meta({ params }: Route.MetaArgs) { return [ { title: `Snapshot ${params.snapshotId}` }, diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index 35fd8a9..6b6e2b7 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -24,6 +24,10 @@ import { logoutMutation, } from "~/client/api-client/@tanstack/react-query.gen"; +export const handle = { + breadcrumb: () => [{ label: "Settings" }], +}; + export function meta(_: Route.MetaArgs) { return [ { title: "Settings" }, diff --git a/app/client/modules/volumes/routes/volume-details.tsx b/app/client/modules/volumes/routes/volume-details.tsx index 1c14d9f..3059e0c 100644 --- a/app/client/modules/volumes/routes/volume-details.tsx +++ b/app/client/modules/volumes/routes/volume-details.tsx @@ -31,6 +31,10 @@ import { unmountVolumeMutation, } from "~/client/api-client/@tanstack/react-query.gen"; +export const handle = { + breadcrumb: (match: Route.MetaArgs) => [{ label: "Volumes", href: "/volumes" }, { label: match.params.name }], +}; + export function meta({ params }: Route.MetaArgs) { return [ { title: params.name }, diff --git a/app/client/modules/volumes/routes/volumes.tsx b/app/client/modules/volumes/routes/volumes.tsx index e4bf4f2..04cb2d8 100644 --- a/app/client/modules/volumes/routes/volumes.tsx +++ b/app/client/modules/volumes/routes/volumes.tsx @@ -15,6 +15,10 @@ import type { Route } from "./+types/volumes"; import { listVolumes } from "~/client/api-client"; import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen"; +export const handle = { + breadcrumb: () => [{ label: "Volumes" }], +}; + export function meta(_: Route.MetaArgs) { return [ { title: "Volumes" }, diff --git a/app/client/routes/root.tsx b/app/client/routes/root.tsx index f7511fb..8e9b448 100644 --- a/app/client/routes/root.tsx +++ b/app/client/routes/root.tsx @@ -1,9 +1,9 @@ import { redirect } from "react-router"; -export const loader = async () => { - return redirect("/volumes"); -}; - export const clientLoader = async () => { return redirect("/volumes"); }; + +export const loader = async () => { + return redirect("/volumes"); +}; diff --git a/app/server/core/scheduler.ts b/app/server/core/scheduler.ts index d8beec9..bb066c5 100644 --- a/app/server/core/scheduler.ts +++ b/app/server/core/scheduler.ts @@ -39,6 +39,14 @@ class SchedulerClass { this.tasks = []; logger.info("Scheduler stopped"); } + + async clear() { + for (const task of this.tasks) { + task.destroy(); + } + this.tasks = []; + logger.info("Scheduler cleared all tasks"); + } } export const Scheduler = new SchedulerClass(); diff --git a/app/server/modules/lifecycle/startup.ts b/app/server/modules/lifecycle/startup.ts index 0a194fd..02b6472 100644 --- a/app/server/modules/lifecycle/startup.ts +++ b/app/server/modules/lifecycle/startup.ts @@ -13,6 +13,7 @@ import { CleanupSessionsJob } from "../../jobs/cleanup-sessions"; export const startup = async () => { await Scheduler.start(); + await Scheduler.clear(); await restic.ensurePassfile().catch((err) => { logger.error(`Error ensuring restic passfile exists: ${err.message}`);