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:
parent
3bf3b22b96
commit
74d34a578b
5 changed files with 75 additions and 0 deletions
52
app/client/components/path-selector.tsx
Normal file
52
app/client/components/path-selector.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "~/client/components/ui/alert-dialog";
|
} from "~/client/components/ui/alert-dialog";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
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 type { Snapshot, Volume } from "~/client/lib/types";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
@ -141,6 +142,7 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||||
path: { name: repositoryName },
|
path: { name: repositoryName },
|
||||||
body: {
|
body: {
|
||||||
snapshotId: snapshot.short_id,
|
snapshotId: snapshot.short_id,
|
||||||
|
target: targetPath || undefined,
|
||||||
include: includePaths,
|
include: includePaths,
|
||||||
delete: deleteExtraFiles,
|
delete: deleteExtraFiles,
|
||||||
excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,
|
excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export const RestoreSnapshotDialog = ({ name, snapshotId }: Props) => {
|
||||||
path: { name },
|
path: { name },
|
||||||
body: {
|
body: {
|
||||||
snapshotId,
|
snapshotId,
|
||||||
|
target: values.target || undefined,
|
||||||
include: include && include.length > 0 ? include : undefined,
|
include: include && include.length > 0 ? include : undefined,
|
||||||
exclude: exclude && exclude.length > 0 ? exclude : undefined,
|
exclude: exclude && exclude.length > 0 ? exclude : undefined,
|
||||||
excludeXattr: excludeXattr && excludeXattr.length > 0 ? excludeXattr : undefined,
|
excludeXattr: excludeXattr && excludeXattr.length > 0 ? excludeXattr : undefined,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { Button } from "~/client/components/ui/button";
|
||||||
|
|
||||||
const restoreSnapshotFormSchema = type({
|
const restoreSnapshotFormSchema = type({
|
||||||
path: "string?",
|
path: "string?",
|
||||||
|
target: "string?",
|
||||||
include: "string?",
|
include: "string?",
|
||||||
exclude: "string?",
|
exclude: "string?",
|
||||||
excludeXattr: "string?",
|
excludeXattr: "string?",
|
||||||
|
|
@ -37,6 +38,7 @@ export const RestoreSnapshotForm = ({ formId, onSubmit, className }: Props) => {
|
||||||
resolver: arktypeResolver(restoreSnapshotFormSchema),
|
resolver: arktypeResolver(restoreSnapshotFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
path: "",
|
path: "",
|
||||||
|
target: "",
|
||||||
include: "",
|
include: "",
|
||||||
exclude: "",
|
exclude: "",
|
||||||
excludeXattr: "",
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="include"
|
name="include"
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,7 @@ export const overwriteModeSchema = type.valueOf(OVERWRITE_MODES);
|
||||||
|
|
||||||
export const restoreSnapshotBody = type({
|
export const restoreSnapshotBody = type({
|
||||||
snapshotId: "string",
|
snapshotId: "string",
|
||||||
|
target: "string?",
|
||||||
include: "string[]?",
|
include: "string[]?",
|
||||||
exclude: "string[]?",
|
exclude: "string[]?",
|
||||||
excludeXattr: "string[]?",
|
excludeXattr: "string[]?",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue