Merge branch 'canary' into feat/add-gitea-repo

This commit is contained in:
Jason Parks
2025-03-17 15:25:02 -06:00
committed by GitHub
28 changed files with 2109 additions and 1230 deletions

View File

@@ -27,6 +27,7 @@ if (typeof window === "undefined") {
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
applicationId: "", applicationId: "",
herokuVersion: "", herokuVersion: "",
cleanCache: false,
watchPaths: [], watchPaths: [],
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",

View File

@@ -7,6 +7,7 @@ import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
applicationId: "", applicationId: "",
herokuVersion: "", herokuVersion: "",
cleanCache: false,
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,

View File

@@ -16,8 +16,8 @@ import {
Ban, Ban,
CheckCircle2, CheckCircle2,
Hammer, Hammer,
HelpCircle,
RefreshCcw, RefreshCcw,
Rocket,
Terminal, Terminal,
} from "lucide-react"; } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -55,7 +55,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle> <CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap"> <CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0} disableHoverableContent={false}>
<DialogAction <DialogAction
title="Deploy Application" title="Deploy Application"
description="Are you sure you want to deploy this application?" description="Are you sure you want to deploy this application?"
@@ -79,12 +79,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button <Button
variant="default" variant="default"
isLoading={data?.applicationStatus === "running"} isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Deploy
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -114,9 +116,24 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
}); });
}} }}
> >
<Button variant="secondary" isLoading={isReloading}> <Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload Reload
<RefreshCcw className="size-4" /> </div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Reload the application without rebuilding it</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button> </Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
@@ -139,13 +156,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button <Button
variant="secondary" variant="secondary"
isLoading={data?.applicationStatus === "running"} isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Rebuild
<Hammer className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Hammer className="size-4 mr-1" />
Rebuild
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -180,13 +198,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button <Button
variant="secondary" variant="secondary"
isLoading={isStarting} isLoading={isStarting}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Start
<CheckCircle2 className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -219,13 +238,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button <Button
variant="destructive" variant="destructive"
isLoading={isStopping} isLoading={isStopping}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Stop
<Ban className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -241,15 +261,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button variant="outline"> <Button
<Terminal /> variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border"> <div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span> <span className="text-sm font-medium">Autodeploy</span>
<Switch <Switch
aria-label="Toggle italic" aria-label="Toggle autodeploy"
checked={data?.autoDeploy || false} checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => { onCheckedChange={async (enabled) => {
await update({ await update({
@@ -264,14 +287,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error updating Auto Deploy"); toast.error("Error updating Auto Deploy");
}); });
}} }}
className="flex flex-row gap-2 items-center" className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/> />
</div> </div>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border"> <div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Clean Cache</span> <span className="text-sm font-medium">Clean Cache</span>
<Switch <Switch
aria-label="Toggle italic" aria-label="Toggle clean cache"
checked={data?.cleanCache || false} checked={data?.cleanCache || false}
onCheckedChange={async (enabled) => { onCheckedChange={async (enabled) => {
await update({ await update({
@@ -286,7 +309,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error updating Clean Cache"); toast.error("Error updating Clean Cache");
}); });
}} }}
className="flex flex-row gap-2 items-center" className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/> />
</div> </div>
</CardContent> </CardContent>

View File

