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
Reload variant="secondary"
<RefreshCcw className="size-4" /> 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
</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 && (
<AddBackup databaseId={id} databaseType={type} refetch={refetch} /> <div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
<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>
<AddBackup <div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
databaseId={id} <AddBackup
databaseType={type} databaseId={id}
refetch={refetch} databaseType={type}
/> 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

@@ -27,146 +27,145 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
if (a !== null) { if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10); const parsed = Number.parseInt(z.string().parse(a), 10);
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>;
interface Props { interface Props {
mariadbId: string; mariadbId: string;
} }
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => { export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery(); const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId }); const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip; const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
}); });
useEffect(() => { useEffect(() => {
if (data?.externalPort) { if (data?.externalPort) {
form.reset({ form.reset({
externalPort: data.externalPort, externalPort: data.externalPort,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => { const onSubmit = async (values: DockerProvider) => {
await mutateAsync({ await mutateAsync({
externalPort: values.externalPort, externalPort: values.externalPort,
mariadbId, mariadbId,
}) })
.then(async () => { .then(async () => {
toast.success("External Port updated"); toast.success("External Port updated");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error saving the external port"); toast.error("Error saving the external port");
}); });
}; };
useEffect(() => { useEffect(() => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [ }, [
data?.appName, data?.appName,
data?.externalPort, data?.externalPort,
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
getIp, getIp,
]); ]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle> <CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription> <CardDescription>
In order to make the database reachable trought internet is In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another required to set a port, make sure the port is not used by another
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!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
{data?.serverId href="/dashboard/settings/server"
? "Remote Servers -> Server -> Edit Server -> Update IP Address" className="text-primary"
: "Web Server -> Server -> Update Server IP"} >
</Link>{" "} {data?.serverId
to fix the database url connection. ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
</AlertBlock> : "Web Server -> Server -> Update Server IP"}
)} </Link>{" "}
<Form {...form}> to fix the database url connection.
<form </AlertBlock>
onSubmit={form.handleSubmit(onSubmit)} )}
className="flex flex-col gap-4" <Form {...form}>
> <form
<div className="grid md:grid-cols-2 gap-4 "> onSubmit={form.handleSubmit(onSubmit)}
<div className="md:col-span-2 space-y-4"> className="flex flex-col gap-4"
<FormField >
control={form.control} <div className="grid md:grid-cols-2 gap-4 ">
name="externalPort" <div className="md:col-span-2 space-y-4">
render={({ field }) => { <FormField
return ( control={form.control}
<FormItem> name="externalPort"
<FormLabel>External Port (Internet)</FormLabel> render={({ field }) => {
<FormControl> return (
<Input <FormItem>
placeholder="3306" <FormLabel>External Port (Internet)</FormLabel>
{...field} <FormControl>
value={field.value || ""} <Input
/> placeholder="3306"
</FormControl> {...field}
<FormMessage /> value={field.value || ""}
</FormItem> />
); </FormControl>
}} <FormMessage />
/> </FormItem>
</div> );
</div> }}
{!!data?.externalPort && ( />
<div className="grid w-full gap-8"> </div>
<div className="flex flex-col gap-3"> </div>
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */} {!!data?.externalPort && (
<Label>External Host</Label> <div className="grid w-full gap-8">
<ToggleVisibilityInput value={connectionUrl} disabled /> <div className="flex flex-col gap-3">
</div> {/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
</div> <Label>External Host</Label>
)} <ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit" isLoading={isLoading}> <Button type="submit" isLoading={isLoading}>
Save Save
</Button> </Button>
</div> </div>
</form> </form>
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</> </>
); );
}; };

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { import {
Ban, Ban,
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Terminal, Rocket,
Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -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>
{data?.applicationStatus === "idle" ? ( </TooltipProvider>
{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"
Open Terminal 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
</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

@@ -27,145 +27,144 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
if (a !== null) { if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10); const parsed = Number.parseInt(z.string().parse(a), 10);
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>;
interface Props { interface Props {
mongoId: string; mongoId: string;
} }
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => { export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery(); const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mongo.one.useQuery({ mongoId }); const { data, refetch } = api.mongo.one.useQuery({ mongoId });
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip; const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
}); });
useEffect(() => { useEffect(() => {
if (data?.externalPort) { if (data?.externalPort) {
form.reset({ form.reset({
externalPort: data.externalPort, externalPort: data.externalPort,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => { const onSubmit = async (values: DockerProvider) => {
await mutateAsync({ await mutateAsync({
externalPort: values.externalPort, externalPort: values.externalPort,
mongoId, mongoId,
}) })
.then(async () => { .then(async () => {
toast.success("External Port updated"); toast.success("External Port updated");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error saving the external port"); toast.error("Error saving the external port");
}); });
}; };
useEffect(() => { useEffect(() => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`; return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [ }, [
data?.appName, data?.appName,
data?.externalPort, data?.externalPort,
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseUser, data?.databaseUser,
getIp, getIp,
]); ]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle> <CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription> <CardDescription>
In order to make the database reachable trought internet is In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another required to set a port, make sure the port is not used by another
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!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
{data?.serverId href="/dashboard/settings/server"
? "Remote Servers -> Server -> Edit Server -> Update IP Address" className="text-primary"
: "Web Server -> Server -> Update Server IP"} >
</Link>{" "} {data?.serverId
to fix the database url connection. ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
</AlertBlock> : "Web Server -> Server -> Update Server IP"}
)} </Link>{" "}
<Form {...form}> to fix the database url connection.
<form </AlertBlock>
onSubmit={form.handleSubmit(onSubmit)} )}
className="flex flex-col gap-4" <Form {...form}>
> <form
<div className="grid grid-cols-2 gap-4 "> onSubmit={form.handleSubmit(onSubmit)}
<div className="col-span-2 space-y-4"> className="flex flex-col gap-4"
<FormField >
control={form.control} <div className="grid grid-cols-2 gap-4 ">
name="externalPort" <div className="col-span-2 space-y-4">
render={({ field }) => { <FormField
return ( control={form.control}
<FormItem> name="externalPort"
<FormLabel>External Port (Internet)</FormLabel> render={({ field }) => {
<FormControl> return (
<Input <FormItem>
placeholder="27017" <FormLabel>External Port (Internet)</FormLabel>
{...field} <FormControl>
value={field.value || ""} <Input
/> placeholder="27017"
</FormControl> {...field}
<FormMessage /> value={field.value || ""}
</FormItem> />
); </FormControl>
}} <FormMessage />
/> </FormItem>
</div> );
</div> }}
{!!data?.externalPort && ( />
<div className="grid w-full gap-8"> </div>
<div className="flex flex-col gap-3"> </div>
<Label>External Host</Label> {!!data?.externalPort && (
<ToggleVisibilityInput value={connectionUrl} disabled /> <div className="grid w-full gap-8">
</div> <div className="flex flex-col gap-3">
</div> <Label>External Host</Label>
)} <ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit" isLoading={isLoading}> <Button type="submit" isLoading={isLoading}>
Save Save
</Button> </Button>
</div> </div>
</form> </form>
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</> </>
); );
}; };

View File

@@ -3,246 +3,256 @@ import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
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 * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { import {
Ban, Ban,
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Terminal, Rocket,
Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props { interface Props {
mongoId: string; mongoId: string;
} }
export const ShowGeneralMongo = ({ mongoId }: Props) => { export const ShowGeneralMongo = ({ mongoId }: Props) => {
const { data, refetch } = api.mongo.one.useQuery( const { data, refetch } = api.mongo.one.useQuery(
{ {
mongoId, mongoId,
}, },
{ enabled: !!mongoId }, { enabled: !!mongoId }
); );
const { mutateAsync: reload, isLoading: isReloading } = const { mutateAsync: reload, isLoading: isReloading } =
api.mongo.reload.useMutation(); api.mongo.reload.useMutation();
const { mutateAsync: start, isLoading: isStarting } = const { mutateAsync: start, isLoading: isStarting } =
api.mongo.start.useMutation(); api.mongo.start.useMutation();
const { mutateAsync: stop, isLoading: isStopping } = const { mutateAsync: stop, isLoading: isStopping } =
api.mongo.stop.useMutation(); api.mongo.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]); const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false); const [isDeploying, setIsDeploying] = useState(false);
api.mongo.deployWithLogs.useSubscription( api.mongo.deployWithLogs.useSubscription(
{ {
mongoId: mongoId, mongoId: mongoId,
}, },
{ {
enabled: isDeploying, enabled: isDeploying,
onData(log) { onData(log) {
if (!isDrawerOpen) { if (!isDrawerOpen) {
setIsDrawerOpen(true); setIsDrawerOpen(true);
} }
if (log === "Deployment completed successfully!") { if (log === "Deployment completed successfully!") {
setIsDeploying(false); setIsDeploying(false);
} }
const parsedLogs = parseLogs(log); const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]); setFilteredLogs((prev) => [...prev, ...parsedLogs]);
}, },
onError(error) { onError(error) {
console.error("Deployment logs error:", error); console.error("Deployment logs error:", error);
setIsDeploying(false); setIsDeploying(false);
}, },
}, }
); );
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader> <CardHeader>
<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}>
<DialogAction <DialogAction
title="Deploy Mongo" title="Deploy Mongo"
description="Are you sure you want to deploy this mongo?" description="Are you sure you want to deploy this mongo?"
type="default" type="default"
onClick={async () => { onClick={async () => {
setIsDeploying(true); setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
refetch(); refetch();
}} }}
> >
<Button <Tooltip>
variant="default" <TooltipTrigger asChild>
isLoading={data?.applicationStatus === "running"} <Button
className="flex items-center gap-1.5" variant="default"
> isLoading={data?.applicationStatus === "running"}
Deploy className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
<Tooltip> >
<TooltipTrigger asChild> <Rocket className="size-4 mr-1" />
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> Deploy
</TooltipTrigger> </Button>
<TooltipPrimitive.Portal> </TooltipTrigger>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipPrimitive.Portal>
<p>Downloads and sets up the MongoDB database</p> <TooltipContent sideOffset={5} className="z-[60]">
</TooltipContent> <p>Downloads and sets up the MongoDB database</p>
</TooltipPrimitive.Portal> </TooltipContent>
</Tooltip> </TooltipPrimitive.Portal>
</Button> </Tooltip>
</DialogAction> </DialogAction>
<DialogAction <DialogAction
title="Reload Mongo" title="Reload Mongo"
description="Are you sure you want to reload this mongo?" description="Are you sure you want to reload this mongo?"
type="default" type="default"
onClick={async () => { onClick={async () => {
await reload({ await reload({
mongoId: mongoId, mongoId: mongoId,
appName: data?.appName || "", appName: data?.appName || "",
}) })
.then(() => { .then(() => {
toast.success("Mongo reloaded successfully"); toast.success("Mongo reloaded successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error reloading Mongo"); toast.error("Error reloading Mongo");
}); });
}} }}
> >
<Button <Tooltip>
variant="secondary" <TooltipTrigger asChild>
isLoading={isReloading} <Button
className="flex items-center gap-1.5" variant="secondary"
> isLoading={isReloading}
Reload className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
<RefreshCcw className="size-4" /> >
<Tooltip> <RefreshCcw className="size-4 mr-1" />
<TooltipTrigger asChild> Reload
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]"> <TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the MongoDB service without rebuilding</p> <p>Restart the MongoDB service without rebuilding</p>
</TooltipContent> </TooltipContent>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
</Tooltip> </Tooltip>
</Button> </DialogAction>
</DialogAction> {data?.applicationStatus === "idle" ? (
{data?.applicationStatus === "idle" ? ( <DialogAction
<DialogAction title="Start Mongo"
title="Start Mongo" description="Are you sure you want to start this mongo?"
description="Are you sure you want to start this mongo?" type="default"
type="default" onClick={async () => {
onClick={async () => { await start({
await start({ mongoId: mongoId,
mongoId: mongoId, })
}) .then(() => {
.then(() => { toast.success("Mongo started successfully");
toast.success("Mongo started successfully"); refetch();
refetch(); })
}) .catch(() => {
.catch(() => { toast.error("Error starting Mongo");
toast.error("Error starting Mongo"); });
}); }}
}} >
> <Tooltip>
<Button <TooltipTrigger asChild>
variant="secondary" <Button
isLoading={isStarting} variant="secondary"
className="flex items-center gap-1.5" isLoading={isStarting}
> className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
Start >
<CheckCircle2 className="size-4" /> <CheckCircle2 className="size-4 mr-1" />
<Tooltip> Start
<TooltipTrigger asChild> </Button>
<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]"> <p>
<p> Start the MongoDB database (requires a previous
Start the MongoDB database (requires a previous successful setup)
successful setup) </p>
</p> </TooltipContent>
</TooltipContent> </TooltipPrimitive.Portal>
</TooltipPrimitive.Portal> </Tooltip>
</Tooltip> </DialogAction>
</Button> ) : (
</DialogAction> <DialogAction
) : ( title="Stop Mongo"
<DialogAction description="Are you sure you want to stop this mongo?"
title="Stop Mongo" onClick={async () => {
description="Are you sure you want to stop this mongo?" await stop({
onClick={async () => { mongoId: mongoId,
await stop({ })
mongoId: mongoId, .then(() => {
}) toast.success("Mongo stopped successfully");
.then(() => { refetch();
toast.success("Mongo stopped successfully"); })
refetch(); .catch(() => {
}) toast.error("Error stopping Mongo");
.catch(() => { });
toast.error("Error stopping Mongo"); }}
}); >
}} <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"
> >
Stop <Ban className="size-4 mr-1" />
<Ban className="size-4" /> Stop
<Tooltip> </Button>
<TooltipTrigger asChild> </TooltipTrigger>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> <TooltipPrimitive.Portal>
</TooltipTrigger> <TooltipContent sideOffset={5} className="z-[60]">
<TooltipPrimitive.Portal> <p>Stop the currently running MongoDB database</p>
<TooltipContent sideOffset={5} className="z-[60]"> </TooltipContent>
<p>Stop the currently running MongoDB database</p> </TooltipPrimitive.Portal>
</TooltipContent> </Tooltip>
</TooltipPrimitive.Portal> </DialogAction>
</Tooltip> )}
</Button> </TooltipProvider>
</DialogAction> <DockerTerminalModal
)} appName={data?.appName || ""}
</TooltipProvider> serverId={data?.serverId || ""}
<DockerTerminalModal >
appName={data?.appName || ""} <Tooltip>
serverId={data?.serverId || ""} <TooltipTrigger asChild>
> <Button
<Button variant="outline"> variant="outline"
<Terminal /> className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
Open Terminal >
</Button> <Terminal className="size-4" />
</DockerTerminalModal> Open Terminal
</CardContent> </Button>
</Card> </TooltipTrigger>
<DrawerLogs <TooltipPrimitive.Portal>
isOpen={isDrawerOpen} <TooltipContent sideOffset={5} className="z-[60]">
onClose={() => { <p>Open a terminal to the MongoDB container</p>
setIsDrawerOpen(false); </TooltipContent>
setFilteredLogs([]); </TooltipPrimitive.Portal>
setIsDeploying(false); </Tooltip>
refetch(); </DockerTerminalModal>
}} </CardContent>
filteredLogs={filteredLogs} </Card>
/> <DrawerLogs
</div> isOpen={isDrawerOpen}
</> onClose={() => {
); setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>
</div>
</>
);
}; };

View File

@@ -27,145 +27,144 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
if (a !== null) { if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10); const parsed = Number.parseInt(z.string().parse(a), 10);
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>;
interface Props { interface Props {
mysqlId: string; mysqlId: string;
} }
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => { export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery(); const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mysql.one.useQuery({ mysqlId }); const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip; const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
}); });
useEffect(() => { useEffect(() => {
if (data?.externalPort) { if (data?.externalPort) {
form.reset({ form.reset({
externalPort: data.externalPort, externalPort: data.externalPort,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => { const onSubmit = async (values: DockerProvider) => {
await mutateAsync({ await mutateAsync({
externalPort: values.externalPort, externalPort: values.externalPort,
mysqlId, mysqlId,
}) })
.then(async () => { .then(async () => {
toast.success("External Port updated"); toast.success("External Port updated");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error saving the external port"); toast.error("Error saving the external port");
}); });
}; };
useEffect(() => { useEffect(() => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [ }, [
data?.appName, data?.appName,
data?.externalPort, data?.externalPort,
data?.databasePassword, data?.databasePassword,
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
form, form,
getIp, getIp,
]); ]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle> <CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription> <CardDescription>
In order to make the database reachable trought internet is In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another required to set a port, make sure the port is not used by another
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!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
{data?.serverId href="/dashboard/settings/server"
? "Remote Servers -> Server -> Edit Server -> Update IP Address" className="text-primary"
: "Web Server -> Server -> Update Server IP"} >
</Link>{" "} {data?.serverId
to fix the database url connection. ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
</AlertBlock> : "Web Server -> Server -> Update Server IP"}
)} </Link>{" "}
<Form {...form}> to fix the database url connection.
<form </AlertBlock>
onSubmit={form.handleSubmit(onSubmit)} )}
className="flex flex-col gap-4" <Form {...form}>
> <form
<div className="grid grid-cols-2 gap-4 "> onSubmit={form.handleSubmit(onSubmit)}
<div className="col-span-2 space-y-4"> className="flex flex-col gap-4"
<FormField >
control={form.control} <div className="grid grid-cols-2 gap-4 ">
name="externalPort" <div className="col-span-2 space-y-4">
render={({ field }) => { <FormField
return ( control={form.control}
<FormItem> name="externalPort"
<FormLabel>External Port (Internet)</FormLabel> render={({ field }) => {
<FormControl> return (
<Input <FormItem>
placeholder="3306" <FormLabel>External Port (Internet)</FormLabel>
{...field} <FormControl>
value={field.value || ""} <Input
/> placeholder="3306"
</FormControl> {...field}
<FormMessage /> value={field.value || ""}
</FormItem> />
); </FormControl>
}} <FormMessage />
/> </FormItem>
</div> );
</div> }}
{!!data?.externalPort && ( />
<div className="grid w-full gap-8"> </div>
<div className="flex flex-col gap-3"> </div>
<Label>External Host</Label> {!!data?.externalPort && (
<ToggleVisibilityInput disabled value={connectionUrl} /> <div className="grid w-full gap-8">
</div> <div className="flex flex-col gap-3">
</div> <Label>External Host</Label>
)} <ToggleVisibilityInput disabled value={connectionUrl} />
</div>
</div>
)}
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit" isLoading={isLoading}> <Button type="submit" isLoading={isLoading}>
Save Save
</Button> </Button>
</div> </div>
</form> </form>
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</> </>
); );
}; };

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { import {
Ban, Ban,
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Terminal, Rocket,
Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -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"
Open Terminal 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
</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

@@ -27,147 +27,146 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
if (a !== null) { if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10); const parsed = Number.parseInt(z.string().parse(a), 10);
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>;
interface Props { interface Props {
postgresId: string; postgresId: string;
} }
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => { export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery(); const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.postgres.one.useQuery({ postgresId }); const { data, refetch } = api.postgres.one.useQuery({ postgresId });
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.postgres.saveExternalPort.useMutation(); api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip; const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
}); });
useEffect(() => { useEffect(() => {
if (data?.externalPort) { if (data?.externalPort) {
form.reset({ form.reset({
externalPort: data.externalPort, externalPort: data.externalPort,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => { const onSubmit = async (values: DockerProvider) => {
await mutateAsync({ await mutateAsync({
externalPort: values.externalPort, externalPort: values.externalPort,
postgresId, postgresId,
}) })
.then(async () => { .then(async () => {
toast.success("External Port updated"); toast.success("External Port updated");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error saving the external port"); toast.error("Error saving the external port");
}); });
}; };
useEffect(() => { useEffect(() => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [ }, [
data?.appName, data?.appName,
data?.externalPort, data?.externalPort,
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseName, data?.databaseName,
getIp, getIp,
]); ]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle> <CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription> <CardDescription>
In order to make the database reachable trought internet is In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another required to set a port, make sure the port is not used by another
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!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
{data?.serverId href="/dashboard/settings/server"
? "Remote Servers -> Server -> Edit Server -> Update IP Address" className="text-primary"
: "Web Server -> Server -> Update Server IP"} >
</Link>{" "} {data?.serverId
to fix the database url connection. ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
</AlertBlock> : "Web Server -> Server -> Update Server IP"}
)} </Link>{" "}
<Form {...form}> to fix the database url connection.
<form </AlertBlock>
onSubmit={form.handleSubmit(onSubmit)} )}
className="flex flex-col gap-4" <Form {...form}>
> <form
<div className="grid grid-cols-2 gap-4 "> onSubmit={form.handleSubmit(onSubmit)}
<div className="col-span-2 space-y-4"> className="flex flex-col gap-4"
<FormField >
control={form.control} <div className="grid grid-cols-2 gap-4 ">
name="externalPort" <div className="col-span-2 space-y-4">
render={({ field }) => { <FormField
return ( control={form.control}
<FormItem> name="externalPort"
<FormLabel>External Port (Internet)</FormLabel> render={({ field }) => {
<FormControl> return (
<Input <FormItem>
placeholder="5432" <FormLabel>External Port (Internet)</FormLabel>
{...field} <FormControl>
value={field.value || ""} <Input
/> placeholder="5432"
</FormControl> {...field}
<FormMessage /> value={field.value || ""}
</FormItem> />
); </FormControl>
}} <FormMessage />
/> </FormItem>
</div> );
</div> }}
{!!data?.externalPort && ( />
<div className="grid w-full gap-8"> </div>
<div className="flex flex-col gap-3"> </div>
<Label>External Host</Label> {!!data?.externalPort && (
<ToggleVisibilityInput value={connectionUrl} disabled /> <div className="grid w-full gap-8">
</div> <div className="flex flex-col gap-3">
</div> <Label>External Host</Label>
)} <ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit" isLoading={isLoading}> <Button type="submit" isLoading={isLoading}>
Save Save
</Button> </Button>
</div> </div>
</form> </form>
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</> </>
); );
}; };

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { import {
Ban, Ban,
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Terminal, Rocket,
Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -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"
Open Terminal 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
</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

@@ -5,58 +5,58 @@ import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
interface Props { interface Props {
postgresId: string; postgresId: string;
} }
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => { export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
const { data } = api.postgres.one.useQuery({ postgresId }); const { data } = api.postgres.one.useQuery({ postgresId });
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Internal Credentials</CardTitle> <CardTitle className="text-xl">Internal Credentials</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-row gap-4"> <CardContent className="flex w-full flex-row gap-4">
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8"> <div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input disabled value={data?.databaseUser} /> <Input disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input disabled value={data?.databaseName} /> <Input disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
<div className="flex flex-row gap-4"> <div className="flex flex-row gap-4">
<ToggleVisibilityInput <ToggleVisibilityInput
value={data?.databasePassword} value={data?.databasePassword}
disabled disabled
/> />
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Port (Container)</Label> <Label>Internal Port (Container)</Label>
<Input disabled value="5432" /> <Input disabled value="5432" />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input disabled value={data?.appName} /> <Input disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Connection URL </Label> <Label>Internal Connection URL </Label>
<ToggleVisibilityInput <ToggleVisibilityInput
disabled disabled
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`} value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
/> />
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</> </>
); );
}; };
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w // ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w

View File

@@ -21,145 +21,146 @@ 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";
import { z } from "zod"; import { z } from "zod";
const updatePostgresSchema = z.object({ const updatePostgresSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
message: "Name is required", message: "Name is required",
}), }),
description: z.string().optional(), description: z.string().optional(),
}); });
type UpdatePostgres = z.infer<typeof updatePostgresSchema>; type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
interface Props { interface Props {
postgresId: string; postgresId: string;
} }
export const UpdatePostgres = ({ postgresId }: Props) => { export const UpdatePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } = const { mutateAsync, error, isError, isLoading } =
api.postgres.update.useMutation(); api.postgres.update.useMutation();
const { data } = api.postgres.one.useQuery( const { data } = api.postgres.one.useQuery(
{ {
postgresId, postgresId,
}, },
{ {
enabled: !!postgresId, enabled: !!postgresId,
}, }
); );
const form = useForm<UpdatePostgres>({ const form = useForm<UpdatePostgres>({
defaultValues: { defaultValues: {
description: data?.description ?? "", description: data?.description ?? "",
name: data?.name ?? "", name: data?.name ?? "",
}, },
resolver: zodResolver(updatePostgresSchema), resolver: zodResolver(updatePostgresSchema),
}); });
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
description: data.description ?? "", description: data.description ?? "",
name: data.name, name: data.name,
}); });
} }
}, [data, form, form.reset]); }, [data, form, form.reset]);
const onSubmit = async (formData: UpdatePostgres) => { const onSubmit = async (formData: UpdatePostgres) => {
await mutateAsync({ await mutateAsync({
name: formData.name, name: formData.name,
postgresId: postgresId, postgresId: postgresId,
description: formData.description || "", description: formData.description || "",
}) })
.then(() => { .then(() => {
toast.success("Postgres updated successfully"); toast.success("Postgres updated successfully");
utils.postgres.one.invalidate({ utils.postgres.one.invalidate({
postgresId: postgresId, postgresId: postgresId,
}); });
setIsOpen(false); setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error updating Postgres"); toast.error("Error updating Postgres");
}) })
.finally(() => {}); .finally(() => {});
}; };
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<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">
<DialogHeader> <DialogHeader>
<DialogTitle>Modify Postgres</DialogTitle> <DialogTitle>Modify Postgres</DialogTitle>
<DialogDescription>Update the Postgres data</DialogDescription> <DialogDescription>Update the Postgres data</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-update-postgres" id="hook-form-update-postgres"
className="grid w-full gap-4 " className="grid w-full gap-4 "
> >
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Vandelay Industries" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="description" name="description"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Description</FormLabel> <FormLabel>Description</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
placeholder="Description about your project..." placeholder="Description about your project..."
className="resize-none" className="resize-none"
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter> <DialogFooter>
<Button <Button
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 >
</Button> Update
</DialogFooter> </Button>
</form> </DialogFooter>
</Form> </form>
</div> </Form>
</div> </div>
</DialogContent> </div>
</Dialog> </DialogContent>
); </Dialog>
);
}; };

