parent
6354705626
commit
2a1eaf29d0
12 changed files with 191 additions and 135 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -43,3 +43,4 @@ qa-output
|
|||
|
||||
.worktrees/
|
||||
.turbo
|
||||
.superset
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
|
||||
const snapshotFiles = {
|
||||
files: [
|
||||
{ name: "project", path: "/mnt/project", type: "dir" },
|
||||
{ name: "a.txt", path: "/mnt/project/a.txt", type: "file" },
|
||||
],
|
||||
};
|
||||
|
||||
await mock.module("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: snapshotFiles, isLoading: false, error: null }),
|
||||
useQueryClient: () => ({
|
||||
ensureQueryData: async () => snapshotFiles,
|
||||
prefetchQuery: async () => undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { SnapshotTreeBrowser } from "../snapshot-tree-browser";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("SnapshotTreeBrowser", () => {
|
||||
test("renders the query root folder when display base path is broader than query base path", () => {
|
||||
render(
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId="repo-1"
|
||||
snapshotId="snap-1"
|
||||
queryBasePath="/mnt/project"
|
||||
displayBasePath="/mnt"
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByRole("button", { name: "project" });
|
||||
});
|
||||
|
||||
test("shows selected folder state when full paths are provided from the parent", () => {
|
||||
render(
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId="repo-1"
|
||||
snapshotId="snap-1"
|
||||
queryBasePath="/mnt/project"
|
||||
displayBasePath="/mnt"
|
||||
withCheckboxes
|
||||
selectedPaths={new Set(["/mnt/project"])}
|
||||
onSelectionChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const row = screen.getByRole("button", { name: "project" });
|
||||
const checkbox = within(row).getByRole("checkbox");
|
||||
|
||||
expect(checkbox.getAttribute("aria-checked")).toBe("true");
|
||||
});
|
||||
|
||||
test("returns the full snapshot path and kind when selecting a displayed folder", () => {
|
||||
let selectedPaths: Set<string> | undefined;
|
||||
let selectedKind: "file" | "dir" | null = null;
|
||||
|
||||
render(
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId="repo-1"
|
||||
snapshotId="snap-1"
|
||||
queryBasePath="/mnt/project"
|
||||
displayBasePath="/mnt"
|
||||
withCheckboxes
|
||||
onSelectionChange={(paths) => {
|
||||
selectedPaths = paths;
|
||||
}}
|
||||
onSingleSelectionKindChange={(kind) => {
|
||||
selectedKind = kind;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const row = screen.getByRole("button", { name: "project" });
|
||||
const checkbox = within(row).getByRole("checkbox");
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(selectedPaths ? Array.from(selectedPaths) : []).toEqual(["/mnt/project"]);
|
||||
expect(selectedKind === "dir").toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,92 +7,91 @@ import { parseError } from "~/client/lib/errors";
|
|||
import { normalizeAbsolutePath } from "@zerobyte/core/utils";
|
||||
import { logger } from "~/client/lib/logger";
|
||||
|
||||
function createPathPrefixFns(basePath: string) {
|
||||
return {
|
||||
strip(path: string) {
|
||||
if (basePath === "/") return path;
|
||||
if (path === basePath) return "/";
|
||||
if (path.startsWith(`${basePath}/`)) return path.slice(basePath.length);
|
||||
return path;
|
||||
},
|
||||
add(displayPath: string) {
|
||||
if (basePath === "/") return displayPath;
|
||||
if (displayPath === "/") return basePath;
|
||||
return `${basePath}${displayPath}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type SnapshotTreeBrowserProps = FileBrowserUiProps & {
|
||||
repositoryId: string;
|
||||
snapshotId: string;
|
||||
basePath?: string;
|
||||
queryBasePath?: string;
|
||||
displayBasePath?: string;
|
||||
pageSize?: number;
|
||||
enabled?: boolean;
|
||||
onSingleSelectionKindChange?: (kind: "file" | "dir" | null) => void;
|
||||
};
|
||||
|
||||
export const SnapshotTreeBrowser = ({
|
||||
repositoryId,
|
||||
snapshotId,
|
||||
basePath = "/",
|
||||
pageSize = 500,
|
||||
enabled = true,
|
||||
...uiProps
|
||||
}: SnapshotTreeBrowserProps) => {
|
||||
export const SnapshotTreeBrowser = (props: SnapshotTreeBrowserProps) => {
|
||||
const {
|
||||
repositoryId,
|
||||
snapshotId,
|
||||
queryBasePath = "/",
|
||||
displayBasePath,
|
||||
pageSize = 500,
|
||||
enabled = true,
|
||||
...uiProps
|
||||
} = props;
|
||||
|
||||
const { selectedPaths, onSelectionChange, onSingleSelectionKindChange, ...fileBrowserUiProps } = uiProps;
|
||||
const queryClient = useQueryClient();
|
||||
const normalizedBasePath = normalizeAbsolutePath(basePath);
|
||||
const normalizedQueryBasePath = normalizeAbsolutePath(queryBasePath);
|
||||
const normalizedDisplayBasePath = normalizeAbsolutePath(displayBasePath ?? normalizedQueryBasePath);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
...listSnapshotFilesOptions({
|
||||
path: { shortId: repositoryId, snapshotId },
|
||||
query: { path: normalizedBasePath },
|
||||
query: { path: normalizedQueryBasePath },
|
||||
}),
|
||||
enabled,
|
||||
});
|
||||
|
||||
const stripBasePath = useCallback(
|
||||
(path: string): string => {
|
||||
if (normalizedBasePath === "/") return path;
|
||||
if (path === normalizedBasePath) return "/";
|
||||
if (path.startsWith(`${normalizedBasePath}/`)) {
|
||||
return path.slice(normalizedBasePath.length);
|
||||
}
|
||||
return path;
|
||||
},
|
||||
[normalizedBasePath],
|
||||
);
|
||||
|
||||
const addBasePath = useCallback(
|
||||
(displayPath: string): string => {
|
||||
if (normalizedBasePath === "/") return displayPath;
|
||||
if (displayPath === "/") return normalizedBasePath;
|
||||
return `${normalizedBasePath}${displayPath}`;
|
||||
},
|
||||
[normalizedBasePath],
|
||||
);
|
||||
const displayPathFns = useMemo(() => createPathPrefixFns(normalizedDisplayBasePath), [normalizedDisplayBasePath]);
|
||||
|
||||
const displaySelectedPaths = useMemo(() => {
|
||||
if (!selectedPaths) return undefined;
|
||||
|
||||
const displayPaths = new Set<string>();
|
||||
for (const fullPath of selectedPaths) {
|
||||
displayPaths.add(stripBasePath(fullPath));
|
||||
displayPaths.add(displayPathFns.strip(fullPath));
|
||||
}
|
||||
|
||||
return displayPaths;
|
||||
}, [selectedPaths, stripBasePath]);
|
||||
}, [displayPathFns, selectedPaths]);
|
||||
|
||||
const fileBrowser = useFileBrowser({
|
||||
initialData: data,
|
||||
isLoading,
|
||||
fetchFolder: async (path, offset = 0) => {
|
||||
fetchFolder: async (displayPath, offset = 0) => {
|
||||
return await queryClient.ensureQueryData(
|
||||
listSnapshotFilesOptions({
|
||||
path: { shortId: repositoryId, snapshotId },
|
||||
query: { path, offset: offset, limit: pageSize },
|
||||
query: { path: displayPath, offset: offset, limit: pageSize },
|
||||
}),
|
||||
);
|
||||
},
|
||||
prefetchFolder: (path) => {
|
||||
prefetchFolder: (displayPath) => {
|
||||
void queryClient
|
||||
.prefetchQuery(
|
||||
listSnapshotFilesOptions({
|
||||
path: { shortId: repositoryId, snapshotId },
|
||||
query: { path, offset: 0, limit: pageSize },
|
||||
query: { path: displayPath, offset: 0, limit: pageSize },
|
||||
}),
|
||||
)
|
||||
.catch((e) => logger.error(e));
|
||||
},
|
||||
pathTransform: {
|
||||
strip: stripBasePath,
|
||||
add: addBasePath,
|
||||
},
|
||||
pathTransform: displayPathFns,
|
||||
});
|
||||
|
||||
const displayPathKinds = useMemo(() => {
|
||||
|
|
@ -109,7 +108,7 @@ export const SnapshotTreeBrowser = ({
|
|||
|
||||
const nextFullPaths = new Set<string>();
|
||||
for (const displayPath of nextDisplayPaths) {
|
||||
nextFullPaths.add(addBasePath(displayPath));
|
||||
nextFullPaths.add(displayPathFns.add(displayPath));
|
||||
}
|
||||
|
||||
if (onSingleSelectionKindChange) {
|
||||
|
|
@ -127,7 +126,7 @@ export const SnapshotTreeBrowser = ({
|
|||
|
||||
onSelectionChange(nextFullPaths);
|
||||
},
|
||||
[onSelectionChange, addBasePath, onSingleSelectionKindChange, displayPathKinds],
|
||||
[displayPathFns, displayPathKinds, onSelectionChange, onSingleSelectionKindChange],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -33,14 +33,15 @@ interface RestoreFormProps {
|
|||
repository: Repository;
|
||||
snapshotId: string;
|
||||
returnPath: string;
|
||||
basePath?: string;
|
||||
queryBasePath?: string;
|
||||
displayBasePath?: string;
|
||||
}
|
||||
|
||||
export function RestoreForm({ repository, snapshotId, returnPath, basePath }: RestoreFormProps) {
|
||||
export function RestoreForm({ repository, snapshotId, returnPath, queryBasePath, displayBasePath }: RestoreFormProps) {
|
||||
const navigate = useNavigate();
|
||||
const { addEventListener } = useServerEvents();
|
||||
|
||||
const volumeBasePath = basePath ?? "/";
|
||||
const snapshotBasePath = queryBasePath ?? "/";
|
||||
|
||||
const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>("original");
|
||||
const [customTargetPath, setCustomTargetPath] = useState("");
|
||||
|
|
@ -346,7 +347,8 @@ export function RestoreForm({ repository, snapshotId, returnPath, basePath }: Re
|
|||
<SnapshotTreeBrowser
|
||||
repositoryId={repository.shortId}
|
||||
snapshotId={snapshotId}
|
||||
basePath={volumeBasePath}
|
||||
queryBasePath={snapshotBasePath}
|
||||
displayBasePath={displayBasePath}
|
||||
pageSize={500}
|
||||
className="flex flex-1 min-h-0 flex-col"
|
||||
treeContainerClassName="overflow-auto flex-1 min-h-0 border border-border rounded-md bg-card m-4"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { RotateCcw, Trash2 } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
import { Button, buttonVariants } from "~/client/components/ui/button";
|
||||
import type { Snapshot } from "~/client/lib/types";
|
||||
|
|
@ -7,14 +6,13 @@ import { useTimeFormat } from "~/client/lib/datetime";
|
|||
import { cn } from "~/client/lib/utils";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser";
|
||||
import { getBackupScheduleOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { getVolumeMountPath } from "~/client/lib/volume-path";
|
||||
import { findCommonAncestor } from "@zerobyte/core/utils";
|
||||
|
||||
interface Props {
|
||||
snapshot: Snapshot;
|
||||
repositoryId: string;
|
||||
backupId?: string;
|
||||
basePath?: string;
|
||||
displayBasePath?: string;
|
||||
onDeleteSnapshot?: (snapshotId: string) => 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 <TreeBrowserFallback />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId={repositoryId}
|
||||
snapshotId={snapshotId}
|
||||
basePath={schedule ? getVolumeMountPath(schedule.volume) : "/"}
|
||||
{...treeProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TreeBrowserFallback = () => (
|
||||
<div className={cn(treeProps.treeContainerClassName, "flex items-center justify-center")}>
|
||||
<p className="text-muted-foreground">Loading volume info...</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -110,27 +76,13 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden flex flex-col p-0">
|
||||
{basePath ? (
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId={repositoryId}
|
||||
snapshotId={snapshot.short_id}
|
||||
basePath={basePath}
|
||||
{...treeProps}
|
||||
/>
|
||||
) : scheduleShortId ? (
|
||||
<ScheduleAwareTreeBrowser
|
||||
scheduleShortId={scheduleShortId}
|
||||
repositoryId={repositoryId}
|
||||
snapshotId={snapshot.short_id}
|
||||
/>
|
||||
) : (
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId={repositoryId}
|
||||
snapshotId={snapshot.short_id}
|
||||
basePath="/"
|
||||
{...treeProps}
|
||||
/>
|
||||
)}
|
||||
<SnapshotTreeBrowser
|
||||
repositoryId={repositoryId}
|
||||
snapshotId={snapshot.short_id}
|
||||
queryBasePath={queryBasePath}
|
||||
displayBasePath={displayBasePath}
|
||||
{...treeProps}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 <RestoreForm repository={repository} snapshotId={snapshotId} returnPath={returnPath} basePath={basePath} />;
|
||||
return (
|
||||
<RestoreForm
|
||||
repository={repository}
|
||||
snapshotId={snapshotId}
|
||||
returnPath={returnPath}
|
||||
queryBasePath={queryBasePath}
|
||||
displayBasePath={displayBasePath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export function SnapshotDetailsPage({ repositoryId, snapshotId, initialSnapshot
|
|||
<SnapshotFileBrowser
|
||||
repositoryId={repositoryId}
|
||||
snapshot={data}
|
||||
basePath={backupSchedule ? getVolumeMountPath(backupSchedule.volume) : undefined}
|
||||
displayBasePath={backupSchedule ? getVolumeMountPath(backupSchedule.volume) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<SnapshotFileBrowserSkeleton />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<RestoreSnapshotPage
|
||||
returnPath={`/backups/${backupId}`}
|
||||
snapshotId={snapshotId}
|
||||
repository={repository}
|
||||
basePath={basePath}
|
||||
queryBasePath={queryBasePath}
|
||||
displayBasePath={displayBasePath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<RestoreSnapshotPage
|
||||
returnPath={`/repositories/${repositoryId}/${snapshotId}`}
|
||||
repository={repository}
|
||||
snapshotId={snapshotId}
|
||||
basePath={basePath}
|
||||
queryBasePath={queryBasePath}
|
||||
displayBasePath={displayBasePath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"include": ["**/*"],
|
||||
"include": ["app/**/*"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue