mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
Compare commits
2 Commits
v0.7.0-alp
...
v0.7.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b8595c17e | ||
|
|
6e6becec3b |
@@ -1,4 +1,4 @@
|
|||||||
import { Link } from "react-router";
|
import { Link, useMatches, type UIMatch } from "react-router";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@@ -7,14 +7,38 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "~/client/components/ui/breadcrumb";
|
} 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() {
|
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 (
|
return (
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbLink asChild></BreadcrumbLink>
|
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
{breadcrumbs.map((breadcrumb, index) => {
|
{breadcrumbs.map((breadcrumb, index) => {
|
||||||
const isLast = index === breadcrumbs.length - 1;
|
const isLast = index === breadcrumbs.length - 1;
|
||||||
@@ -22,14 +46,12 @@ export function AppBreadcrumb() {
|
|||||||
return (
|
return (
|
||||||
<div key={`${breadcrumb.label}-${index}`} className="contents">
|
<div key={`${breadcrumb.label}-${index}`} className="contents">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
{isLast || breadcrumb.isCurrentPage ? (
|
{isLast || !breadcrumb.href ? (
|
||||||
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
|
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
|
||||||
) : breadcrumb.href ? (
|
) : (
|
||||||
<BreadcrumbLink asChild>
|
<BreadcrumbLink asChild>
|
||||||
<Link to={breadcrumb.href}>{breadcrumb.label}</Link>
|
<Link to={breadcrumb.href}>{breadcrumb.label}</Link>
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
) : (
|
|
||||||
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
|
|
||||||
)}
|
)}
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
{!isLast && <BreadcrumbSeparator />}
|
{!isLast && <BreadcrumbSeparator />}
|
||||||
|
|||||||
@@ -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<string, string | undefined>): 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);
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,13 @@ import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
|
|||||||
import { SnapshotTimeline } from "../components/snapshot-timeline";
|
import { SnapshotTimeline } from "../components/snapshot-timeline";
|
||||||
import { getBackupSchedule } from "~/client/api-client";
|
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) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "Backup Job Details" },
|
{ title: "Backup Job Details" },
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import type { Route } from "./+types/backups";
|
|||||||
import { listBackupSchedules } from "~/client/api-client";
|
import { listBackupSchedules } from "~/client/api-client";
|
||||||
import { listBackupSchedulesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listBackupSchedulesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
export const handle = {
|
||||||
|
breadcrumb: () => [{ label: "Backups" }],
|
||||||
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "Backup Jobs" },
|
{ title: "Backup Jobs" },
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import { CreateScheduleForm, type BackupScheduleFormValues } from "../components
|
|||||||
import type { Route } from "./+types/create-backup";
|
import type { Route } from "./+types/create-backup";
|
||||||
import { listRepositories, listVolumes } from "~/client/api-client";
|
import { listRepositories, listVolumes } from "~/client/api-client";
|
||||||
|
|
||||||
|
export const handle = {
|
||||||
|
breadcrumb: () => [{ label: "Backups", href: "/backups" }, { label: "Create" }],
|
||||||
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "Create Backup Job" },
|
{ title: "Create Backup Job" },
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import type { Route } from "./+types/repositories";
|
|||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
import { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
|
|
||||||
|
export const handle = {
|
||||||
|
breadcrumb: () => [{ label: "Repositories" }],
|
||||||
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "Repositories" },
|
{ title: "Repositories" },
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ import { RepositoryInfoTabContent } from "../tabs/info";
|
|||||||
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
||||||
import { Loader2 } from "lucide-react";
|
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) {
|
export function meta({ params }: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: params.name },
|
{ title: params.name },
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapsho
|
|||||||
import { getSnapshotDetails } from "~/client/api-client";
|
import { getSnapshotDetails } from "~/client/api-client";
|
||||||
import type { Route } from "./+types/snapshot-details";
|
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) {
|
export function meta({ params }: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: `Snapshot ${params.snapshotId}` },
|
{ title: `Snapshot ${params.snapshotId}` },
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import {
|
|||||||
logoutMutation,
|
logoutMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
export const handle = {
|
||||||
|
breadcrumb: () => [{ label: "Settings" }],
|
||||||
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "Settings" },
|
{ title: "Settings" },
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ import {
|
|||||||
unmountVolumeMutation,
|
unmountVolumeMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} 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) {
|
export function meta({ params }: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: params.name },
|
{ title: params.name },
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import type { Route } from "./+types/volumes";
|
|||||||
import { listVolumes } from "~/client/api-client";
|
import { listVolumes } from "~/client/api-client";
|
||||||
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
export const handle = {
|
||||||
|
breadcrumb: () => [{ label: "Volumes" }],
|
||||||
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "Volumes" },
|
{ title: "Volumes" },
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { redirect } from "react-router";
|
import { redirect } from "react-router";
|
||||||
|
|
||||||
export const loader = async () => {
|
|
||||||
return redirect("/volumes");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clientLoader = async () => {
|
export const clientLoader = async () => {
|
||||||
return redirect("/volumes");
|
return redirect("/volumes");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loader = async () => {
|
||||||
|
return redirect("/volumes");
|
||||||
|
};
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ class SchedulerClass {
|
|||||||
this.tasks = [];
|
this.tasks = [];
|
||||||
logger.info("Scheduler stopped");
|
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();
|
export const Scheduler = new SchedulerClass();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { toMessage } from "../../utils/errors";
|
|||||||
const COOKIE_NAME = "session_id";
|
const COOKIE_NAME = "session_id";
|
||||||
const COOKIE_OPTIONS = {
|
const COOKIE_OPTIONS = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: false,
|
||||||
sameSite: "lax" as const,
|
sameSite: "lax" as const,
|
||||||
path: "/",
|
path: "/",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
|
|||||||
|
|
||||||
export const startup = async () => {
|
export const startup = async () => {
|
||||||
await Scheduler.start();
|
await Scheduler.start();
|
||||||
|
await Scheduler.clear();
|
||||||
|
|
||||||
await restic.ensurePassfile().catch((err) => {
|
await restic.ensurePassfile().catch((err) => {
|
||||||
logger.error(`Error ensuring restic passfile exists: ${err.message}`);
|
logger.error(`Error ensuring restic passfile exists: ${err.message}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user