mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
380 lines
10 KiB
TypeScript
380 lines
10 KiB
TypeScript
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { cn } from "@/lib/utils";
|
|
import { api } from "@/utils/api";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import copy from "copy-to-clipboard";
|
|
import { debounce } from "lodash";
|
|
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
|
|
|
interface Props {
|
|
databaseId: string;
|
|
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
|
serverId?: string | null;
|
|
}
|
|
|
|
const RestoreBackupSchema = z.object({
|
|
destinationId: z
|
|
.string({
|
|
required_error: "Please select a destination",
|
|
})
|
|
.min(1, {
|
|
message: "Destination is required",
|
|
}),
|
|
backupFile: z
|
|
.string({
|
|
required_error: "Please select a backup file",
|
|
})
|
|
.min(1, {
|
|
message: "Backup file is required",
|
|
}),
|
|
databaseName: z
|
|
.string({
|
|
required_error: "Please enter a database name",
|
|
})
|
|
.min(1, {
|
|
message: "Database name is required",
|
|
}),
|
|
});
|
|
|
|
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
|
|
|
|
export const RestoreBackup = ({
|
|
databaseId,
|
|
databaseType,
|
|
serverId,
|
|
}: Props) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
|
|
const { data: destinations = [] } = api.destination.all.useQuery();
|
|
|
|
const form = useForm<RestoreBackup>({
|
|
defaultValues: {
|
|
destinationId: "",
|
|
backupFile: "",
|
|
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
|
},
|
|
resolver: zodResolver(RestoreBackupSchema),
|
|
});
|
|
|
|
const destionationId = form.watch("destinationId");
|
|
|
|
const debouncedSetSearch = debounce((value: string) => {
|
|
setSearch(value);
|
|
}, 300);
|
|
|
|
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
|
{
|
|
destinationId: destionationId,
|
|
search,
|
|
serverId: serverId ?? "",
|
|
},
|
|
{
|
|
enabled: isOpen && !!destionationId,
|
|
},
|
|
);
|
|
|
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
|
const [isDeploying, setIsDeploying] = useState(false);
|
|
|
|
// const { mutateAsync: restore, isLoading: isRestoring } =
|
|
// api.backup.restoreBackup.useMutation();
|
|
|
|
api.backup.restoreBackupWithLogs.useSubscription(
|
|
{
|
|
databaseId,
|
|
databaseType,
|
|
databaseName: form.watch("databaseName"),
|
|
backupFile: form.watch("backupFile"),
|
|
destinationId: form.watch("destinationId"),
|
|
},
|
|
{
|
|
enabled: isDeploying,
|
|
onData(log) {
|
|
if (!isDrawerOpen) {
|
|
setIsDrawerOpen(true);
|
|
}
|
|
|
|
if (log === "Restore completed successfully!") {
|
|
setIsDeploying(false);
|
|
}
|
|
const parsedLogs = parseLogs(log);
|
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
|
},
|
|
onError(error) {
|
|
console.error("Restore logs error:", error);
|
|
setIsDeploying(false);
|
|
},
|
|
},
|
|
);
|
|
|
|
const onSubmit = async (_data: RestoreBackup) => {
|
|
setIsDeploying(true);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline">
|
|
<RotateCcw className="mr-2 size-4" />
|
|
Restore Backup
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center">
|
|
<RotateCcw className="mr-2 size-4" />
|
|
Restore Backup
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Select a destination and search for backup files
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<Form {...form}>
|
|
<form
|
|
id="hook-form-restore-backup"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="grid w-full gap-4"
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name="destinationId"
|
|
render={({ field }) => (
|
|
<FormItem className="">
|
|
<FormLabel>Destination</FormLabel>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<FormControl>
|
|
<Button
|
|
variant="outline"
|
|
className={cn(
|
|
"w-full justify-between !bg-input",
|
|
!field.value && "text-muted-foreground",
|
|
)}
|
|
>
|
|
{field.value
|
|
? destinations.find(
|
|
(d) => d.destinationId === field.value,
|
|
)?.name
|
|
: "Select Destination"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</FormControl>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start">
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="Search destinations..."
|
|
className="h-9"
|
|
/>
|
|
<CommandEmpty>No destinations found.</CommandEmpty>
|
|
<ScrollArea className="h-64">
|
|
<CommandGroup>
|
|
{destinations.map((destination) => (
|
|
<CommandItem
|
|
value={destination.destinationId}
|
|
key={destination.destinationId}
|
|
onSelect={() => {
|
|
form.setValue(
|
|
"destinationId",
|
|
destination.destinationId,
|
|
);
|
|
}}
|
|
>
|
|
{destination.name}
|
|
<CheckIcon
|
|
className={cn(
|
|
"ml-auto h-4 w-4",
|
|
destination.destinationId === field.value
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</ScrollArea>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="backupFile"
|
|
render={({ field }) => (
|
|
<FormItem className="">
|
|
<FormLabel className="flex items-center justify-between">
|
|
Search Backup Files
|
|
{field.value && (
|
|
<Badge variant="outline">
|
|
{field.value}
|
|
<Copy
|
|
className="ml-2 size-4 cursor-pointer"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
copy(field.value);
|
|
toast.success("Backup file copied to clipboard");
|
|
}}
|
|
/>
|
|
</Badge>
|
|
)}
|
|
</FormLabel>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<FormControl>
|
|
<Button
|
|
variant="outline"
|
|
className={cn(
|
|
"w-full justify-between !bg-input",
|
|
!field.value && "text-muted-foreground",
|
|
)}
|
|
>
|
|
{field.value || "Search and select a backup file"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</FormControl>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start">
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="Search backup files..."
|
|
onValueChange={debouncedSetSearch}
|
|
className="h-9"
|
|
/>
|
|
{isLoading ? (
|
|
<div className="py-6 text-center text-sm">
|
|
Loading backup files...
|
|
</div>
|
|
) : files.length === 0 && search ? (
|
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
No backup files found for "{search}"
|
|
</div>
|
|
) : files.length === 0 ? (
|
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
No backup files available
|
|
</div>
|
|
) : (
|
|
<ScrollArea className="h-64">
|
|
<CommandGroup>
|
|
{files.map((file) => (
|
|
<CommandItem
|
|
value={file}
|
|
key={file}
|
|
onSelect={() => {
|
|
form.setValue("backupFile", file);
|
|
}}
|
|
>
|
|
<div className="flex w-full justify-between">
|
|
<span>{file}</span>
|
|
</div>
|
|
<CheckIcon
|
|
className={cn(
|
|
"ml-auto h-4 w-4",
|
|
file === field.value
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</ScrollArea>
|
|
)}
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="databaseName"
|
|
render={({ field }) => (
|
|
<FormItem className="">
|
|
<FormLabel>Database Name</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
disabled={databaseType === "web-server"}
|
|
{...field}
|
|
placeholder="Enter database name"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<DialogFooter>
|
|
<Button
|
|
isLoading={isDeploying}
|
|
form="hook-form-restore-backup"
|
|
type="submit"
|
|
disabled={!form.watch("backupFile")}
|
|
>
|
|
Restore
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</Form>
|
|
|
|
<DrawerLogs
|
|
isOpen={isDrawerOpen}
|
|
onClose={() => {
|
|
setIsDrawerOpen(false);
|
|
setFilteredLogs([]);
|
|
setIsDeploying(false);
|
|
// refetch();
|
|
}}
|
|
filteredLogs={filteredLogs}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|