mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6a288781f | ||
|
|
724bed9832 | ||
|
|
2405e5a93a | ||
|
|
e97c8f42b3 | ||
|
|
d805f6a7aa | ||
|
|
45d05b2aa4 | ||
|
|
6d350a23a9 | ||
|
|
5965b73342 | ||
|
|
b8e06feaff | ||
|
|
3c5a005165 | ||
|
|
12d31c89f3 | ||
|
|
3cf7c697b8 |
@@ -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,
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import {
|
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?"
|
||||||
@@ -76,25 +76,25 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Downloads the source code and performs a complete build
|
<p>
|
||||||
</p>
|
Downloads the source code and performs a complete build
|
||||||
</TooltipContent>
|
</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Application"
|
title="Reload Application"
|
||||||
@@ -114,10 +114,23 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
<Tooltip>
|
||||||
Reload
|
<TooltipTrigger asChild>
|
||||||
<RefreshCcw className="size-4" />
|
<Button
|
||||||
</Button>
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Reload the application without rebuilding it</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Rebuild Application"
|
title="Rebuild Application"
|
||||||
@@ -136,27 +149,25 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Rebuild
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Hammer className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Hammer className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Rebuild
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Only rebuilds the application without downloading new
|
Only rebuilds the application without downloading new code
|
||||||
code
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
@@ -177,27 +188,26 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStarting}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isStarting}
|
||||||
Start
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<CheckCircle2 className="size-4" />
|
>
|
||||||
<Tooltip>
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Start
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the application (requires a previous successful
|
Start the application (requires a previous successful
|
||||||
build)
|
build)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -216,24 +226,23 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="destructive"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStopping}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="destructive"
|
||||||
>
|
isLoading={isStopping}
|
||||||
Stop
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Ban className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Ban className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Stop
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running application</p>
|
<p>Stop the currently running application</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -241,15 +250,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 +276,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 +298,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>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, 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 +34,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?"
|
||||||
@@ -55,59 +55,58 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.composeStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.composeStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads the source code and performs a complete build</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads the source code and performs a complete build</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</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
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.composeStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={data?.composeStatus === "running"}
|
||||||
Rebuild
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Hammer className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>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>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{data?.composeType === "docker-compose" &&
|
{data?.composeType === "docker-compose" &&
|
||||||
data?.composeStatus === "idle" ? (
|
data?.composeStatus === "idle" ? (
|
||||||
@@ -128,26 +127,25 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStarting}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isStarting}
|
||||||
Start
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<CheckCircle2 className="size-4" />
|
>
|
||||||
<Tooltip>
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Start
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the compose (requires a previous successful build)
|
Start the compose (requires a previous successful build)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -166,24 +164,23 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="destructive"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStopping}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="destructive"
|
||||||
>
|
isLoading={isStopping}
|
||||||
Stop
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Ban className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Ban className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Stop
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running compose</p>
|
<p>Stop the currently running compose</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -191,15 +188,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 +214,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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ 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";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -27,7 +28,9 @@ interface Props {
|
|||||||
type: Exclude<ServiceType, "application" | "redis">;
|
type: Exclude<ServiceType, "application" | "redis">;
|
||||||
}
|
}
|
||||||
export const ShowBackups = ({ id, type }: Props) => {
|
export const ShowBackups = ({ id, type }: Props) => {
|
||||||
const [activeManualBackup, setActiveManualBackup] = useState<string | undefined>();
|
const [activeManualBackup, setActiveManualBackup] = useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
@@ -69,7 +72,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||||
|
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
||||||
|
<RestoreBackup databaseId={id} databaseType={type} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -96,11 +102,14 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No backups configured
|
No backups configured
|
||||||
</span>
|
</span>
|
||||||
<AddBackup
|
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||||
databaseId={id}
|
<AddBackup
|
||||||
databaseType={type}
|
databaseId={id}
|
||||||
refetch={refetch}
|
databaseType={type}
|
||||||
/>
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
<RestoreBackup databaseId={id} databaseType={type} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -142,7 +151,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Keep Latest</span>
|
<span className="font-medium">Keep Latest</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{backup.keepLatestCount || 'All'}
|
{backup.keepLatestCount || "All"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +162,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
isLoading={isManualBackup && activeManualBackup === backup.backupId}
|
isLoading={
|
||||||
|
isManualBackup &&
|
||||||
|
activeManualBackup === backup.backupId
|
||||||
|
}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setActiveManualBackup(backup.backupId);
|
setActiveManualBackup(backup.backupId);
|
||||||
await manualBackup({
|
await manualBackup({
|
||||||
@@ -178,6 +190,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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,153 +20,152 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mariadbId,
|
mariadbId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
{data?.serverId
|
href="/dashboard/settings/server"
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
className="text-primary"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
>
|
||||||
</Link>{" "}
|
{data?.serverId
|
||||||
to fix the database url connection.
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
</AlertBlock>
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
)}
|
</Link>{" "}
|
||||||
<Form {...form}>
|
to fix the database url connection.
|
||||||
<form
|
</AlertBlock>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
)}
|
||||||
className="flex flex-col gap-4"
|
<Form {...form}>
|
||||||
>
|
<form
|
||||||
<div className="grid md:grid-cols-2 gap-4 ">
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<div className="md:col-span-2 space-y-4">
|
className="flex flex-col gap-4"
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<div className="grid md:grid-cols-2 gap-4 ">
|
||||||
name="externalPort"
|
<div className="md:col-span-2 space-y-4">
|
||||||
render={({ field }) => {
|
<FormField
|
||||||
return (
|
control={form.control}
|
||||||
<FormItem>
|
name="externalPort"
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
render={({ field }) => {
|
||||||
<FormControl>
|
return (
|
||||||
<Input
|
<FormItem>
|
||||||
placeholder="3306"
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{...field}
|
<FormControl>
|
||||||
value={field.value || ""}
|
<Input
|
||||||
/>
|
placeholder="3306"
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
value={field.value || ""}
|
||||||
</FormItem>
|
/>
|
||||||
);
|
</FormControl>
|
||||||
}}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</div>
|
);
|
||||||
</div>
|
}}
|
||||||
{!!data?.externalPort && (
|
/>
|
||||||
<div className="grid w-full gap-8">
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
</div>
|
||||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
{!!data?.externalPort && (
|
||||||
<Label>External Host</Label>
|
<div className="grid w-full gap-8">
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<div className="flex flex-col gap-3">
|
||||||
</div>
|
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||||
</div>
|
<Label>External Host</Label>
|
||||||
)}
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,242 +8,245 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
const { data, refetch } = api.mariadb.one.useQuery(
|
||||||
{
|
{
|
||||||
mariadbId,
|
mariadbId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mariadbId },
|
{ enabled: !!mariadbId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.mariadb.reload.useMutation();
|
api.mariadb.reload.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.mariadb.start.useMutation();
|
api.mariadb.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.mariadb.stop.useMutation();
|
api.mariadb.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.mariadb.deployWithLogs.useSubscription(
|
api.mariadb.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Mariadb"
|
title="Deploy Mariadb"
|
||||||
description="Are you sure you want to deploy this mariadb?"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the MariaDB database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the MariaDB database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<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?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mariadb reloaded successfully");
|
toast.success("Mariadb reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mariadb");
|
toast.error("Error reloading Mariadb");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MariaDB service without rebuilding</p>
|
<p>Restart the MariaDB service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start Mariadb"
|
||||||
title="Start Mariadb"
|
description="Are you sure you want to start this mariadb?"
|
||||||
description="Are you sure you want to start this mariadb?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await start({
|
||||||
await start({
|
mariadbId: mariadbId,
|
||||||
mariadbId: mariadbId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Mariadb started successfully");
|
||||||
toast.success("Mariadb started successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error starting Mariadb");
|
||||||
toast.error("Error starting Mariadb");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the MariaDB database (requires a previous
|
||||||
Start the MariaDB database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Mariadb"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this mariadb?"
|
||||||
title="Stop Mariadb"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this mariadb?"
|
await stop({
|
||||||
onClick={async () => {
|
mariadbId: mariadbId,
|
||||||
await stop({
|
})
|
||||||
mariadbId: mariadbId,
|
.then(() => {
|
||||||
})
|
toast.success("Mariadb stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Mariadb stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Mariadb");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Mariadb");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running MariaDB database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running MariaDB database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the MariaDB container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,152 +20,151 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mongoId,
|
mongoId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
{data?.serverId
|
href="/dashboard/settings/server"
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
className="text-primary"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
>
|
||||||
</Link>{" "}
|
{data?.serverId
|
||||||
to fix the database url connection.
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
</AlertBlock>
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
)}
|
</Link>{" "}
|
||||||
<Form {...form}>
|
to fix the database url connection.
|
||||||
<form
|
</AlertBlock>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
)}
|
||||||
className="flex flex-col gap-4"
|
<Form {...form}>
|
||||||
>
|
<form
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<div className="col-span-2 space-y-4">
|
className="flex flex-col gap-4"
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
name="externalPort"
|
<div className="col-span-2 space-y-4">
|
||||||
render={({ field }) => {
|
<FormField
|
||||||
return (
|
control={form.control}
|
||||||
<FormItem>
|
name="externalPort"
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
render={({ field }) => {
|
||||||
<FormControl>
|
return (
|
||||||
<Input
|
<FormItem>
|
||||||
placeholder="27017"
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{...field}
|
<FormControl>
|
||||||
value={field.value || ""}
|
<Input
|
||||||
/>
|
placeholder="27017"
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
value={field.value || ""}
|
||||||
</FormItem>
|
/>
|
||||||
);
|
</FormControl>
|
||||||
}}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</div>
|
);
|
||||||
</div>
|
}}
|
||||||
{!!data?.externalPort && (
|
/>
|
||||||
<div className="grid w-full gap-8">
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
</div>
|
||||||
<Label>External Host</Label>
|
{!!data?.externalPort && (
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<div className="grid w-full gap-8">
|
||||||
</div>
|
<div className="flex flex-col gap-3">
|
||||||
</div>
|
<Label>External Host</Label>
|
||||||
)}
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,246 +3,249 @@ import { DrawerLogs } from "@/components/shared/drawer-logs";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mongoId },
|
{ enabled: !!mongoId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.mongo.reload.useMutation();
|
api.mongo.reload.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.mongo.start.useMutation();
|
api.mongo.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.mongo.stop.useMutation();
|
api.mongo.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.mongo.deployWithLogs.useSubscription(
|
api.mongo.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to deploy this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the MongoDB database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the MongoDB database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mongo"
|
title="Reload Mongo"
|
||||||
description="Are you sure you want to reload this mongo?"
|
description="Are you sure you want to reload this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mongo reloaded successfully");
|
toast.success("Mongo reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mongo");
|
toast.error("Error reloading Mongo");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MongoDB service without rebuilding</p>
|
<p>Restart the MongoDB service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start Mongo"
|
||||||
title="Start Mongo"
|
description="Are you sure you want to start this mongo?"
|
||||||
description="Are you sure you want to start this mongo?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await start({
|
||||||
await start({
|
mongoId: mongoId,
|
||||||
mongoId: mongoId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Mongo started successfully");
|
||||||
toast.success("Mongo started successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error starting Mongo");
|
||||||
toast.error("Error starting Mongo");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the MongoDB database (requires a previous
|
||||||
Start the MongoDB database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Mongo"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this mongo?"
|
||||||
title="Stop Mongo"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this mongo?"
|
await stop({
|
||||||
onClick={async () => {
|
mongoId: mongoId,
|
||||||
await stop({
|
})
|
||||||
mongoId: mongoId,
|
.then(() => {
|
||||||
})
|
toast.success("Mongo stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Mongo stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Mongo");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Mongo");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running MongoDB database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running MongoDB database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the MongoDB container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,152 +20,151 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mysqlId,
|
mysqlId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
form,
|
form,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
{data?.serverId
|
href="/dashboard/settings/server"
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
className="text-primary"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
>
|
||||||
</Link>{" "}
|
{data?.serverId
|
||||||
to fix the database url connection.
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
</AlertBlock>
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
)}
|
</Link>{" "}
|
||||||
<Form {...form}>
|
to fix the database url connection.
|
||||||
<form
|
</AlertBlock>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
)}
|
||||||
className="flex flex-col gap-4"
|
<Form {...form}>
|
||||||
>
|
<form
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<div className="col-span-2 space-y-4">
|
className="flex flex-col gap-4"
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
name="externalPort"
|
<div className="col-span-2 space-y-4">
|
||||||
render={({ field }) => {
|
<FormField
|
||||||
return (
|
control={form.control}
|
||||||
<FormItem>
|
name="externalPort"
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
render={({ field }) => {
|
||||||
<FormControl>
|
return (
|
||||||
<Input
|
<FormItem>
|
||||||
placeholder="3306"
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{...field}
|
<FormControl>
|
||||||
value={field.value || ""}
|
<Input
|
||||||
/>
|
placeholder="3306"
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
value={field.value || ""}
|
||||||
</FormItem>
|
/>
|
||||||
);
|
</FormControl>
|
||||||
}}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</div>
|
);
|
||||||
</div>
|
}}
|
||||||
{!!data?.externalPort && (
|
/>
|
||||||
<div className="grid w-full gap-8">
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
</div>
|
||||||
<Label>External Host</Label>
|
{!!data?.externalPort && (
|
||||||
<ToggleVisibilityInput disabled value={connectionUrl} />
|
<div className="grid w-full gap-8">
|
||||||
</div>
|
<div className="flex flex-col gap-3">
|
||||||
</div>
|
<Label>External Host</Label>
|
||||||
)}
|
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,239 +8,242 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||||
const { data, refetch } = api.mysql.one.useQuery(
|
const { data, refetch } = api.mysql.one.useQuery(
|
||||||
{
|
{
|
||||||
mysqlId,
|
mysqlId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mysqlId },
|
{ enabled: !!mysqlId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.mysql.reload.useMutation();
|
api.mysql.reload.useMutation();
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.mysql.start.useMutation();
|
api.mysql.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.mysql.stop.useMutation();
|
api.mysql.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.mysql.deployWithLogs.useSubscription(
|
api.mysql.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy 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 () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the MySQL database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the MySQL database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</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 () => {
|
||||||
await reload({
|
await reload({
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
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
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MySQL service without rebuilding</p>
|
<p>Restart the MySQL service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start 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 () => {
|
await start({
|
||||||
await start({
|
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");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the MySQL database (requires a previous
|
||||||
Start the MySQL database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Mysql"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this mysql?"
|
||||||
title="Stop Mysql"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this mysql?"
|
await stop({
|
||||||
onClick={async () => {
|
mysqlId: mysqlId,
|
||||||
await stop({
|
})
|
||||||
mysqlId: mysqlId,
|
.then(() => {
|
||||||
})
|
toast.success("Mysql stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Mysql stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Mysql");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Mysql");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running MySQL database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running MySQL database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the MySQL container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,154 +20,153 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.postgres.saveExternalPort.useMutation();
|
api.postgres.saveExternalPort.useMutation();
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
postgresId,
|
postgresId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
{data?.serverId
|
href="/dashboard/settings/server"
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
className="text-primary"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
>
|
||||||
</Link>{" "}
|
{data?.serverId
|
||||||
to fix the database url connection.
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
</AlertBlock>
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
)}
|
</Link>{" "}
|
||||||
<Form {...form}>
|
to fix the database url connection.
|
||||||
<form
|
</AlertBlock>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
)}
|
||||||
className="flex flex-col gap-4"
|
<Form {...form}>
|
||||||
>
|
<form
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<div className="col-span-2 space-y-4">
|
className="flex flex-col gap-4"
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
name="externalPort"
|
<div className="col-span-2 space-y-4">
|
||||||
render={({ field }) => {
|
<FormField
|
||||||
return (
|
control={form.control}
|
||||||
<FormItem>
|
name="externalPort"
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
render={({ field }) => {
|
||||||
<FormControl>
|
return (
|
||||||
<Input
|
<FormItem>
|
||||||
placeholder="5432"
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{...field}
|
<FormControl>
|
||||||
value={field.value || ""}
|
<Input
|
||||||
/>
|
placeholder="5432"
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
value={field.value || ""}
|
||||||
</FormItem>
|
/>
|
||||||
);
|
</FormControl>
|
||||||
}}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</div>
|
);
|
||||||
</div>
|
}}
|
||||||
{!!data?.externalPort && (
|
/>
|
||||||
<div className="grid w-full gap-8">
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
</div>
|
||||||
<Label>External Host</Label>
|
{!!data?.externalPort && (
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<div className="grid w-full gap-8">
|
||||||
</div>
|
<div className="flex flex-col gap-3">
|
||||||
</div>
|
<Label>External Host</Label>
|
||||||
)}
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,15 +8,9 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
@@ -78,7 +72,7 @@ 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 Postgres"
|
||||||
description="Are you sure you want to deploy this postgres?"
|
description="Are you sure you want to deploy this postgres?"
|
||||||
@@ -89,23 +83,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the PostgreSQL database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the PostgreSQL database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Postgres"
|
title="Reload Postgres"
|
||||||
@@ -125,24 +119,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the PostgreSQL service without rebuilding</p>
|
<p>Reload the PostgreSQL without rebuilding it</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -162,27 +155,26 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStarting}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isStarting}
|
||||||
Start
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<CheckCircle2 className="size-4" />
|
>
|
||||||
<Tooltip>
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Start
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the PostgreSQL database (requires a previous
|
Start the PostgreSQL database (requires a previous
|
||||||
successful setup)
|
successful setup)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -201,24 +193,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="destructive"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStopping}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="destructive"
|
||||||
>
|
isLoading={isStopping}
|
||||||
Stop
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Ban className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Ban className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Stop
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running PostgreSQL database</p>
|
<p>Stop the currently running PostgreSQL database</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -226,8 +217,11 @@ 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"
|
||||||
|
>
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
|
|||||||
@@ -5,58 +5,58 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>User</Label>
|
<Label>User</Label>
|
||||||
<Input disabled value={data?.databaseUser} />
|
<Input disabled value={data?.databaseUser} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Database Name</Label>
|
<Label>Database Name</Label>
|
||||||
<Input disabled value={data?.databaseName} />
|
<Input disabled value={data?.databaseName} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Port (Container)</Label>
|
<Label>Internal Port (Container)</Label>
|
||||||
<Input disabled value="5432" />
|
<Input disabled value="5432" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Host</Label>
|
<Label>Internal Host</Label>
|
||||||
<Input disabled value={data?.appName} />
|
<Input disabled value={data?.appName} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w
|
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w
|
||||||
|
|||||||
@@ -21,145 +21,146 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon } from "lucide-react";
|
import { PenBox } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const updatePostgresSchema = z.object({
|
const updatePostgresSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
|
type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdatePostgres = ({ postgresId }: Props) => {
|
export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
api.postgres.update.useMutation();
|
api.postgres.update.useMutation();
|
||||||
const { data } = api.postgres.one.useQuery(
|
const { data } = api.postgres.one.useQuery(
|
||||||
{
|
{
|
||||||
postgresId,
|
postgresId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!postgresId,
|
enabled: !!postgresId,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const form = useForm<UpdatePostgres>({
|
const form = useForm<UpdatePostgres>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
description: data?.description ?? "",
|
description: data?.description ?? "",
|
||||||
name: data?.name ?? "",
|
name: data?.name ?? "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(updatePostgresSchema),
|
resolver: zodResolver(updatePostgresSchema),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
description: data.description ?? "",
|
description: data.description ?? "",
|
||||||
name: data.name,
|
name: data.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form, form.reset]);
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: UpdatePostgres) => {
|
const onSubmit = async (formData: UpdatePostgres) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Postgres updated successfully");
|
toast.success("Postgres updated successfully");
|
||||||
utils.postgres.one.invalidate({
|
utils.postgres.one.invalidate({
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating Postgres");
|
toast.error("Error updating Postgres");
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-blue-500/10 "
|
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Postgres</DialogTitle>
|
<DialogTitle>Modify Postgres</DialogTitle>
|
||||||
<DialogDescription>Update the Postgres data</DialogDescription>
|
<DialogDescription>Update the Postgres data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
id="hook-form-update-postgres"
|
id="hook-form-update-postgres"
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-4 "
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Vandelay Industries" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Description about your project..."
|
placeholder="Description about your project..."
|
||||||
className="resize-none"
|
className="resize-none"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-update-postgres"
|
form="hook-form-update-postgres"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Update
|
>
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
</form>
|
</DialogFooter>
|
||||||
</Form>
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</div>
|
||||||
</Dialog>
|
</DialogContent>
|
||||||
);
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,146 +20,145 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
redisId,
|
redisId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const _hostname = window.location.hostname;
|
const _hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link href="/dashboard/settings" className="text-primary">
|
<Link
|
||||||
{data?.serverId
|
href="/dashboard/settings/server"
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
className="text-primary"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
>
|
||||||
</Link>{" "}
|
{data?.serverId
|
||||||
to fix the database url connection.
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
</AlertBlock>
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
)}
|
</Link>{" "}
|
||||||
<Form {...form}>
|
to fix the database url connection.
|
||||||
<form
|
</AlertBlock>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
)}
|
||||||
className="flex flex-col gap-4"
|
<Form {...form}>
|
||||||
>
|
<form
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<div className="col-span-2 space-y-4">
|
className="flex flex-col gap-4"
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
name="externalPort"
|
<div className="col-span-2 space-y-4">
|
||||||
render={({ field }) => {
|
<FormField
|
||||||
return (
|
control={form.control}
|
||||||
<FormItem>
|
name="externalPort"
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
render={({ field }) => {
|
||||||
<FormControl>
|
return (
|
||||||
<Input
|
<FormItem>
|
||||||
placeholder="6379"
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{...field}
|
<FormControl>
|
||||||
value={field.value || ""}
|
<Input
|
||||||
/>
|
placeholder="6379"
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
value={field.value || ""}
|
||||||
</FormItem>
|
/>
|
||||||
);
|
</FormControl>
|
||||||
}}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</div>
|
);
|
||||||
</div>
|
}}
|
||||||
{!!data?.externalPort && (
|
/>
|
||||||
<div className="grid w-full gap-8">
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
</div>
|
||||||
<Label>External Host</Label>
|
{!!data?.externalPort && (
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<div className="grid w-full gap-8">
|
||||||
</div>
|
<div className="flex flex-col gap-3">
|
||||||
</div>
|
<Label>External Host</Label>
|
||||||
)}
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,241 +8,244 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||||
const { data, refetch } = api.redis.one.useQuery(
|
const { data, refetch } = api.redis.one.useQuery(
|
||||||
{
|
{
|
||||||
redisId,
|
redisId,
|
||||||
},
|
},
|
||||||
{ enabled: !!redisId },
|
{ enabled: !!redisId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.redis.reload.useMutation();
|
api.redis.reload.useMutation();
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.redis.start.useMutation();
|
api.redis.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.redis.stop.useMutation();
|
api.redis.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.redis.deployWithLogs.useSubscription(
|
api.redis.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
redisId: redisId,
|
redisId: redisId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Redis"
|
title="Deploy Redis"
|
||||||
description="Are you sure you want to deploy this redis?"
|
description="Are you sure you want to deploy this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the Redis database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the Redis database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Redis"
|
title="Reload Redis"
|
||||||
description="Are you sure you want to reload this redis?"
|
description="Are you sure you want to reload this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
redisId: redisId,
|
redisId: redisId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Redis reloaded successfully");
|
toast.success("Redis reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Redis");
|
toast.error("Error reloading Redis");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the Redis service without rebuilding</p>
|
<p>Restart the Redis service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start Redis"
|
||||||
title="Start Redis"
|
description="Are you sure you want to start this redis?"
|
||||||
description="Are you sure you want to start this redis?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await start({
|
||||||
await start({
|
redisId: redisId,
|
||||||
redisId: redisId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Redis started successfully");
|
||||||
toast.success("Redis started successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error starting Redis");
|
||||||
toast.error("Error starting Redis");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the Redis database (requires a previous
|
||||||
Start the Redis database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Redis"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this redis?"
|
||||||
title="Stop Redis"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this redis?"
|
await stop({
|
||||||
onClick={async () => {
|
redisId: redisId,
|
||||||
await stop({
|
})
|
||||||
redisId: redisId,
|
.then(() => {
|
||||||
})
|
toast.success("Redis stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Redis stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Redis");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Redis");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running Redis database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running Redis database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the Redis container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
restoreBackupWithLogs: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
enabled: false,
|
||||||
|
path: "/restore-backup-with-logs",
|
||||||
|
method: "POST",
|
||||||
|
override: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
databaseId: z.string(),
|
||||||
|
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
|
||||||
|
databaseName: z.string().min(1),
|
||||||
|
backupFile: z.string().min(1),
|
||||||
|
destinationId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscription(async ({ input }) => {
|
||||||
|
const destination = await findDestinationById(input.destinationId);
|
||||||
|
if (input.databaseType === "postgres") {
|
||||||
|
const postgres = await findPostgresById(input.databaseId);
|
||||||
|
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restorePostgresBackup(
|
||||||
|
postgres,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mysql") {
|
||||||
|
const mysql = await findMySqlById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMySqlBackup(
|
||||||
|
mysql,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mariadb") {
|
||||||
|
const mariadb = await findMariadbById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMariadbBackup(
|
||||||
|
mariadb,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mongo") {
|
||||||
|
const mongo = await findMongoById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMongoBackup(
|
||||||
|
mongo,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// export const getAdminId = async (backupId: string) => {
|
|
||||||
// const backup = await findBackupById(backupId);
|
|
||||||
|
|
||||||
// if (backup.databaseType === "postgres" && backup.postgresId) {
|
|
||||||
// const postgres = await findPostgresById(backup.postgresId);
|
|
||||||
// return postgres.project.adminId;
|
|
||||||
// }
|
|
||||||
// if (backup.databaseType === "mariadb" && backup.mariadbId) {
|
|
||||||
// const mariadb = await findMariadbById(backup.mariadbId);
|
|
||||||
// return mariadb.project.adminId;
|
|
||||||
// }
|
|
||||||
// if (backup.databaseType === "mysql" && backup.mysqlId) {
|
|
||||||
// const mysql = await findMySqlById(backup.mysqlId);
|
|
||||||
// return mysql.project.adminId;
|
|
||||||
// }
|
|
||||||
// if (backup.databaseType === "mongo" && backup.mongoId) {
|
|
||||||
// const mongo = await findMongoById(backup.mongoId);
|
|
||||||
// return mongo.project.adminId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return null;
|
|
||||||
// };
|
|
||||||
|
|||||||
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