feat: add custom restore target directory

Adds the ability to restore snapshots to a custom directory instead of
only the original path. Closes #12.

Changes:
- Add target parameter to restore API endpoint
- Add directory picker UI in file browser restore dialog
- Add target input field in snapshot restore form
- Create reusable PathSelector component

Note: Run `bun run gen:api-client` after merging to regenerate types.
This commit is contained in:
Deepseek1 2025-11-28 12:09:14 +01:00 committed by Nicolas Meienberger
parent 3bf3b22b96
commit 74d34a578b
5 changed files with 75 additions and 0 deletions

View file

@ -0,0 +1,52 @@
import { useState } from "react";
import { DirectoryBrowser } from "./directory-browser";
import { Button } from "./ui/button";
type Props = {
value: string;
onChange: (path: string) => void;
label?: string;
};
/**
* A reusable path selector component that shows the selected path
* with a "Change" button, and expands to a DirectoryBrowser when clicked.
* Matches the pattern used in the volume creation form.
*/
export const PathSelector = ({ value, onChange, label = "Selected path:" }: Props) => {
const [showBrowser, setShowBrowser] = useState(false);
if (showBrowser) {
return (
<div className="space-y-2">
<DirectoryBrowser
onSelectPath={(path) => {
onChange(path);
setShowBrowser(false);
}}
selectedPath={value}
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowBrowser(false)}
>
Cancel
</Button>
</div>
);
}
return (
<div className="flex items-center gap-2">
<div className="flex-1 border rounded-md p-3 bg-muted/50">
<div className="text-xs font-medium text-muted-foreground mb-1">{label}</div>
<div className="text-sm font-mono break-all">{value}</div>
</div>
<Button type="button" variant="outline" size="sm" onClick={() => setShowBrowser(true)}>
Change
</Button>
</div>
);
};

View file

@ -19,6 +19,7 @@ import {
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
import { PathSelector } from "~/client/components/path-selector";
import type { Snapshot, Volume } from "~/client/lib/types";
import { toast } from "sonner";
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
@ -141,6 +142,7 @@ export const SnapshotFileBrowser = (props: Props) => {
path: { name: repositoryName },
body: {
snapshotId: snapshot.short_id,
target: targetPath || undefined,
include: includePaths,
delete: deleteExtraFiles,
excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,

View file

@ -61,6 +61,7 @@ export const RestoreSnapshotDialog = ({ name, snapshotId }: Props) => {
path: { name },
body: {
snapshotId,
target: values.target || undefined,
include: include && include.length > 0 ? include : undefined,
exclude: exclude && exclude.length > 0 ? exclude : undefined,
excludeXattr: excludeXattr && excludeXattr.length > 0 ? excludeXattr : undefined,

View file

@ -17,6 +17,7 @@ import { Button } from "~/client/components/ui/button";
const restoreSnapshotFormSchema = type({
path: "string?",
target: "string?",
include: "string?",
exclude: "string?",
excludeXattr: "string?",
@ -37,6 +38,7 @@ export const RestoreSnapshotForm = ({ formId, onSubmit, className }: Props) => {
resolver: arktypeResolver(restoreSnapshotFormSchema),
defaultValues: {
path: "",
target: "",
include: "",
exclude: "",
excludeXattr: "",
@ -68,6 +70,23 @@ export const RestoreSnapshotForm = ({ formId, onSubmit, className }: Props) => {
)}
/>
<FormField
control={form.control}
name="target"
render={({ field }) => (
<FormItem>
<FormLabel>Target Directory (Optional)</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormDescription>
Restore to a custom location instead of the original path (defaults to /)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="include"

View file

@ -279,6 +279,7 @@ export const overwriteModeSchema = type.valueOf(OVERWRITE_MODES);
export const restoreSnapshotBody = type({
snapshotId: "string",
target: "string?",
include: "string[]?",
exclude: "string[]?",
excludeXattr: "string[]?",