@@ -8,8 +8,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal, RefreshCcw, Rocket } from "lucide-react";
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { toast } from "sonner"; import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -34,7 +33,7 @@ export const ComposeActions = ({ composeId }: Props) => {
api.compose.stop.useMutation(); api.compose.stop.useMutation();
return ( return (
<div className="flex flex-row gap-4 w-full flex-wrap "> <div className="flex flex-row gap-4 w-full flex-wrap ">
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0} disableHoverableContent={false}>
<DialogAction <DialogAction
title="Deploy Compose" title="Deploy Compose"
description="Are you sure you want to deploy this compose?" description="Are you sure you want to deploy this compose?"
@@ -58,12 +57,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button <Button
variant="default" variant="default"
isLoading={data?.composeStatus === "running"} isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Deploy
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -74,36 +75,37 @@ export const ComposeActions = ({ composeId }: Props) => {
</Button> </Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Rebuild Compose" title="Reload Compose"
description="Are you sure you want to rebuild this compose?" description="Are you sure you want to reload this compose?"
type="default" type="default"
onClick={async () => { onClick={async () => {
await redeploy({ await redeploy({
composeId: composeId, composeId: composeId,
}) })
.then(() => { .then(() => {
toast.success("Compose rebuilt successfully"); toast.success("Compose reloaded successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error rebuilding compose"); toast.error("Error reloading compose");
}); });
}} }}
> >
<Button <Button
variant="secondary" variant="secondary"
isLoading={data?.composeStatus === "running"} isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Rebuild
<Hammer className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
<p>Only rebuilds the compose without downloading new code</p> <p>Reload the compose without rebuilding it</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
</Tooltip> </Tooltip>
@@ -131,13 +133,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button <Button
variant="secondary" variant="secondary"
isLoading={isStarting} isLoading={isStarting}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Start
<CheckCircle2 className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -169,13 +172,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button <Button
variant="destructive" variant="destructive"
isLoading={isStopping} isLoading={isStopping}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Stop
<Ban className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -191,15 +195,18 @@ export const ComposeActions = ({ composeId }: Props) => {
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button variant="outline"> <Button
<Terminal /> variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border"> <div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span> <span className="text-sm font-medium">Autodeploy</span>
<Switch <Switch
aria-label="Toggle italic" aria-label="Toggle autodeploy"
checked={data?.autoDeploy || false} checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => { onCheckedChange={async (enabled) => {
await update({ await update({
@@ -214,7 +221,7 @@ export const ComposeActions = ({ composeId }: Props) => {
toast.error("Error updating Auto Deploy"); toast.error("Error updating Auto Deploy");
}); });
}} }}
className="flex flex-row gap-2 items-center" className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/> />
</div> </div>
</div> </div>

View File

@@ -0,0 +1,367 @@
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 {
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 { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources";
import { debounce } from "lodash";
import { Input } from "@/components/ui/input";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
interface Props {
databaseId: string;
databaseType: Exclude<ServiceType, "application" | "redis">;
}
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 }: 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: "",
},
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,
},
{
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);
}}
>
{file}
<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 {...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>
);
};

View File

