mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Fix: Consistent Component Styling and Server URL
This commit is contained in:
@@ -10,289 +10,304 @@ 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";
|
||||||
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";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId }
|
||||||
);
|
);
|
||||||
const { mutateAsync: update } = api.application.update.useMutation();
|
const { mutateAsync: update } = api.application.update.useMutation();
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.application.start.useMutation();
|
api.application.start.useMutation();
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.application.stop.useMutation();
|
api.application.stop.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.application.reload.useMutation();
|
api.application.reload.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
|
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<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} 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?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deploy({
|
await deploy({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application deployed successfully");
|
toast.success("Application deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
router.push(
|
router.push(
|
||||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error deploying application");
|
toast.error("Error deploying application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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"
|
||||||
description="Are you sure you want to reload this application?"
|
description="Are you sure you want to reload this application?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application reloaded successfully");
|
toast.success("Application reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading application");
|
toast.error("Error reloading application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
<Tooltip>
|
||||||
Reload
|
<TooltipTrigger asChild>
|
||||||
<RefreshCcw className="size-4" />
|
<Button
|
||||||
</Button>
|
variant="secondary"
|
||||||
</DialogAction>
|
isLoading={isReloading}
|
||||||
<DialogAction
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
title="Rebuild Application"
|
>
|
||||||
description="Are you sure you want to rebuild this application?"
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
type="default"
|
Reload
|
||||||
onClick={async () => {
|
</Button>
|
||||||
await redeploy({
|
</TooltipTrigger>
|
||||||
applicationId: applicationId,
|
<TooltipPrimitive.Portal>
|
||||||
})
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
.then(() => {
|
<p>
|
||||||
toast.success("Application rebuilt successfully");
|
Reload the application when you change configuration or
|
||||||
refetch();
|
environment variables
|
||||||
})
|
</p>
|
||||||
.catch(() => {
|
</TooltipContent>
|
||||||
toast.error("Error rebuilding application");
|
</TooltipPrimitive.Portal>
|
||||||
});
|
</Tooltip>
|
||||||
}}
|
</DialogAction>
|
||||||
>
|
<DialogAction
|
||||||
<Button
|
title="Rebuild Application"
|
||||||
variant="secondary"
|
description="Are you sure you want to rebuild this application?"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
type="default"
|
||||||
className="flex items-center gap-1.5"
|
onClick={async () => {
|
||||||
>
|
await redeploy({
|
||||||
Rebuild
|
applicationId: applicationId,
|
||||||
<Hammer className="size-4" />
|
})
|
||||||
<Tooltip>
|
.then(() => {
|
||||||
<TooltipTrigger asChild>
|
toast.success("Application rebuilt successfully");
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
refetch();
|
||||||
</TooltipTrigger>
|
})
|
||||||
<TooltipPrimitive.Portal>
|
.catch(() => {
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
toast.error("Error rebuilding application");
|
||||||
<p>
|
});
|
||||||
Only rebuilds the application without downloading new
|
}}
|
||||||
code
|
>
|
||||||
</p>
|
<Tooltip>
|
||||||
</TooltipContent>
|
<TooltipTrigger asChild>
|
||||||
</TooltipPrimitive.Portal>
|
<Button
|
||||||
</Tooltip>
|
variant="secondary"
|
||||||
</Button>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
</DialogAction>
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Hammer className="size-4 mr-1" />
|
||||||
|
Rebuild
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Only rebuilds the application without downloading new code
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Application"
|
title="Start Application"
|
||||||
description="Are you sure you want to start this application?"
|
description="Are you sure you want to start this application?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await start({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application started successfully");
|
toast.success("Application started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting application");
|
toast.error("Error starting application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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
|
title="Stop Application"
|
||||||
title="Stop Application"
|
description="Are you sure you want to stop this application?"
|
||||||
description="Are you sure you want to stop this application?"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await stop({
|
||||||
await stop({
|
applicationId: applicationId,
|
||||||
applicationId: applicationId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Application stopped successfully");
|
||||||
toast.success("Application stopped successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error stopping application");
|
||||||
toast.error("Error stopping application");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="destructive"
|
<Button
|
||||||
isLoading={isStopping}
|
variant="destructive"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStopping}
|
||||||
>
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Stop
|
>
|
||||||
<Ban className="size-4" />
|
<Ban className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Stop
|
||||||
<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>Stop the currently running application</p>
|
||||||
<p>Stop the currently running application</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
)}
|
||||||
</DialogAction>
|
</TooltipProvider>
|
||||||
)}
|
<DockerTerminalModal
|
||||||
</TooltipProvider>
|
appName={data?.appName || ""}
|
||||||
<DockerTerminalModal
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
>
|
||||||
serverId={data?.serverId || ""}
|
<Button
|
||||||
>
|
variant="outline"
|
||||||
<Button variant="outline">
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Terminal />
|
>
|
||||||
Open Terminal
|
<Terminal className="size-4 mr-1" />
|
||||||
</Button>
|
Open Terminal
|
||||||
</DockerTerminalModal>
|
</Button>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
</DockerTerminalModal>
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
aria-label="Toggle italic"
|
<Switch
|
||||||
checked={data?.autoDeploy || false}
|
aria-label="Toggle autodeploy"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.autoDeploy || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
applicationId,
|
await update({
|
||||||
autoDeploy: enabled,
|
applicationId,
|
||||||
})
|
autoDeploy: enabled,
|
||||||
.then(async () => {
|
})
|
||||||
toast.success("Auto Deploy Updated");
|
.then(async () => {
|
||||||
await refetch();
|
toast.success("Auto Deploy Updated");
|
||||||
})
|
await refetch();
|
||||||
.catch(() => {
|
})
|
||||||
toast.error("Error updating Auto Deploy");
|
.catch(() => {
|
||||||
});
|
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({
|
||||||
applicationId,
|
applicationId,
|
||||||
cleanCache: enabled,
|
cleanCache: enabled,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Clean Cache Updated");
|
toast.success("Clean Cache Updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
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>
|
||||||
</Card>
|
</Card>
|
||||||
<ShowProviderForm applicationId={applicationId} />
|
<ShowProviderForm applicationId={applicationId} />
|
||||||
<ShowBuildChooseForm applicationId={applicationId} />
|
<ShowBuildChooseForm applicationId={applicationId} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,216 +7,219 @@ 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";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
export const ComposeActions = ({ composeId }: Props) => {
|
export const ComposeActions = ({ composeId }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
},
|
},
|
||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId }
|
||||||
);
|
);
|
||||||
const { mutateAsync: update } = api.compose.update.useMutation();
|
const { mutateAsync: update } = api.compose.update.useMutation();
|
||||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||||
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
|
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.compose.start.useMutation();
|
api.compose.start.useMutation();
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
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?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deploy({
|
await deploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose deployed successfully");
|
toast.success("Compose deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
router.push(
|
router.push(
|
||||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error deploying compose");
|
toast.error("Error deploying compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
</TooltipContent>
|
Reload the compose when you change configuration or
|
||||||
</TooltipPrimitive.Portal>
|
environment variables
|
||||||
</Tooltip>
|
</p>
|
||||||
</Button>
|
</TooltipContent>
|
||||||
</DialogAction>
|
</TooltipPrimitive.Portal>
|
||||||
{data?.composeType === "docker-compose" &&
|
</Tooltip>
|
||||||
data?.composeStatus === "idle" ? (
|
</DialogAction>
|
||||||
<DialogAction
|
{data?.composeType === "docker-compose" &&
|
||||||
title="Start Compose"
|
data?.composeStatus === "idle" ? (
|
||||||
description="Are you sure you want to start this compose?"
|
<DialogAction
|
||||||
type="default"
|
title="Start Compose"
|
||||||
onClick={async () => {
|
description="Are you sure you want to start this compose?"
|
||||||
await start({
|
type="default"
|
||||||
composeId: composeId,
|
onClick={async () => {
|
||||||
})
|
await start({
|
||||||
.then(() => {
|
composeId: composeId,
|
||||||
toast.success("Compose started successfully");
|
})
|
||||||
refetch();
|
.then(() => {
|
||||||
})
|
toast.success("Compose started successfully");
|
||||||
.catch(() => {
|
refetch();
|
||||||
toast.error("Error starting compose");
|
})
|
||||||
});
|
.catch(() => {
|
||||||
}}
|
toast.error("Error starting compose");
|
||||||
>
|
});
|
||||||
<Button
|
}}
|
||||||
variant="secondary"
|
>
|
||||||
isLoading={isStarting}
|
<Tooltip>
|
||||||
className="flex items-center gap-1.5"
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
Start
|
variant="secondary"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={isStarting}
|
||||||
<Tooltip>
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
</TooltipTrigger>
|
Start
|
||||||
<TooltipPrimitive.Portal>
|
</Button>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipTrigger>
|
||||||
<p>
|
<TooltipPrimitive.Portal>
|
||||||
Start the compose (requires a previous successful build)
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</p>
|
<p>
|
||||||
</TooltipContent>
|
Start the compose (requires a previous successful build)
|
||||||
</TooltipPrimitive.Portal>
|
</p>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
</Button>
|
</TooltipPrimitive.Portal>
|
||||||
</DialogAction>
|
</Tooltip>
|
||||||
) : (
|
</DialogAction>
|
||||||
<DialogAction
|
) : (
|
||||||
title="Stop Compose"
|
<DialogAction
|
||||||
description="Are you sure you want to stop this compose?"
|
title="Stop Compose"
|
||||||
onClick={async () => {
|
description="Are you sure you want to stop this compose?"
|
||||||
await stop({
|
onClick={async () => {
|
||||||
composeId: composeId,
|
await stop({
|
||||||
})
|
composeId: composeId,
|
||||||
.then(() => {
|
})
|
||||||
toast.success("Compose stopped successfully");
|
.then(() => {
|
||||||
refetch();
|
toast.success("Compose stopped successfully");
|
||||||
})
|
refetch();
|
||||||
.catch(() => {
|
})
|
||||||
toast.error("Error stopping compose");
|
.catch(() => {
|
||||||
});
|
toast.error("Error stopping compose");
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
<Button
|
>
|
||||||
variant="destructive"
|
<Tooltip>
|
||||||
isLoading={isStopping}
|
<TooltipTrigger asChild>
|
||||||
className="flex items-center gap-1.5"
|
<Button
|
||||||
>
|
variant="destructive"
|
||||||
Stop
|
isLoading={isStopping}
|
||||||
<Ban className="size-4" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Ban className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Stop
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Stop the currently running compose</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Stop the currently running compose</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
Open Terminal
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
</Button>
|
>
|
||||||
</DockerTerminalModal>
|
<Terminal className="size-4 mr-1" />
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
Open Terminal
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
</Button>
|
||||||
<Switch
|
</DockerTerminalModal>
|
||||||
aria-label="Toggle italic"
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
checked={data?.autoDeploy || false}
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
onCheckedChange={async (enabled) => {
|
<Switch
|
||||||
await update({
|
aria-label="Toggle autodeploy"
|
||||||
composeId,
|
checked={data?.autoDeploy || false}
|
||||||
autoDeploy: enabled,
|
onCheckedChange={async (enabled) => {
|
||||||
})
|
await update({
|
||||||
.then(async () => {
|
composeId,
|
||||||
toast.success("Auto Deploy Updated");
|
autoDeploy: enabled,
|
||||||
await refetch();
|
})
|
||||||
})
|
.then(async () => {
|
||||||
.catch(() => {
|
toast.success("Auto Deploy Updated");
|
||||||
toast.error("Error updating Auto Deploy");
|
await refetch();
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
className="flex flex-row gap-2 items-center"
|
toast.error("Error updating Auto Deploy");
|
||||||
/>
|
});
|
||||||
</div>
|
}}
|
||||||
</div>
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
);
|
/>
|
||||||
|
</div>
|
||||||
|
</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,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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,247 +3,244 @@ 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 {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||||
const { data, refetch } = api.postgres.one.useQuery(
|
const { data, refetch } = api.postgres.one.useQuery(
|
||||||
{
|
{
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
},
|
},
|
||||||
{ enabled: !!postgresId },
|
{ enabled: !!postgresId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.postgres.reload.useMutation();
|
api.postgres.reload.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.postgres.stop.useMutation();
|
api.postgres.stop.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.postgres.start.useMutation();
|
api.postgres.start.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.postgres.deployWithLogs.useSubscription(
|
api.postgres.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 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?"
|
||||||
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 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"
|
||||||
description="Are you sure you want to reload this postgres?"
|
description="Are you sure you want to reload this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Postgres reloaded successfully");
|
toast.success("Postgres reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Postgres");
|
toast.error("Error reloading Postgres");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
</TooltipContent>
|
Reload the PostgreSQL when you change configuration or
|
||||||
</TooltipPrimitive.Portal>
|
environment variables
|
||||||
</Tooltip>
|
</p>
|
||||||
</Button>
|
</TooltipContent>
|
||||||
</DialogAction>
|
</TooltipPrimitive.Portal>
|
||||||
{data?.applicationStatus === "idle" ? (
|
</Tooltip>
|
||||||
<DialogAction
|
</DialogAction>
|
||||||
title="Start Postgres"
|
{data?.applicationStatus === "idle" ? (
|
||||||
description="Are you sure you want to start this postgres?"
|
<DialogAction
|
||||||
type="default"
|
title="Start Postgres"
|
||||||
onClick={async () => {
|
description="Are you sure you want to start this postgres?"
|
||||||
await start({
|
type="default"
|
||||||
postgresId: postgresId,
|
onClick={async () => {
|
||||||
})
|
await start({
|
||||||
.then(() => {
|
postgresId: postgresId,
|
||||||
toast.success("Postgres started successfully");
|
})
|
||||||
refetch();
|
.then(() => {
|
||||||
})
|
toast.success("Postgres started successfully");
|
||||||
.catch(() => {
|
refetch();
|
||||||
toast.error("Error starting Postgres");
|
})
|
||||||
});
|
.catch(() => {
|
||||||
}}
|
toast.error("Error starting Postgres");
|
||||||
>
|
});
|
||||||
<Button
|
}}
|
||||||
variant="secondary"
|
>
|
||||||
isLoading={isStarting}
|
<Tooltip>
|
||||||
className="flex items-center gap-1.5"
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
Start
|
variant="secondary"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={isStarting}
|
||||||
<Tooltip>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
</TooltipTrigger>
|
Start
|
||||||
<TooltipPrimitive.Portal>
|
</Button>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipTrigger>
|
||||||
<p>
|
<TooltipPrimitive.Portal>
|
||||||
Start the PostgreSQL database (requires a previous
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
successful setup)
|
<p>
|
||||||
</p>
|
Start the PostgreSQL database (requires a previous
|
||||||
</TooltipContent>
|
successful setup)
|
||||||
</TooltipPrimitive.Portal>
|
</p>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
</Button>
|
</TooltipPrimitive.Portal>
|
||||||
</DialogAction>
|
</Tooltip>
|
||||||
) : (
|
</DialogAction>
|
||||||
<DialogAction
|
) : (
|
||||||
title="Stop Postgres"
|
<DialogAction
|
||||||
description="Are you sure you want to stop this postgres?"
|
title="Stop Postgres"
|
||||||
onClick={async () => {
|
description="Are you sure you want to stop this postgres?"
|
||||||
await stop({
|
onClick={async () => {
|
||||||
postgresId: postgresId,
|
await stop({
|
||||||
})
|
postgresId: postgresId,
|
||||||
.then(() => {
|
})
|
||||||
toast.success("Postgres stopped successfully");
|
.then(() => {
|
||||||
refetch();
|
toast.success("Postgres stopped successfully");
|
||||||
})
|
refetch();
|
||||||
.catch(() => {
|
})
|
||||||
toast.error("Error stopping Postgres");
|
.catch(() => {
|
||||||
});
|
toast.error("Error stopping Postgres");
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
<Button
|
>
|
||||||
variant="destructive"
|
<Tooltip>
|
||||||
isLoading={isStopping}
|
<TooltipTrigger asChild>
|
||||||
className="flex items-center gap-1.5"
|
<Button
|
||||||
>
|
variant="destructive"
|
||||||
Stop
|
isLoading={isStopping}
|
||||||
<Ban className="size-4" />
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Ban className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Stop
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Stop the currently running PostgreSQL database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Stop the currently running PostgreSQL database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
Open Terminal
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
</Button>
|
>
|
||||||
</DockerTerminalModal>
|
<Terminal className="size-4 mr-1" />
|
||||||
</CardContent>
|
Open Terminal
|
||||||
</Card>
|
</Button>
|
||||||
<DrawerLogs
|
</DockerTerminalModal>
|
||||||
isOpen={isDrawerOpen}
|
</CardContent>
|
||||||
onClose={() => {
|
</Card>
|
||||||
setIsDrawerOpen(false);
|
<DrawerLogs
|
||||||
setFilteredLogs([]);
|
isOpen={isDrawerOpen}
|
||||||
setIsDeploying(false);
|
onClose={() => {
|
||||||
refetch();
|
setIsDrawerOpen(false);
|
||||||
}}
|
setFilteredLogs([]);
|
||||||
filteredLogs={filteredLogs}
|
setIsDeploying(false);
|
||||||
/>
|
refetch();
|
||||||
</div>
|
}}
|
||||||
</>
|
filteredLogs={filteredLogs}
|
||||||
);
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user