View File

@@ -27,139 +27,138 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
if (a !== null) { if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10); const parsed = Number.parseInt(z.string().parse(a), 10);
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>;
interface Props { interface Props {
redisId: string; redisId: string;
} }
export const ShowExternalRedisCredentials = ({ redisId }: Props) => { export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery(); const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.redis.one.useQuery({ redisId }); const { data, refetch } = api.redis.one.useQuery({ redisId });
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip; const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
}); });
useEffect(() => { useEffect(() => {
if (data?.externalPort) { if (data?.externalPort) {
form.reset({ form.reset({
externalPort: data.externalPort, externalPort: data.externalPort,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => { const onSubmit = async (values: DockerProvider) => {
await mutateAsync({ await mutateAsync({
externalPort: values.externalPort, externalPort: values.externalPort,
redisId, redisId,
}) })
.then(async () => { .then(async () => {
toast.success("External Port updated"); toast.success("External Port updated");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error saving the external port"); toast.error("Error saving the external port");
}); });
}; };
useEffect(() => { useEffect(() => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const _hostname = window.location.hostname; const _hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `redis://default:${data?.databasePassword}@${getIp}:${port}`; return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]); }, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle> <CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription> <CardDescription>
In order to make the database reachable trought internet is In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another required to set a port, make sure the port is not used by another
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!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
{data?.serverId href="/dashboard/settings/server"
? "Remote Servers -> Server -> Edit Server -> Update IP Address" className="text-primary"
: "Web Server -> Server -> Update Server IP"} >
</Link>{" "} {data?.serverId
to fix the database url connection. ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
</AlertBlock> : "Web Server -> Server -> Update Server IP"}
)} </Link>{" "}
<Form {...form}> to fix the database url connection.
<form </AlertBlock>
onSubmit={form.handleSubmit(onSubmit)} )}
className="flex flex-col gap-4" <Form {...form}>
> <form
<div className="grid grid-cols-2 gap-4 "> onSubmit={form.handleSubmit(onSubmit)}
<div className="col-span-2 space-y-4"> className="flex flex-col gap-4"
<FormField >
control={form.control} <div className="grid grid-cols-2 gap-4 ">
name="externalPort" <div className="col-span-2 space-y-4">
render={({ field }) => { <FormField
return ( control={form.control}
<FormItem> name="externalPort"
<FormLabel>External Port (Internet)</FormLabel> render={({ field }) => {
<FormControl> return (
<Input <FormItem>
placeholder="6379" <FormLabel>External Port (Internet)</FormLabel>
{...field} <FormControl>
value={field.value || ""} <Input
/> placeholder="6379"
</FormControl> {...field}
<FormMessage /> value={field.value || ""}
</FormItem> />
); </FormControl>
}} <FormMessage />
/> </FormItem>
</div> );
</div> }}
{!!data?.externalPort && ( />
<div className="grid w-full gap-8"> </div>
<div className="flex flex-col gap-3"> </div>
<Label>External Host</Label> {!!data?.externalPort && (
<ToggleVisibilityInput value={connectionUrl} disabled /> <div className="grid w-full gap-8">
</div> <div className="flex flex-col gap-3">
</div> <Label>External Host</Label>
)} <ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit" isLoading={isLoading}> <Button type="submit" isLoading={isLoading}>
Save Save
</Button> </Button>
</div> </div>
</form> </form>
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</> </>
); );
}; };

View File

@@ -11,11 +11,12 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { import {
Ban, Ban,
CheckCircle2, CheckCircle2,
HelpCircle, HelpCircle,
RefreshCcw, RefreshCcw,
Terminal, Rocket,
Terminal,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -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"
Open Terminal 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
</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,
});
}
}),
restoreBackupWithLogs: protectedProcedure
.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);
return observable<string>((emit) => {
restorePostgresBackup(
postgres,
destination,
input.databaseName,
input.backupFile,
(log) => {
emit.next(log);
},
);
});
}
if (input.databaseType === "mysql") {
const mysql = await findMySqlById(input.databaseId);
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 true;
}),
}); });
// export const getAdminId = async (backupId: string) => {
// const backup = await findBackupById(backupId);
// if (backup.databaseType === "postgres" && backup.postgresId) {
// const postgres = await findPostgresById(backup.postgresId);
// return postgres.project.adminId;
// }
// if (backup.databaseType === "mariadb" && backup.mariadbId) {
// const mariadb = await findMariadbById(backup.mariadbId);
// return mariadb.project.adminId;
// }
// if (backup.databaseType === "mysql" && backup.mysqlId) {
// const mysql = await findMySqlById(backup.mysqlId);
// return mysql.project.adminId;
// }
// if (backup.databaseType === "mongo" && backup.mongoId) {
// const mongo = await findMongoById(backup.mongoId);
// return mongo.project.adminId;
// }
// return null;
// };

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;
}
};