mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into feat/add-gitea-repo
This commit is contained in:
@@ -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: "",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
Ban,
|
Ban,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Hammer,
|
Hammer,
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -55,7 +55,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Application"
|
title="Deploy Application"
|
||||||
description="Are you sure you want to deploy this application?"
|
description="Are you sure you want to deploy this application?"
|
||||||
@@ -79,12 +79,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Deploy
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Rocket className="size-4 mr-1" />
|
||||||
|
Deploy
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -114,9 +116,24 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
Reload
|
Reload
|
||||||
<RefreshCcw className="size-4" />
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Reload the application without rebuilding it</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -139,13 +156,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Hammer className="size-4 mr-1" />
|
||||||
|
Rebuild
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -180,13 +198,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isStarting}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Start
|
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -219,13 +238,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -241,15 +261,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle italic"
|
aria-label="Toggle autodeploy"
|
||||||
checked={data?.autoDeploy || false}
|
checked={data?.autoDeploy || false}
|
||||||
onCheckedChange={async (enabled) => {
|
onCheckedChange={async (enabled) => {
|
||||||
await update({
|
await update({
|
||||||
@@ -264,14 +287,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
toast.error("Error updating Auto Deploy");
|
toast.error("Error updating Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<span className="text-sm font-medium">Clean Cache</span>
|
<span className="text-sm font-medium">Clean Cache</span>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle italic"
|
aria-label="Toggle clean cache"
|
||||||
checked={data?.cleanCache || false}
|
checked={data?.cleanCache || false}
|
||||||
onCheckedChange={async (enabled) => {
|
onCheckedChange={async (enabled) => {
|
||||||
await update({
|
await update({
|
||||||
@@ -286,7 +309,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
toast.error("Error updating Clean Cache");
|
toast.error("Error updating Clean Cache");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,6 +21,8 @@ import { toast } from "sonner";
|
|||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { AddBackup } from "./add-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
import { UpdateBackup } from "./update-backup";
|
import { UpdateBackup } from "./update-backup";
|
||||||
|
import { RestoreBackup } from "./restore-backup";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -71,7 +73,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
||||||
|
<RestoreBackup databaseId={id} databaseType={type} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -98,11 +103,14 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No backups configured
|
No backups configured
|
||||||
</span>
|
</span>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||||
<AddBackup
|
<AddBackup
|
||||||
databaseId={id}
|
databaseId={id}
|
||||||
databaseType={type}
|
databaseType={type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
|
<RestoreBackup databaseId={id} databaseType={type} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -183,6 +191,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<TooltipContent>Run Manual Backup</TooltipContent>
|
<TooltipContent>Run Manual Backup</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<UpdateBackup
|
<UpdateBackup
|
||||||
backupId={backup.backupId}
|
backupId={backup.backupId}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
@@ -111,7 +107,10 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
|
href="/dashboard/settings/server"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -92,12 +93,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Deploy
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Rocket className="size-4 mr-1" />
|
||||||
|
Deploy
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -107,6 +110,8 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mariadb"
|
title="Reload Mariadb"
|
||||||
description="Are you sure you want to reload this mariadb?"
|
description="Are you sure you want to reload this mariadb?"
|
||||||
@@ -128,13 +133,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isReloading}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -144,7 +150,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mariadb"
|
title="Start Mariadb"
|
||||||
description="Are you sure you want to start this mariadb?"
|
description="Are you sure you want to start this mariadb?"
|
||||||
@@ -165,13 +173,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isStarting}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Start
|
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -184,7 +193,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
) : (
|
) : (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mariadb"
|
title="Stop Mariadb"
|
||||||
description="Are you sure you want to stop this mariadb?"
|
description="Are you sure you want to stop this mariadb?"
|
||||||
@@ -204,13 +215,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -220,15 +232,29 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Open a terminal to the MariaDB container</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
@@ -111,7 +107,10 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
|
href="/dashboard/settings/server"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -30,7 +31,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mongoId },
|
{ enabled: !!mongoId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
@@ -67,7 +68,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -88,15 +89,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
|
<Rocket className="size-4 mr-1" />
|
||||||
Deploy
|
Deploy
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -104,7 +106,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mongo"
|
title="Reload Mongo"
|
||||||
@@ -124,16 +125,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isReloading}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
Reload
|
Reload
|
||||||
<RefreshCcw className="size-4" />
|
</Button>
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -141,7 +142,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -161,16 +161,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isStarting}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
Start
|
Start
|
||||||
<CheckCircle2 className="size-4" />
|
</Button>
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -181,7 +181,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -200,16 +199,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
Stop
|
Stop
|
||||||
<Ban className="size-4" />
|
</Button>
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -217,7 +216,6 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -225,10 +223,22 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Tooltip>
|
||||||
<Terminal />
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Terminal className="size-4" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Open a terminal to the MongoDB container</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
@@ -111,7 +107,10 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
|
href="/dashboard/settings/server"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -77,7 +78,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Mysql"
|
title="Deploy MySQL"
|
||||||
description="Are you sure you want to deploy this mysql?"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -89,12 +90,14 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Deploy
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Rocket className="size-4 mr-1" />
|
||||||
|
Deploy
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -105,7 +108,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mysql"
|
title="Reload MySQL"
|
||||||
description="Are you sure you want to reload this mysql?"
|
description="Are you sure you want to reload this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -114,24 +117,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mysql reloaded successfully");
|
toast.success("MySQL reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mysql");
|
toast.error("Error reloading MySQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isReloading}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -143,7 +147,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mysql"
|
title="Start MySQL"
|
||||||
description="Are you sure you want to start this mysql?"
|
description="Are you sure you want to start this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -151,24 +155,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mysql started successfully");
|
toast.success("MySQL started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting Mysql");
|
toast.error("Error starting MySQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isStarting}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Start
|
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -183,31 +188,32 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mysql"
|
title="Stop MySQL"
|
||||||
description="Are you sure you want to stop this mysql?"
|
description="Are you sure you want to stop this mysql?"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await stop({
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mysql stopped successfully");
|
toast.success("MySQL stopped successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Mysql");
|
toast.error("Error stopping MySQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -223,9 +229,23 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Open a terminal to the MySQL container</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
@@ -113,7 +109,10 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
|
href="/dashboard/settings/server"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -78,9 +79,9 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider disableHoverableContent={false}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Postgres"
|
title="Deploy PostgreSQL"
|
||||||
description="Are you sure you want to deploy this postgres?"
|
description="Are you sure you want to deploy this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -92,12 +93,14 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Deploy
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Rocket className="size-4 mr-1" />
|
||||||
|
Deploy
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -108,7 +111,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Postgres"
|
title="Reload PostgreSQL"
|
||||||
description="Are you sure you want to reload this postgres?"
|
description="Are you sure you want to reload this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -117,24 +120,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Postgres reloaded successfully");
|
toast.success("PostgreSQL reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Postgres");
|
toast.error("Error reloading PostgreSQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isReloading}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -146,7 +150,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Postgres"
|
title="Start PostgreSQL"
|
||||||
description="Are you sure you want to start this postgres?"
|
description="Are you sure you want to start this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -154,24 +158,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Postgres started successfully");
|
toast.success("PostgreSQL started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting Postgres");
|
toast.error("Error starting PostgreSQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isStarting}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Start
|
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -186,31 +191,32 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Postgres"
|
title="Stop PostgreSQL"
|
||||||
description="Are you sure you want to stop this postgres?"
|
description="Are you sure you want to stop this postgres?"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await stop({
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Postgres stopped successfully");
|
toast.success("PostgreSQL stopped successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Postgres");
|
toast.error("Error stopping PostgreSQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -226,9 +232,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Open a terminal to the PostgreSQL container</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon } from "lucide-react";
|
import { PenBox } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -51,7 +51,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!postgresId,
|
enabled: !!postgresId,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const form = useForm<UpdatePostgres>({
|
const form = useForm<UpdatePostgres>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -94,9 +94,9 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-blue-500/10 "
|
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
@@ -151,6 +151,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-update-postgres"
|
form="hook-form-update-postgres"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
@@ -105,7 +101,10 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
|
href="/dashboard/settings/server"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -91,12 +92,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Deploy
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Rocket className="size-4 mr-1" />
|
||||||
|
Deploy
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -127,13 +130,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isReloading}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -164,13 +168,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isStarting}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Start
|
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -203,13 +208,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
@@ -225,9 +231,23 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Open a terminal to the Redis container</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ import {
|
|||||||
createBackup,
|
createBackup,
|
||||||
findBackupById,
|
findBackupById,
|
||||||
findMariadbByBackupId,
|
findMariadbByBackupId,
|
||||||
|
findMariadbById,
|
||||||
findMongoByBackupId,
|
findMongoByBackupId,
|
||||||
|
findMongoById,
|
||||||
findMySqlByBackupId,
|
findMySqlByBackupId,
|
||||||
|
findMySqlById,
|
||||||
findPostgresByBackupId,
|
findPostgresByBackupId,
|
||||||
|
findPostgresById,
|
||||||
findServerById,
|
findServerById,
|
||||||
removeBackupById,
|
removeBackupById,
|
||||||
removeScheduleBackup,
|
removeScheduleBackup,
|
||||||
@@ -26,6 +30,17 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { execAsync } from "@dokploy/server/utils/process/execAsync";
|
||||||
|
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
||||||
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
|
import {
|
||||||
|
restoreMariadbBackup,
|
||||||
|
restoreMongoBackup,
|
||||||
|
restoreMySqlBackup,
|
||||||
|
restorePostgresBackup,
|
||||||
|
} from "@dokploy/server/utils/restore";
|
||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
export const backupRouter = createTRPCRouter({
|
export const backupRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -209,27 +224,136 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
listBackupFiles: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
destinationId: z.string(),
|
||||||
|
search: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const destination = await findDestinationById(input.destinationId);
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
|
||||||
|
const lastSlashIndex = input.search.lastIndexOf("/");
|
||||||
|
const baseDir =
|
||||||
|
lastSlashIndex !== -1
|
||||||
|
? input.search.slice(0, lastSlashIndex + 1)
|
||||||
|
: "";
|
||||||
|
const searchTerm =
|
||||||
|
lastSlashIndex !== -1
|
||||||
|
? input.search.slice(lastSlashIndex + 1)
|
||||||
|
: input.search;
|
||||||
|
|
||||||
|
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
|
||||||
|
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(listCommand);
|
||||||
|
const files = stdout.split("\n").filter(Boolean);
|
||||||
|
|
||||||
|
const results = baseDir
|
||||||
|
? files.map((file) => `${baseDir}${file}`)
|
||||||
|
: files;
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
return results.filter((file) =>
|
||||||
|
file.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error listing backup files",
|
||||||
|
cause: error,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
// export const getAdminId = async (backupId: string) => {
|
restoreBackupWithLogs: protectedProcedure
|
||||||
// const backup = await findBackupById(backupId);
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
enabled: false,
|
||||||
|
path: "/restore-backup-with-logs",
|
||||||
|
method: "POST",
|
||||||
|
override: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
databaseId: z.string(),
|
||||||
|
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
|
||||||
|
databaseName: z.string().min(1),
|
||||||
|
backupFile: z.string().min(1),
|
||||||
|
destinationId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscription(async ({ input }) => {
|
||||||
|
const destination = await findDestinationById(input.destinationId);
|
||||||
|
if (input.databaseType === "postgres") {
|
||||||
|
const postgres = await findPostgresById(input.databaseId);
|
||||||
|
|
||||||
// if (backup.databaseType === "postgres" && backup.postgresId) {
|
return observable<string>((emit) => {
|
||||||
// const postgres = await findPostgresById(backup.postgresId);
|
restorePostgresBackup(
|
||||||
// return postgres.project.adminId;
|
postgres,
|
||||||
// }
|
destination,
|
||||||
// if (backup.databaseType === "mariadb" && backup.mariadbId) {
|
input.databaseName,
|
||||||
// const mariadb = await findMariadbById(backup.mariadbId);
|
input.backupFile,
|
||||||
// return mariadb.project.adminId;
|
(log) => {
|
||||||
// }
|
emit.next(log);
|
||||||
// if (backup.databaseType === "mysql" && backup.mysqlId) {
|
},
|
||||||
// const mysql = await findMySqlById(backup.mysqlId);
|
);
|
||||||
// return mysql.project.adminId;
|
});
|
||||||
// }
|
}
|
||||||
// if (backup.databaseType === "mongo" && backup.mongoId) {
|
if (input.databaseType === "mysql") {
|
||||||
// const mongo = await findMongoById(backup.mongoId);
|
const mysql = await findMySqlById(input.databaseId);
|
||||||
// return mongo.project.adminId;
|
return observable<string>((emit) => {
|
||||||
// }
|
restoreMySqlBackup(
|
||||||
|
mysql,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mariadb") {
|
||||||
|
const mariadb = await findMariadbById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMariadbBackup(
|
||||||
|
mariadb,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mongo") {
|
||||||
|
const mongo = await findMongoById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMongoBackup(
|
||||||
|
mongo,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// return null;
|
return true;
|
||||||
// };
|
}),
|
||||||
|
});
|
||||||
|
|||||||
4
packages/server/src/utils/restore/index.ts
Normal file
4
packages/server/src/utils/restore/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { restorePostgresBackup } from "./postgres";
|
||||||
|
export { restoreMySqlBackup } from "./mysql";
|
||||||
|
export { restoreMariadbBackup } from "./mariadb";
|
||||||
|
export { restoreMongoBackup } from "./mongo";
|
||||||
56
packages/server/src/utils/restore/mariadb.ts
Normal file
56
packages/server/src/utils/restore/mariadb.ts
Normal 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
64
packages/server/src/utils/restore/mongo.ts
Normal file
64
packages/server/src/utils/restore/mongo.ts
Normal 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
54
packages/server/src/utils/restore/mysql.ts
Normal file
54
packages/server/src/utils/restore/mysql.ts
Normal 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
60
packages/server/src/utils/restore/postgres.ts
Normal file
60
packages/server/src/utils/restore/postgres.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user