void;
isDeletingSnapshot?: boolean;
}
@@ -28,43 +26,11 @@ const treeProps = {
stateClassName: "flex-1 min-h-0",
} as const;
-interface ScheduleAwareTreeBrowserProps {
- scheduleShortId: string;
- repositoryId: string;
- snapshotId: string;
-}
-
-const ScheduleAwareTreeBrowser = ({ scheduleShortId, repositoryId, snapshotId }: ScheduleAwareTreeBrowserProps) => {
- const { data: schedule, isPending } = useQuery({
- ...getBackupScheduleOptions({ path: { shortId: scheduleShortId } }),
- retry: false,
- });
-
- if (isPending) {
- return ;
- }
-
- return (
-
- );
-};
-
-const TreeBrowserFallback = () => (
-
-
Loading volume info...
-
-);
-
export const SnapshotFileBrowser = (props: Props) => {
- const { snapshot, repositoryId, backupId, basePath, onDeleteSnapshot, isDeletingSnapshot } = props;
+ const { snapshot, repositoryId, backupId, displayBasePath, onDeleteSnapshot, isDeletingSnapshot } = props;
const { formatDateTime } = useTimeFormat();
- const scheduleShortId = !basePath ? backupId || snapshot.tags?.[0] : undefined;
+ const queryBasePath = findCommonAncestor(snapshot.paths);
return (
@@ -110,27 +76,13 @@ export const SnapshotFileBrowser = (props: Props) => {
- {basePath ? (
-
- ) : scheduleShortId ? (
-
- ) : (
-
- )}
+
diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx
index a581a7b3..48b6bca6 100644
--- a/app/client/modules/backups/routes/backup-details.tsx
+++ b/app/client/modules/backups/routes/backup-details.tsx
@@ -34,6 +34,7 @@ import { ScheduleNotificationsConfig } from "../components/schedule-notification
import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
import { BackupSummaryCard } from "~/client/components/backup-summary-card";
import { cn } from "~/client/lib/utils";
+import { getVolumeMountPath } from "~/client/lib/volume-path";
import type {
BackupSchedule,
NotificationDestination,
@@ -303,6 +304,7 @@ export function ScheduleDetailsPage(props: Props) {
snapshot={selectedSnapshot}
repositoryId={schedule.repository.shortId}
backupId={schedule.shortId}
+ displayBasePath={getVolumeMountPath(schedule.volume)}
onDeleteSnapshot={handleDeleteSnapshot}
isDeletingSnapshot={deleteSnapshot.isPending}
/>
diff --git a/app/client/modules/repositories/routes/restore-snapshot.tsx b/app/client/modules/repositories/routes/restore-snapshot.tsx
index 2459aa21..24761b42 100644
--- a/app/client/modules/repositories/routes/restore-snapshot.tsx
+++ b/app/client/modules/repositories/routes/restore-snapshot.tsx
@@ -5,11 +5,20 @@ type Props = {
repository: Repository;
snapshotId: string;
returnPath: string;
- basePath?: string;
+ queryBasePath?: string;
+ displayBasePath?: string;
};
export function RestoreSnapshotPage(props: Props) {
- const { returnPath, snapshotId, repository, basePath } = props;
+ const { returnPath, snapshotId, repository, queryBasePath, displayBasePath } = props;
- return ;
+ return (
+
+ );
}
diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx
index 43a6f301..832a3db4 100644
--- a/app/client/modules/repositories/routes/snapshot-details.tsx
+++ b/app/client/modules/repositories/routes/snapshot-details.tsx
@@ -101,7 +101,7 @@ export function SnapshotDetailsPage({ repositoryId, snapshotId, initialSnapshot
) : (
diff --git a/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx b/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx
index e9f80b4f..2082f56f 100644
--- a/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx
+++ b/app/routes/(dashboard)/backups/$backupId/$snapshotId.restore.tsx
@@ -3,6 +3,7 @@ import { getBackupSchedule } from "~/client/api-client";
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
import { getVolumeMountPath } from "~/client/lib/volume-path";
+import { findCommonAncestor } from "@zerobyte/core/utils";
export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId/restore")({
component: RouteComponent,
@@ -29,7 +30,8 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId
snapshot,
repository,
schedule: schedule.data,
- basePath: getVolumeMountPath(schedule.data.volume),
+ queryBasePath: findCommonAncestor(snapshot.paths),
+ displayBasePath: getVolumeMountPath(schedule.data.volume),
};
},
head: ({ params }) => ({
@@ -53,14 +55,15 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/$snapshotId
function RouteComponent() {
const { backupId, snapshotId } = Route.useParams();
- const { repository, basePath } = Route.useLoaderData();
+ const { repository, queryBasePath, displayBasePath } = Route.useLoaderData();
return (
);
}
diff --git a/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx
index cbf4f488..f5c298c1 100644
--- a/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx
+++ b/app/routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore.tsx
@@ -3,6 +3,7 @@ import { getBackupSchedule } from "~/client/api-client";
import { getRepositoryOptions, getSnapshotDetailsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { RestoreSnapshotPage } from "~/client/modules/repositories/routes/restore-snapshot";
import { getVolumeMountPath } from "~/client/lib/volume-path";
+import { findCommonAncestor } from "@zerobyte/core/utils";
export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$snapshotId/restore")({
component: RouteComponent,
@@ -15,16 +16,16 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s
context.queryClient.ensureQueryData({ ...getRepositoryOptions({ path: { shortId: params.repositoryId } }) }),
]);
- let basePath: string | undefined;
+ let displayBasePath: string | undefined;
const scheduleShortId = snapshot.tags?.[0];
if (scheduleShortId) {
const scheduleRes = await getBackupSchedule({ path: { shortId: scheduleShortId } });
if (scheduleRes.data) {
- basePath = getVolumeMountPath(scheduleRes.data.volume);
+ displayBasePath = getVolumeMountPath(scheduleRes.data.volume);
}
}
- return { snapshot, repository, basePath };
+ return { snapshot, repository, queryBasePath: findCommonAncestor(snapshot.paths), displayBasePath };
},
staticData: {
breadcrumb: (match) => [
@@ -47,14 +48,15 @@ export const Route = createFileRoute("/(dashboard)/repositories/$repositoryId/$s
function RouteComponent() {
const { repositoryId, snapshotId } = Route.useParams();
- const { repository, basePath } = Route.useLoaderData();
+ const { repository, queryBasePath, displayBasePath } = Route.useLoaderData();
return (
);
}
diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts
index be71ef20..efd01a4e 100644
--- a/e2e/0002-backup-restore.spec.ts
+++ b/e2e/0002-backup-restore.spec.ts
@@ -35,7 +35,7 @@ function getRunId(testInfo: TestInfo) {
return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`;
}
-function getWorkerTestDataPath(runId: string) {
+function getWorkerTestDataPath() {
fs.mkdirSync(testDataPath, { recursive: true });
return testDataPath;
}
@@ -49,7 +49,7 @@ function getScenarioNames(runId: string): ScenarioNames {
}
function prepareTestFile(runId: string, fileName = "test.json"): string {
- const runPath = path.join(getWorkerTestDataPath(runId), runId);
+ const runPath = path.join(getWorkerTestDataPath(), runId);
fs.mkdirSync(runPath, { recursive: true });
const filePath = path.join(runPath, fileName);
@@ -58,8 +58,8 @@ function prepareTestFile(runId: string, fileName = "test.json"): string {
return filePath;
}
-async function createBackupScenario(page: Page, names: ScenarioNames, runId: string, options: ScenarioOptions = {}) {
- getWorkerTestDataPath(runId);
+async function createBackupScenario(page: Page, names: ScenarioNames, options: ScenarioOptions = {}) {
+ getWorkerTestDataPath();
const volumeNameInput = page.getByRole("textbox", { name: "Name" });
await expect(async () => {
@@ -153,7 +153,7 @@ test("can backup & restore a file", async ({ page }, testInfo) => {
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId);
+ await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible();
@@ -177,7 +177,7 @@ test("can backup & restore a file", async ({ page }, testInfo) => {
test("can restore a single selected file to a custom location", async ({ page }, testInfo) => {
const runId = getRunId(testInfo);
const names = getScenarioNames(runId);
- const workerTestDataPath = getWorkerTestDataPath(runId);
+ const workerTestDataPath = getWorkerTestDataPath();
const fileName = `single-file-${runId}.json`;
const filePath = prepareTestFile(runId, fileName);
const restoreTargetPath = path.join(workerTestDataPath, fileName);
@@ -188,7 +188,7 @@ test("can restore a single selected file to a custom location", async ({ page },
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId);
+ await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible();
@@ -234,7 +234,7 @@ test("can re-tag a snapshot to another backup schedule", async ({ page }, testIn
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId);
+ await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible();
@@ -277,7 +277,7 @@ test("can delete a snapshot from the repository snapshots tab", async ({ page },
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId);
+ await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible();
@@ -302,7 +302,7 @@ test("can delete a snapshot from the repository snapshots tab", async ({ page },
test("can download a selected snapshot directory as a tar archive", async ({ page }, testInfo) => {
const runId = getRunId(testInfo);
const names = getScenarioNames(runId);
- const workerTestDataPath = getWorkerTestDataPath(runId);
+ const workerTestDataPath = getWorkerTestDataPath();
const fileName = `download-${runId}.json`;
const filePath = prepareTestFile(runId, fileName);
const downloadedPath = path.join(workerTestDataPath, `downloaded-${runId}.tar`);
@@ -312,7 +312,7 @@ test("can download a selected snapshot directory as a tar archive", async ({ pag
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId);
+ await createBackupScenario(page, names);
await page.getByRole("button", { name: "Backup now" }).click();
await expect(page.getByText("Backup started successfully")).toBeVisible();
@@ -351,7 +351,7 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId);
+ await createBackupScenario(page, names);
await gotoAndWaitForAppReady(page, "/backups");
await page.getByText(names.backupName, { exact: true }).first().click();
@@ -378,7 +378,7 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page
test("backup respects include globs, exclusion patterns, and exclude-if-present", async ({ page }, testInfo) => {
const runId = getRunId(testInfo);
const names = getScenarioNames(runId);
- const workerTestDataPath = getWorkerTestDataPath(runId);
+ const workerTestDataPath = getWorkerTestDataPath();
const keptDir = `kept-${runId}`;
const secondKeptDir = `second-kept-${runId}`;
@@ -433,7 +433,7 @@ test("backup respects include globs, exclusion patterns, and exclude-if-present"
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId, {
+ await createBackupScenario(page, names, {
includePatterns: [
`/${keptDir}`,
`/${secondKeptDir}`,
@@ -492,7 +492,7 @@ test("backup respects include globs, exclusion patterns, and exclude-if-present"
test("backup can include a selected folder whose name contains brackets", async ({ page }, testInfo) => {
const runId = getRunId(testInfo);
const names = getScenarioNames(runId);
- const workerTestDataPath = getWorkerTestDataPath(runId);
+ const workerTestDataPath = getWorkerTestDataPath();
const bracketDir = `movies [${runId}]`;
const bracketPath = path.join(workerTestDataPath, bracketDir);
const fileName = `inside-${runId}.txt`;
@@ -503,7 +503,7 @@ test("backup can include a selected folder whose name contains brackets", async
await gotoAndWaitForAppReady(page, "/");
await expect(page).toHaveURL("/volumes");
- await createBackupScenario(page, names, runId, {
+ await createBackupScenario(page, names, {
selectedPaths: [`/${bracketDir}`],
});
diff --git a/tsconfig.json b/tsconfig.json
index bfd4ee03..6956e49b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,5 @@
{
- "include": ["**/*"],
+ "include": ["app/**/*"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],