@@ -21,6 +21,8 @@ import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources"; import type { ServiceType } from "../../application/advanced/show-resources";
import { AddBackup } from "./add-backup"; import { AddBackup } from "./add-backup";
import { UpdateBackup } from "./update-backup"; import { UpdateBackup } from "./update-backup";
import { RestoreBackup } from "./restore-backup";
import { useState } from "react";
interface Props { interface Props {
id: string; id: string;
@@ -71,7 +73,10 @@ export const ShowBackups = ({ id, type }: Props) => {
</div> </div>
{postgres && postgres?.backups?.length > 0 && ( {postgres && postgres?.backups?.length > 0 && (
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
<AddBackup databaseId={id} databaseType={type} refetch={refetch} /> <AddBackup databaseId={id} databaseType={type} refetch={refetch} />
<RestoreBackup databaseId={id} databaseType={type} />
</div>
)} )}
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
@@ -98,11 +103,14 @@ export const ShowBackups = ({ id, type }: Props) => {
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
No backups configured No backups configured
</span> </span>
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<AddBackup <AddBackup
databaseId={id} databaseId={id}
databaseType={type} databaseType={type}
refetch={refetch} refetch={refetch}
/> />
<RestoreBackup databaseId={id} databaseType={type} />
</div>
</div> </div>
) : ( ) : (
<div className="flex flex-col pt-2"> <div className="flex flex-col pt-2">
@@ -183,6 +191,7 @@ export const ShowBackups = ({ id, type }: Props) => {
<TooltipContent>Run Manual Backup</TooltipContent> <TooltipContent>Run Manual Backup</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<UpdateBackup <UpdateBackup
backupId={backup.backupId} backupId={backup.backupId}
refetch={refetch} refetch={refetch}

View File

@@ -119,7 +119,6 @@ export const DockerLogsId: React.FC<Props> = ({
const wsUrl = `${protocol}//${ const wsUrl = `${protocol}//${
window.location.host window.location.host
}/docker-container-logs?${params.toString()}`; }/docker-container-logs?${params.toString()}`;
console.log("Connecting to WebSocket:", wsUrl);
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
const resetNoDataTimeout = () => { const resetNoDataTimeout = () => {
@@ -136,7 +135,6 @@ export const DockerLogsId: React.FC<Props> = ({
ws.close(); ws.close();
return; return;
} }
console.log("WebSocket connected");
resetNoDataTimeout(); resetNoDataTimeout();
}; };

View File

@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
return Number.isNaN(parsed) ? null : parsed; return Number.isNaN(parsed) ? null : parsed;
} }
return null; return null;
}, z }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
}); });
type DockerProvider = z.infer<typeof DockerProviderSchema>; type DockerProvider = z.infer<typeof DockerProviderSchema>;
@@ -111,7 +107,10 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
{!getIp && ( {!getIp && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You need to set an IP address in your{" "} You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary"> <Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId {data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address" ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"} : "Web Server -> Server -> Update Server IP"}

View File

@@ -15,6 +15,7 @@ import {
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Rocket,
Terminal, Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -92,12 +93,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button <Button
variant="default" variant="default"
isLoading={data?.applicationStatus === "running"} isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Deploy
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -107,6 +110,8 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip> </Tooltip>
</Button> </Button>
</DialogAction> </DialogAction>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<DialogAction <DialogAction
title="Reload Mariadb" title="Reload Mariadb"
description="Are you sure you want to reload this mariadb?" description="Are you sure you want to reload this mariadb?"
@@ -128,13 +133,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button <Button
variant="secondary" variant="secondary"
isLoading={isReloading} isLoading={isReloading}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Reload
<RefreshCcw className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -144,7 +150,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip> </Tooltip>
</Button> </Button>
</DialogAction> </DialogAction>
</TooltipProvider>
{data?.applicationStatus === "idle" ? ( {data?.applicationStatus === "idle" ? (
<TooltipProvider delayDuration={0}>
<DialogAction <DialogAction
title="Start Mariadb" title="Start Mariadb"
description="Are you sure you want to start this mariadb?" description="Are you sure you want to start this mariadb?"
@@ -165,13 +173,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button <Button
variant="secondary" variant="secondary"
isLoading={isStarting} isLoading={isStarting}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Start
<CheckCircle2 className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -184,7 +193,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip> </Tooltip>
</Button> </Button>
</DialogAction> </DialogAction>
</TooltipProvider>
) : ( ) : (
<TooltipProvider delayDuration={0}>
<DialogAction <DialogAction
title="Stop Mariadb" title="Stop Mariadb"
description="Are you sure you want to stop this mariadb?" description="Are you sure you want to stop this mariadb?"
@@ -204,13 +215,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button <Button
variant="destructive" variant="destructive"
isLoading={isStopping} isLoading={isStopping}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Stop
<Ban className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -220,15 +232,29 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip> </Tooltip>
</Button> </Button>
</DialogAction> </DialogAction>
)}
</TooltipProvider> </TooltipProvider>
)}
<DockerTerminalModal <DockerTerminalModal
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button variant="outline"> <Button
<Terminal /> variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MariaDB container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
</CardContent> </CardContent>

View File

@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
return Number.isNaN(parsed) ? null : parsed; return Number.isNaN(parsed) ? null : parsed;
} }
return null; return null;
}, z }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
}); });
type DockerProvider = z.infer<typeof DockerProviderSchema>; type DockerProvider = z.infer<typeof DockerProviderSchema>;
@@ -111,7 +107,10 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
{!getIp && ( {!getIp && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You need to set an IP address in your{" "} You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary"> <Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId {data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address" ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"} : "Web Server -> Server -> Update Server IP"}

View File

@@ -15,6 +15,7 @@ import {
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Rocket,
Terminal, Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -30,7 +31,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
{ {
mongoId, mongoId,
}, },
{ enabled: !!mongoId }, { enabled: !!mongoId }
); );
const { mutateAsync: reload, isLoading: isReloading } = const { mutateAsync: reload, isLoading: isReloading } =
@@ -67,7 +68,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
console.error("Deployment logs error:", error); console.error("Deployment logs error:", error);
setIsDeploying(false); setIsDeploying(false);
}, },
}, }
); );
return ( return (
<> <>
@@ -88,15 +89,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
refetch(); refetch();
}} }}
> >
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant="default" variant="default"
isLoading={data?.applicationStatus === "running"} isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
<Rocket className="size-4 mr-1" />
Deploy Deploy
<Tooltip> </Button>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -104,7 +106,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
</Tooltip> </Tooltip>
</Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Reload Mongo" title="Reload Mongo"
@@ -124,16 +125,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
}); });
}} }}
> >
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant="secondary" variant="secondary"
isLoading={isReloading} isLoading={isReloading}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
<RefreshCcw className="size-4 mr-1" />
Reload Reload
<RefreshCcw className="size-4" /> </Button>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -141,7 +142,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
</Tooltip> </Tooltip>
</Button>
</DialogAction> </DialogAction>
{data?.applicationStatus === "idle" ? ( {data?.applicationStatus === "idle" ? (
<DialogAction <DialogAction
@@ -161,16 +161,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
}); });
}} }}
> >
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant="secondary" variant="secondary"
isLoading={isStarting} isLoading={isStarting}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
<CheckCircle2 className="size-4 mr-1" />
Start Start
<CheckCircle2 className="size-4" /> </Button>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -181,7 +181,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
</Tooltip> </Tooltip>
</Button>
</DialogAction> </DialogAction>
) : ( ) : (
<DialogAction <DialogAction
@@ -200,16 +199,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
}); });
}} }}
> >
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant="destructive" variant="destructive"
isLoading={isStopping} isLoading={isStopping}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
<Ban className="size-4 mr-1" />
Stop Stop
<Ban className="size-4" /> </Button>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -217,7 +216,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
</Tooltip> </Tooltip>
</Button>
</DialogAction> </DialogAction>
)} )}
</TooltipProvider> </TooltipProvider>
@@ -225,10 +223,22 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button variant="outline"> <Tooltip>
<Terminal /> <TooltipTrigger asChild>
<Button
variant="outline"
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4" />
Open Terminal Open Terminal
</Button> </Button>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MongoDB container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</DockerTerminalModal> </DockerTerminalModal>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
return Number.isNaN(parsed) ? null : parsed; return Number.isNaN(parsed) ? null : parsed;
} }
return null; return null;
}, z }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
}); });
type DockerProvider = z.infer<typeof DockerProviderSchema>; type DockerProvider = z.infer<typeof DockerProviderSchema>;
@@ -111,7 +107,10 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
{!getIp && ( {!getIp && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You need to set an IP address in your{" "} You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary"> <Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId {data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address" ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"} : "Web Server -> Server -> Update Server IP"}

View File

@@ -15,6 +15,7 @@ import {
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Rocket,
Terminal, Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -77,7 +78,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<CardContent className="flex flex-row gap-4 flex-wrap"> <CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<DialogAction <DialogAction
title="Deploy Mysql" title="Deploy MySQL"
description="Are you sure you want to deploy this mysql?" description="Are you sure you want to deploy this mysql?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -89,12 +90,14 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<Button <Button
variant="default" variant="default"
isLoading={data?.applicationStatus === "running"} isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Deploy
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -105,7 +108,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</Button> </Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Reload Mysql" title="Reload MySQL"
description="Are you sure you want to reload this mysql?" description="Are you sure you want to reload this mysql?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -114,24 +117,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
appName: data?.appName || "", appName: data?.appName || "",
}) })
.then(() => { .then(() => {
toast.success("Mysql reloaded successfully"); toast.success("MySQL reloaded successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error reloading Mysql"); toast.error("Error reloading MySQL");
}); });
}} }}
> >
<Button <Button
variant="secondary" variant="secondary"
isLoading={isReloading} isLoading={isReloading}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Reload
<RefreshCcw className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -143,7 +147,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</DialogAction> </DialogAction>
{data?.applicationStatus === "idle" ? ( {data?.applicationStatus === "idle" ? (
<DialogAction <DialogAction
title="Start Mysql" title="Start MySQL"
description="Are you sure you want to start this mysql?" description="Are you sure you want to start this mysql?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -151,24 +155,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
mysqlId: mysqlId, mysqlId: mysqlId,
}) })
.then(() => { .then(() => {
toast.success("Mysql started successfully"); toast.success("MySQL started successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error starting Mysql"); toast.error("Error starting MySQL");
}); });
}} }}
> >
<Button <Button
variant="secondary" variant="secondary"
isLoading={isStarting} isLoading={isStarting}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Start
<CheckCircle2 className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -183,31 +188,32 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</DialogAction> </DialogAction>
) : ( ) : (
<DialogAction <DialogAction
title="Stop Mysql" title="Stop MySQL"
description="Are you sure you want to stop this mysql?" description="Are you sure you want to stop this mysql?"
onClick={async () => { onClick={async () => {
await stop({ await stop({
mysqlId: mysqlId, mysqlId: mysqlId,
}) })
.then(() => { .then(() => {
toast.success("Mysql stopped successfully"); toast.success("MySQL stopped successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error stopping Mysql"); toast.error("Error stopping MySQL");
}); });
}} }}
> >
<Button <Button
variant="destructive" variant="destructive"
isLoading={isStopping} isLoading={isStopping}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Stop
<Ban className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -223,9 +229,23 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button variant="outline"> <Button
<Terminal /> variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MySQL container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
</CardContent> </CardContent>

View File

@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
return Number.isNaN(parsed) ? null : parsed; return Number.isNaN(parsed) ? null : parsed;
} }
return null; return null;
}, z }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
}); });
type DockerProvider = z.infer<typeof DockerProviderSchema>; type DockerProvider = z.infer<typeof DockerProviderSchema>;
@@ -113,7 +109,10 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
{!getIp && ( {!getIp && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You need to set an IP address in your{" "} You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary"> <Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId {data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address" ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"} : "Web Server -> Server -> Update Server IP"}

View File

@@ -15,6 +15,7 @@ import {
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Rocket,
Terminal, Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -78,9 +79,9 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle> <CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap"> <CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}> <TooltipProvider disableHoverableContent={false}>
<DialogAction <DialogAction
title="Deploy Postgres" title="Deploy PostgreSQL"
description="Are you sure you want to deploy this postgres?" description="Are you sure you want to deploy this postgres?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -92,12 +93,14 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<Button <Button
variant="default" variant="default"
isLoading={data?.applicationStatus === "running"} isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Deploy
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -108,7 +111,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</Button> </Button>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Reload Postgres" title="Reload PostgreSQL"
description="Are you sure you want to reload this postgres?" description="Are you sure you want to reload this postgres?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -117,24 +120,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
appName: data?.appName || "", appName: data?.appName || "",
}) })
.then(() => { .then(() => {
toast.success("Postgres reloaded successfully"); toast.success("PostgreSQL reloaded successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error reloading Postgres"); toast.error("Error reloading PostgreSQL");
}); });
}} }}
> >
<Button <Button
variant="secondary" variant="secondary"
isLoading={isReloading} isLoading={isReloading}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Reload
<RefreshCcw className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -146,7 +150,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</DialogAction> </DialogAction>
{data?.applicationStatus === "idle" ? ( {data?.applicationStatus === "idle" ? (
<DialogAction <DialogAction
title="Start Postgres" title="Start PostgreSQL"
description="Are you sure you want to start this postgres?" description="Are you sure you want to start this postgres?"
type="default" type="default"
onClick={async () => { onClick={async () => {
@@ -154,24 +158,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
postgresId: postgresId, postgresId: postgresId,
}) })
.then(() => { .then(() => {
toast.success("Postgres started successfully"); toast.success("PostgreSQL started successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error starting Postgres"); toast.error("Error starting PostgreSQL");
}); });
}} }}
> >
<Button <Button
variant="secondary" variant="secondary"
isLoading={isStarting} isLoading={isStarting}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Start
<CheckCircle2 className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -186,31 +191,32 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</DialogAction> </DialogAction>
) : ( ) : (
<DialogAction <DialogAction
title="Stop Postgres" title="Stop PostgreSQL"
description="Are you sure you want to stop this postgres?" description="Are you sure you want to stop this postgres?"
onClick={async () => { onClick={async () => {
await stop({ await stop({
postgresId: postgresId, postgresId: postgresId,
}) })
.then(() => { .then(() => {
toast.success("Postgres stopped successfully"); toast.success("PostgreSQL stopped successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error stopping Postgres"); toast.error("Error stopping PostgreSQL");
}); });
}} }}
> >
<Button <Button
variant="destructive" variant="destructive"
isLoading={isStopping} isLoading={isStopping}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Stop
<Ban className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -226,9 +232,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button variant="outline"> <Button
<Terminal /> variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the PostgreSQL container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
</CardContent> </CardContent>

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react"; import { PenBox } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -51,7 +51,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
}, },
{ {
enabled: !!postgresId, enabled: !!postgresId,
}, }
); );
const form = useForm<UpdatePostgres>({ const form = useForm<UpdatePostgres>({
defaultValues: { defaultValues: {
@@ -94,9 +94,9 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="group hover:bg-blue-500/10 " className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" /> <PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg"> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
@@ -151,6 +151,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
isLoading={isLoading} isLoading={isLoading}
form="hook-form-update-postgres" form="hook-form-update-postgres"
type="submit" type="submit"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Update Update
</Button> </Button>

View File

@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
return Number.isNaN(parsed) ? null : parsed; return Number.isNaN(parsed) ? null : parsed;
} }
return null; return null;
}, z }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
}); });
type DockerProvider = z.infer<typeof DockerProviderSchema>; type DockerProvider = z.infer<typeof DockerProviderSchema>;
@@ -105,7 +101,10 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
{!getIp && ( {!getIp && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You need to set an IP address in your{" "} You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary"> <Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId {data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address" ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"} : "Web Server -> Server -> Update Server IP"}

View File

@@ -15,6 +15,7 @@ import {
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Rocket,
Terminal, Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -91,12 +92,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button <Button
variant="default" variant="default"
isLoading={data?.applicationStatus === "running"} isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Deploy
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -127,13 +130,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button <Button
variant="secondary" variant="secondary"
isLoading={isReloading} isLoading={isReloading}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Reload
<RefreshCcw className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -164,13 +168,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button <Button
variant="secondary" variant="secondary"
isLoading={isStarting} isLoading={isStarting}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Start
<CheckCircle2 className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -203,13 +208,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button <Button
variant="destructive" variant="destructive"
isLoading={isStopping} isLoading={isStopping}
className="flex items-center gap-1.5" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
Stop
<Ban className="size-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
@@ -225,9 +231,23 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
> >
<Button variant="outline"> <Button
<Terminal /> variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the Redis container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
</CardContent> </CardContent>

View File

@@ -39,7 +39,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
className={cn("text-left", className)} className={cn("text-left", className)}
ref={ref} ref={ref}
{...props} {...props}
value={props.value === undefined ? undefined : String(props.value)} value={props.value === undefined || props.value === "" ? "" : String(props.value)}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
if (value === "") { if (value === "") {
@@ -60,6 +60,21 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
} }
} }
}} }}
onBlur={(e) => {
// If input is empty, make 0 when focus is lost
if (e.target.value === "") {
const syntheticEvent = {
...e,
target: {
...e.target,
value: "0",
},
};
props.onChange?.(
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
);
}
}}
/> />
); );
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.20.4", "version": "v0.20.5",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -93,6 +93,7 @@ export default async function handler(
try { try {
const branchName = githubBody?.ref?.replace("refs/heads/", ""); const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name; const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body); const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body); const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name; const owner = githubBody?.repository?.owner?.name;
@@ -107,6 +108,7 @@ export default async function handler(
eq(applications.branch, branchName), eq(applications.branch, branchName),
eq(applications.repository, repository), eq(applications.repository, repository),
eq(applications.owner, owner), eq(applications.owner, owner),
eq(applications.githubId, githubResult.githubId),
), ),
}); });
@@ -151,6 +153,7 @@ export default async function handler(
eq(compose.branch, branchName), eq(compose.branch, branchName),
eq(compose.repository, repository), eq(compose.repository, repository),
eq(compose.owner, owner), eq(compose.owner, owner),
eq(compose.githubId, githubResult.githubId),
), ),
}); });
@@ -240,6 +243,7 @@ export default async function handler(
eq(applications.branch, branch), eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true), eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner), eq(applications.owner, owner),
eq(applications.githubId, githubResult.githubId),
), ),
with: { with: {
previewDeployments: true, previewDeployments: true,

View File

@@ -11,9 +11,13 @@ import {
createBackup, createBackup,
findBackupById, findBackupById,
findMariadbByBackupId, findMariadbByBackupId,
findMariadbById,
findMongoByBackupId, findMongoByBackupId,
findMongoById,
findMySqlByBackupId, findMySqlByBackupId,
findMySqlById,
findPostgresByBackupId, findPostgresByBackupId,
findPostgresById,
findServerById, findServerById,
removeBackupById, removeBackupById,
removeScheduleBackup, removeScheduleBackup,
@@ -26,6 +30,17 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { execAsync } from "@dokploy/server/utils/process/execAsync";
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import {
restoreMariadbBackup,
restoreMongoBackup,
restoreMySqlBackup,
restorePostgresBackup,
} from "@dokploy/server/utils/restore";
import { observable } from "@trpc/server/observable";
export const backupRouter = createTRPCRouter({ export const backupRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -209,27 +224,136 @@ export const backupRouter = createTRPCRouter({
}); });
} }
}), }),
listBackupFiles: protectedProcedure
.input(
z.object({
destinationId: z.string(),
search: z.string(),
}),
)
.query(async ({ input }) => {
try {
const destination = await findDestinationById(input.destinationId);
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const lastSlashIndex = input.search.lastIndexOf("/");
const baseDir =
lastSlashIndex !== -1
? input.search.slice(0, lastSlashIndex + 1)
: "";
const searchTerm =
lastSlashIndex !== -1
? input.search.slice(lastSlashIndex + 1)
: input.search;
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
const { stdout } = await execAsync(listCommand);
const files = stdout.split("\n").filter(Boolean);
const results = baseDir
? files.map((file) => `${baseDir}${file}`)
: files;
if (searchTerm) {
return results.filter((file) =>
file.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return results;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error.message
: "Error listing backup files",
cause: error,
}); });
}
}),
// export const getAdminId = async (backupId: string) => { restoreBackupWithLogs: protectedProcedure
// const backup = await findBackupById(backupId); .meta({
openapi: {
enabled: false,
path: "/restore-backup-with-logs",
method: "POST",
override: true,
},
})
.input(
z.object({
databaseId: z.string(),
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
databaseName: z.string().min(1),
backupFile: z.string().min(1),
destinationId: z.string().min(1),
}),
)
.subscription(async ({ input }) => {
const destination = await findDestinationById(input.destinationId);
if (input.databaseType === "postgres") {
const postgres = await findPostgresById(input.databaseId);
// if (backup.databaseType === "postgres" && backup.postgresId) { return observable<string>((emit) => {
// const postgres = await findPostgresById(backup.postgresId); restorePostgresBackup(
// return postgres.project.adminId; postgres,
// } destination,
// if (backup.databaseType === "mariadb" && backup.mariadbId) { input.databaseName,
// const mariadb = await findMariadbById(backup.mariadbId); input.backupFile,
// return mariadb.project.adminId; (log) => {
// } emit.next(log);
// if (backup.databaseType === "mysql" && backup.mysqlId) { },
// const mysql = await findMySqlById(backup.mysqlId); );
// return mysql.project.adminId; });
// } }
// if (backup.databaseType === "mongo" && backup.mongoId) { if (input.databaseType === "mysql") {
// const mongo = await findMongoById(backup.mongoId); const mysql = await findMySqlById(input.databaseId);
// return mongo.project.adminId; return observable<string>((emit) => {
// } restoreMySqlBackup(
mysql,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
if (input.databaseType === "mariadb") {
const mariadb = await findMariadbById(input.databaseId);
return observable<string>((emit) => {
restoreMariadbBackup(
mariadb,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
if (input.databaseType === "mongo") {
const mongo = await findMongoById(input.databaseId);
return observable<string>((emit) => {
restoreMongoBackup(
mongo,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
// return null; return true;
// }; }),
});

View File

@@ -0,0 +1,4 @@
export { restorePostgresBackup } from "./postgres";
export { restoreMySqlBackup } from "./mysql";
export { restoreMariadbBackup } from "./mariadb";
export { restoreMongoBackup } from "./mongo";

View File

@@ -0,0 +1,56 @@
import type { Mariadb } from "@dokploy/server/services/mariadb";
import type { Destination } from "@dokploy/server/services/destination";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restoreMariadbBackup = async (
mariadb: Mariadb,
destination: Destination,
database: string,
backupFile: string,
emit: (log: string) => void,
) => {
try {
const { appName, databasePassword, databaseUser, serverId } = mariadb;
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const backupPath = `${bucketPath}/${backupFile}`;
const { Id: containerName } = serverId
? await getRemoteServiceContainer(serverId, appName)
: await getServiceContainer(appName);
const restoreCommand = `
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerName} mariadb -u ${databaseUser} -p${databasePassword} ${database}
`;
emit("Starting restore...");
emit(`Executing command: ${restoreCommand}`);
if (serverId) {
await execAsyncRemote(serverId, restoreCommand);
} else {
await execAsync(restoreCommand);
}
emit("Restore completed successfully!");
} catch (error) {
console.error(error);
emit(
`Error: ${
error instanceof Error
? error.message
: "Error restoring mariadb backup"
}`,
);
throw new Error(
error instanceof Error ? error.message : "Error restoring mariadb backup",
);
}
};

View File

@@ -0,0 +1,64 @@
import type { Mongo } from "@dokploy/server/services/mongo";
import type { Destination } from "@dokploy/server/services/destination";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restoreMongoBackup = async (
mongo: Mongo,
destination: Destination,
database: string,
backupFile: string,
emit: (log: string) => void,
) => {
try {
const { appName, databasePassword, databaseUser, serverId } = mongo;
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const backupPath = `${bucketPath}/${backupFile}`;
const { Id: containerName } = serverId
? await getRemoteServiceContainer(serverId, appName)
: await getServiceContainer(appName);
// For MongoDB, we need to first download the backup file since mongorestore expects a directory
const tempDir = "/tmp/dokploy-restore";
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
const decompressedName = fileName.replace(".gz", "");
const downloadCommand = `\
rm -rf ${tempDir} && \
mkdir -p ${tempDir} && \
rclone copy ${rcloneFlags.join(" ")} "${backupPath}" ${tempDir} && \
cd ${tempDir} && \
gunzip -f "${fileName}" && \
docker exec -i ${containerName} mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive < "${decompressedName}" && \
rm -rf ${tempDir}`;
emit("Starting restore...");
emit(`Executing command: ${downloadCommand}`);
if (serverId) {
await execAsyncRemote(serverId, downloadCommand);
} else {
await execAsync(downloadCommand);
}
emit("Restore completed successfully!");
} catch (error) {
console.error(error);
emit(
`Error: ${
error instanceof Error ? error.message : "Error restoring mongo backup"
}`,
);
throw new Error(
error instanceof Error ? error.message : "Error restoring mongo backup",
);
}
};

View File

@@ -0,0 +1,54 @@
import type { MySql } from "@dokploy/server/services/mysql";
import type { Destination } from "@dokploy/server/services/destination";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restoreMySqlBackup = async (
mysql: MySql,
destination: Destination,
database: string,
backupFile: string,
emit: (log: string) => void,
) => {
try {
const { appName, databaseRootPassword, serverId } = mysql;
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const backupPath = `${bucketPath}/${backupFile}`;
const { Id: containerName } = serverId
? await getRemoteServiceContainer(serverId, appName)
: await getServiceContainer(appName);
const restoreCommand = `
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerName} mysql -u root -p${databaseRootPassword} ${database}
`;
emit("Starting restore...");
emit(`Executing command: ${restoreCommand}`);
if (serverId) {
await execAsyncRemote(serverId, restoreCommand);
} else {
await execAsync(restoreCommand);
}
emit("Restore completed successfully!");
} catch (error) {
console.error(error);
emit(
`Error: ${
error instanceof Error ? error.message : "Error restoring mysql backup"
}`,
);
throw new Error(
error instanceof Error ? error.message : "Error restoring mysql backup",
);
}
};

View File

@@ -0,0 +1,60 @@
import type { Postgres } from "@dokploy/server/services/postgres";
import type { Destination } from "@dokploy/server/services/destination";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restorePostgresBackup = async (
postgres: Postgres,
destination: Destination,
database: string,
backupFile: string,
emit: (log: string) => void,
) => {
try {
const { appName, databaseUser, serverId } = postgres;
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const backupPath = `${bucketPath}/${backupFile}`;
const { Id: containerName } = serverId
? await getRemoteServiceContainer(serverId, appName)
: await getServiceContainer(appName);
emit("Starting restore...");
emit(`Backup path: ${backupPath}`);
const command = `\
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerName} pg_restore -U ${databaseUser} -d ${database} --clean --if-exists`;
emit(`Executing command: ${command}`);
if (serverId) {
const { stdout, stderr } = await execAsyncRemote(serverId, command);
emit(stdout);
emit(stderr);
} else {
const { stdout, stderr } = await execAsync(command);
console.log("stdout", stdout);
console.log("stderr", stderr);
emit(stdout);
emit(stderr);
}
emit("Restore completed successfully!");
} catch (error) {
emit(
`Error: ${
error instanceof Error
? error.message
: "Error restoring postgres backup"
}`,
);
throw error;
}
};