Compare commits

..

1 Commits

Author SHA1 Message Date
Mauricio Siu
da0e726326 feat(preview-deployment): enhance external deployment support
- Add support for external preview deployments with optional GitHub comment handling
- Modify deployment services to conditionally update GitHub issue comments
- Update queue types and deployment worker to handle external deployment flag
- Refactor preview deployment creation to support external deployments
- Improve preview deployment router with more flexible deployment creation logic
2025-03-08 17:07:07 -06:00
30 changed files with 989 additions and 7208 deletions

View File

@@ -132,8 +132,8 @@ export const ShowEnvironment = ({ id, type }: Props) => {
control={form.control}
name="environment"
render={({ field }) => (
<FormItem>
<FormControl className="">
<FormItem className="w-full">
<FormControl>
<CodeEditor
style={
{
@@ -142,14 +142,14 @@ export const ShowEnvironment = ({ id, type }: Props) => {
}
language="properties"
disabled={isEnvVisible}
className="font-mono"
wrapperClassName="compose-file-editor"
placeholder={`NODE_ENV=production
PORT=3000
`}
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}

View File

@@ -11,7 +11,6 @@ import { z } from "zod";
const addEnvironmentSchema = z.object({
env: z.string(),
buildArgs: z.string(),
buildSecrets: z.record(z.string(), z.string()),
});
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
@@ -37,7 +36,6 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
defaultValues: {
env: data?.env || "",
buildArgs: data?.buildArgs || "",
buildSecrets: data?.buildSecrets || {},
},
resolver: zodResolver(addEnvironmentSchema),
});
@@ -46,7 +44,6 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
mutateAsync({
env: data.env,
buildArgs: data.buildArgs,
buildSecrets: data.buildSecrets,
applicationId,
})
.then(async () => {
@@ -72,63 +69,25 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
/>
{data?.buildType === "dockerfile" && (
<>
<Secrets
name="buildArgs"
title="Build-time Variables"
description={
<span>
Available only at build-time. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/guide/build-args/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
<Secrets
name="buildSecrets"
title="Build Secrets"
description={
<span>
Secrets available only during build-time and not in the
final image. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/building/secrets/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="API_TOKEN=xyz"
transformValue={(value) => {
// Convert the string format to object
const lines = value.split("\n").filter((line) => line.trim());
return Object.fromEntries(
lines.map((line) => {
const [key, ...valueParts] = line.split("=");
return [key.trim(), valueParts.join("=").trim()];
}),
);
}}
formatValue={(value) => {
// Convert the object back to string format
return Object.entries(value as Record<string, string>)
.map(([key, val]) => `${key}=${val}`)
.join("\n");
}}
/>
</>
<Secrets
name="buildArgs"
title="Build-time Variables"
description={
<span>
Available only at build-time. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/guide/build-args/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
)}
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">

View File

@@ -4,22 +4,8 @@ import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
Hammer,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -55,188 +41,128 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Application"
description="Are you sure you want to deploy this application?"
type="default"
onClick={async () => {
await deploy({
applicationId: applicationId,
<DialogAction
title="Deploy Application"
description="Are you sure you want to deploy this application?"
type="default"
onClick={async () => {
await deploy({
applicationId: applicationId,
})
.then(() => {
toast.success("Application deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
);
})
.then(() => {
toast.success("Application deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error deploying application");
});
}}
.catch(() => {
toast.error("Error deploying application");
});
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Downloads the source code and performs a complete build
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
<DialogAction
title="Reload Application"
description="Are you sure you want to reload this application?"
type="default"
onClick={async () => {
await reload({
applicationId: applicationId,
appName: data?.appName || "",
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Application"
description="Are you sure you want to reload this application?"
type="default"
onClick={async () => {
await reload({
applicationId: applicationId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Application reloaded successfully");
refetch();
})
.then(() => {
toast.success("Application reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading application");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
<DialogAction
title="Rebuild Application"
description="Are you sure you want to rebuild this application?"
type="default"
onClick={async () => {
await redeploy({
applicationId: applicationId,
.catch(() => {
toast.error("Error reloading application");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
<DialogAction
title="Rebuild Application"
description="Are you sure you want to rebuild this application?"
type="default"
onClick={async () => {
await redeploy({
applicationId: applicationId,
})
.then(() => {
toast.success("Application rebuilt successfully");
refetch();
})
.then(() => {
toast.success("Application rebuilt successfully");
refetch();
})
.catch(() => {
toast.error("Error rebuilding application");
});
}}
.catch(() => {
toast.error("Error rebuilding application");
});
}}
>
<Button
variant="secondary"
isLoading={data?.applicationStatus === "running"}
>
<Button
variant="secondary"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Rebuild
<Hammer className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Only rebuilds the application without downloading new
code
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
Rebuild
<Hammer className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Application"
description="Are you sure you want to start this application?"
type="default"
onClick={async () => {
await start({
applicationId: applicationId,
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Application"
description="Are you sure you want to start this application?"
type="default"
onClick={async () => {
await start({
applicationId: applicationId,
})
.then(() => {
toast.success("Application started successfully");
refetch();
})
.then(() => {
toast.success("Application started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting application");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the application (requires a previous successful
build)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Application"
description="Are you sure you want to stop this application?"
onClick={async () => {
await stop({
applicationId: applicationId,
.catch(() => {
toast.error("Error starting application");
});
}}
>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Application"
description="Are you sure you want to stop this application?"
onClick={async () => {
await stop({
applicationId: applicationId,
})
.then(() => {
toast.success("Application stopped successfully");
refetch();
})
.then(() => {
toast.success("Application stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping application");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running application</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
.catch(() => {
toast.error("Error stopping application");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}

View File

@@ -1,15 +1,8 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -34,159 +27,103 @@ export const ComposeActions = ({ composeId }: Props) => {
api.compose.stop.useMutation();
return (
<div className="flex flex-row gap-4 w-full flex-wrap ">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Compose"
description="Are you sure you want to deploy this compose?"
type="default"
onClick={async () => {
await deploy({
composeId: composeId,
})
.then(() => {
toast.success("Compose deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error deploying compose");
});
}}
>
<Button variant="default" isLoading={data?.composeStatus === "running"}>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Rebuild Compose"
description="Are you sure you want to rebuild this compose?"
type="default"
onClick={async () => {
await redeploy({
composeId: composeId,
})
.then(() => {
toast.success("Compose rebuilt successfully");
refetch();
})
.catch(() => {
toast.error("Error rebuilding compose");
});
}}
>
<Button
variant="secondary"
isLoading={data?.composeStatus === "running"}
>
Rebuild
<Hammer className="size-4" />
</Button>
</DialogAction>
{data?.composeType === "docker-compose" &&
data?.composeStatus === "idle" ? (
<DialogAction
title="Deploy Compose"
description="Are you sure you want to deploy this compose?"
title="Start Compose"
description="Are you sure you want to start this compose?"
type="default"
onClick={async () => {
await deploy({
await start({
composeId: composeId,
})
.then(() => {
toast.success("Compose deployed successfully");
toast.success("Compose started successfully");
refetch();
router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error deploying compose");
toast.error("Error starting compose");
});
}}
>
<Button
variant="default"
isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads the source code and performs a complete build</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
) : (
<DialogAction
title="Rebuild Compose"
description="Are you sure you want to rebuild this compose?"
type="default"
title="Stop Compose"
description="Are you sure you want to stop this compose?"
onClick={async () => {
await redeploy({
await stop({
composeId: composeId,
})
.then(() => {
toast.success("Compose rebuilt successfully");
toast.success("Compose stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error rebuilding compose");
toast.error("Error stopping compose");
});
}}
>
<Button
variant="secondary"
isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5"
>
Rebuild
<Hammer className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Only rebuilds the compose without downloading new code</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
{data?.composeType === "docker-compose" &&
data?.composeStatus === "idle" ? (
<DialogAction
title="Start Compose"
description="Are you sure you want to start this compose?"
type="default"
onClick={async () => {
await start({
composeId: composeId,
})
.then(() => {
toast.success("Compose started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting compose");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the compose (requires a previous successful build)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Compose"
description="Are you sure you want to stop this compose?"
onClick={async () => {
await stop({
composeId: composeId,
})
.then(() => {
toast.success("Compose stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping compose");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running compose</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}

View File

@@ -2,21 +2,8 @@ import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -78,150 +65,92 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Mariadb"
description="Are you sure you want to deploy this mariadb?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
<DialogAction
title="Deploy Mariadb"
description="Are you sure you want to deploy this mariadb?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the MariaDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Mariadb"
description="Are you sure you want to reload this mariadb?"
type="default"
onClick={async () => {
await reload({
mariadbId: mariadbId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mariadb reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mariadb");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Reload Mariadb"
description="Are you sure you want to reload this mariadb?"
title="Start Mariadb"
description="Are you sure you want to start this mariadb?"
type="default"
onClick={async () => {
await reload({
await start({
mariadbId: mariadbId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mariadb reloaded successfully");
toast.success("Mariadb started successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mariadb");
toast.error("Error starting Mariadb");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the MariaDB service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Mariadb"
description="Are you sure you want to start this mariadb?"
type="default"
onClick={async () => {
await start({
mariadbId: mariadbId,
) : (
<DialogAction
title="Stop Mariadb"
description="Are you sure you want to stop this mariadb?"
onClick={async () => {
await stop({
mariadbId: mariadbId,
})
.then(() => {
toast.success("Mariadb stopped successfully");
refetch();
})
.then(() => {
toast.success("Mariadb started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mariadb");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the MariaDB database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Mariadb"
description="Are you sure you want to stop this mariadb?"
onClick={async () => {
await stop({
mariadbId: mariadbId,
})
.then(() => {
toast.success("Mariadb stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mariadb");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running MariaDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
.catch(() => {
toast.error("Error stopping Mariadb");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}

View File

@@ -2,21 +2,8 @@ import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -77,150 +64,93 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Mongo"
description="Are you sure you want to deploy this mongo?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
<DialogAction
title="Deploy Mongo"
description="Are you sure you want to deploy this mongo?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the MongoDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Mongo"
description="Are you sure you want to reload this mongo?"
type="default"
onClick={async () => {
await reload({
mongoId: mongoId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mongo reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mongo");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Reload Mongo"
description="Are you sure you want to reload this mongo?"
title="Start Mongo"
description="Are you sure you want to start this mongo?"
type="default"
onClick={async () => {
await reload({
await start({
mongoId: mongoId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mongo reloaded successfully");
toast.success("Mongo started successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mongo");
toast.error("Error starting Mongo");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the MongoDB service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Mongo"
description="Are you sure you want to start this mongo?"
type="default"
onClick={async () => {
await start({
mongoId: mongoId,
) : (
<DialogAction
title="Stop Mongo"
description="Are you sure you want to stop this mongo?"
type="default"
onClick={async () => {
await stop({
mongoId: mongoId,
})
.then(() => {
toast.success("Mongo stopped successfully");
refetch();
})
.then(() => {
toast.success("Mongo started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mongo");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the MongoDB database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Mongo"
description="Are you sure you want to stop this mongo?"
onClick={async () => {
await stop({
mongoId: mongoId,
})
.then(() => {
toast.success("Mongo stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mongo");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running MongoDB database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
.catch(() => {
toast.error("Error stopping Mongo");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}

View File

@@ -2,21 +2,8 @@ import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -75,150 +62,93 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Mysql"
description="Are you sure you want to deploy this mysql?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
<DialogAction
title="Deploy Mysql"
description="Are you sure you want to deploy this mysql?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the MySQL database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Mysql"
description="Are you sure you want to reload this mysql?"
type="default"
onClick={async () => {
await reload({
mysqlId: mysqlId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mysql reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mysql");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Reload Mysql"
description="Are you sure you want to reload this mysql?"
title="Start Mysql"
description="Are you sure you want to start this mysql?"
type="default"
onClick={async () => {
await reload({
await start({
mysqlId: mysqlId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mysql reloaded successfully");
toast.success("Mysql started successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mysql");
toast.error("Error starting Mysql");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the MySQL service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Mysql"
description="Are you sure you want to start this mysql?"
type="default"
onClick={async () => {
await start({
mysqlId: mysqlId,
) : (
<DialogAction
title="Stop Mysql"
description="Are you sure you want to stop this mysql?"
onClick={async () => {
await stop({
mysqlId: mysqlId,
})
.then(() => {
toast.success("Mysql stopped successfully");
refetch();
})
.then(() => {
toast.success("Mysql started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mysql");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the MySQL database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Mysql"
description="Are you sure you want to stop this mysql?"
onClick={async () => {
await stop({
mysqlId: mysqlId,
})
.then(() => {
toast.success("Mysql stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mysql");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running MySQL database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
.catch(() => {
toast.error("Error stopping Mysql");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}

View File

@@ -2,26 +2,12 @@ import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props {
postgresId: string;
}
@@ -71,179 +57,122 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Postgres"
description="Are you sure you want to deploy this postgres?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the PostgreSQL database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
<DialogAction
title="Reload Postgres"
description="Are you sure you want to reload this postgres?"
type="default"
onClick={async () => {
await reload({
postgresId: postgresId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Postgres reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Postgres");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the PostgreSQL service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Postgres"
description="Are you sure you want to start this postgres?"
type="default"
onClick={async () => {
await start({
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Postgres");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the PostgreSQL database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Postgres"
description="Are you sure you want to stop this postgres?"
onClick={async () => {
await stop({
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Postgres");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running PostgreSQL database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader className="pb-4">
<CardTitle className="text-xl">General</CardTitle>
</CardHeader>
<CardContent className="flex gap-4">
<DialogAction
title="Deploy Postgres"
description="Are you sure you want to deploy this postgres?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
<Button variant="outline">
<Terminal />
Open Terminal
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Postgres"
description="Are you sure you want to reload this postgres?"
type="default"
onClick={async () => {
await reload({
postgresId: postgresId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Postgres reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Postgres");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Postgres"
description="Are you sure you want to start this postgres?"
type="default"
onClick={async () => {
await start({
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Postgres");
});
}}
>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DockerTerminalModal>
</CardContent>
</Card>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>
</div>
</>
</DialogAction>
) : (
<DialogAction
title="Stop Postgres"
description="Are you sure you want to stop this postgres?"
onClick={async () => {
await stop({
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Postgres");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
</Button>
</DockerTerminalModal>
</CardContent>
</Card>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>
</div>
);
};

View File

@@ -2,26 +2,12 @@ import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props {
redisId: string;
}
@@ -77,150 +63,94 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Redis"
description="Are you sure you want to deploy this redis?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
<DialogAction
title="Deploy Redis"
description="Are you sure you want to deploy this redis?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Downloads and sets up the Redis database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Redis"
description="Are you sure you want to reload this redis?"
type="default"
onClick={async () => {
await reload({
redisId: redisId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Redis reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Redis");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
{/* <ResetRedis redisId={redisId} appName={data?.appName || ""} /> */}
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Reload Redis"
description="Are you sure you want to reload this redis?"
title="Start Redis"
description="Are you sure you want to start this redis?"
type="default"
onClick={async () => {
await reload({
await start({
redisId: redisId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Redis reloaded successfully");
toast.success("Redis started successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Redis");
toast.error("Error starting Redis");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Restart the Redis service without rebuilding</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Redis"
description="Are you sure you want to start this redis?"
type="default"
onClick={async () => {
await start({
redisId: redisId,
) : (
<DialogAction
title="Stop Redis"
description="Are you sure you want to stop this redis?"
onClick={async () => {
await stop({
redisId: redisId,
})
.then(() => {
toast.success("Redis stopped successfully");
refetch();
})
.then(() => {
toast.success("Redis started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Redis");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Start the Redis database (requires a previous
successful setup)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
) : (
<DialogAction
title="Stop Redis"
description="Are you sure you want to stop this redis?"
onClick={async () => {
await stop({
redisId: redisId,
})
.then(() => {
toast.success("Redis stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Redis");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Stop the currently running Redis database</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
.catch(() => {
toast.error("Error stopping Redis");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}

View File

@@ -1,2 +0,0 @@
ALTER TABLE "application" ADD COLUMN "buildSecrets" text;--> statement-breakpoint
ALTER TABLE "user_temp" DROP COLUMN "enableLogRotation";

File diff suppressed because it is too large Load Diff

View File

@@ -505,13 +505,6 @@
"when": 1741460060541,
"tag": "0071_flaky_black_queen",
"breakpoints": true
},
{
"idx": 72,
"version": "7",
"when": 1741481694393,
"tag": "0072_milky_lyja",
"breakpoints": true
}
]
}

View File

@@ -65,7 +65,6 @@ import {
PlusIcon,
Search,
X,
Trash2,
} from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -73,25 +72,9 @@ import type {
} from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { type ReactElement, useMemo, useState, useEffect } from "react";
import { type ReactElement, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export type Services = {
appName: string;
@@ -220,47 +203,10 @@ const Project = (
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
const { projectId } = props;
const { data: auth } = api.user.get.useQuery();
const [sortBy, setSortBy] = useState<string>(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("servicesSort") || "createdAt-desc";
}
return "createdAt-desc";
});
useEffect(() => {
localStorage.setItem("servicesSort", sortBy);
}, [sortBy]);
const sortServices = (services: Services[]) => {
const [field, direction] = sortBy.split("-");
return [...services].sort((a, b) => {
let comparison = 0;
switch (field) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "type":
comparison = a.type.localeCompare(b.type);
break;
case "createdAt":
comparison =
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
break;
default:
comparison = 0;
}
return direction === "asc" ? comparison : -comparison;
});
};
const { data, isLoading, refetch } = api.project.one.useQuery({ projectId });
const { data: allProjects } = api.project.all.useQuery();
const router = useRouter();
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
const [selectedTargetProject, setSelectedTargetProject] =
useState<string>("");
const emptyServices =
data?.mariadb?.length === 0 &&
data?.mongo?.length === 0 &&
@@ -308,38 +254,6 @@ const Project = (
const composeActions = {
start: api.compose.start.useMutation(),
stop: api.compose.stop.useMutation(),
move: api.compose.move.useMutation(),
delete: api.compose.delete.useMutation(),
};
const applicationActions = {
move: api.application.move.useMutation(),
delete: api.application.delete.useMutation(),
};
const postgresActions = {
move: api.postgres.move.useMutation(),
delete: api.postgres.remove.useMutation(),
};
const mysqlActions = {
move: api.mysql.move.useMutation(),
delete: api.mysql.remove.useMutation(),
};
const mariadbActions = {
move: api.mariadb.move.useMutation(),
delete: api.mariadb.remove.useMutation(),
};
const redisActions = {
move: api.redis.move.useMutation(),
delete: api.redis.remove.useMutation(),
};
const mongoActions = {
move: api.mongo.move.useMutation(),
delete: api.mongo.remove.useMutation(),
};
const handleBulkStart = async () => {
@@ -382,145 +296,9 @@ const Project = (
setIsBulkActionLoading(false);
};
const handleBulkMove = async () => {
if (!selectedTargetProject) {
toast.error("Please select a target project");
return;
}
let success = 0;
setIsBulkActionLoading(true);
for (const serviceId of selectedServices) {
try {
const service = filteredServices.find((s) => s.id === serviceId);
if (!service) continue;
switch (service.type) {
case "application":
await applicationActions.move.mutateAsync({
applicationId: serviceId,
targetProjectId: selectedTargetProject,
});
break;
case "compose":
await composeActions.move.mutateAsync({
composeId: serviceId,
targetProjectId: selectedTargetProject,
});
break;
case "postgres":
await postgresActions.move.mutateAsync({
postgresId: serviceId,
targetProjectId: selectedTargetProject,
});
break;
case "mysql":
await mysqlActions.move.mutateAsync({
mysqlId: serviceId,
targetProjectId: selectedTargetProject,
});
break;
case "mariadb":
await mariadbActions.move.mutateAsync({
mariadbId: serviceId,
targetProjectId: selectedTargetProject,
});
break;
case "redis":
await redisActions.move.mutateAsync({
redisId: serviceId,
targetProjectId: selectedTargetProject,
});
break;
case "mongo":
await mongoActions.move.mutateAsync({
mongoId: serviceId,
targetProjectId: selectedTargetProject,
});
break;
}
success++;
} catch (error) {
toast.error(
`Error moving service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
if (success > 0) {
toast.success(`${success} services moved successfully`);
refetch();
}
setSelectedServices([]);
setIsDropdownOpen(false);
setIsMoveDialogOpen(false);
setIsBulkActionLoading(false);
};
const handleBulkDelete = async () => {
let success = 0;
setIsBulkActionLoading(true);
for (const serviceId of selectedServices) {
try {
const service = filteredServices.find((s) => s.id === serviceId);
if (!service) continue;
switch (service.type) {
case "application":
await applicationActions.delete.mutateAsync({
applicationId: serviceId,
});
break;
case "compose":
await composeActions.delete.mutateAsync({
composeId: serviceId,
deleteVolumes: false,
});
break;
case "postgres":
await postgresActions.delete.mutateAsync({
postgresId: serviceId,
});
break;
case "mysql":
await mysqlActions.delete.mutateAsync({
mysqlId: serviceId,
});
break;
case "mariadb":
await mariadbActions.delete.mutateAsync({
mariadbId: serviceId,
});
break;
case "redis":
await redisActions.delete.mutateAsync({
redisId: serviceId,
});
break;
case "mongo":
await mongoActions.delete.mutateAsync({
mongoId: serviceId,
});
break;
}
success++;
} catch (error) {
toast.error(
`Error deleting service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
if (success > 0) {
toast.success(`${success} services deleted successfully`);
refetch();
}
setSelectedServices([]);
setIsDropdownOpen(false);
setIsBulkActionLoading(false);
};
const filteredServices = useMemo(() => {
if (!applications) return [];
const filtered = applications.filter(
return applications.filter(
(service) =>
(service.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
service.description
@@ -528,8 +306,7 @@ const Project = (
.includes(searchQuery.toLowerCase())) &&
(selectedTypes.length === 0 || selectedTypes.includes(service.type)),
);
return sortServices(filtered);
}, [applications, searchQuery, selectedTypes, sortBy]);
}, [applications, searchQuery, selectedTypes]);
return (
<div>
@@ -603,7 +380,7 @@ const Project = (
</div>
) : (
<>
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Checkbox
@@ -668,107 +445,11 @@ const Project = (
Stop
</Button>
</DialogAction>
{(auth?.role === "owner" ||
auth?.canDeleteServices) && (
<DialogAction
title="Delete Services"
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
type="destructive"
onClick={handleBulkDelete}
>
<Button
variant="ghost"
className="w-full justify-start text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
</DialogAction>
)}
<Dialog
open={isMoveDialogOpen}
onOpenChange={setIsMoveDialogOpen}
>
<DialogTrigger asChild>
<Button
variant="ghost"
className="w-full justify-start"
>
<FolderInput className="mr-2 h-4 w-4" />
Move
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Move Services</DialogTitle>
<DialogDescription>
Select the target project to move{" "}
{selectedServices.length} services
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
{allProjects?.filter(
(p) => p.projectId !== projectId,
).length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-4">
<FolderInput className="h-8 w-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground text-center">
No other projects available. Create a new
project first to move services.
</p>
</div>
) : (
<Select
value={selectedTargetProject}
onValueChange={setSelectedTargetProject}
>
<SelectTrigger>
<SelectValue placeholder="Select target project" />
</SelectTrigger>
<SelectContent>
{allProjects
?.filter(
(p) => p.projectId !== projectId,
)
.map((project) => (
<SelectItem
key={project.projectId}
value={project.projectId}
>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsMoveDialogOpen(false)}
>
Cancel
</Button>
<Button
onClick={handleBulkMove}
isLoading={isBulkActionLoading}
disabled={
allProjects?.filter(
(p) => p.projectId !== projectId,
).length === 0
}
>
Move Services
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex flex-col gap-2 lg:flex-row lg:gap-4 lg:items-center">
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4 sm:items-center">
<div className="w-full relative">
<Input
placeholder="Filter services..."
@@ -778,23 +459,6 @@ const Project = (
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="lg:w-[280px]">
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="createdAt-desc">
Newest first
</SelectItem>
<SelectItem value="createdAt-asc">
Oldest first
</SelectItem>
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
<SelectItem value="type-asc">Type (A-Z)</SelectItem>
<SelectItem value="type-desc">Type (Z-A)</SelectItem>
</SelectContent>
</Select>
<Popover
open={openCombobox}
onOpenChange={setOpenCombobox}

View File

@@ -298,7 +298,6 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, {
env: input.env,
buildArgs: input.buildArgs,
buildSecrets: input.buildSecrets,
});
return true;
}),
@@ -669,49 +668,4 @@ export const applicationRouter = createTRPCRouter({
return stats;
}),
move: protectedProcedure
.input(
z.object({
applicationId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this application",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the application's projectId
const updatedApplication = await db
.update(applications)
.set({
projectId: input.targetProjectId,
})
.where(eq(applications.applicationId, input.applicationId))
.returning()
.then((res) => res[0]);
if (!updatedApplication) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move application",
});
}
return updatedApplication;
}),
});

View File

@@ -8,7 +8,7 @@ import {
apiFindCompose,
apiRandomizeCompose,
apiUpdateCompose,
compose as composeTable,
compose,
} from "@/server/db/schema";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates";
@@ -24,7 +24,6 @@ import { dump } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { deploy } from "@/server/utils/deploy";
@@ -158,8 +157,8 @@ export const composeRouter = createTRPCRouter({
4;
const result = await db
.delete(composeTable)
.where(eq(composeTable.composeId, input.composeId))
.delete(compose)
.where(eq(compose.composeId, input.composeId))
.returning();
const cleanupOperations = [
@@ -502,48 +501,4 @@ export const composeRouter = createTRPCRouter({
const uniqueTags = _.uniq(allTags);
return uniqueTags;
}),
move: protectedProcedure
.input(
z.object({
composeId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this compose",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the compose's projectId
const updatedCompose = await db
.update(composeTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(composeTable.composeId, input.composeId))
.returning()
.then((res) => res[0]);
if (!updatedCompose) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move compose",
});
}
return updatedCompose;
}),
});

View File

@@ -8,7 +8,6 @@ import {
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
mariadb as mariadbTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -31,9 +30,6 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
@@ -326,47 +322,4 @@ export const mariadbRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
mariadbId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mariadb",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mariadb's projectId
const updatedMariadb = await db
.update(mariadbTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mariadbTable.mariadbId, input.mariadbId))
.returning()
.then((res) => res[0]);
if (!updatedMariadb) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mariadb",
});
}
return updatedMariadb;
}),
});

View File

@@ -8,7 +8,6 @@ import {
apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
mongo as mongoTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -31,9 +30,6 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
@@ -340,47 +336,4 @@ export const mongoRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
mongoId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mongo",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mongo's projectId
const updatedMongo = await db
.update(mongoTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mongoTable.mongoId, input.mongoId))
.returning()
.then((res) => res[0]);
if (!updatedMongo) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mongo",
});
}
return updatedMongo;
}),
});

View File

@@ -8,7 +8,6 @@ import {
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
mysql as mysqlTable,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -33,9 +32,6 @@ import {
updateMySqlById,
} from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const mysqlRouter = createTRPCRouter({
create: protectedProcedure
@@ -336,47 +332,4 @@ export const mysqlRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
mysqlId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mysql",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mysql's projectId
const updatedMysql = await db
.update(mysqlTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mysqlTable.mysqlId, input.mysqlId))
.returning()
.then((res) => res[0]);
if (!updatedMysql) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mysql",
});
}
return updatedMysql;
}),
});

View File

@@ -8,7 +8,6 @@ import {
apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
postgres as postgresTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -31,9 +30,6 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
@@ -356,49 +352,4 @@ export const postgresRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
postgresId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this postgres",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the postgres's projectId
const updatedPostgres = await db
.update(postgresTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(postgresTable.postgresId, input.postgresId))
.returning()
.then((res) => res[0]);
if (!updatedPostgres) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move postgres",
});
}
return updatedPostgres;
}),
});

View File

@@ -1,13 +1,23 @@
import { apiFindAllByApplication } from "@/server/db/schema";
import { db } from "@/server/db";
import { apiFindAllByApplication, applications } from "@/server/db/schema";
import {
createPreviewDeployment,
findApplicationById,
findPreviewDeploymentByApplicationId,
findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId,
findPreviewDeploymentsByPullRequestId,
IS_CLOUD,
removePreviewDeployment,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { eq } from "drizzle-orm";
import { and } from "drizzle-orm";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import type { DeploymentJob } from "@/server/queues/queue-types";
export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
}
return previewDeployment;
}),
create: protectedProcedure
.input(
z.object({
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
pullRequestId: z.string(),
repository: z.string(),
owner: z.string(),
branch: z.string(),
deploymentHash: z.string(),
prBranch: z.string(),
prNumber: z.any(),
prTitle: z.string(),
prURL: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const organizationId = ctx.session.activeOrganizationId;
const action = input.action;
const prId = input.pullRequestId;
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
(previewDeployment) =>
previewDeployment.application.project.organizationId ===
organizationId,
);
if (filteredPreviewDeploymentResult.length > 0) {
for (const previewDeployment of filteredPreviewDeploymentResult) {
try {
await removePreviewDeployment(
previewDeployment.previewDeploymentId,
);
} catch (error) {
console.log(error);
}
}
}
return {
message: "Preview Deployments Closed",
};
}
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const deploymentHash = input.deploymentHash;
const prBranch = input.prBranch;
const prNumber = input.prNumber;
const prTitle = input.prTitle;
const prURL = input.prURL;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, input.repository),
eq(applications.branch, input.branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, input.owner),
),
with: {
previewDeployments: true,
project: true,
},
});
const filteredApps = apps.filter(
(app) => app.project.organizationId === organizationId,
);
console.log(filteredApps);
for (const app of filteredApps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
try {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
console.log(previewDeployment);
previewDeploymentId = previewDeployment.previewDeploymentId;
} catch (error) {
console.log(error);
}
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
isExternal: true,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
}
return {
message: "Preview Deployments Created",
};
}),
});

View File

@@ -8,7 +8,6 @@ import {
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
redis as redisTable,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -31,9 +30,6 @@ import {
updateRedisById,
} from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
@@ -320,47 +316,4 @@ export const redisRouter = createTRPCRouter({
return true;
}),
move: protectedProcedure
.input(
z.object({
redisId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this redis",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the redis's projectId
const updatedRedis = await db
.update(redisTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(redisTable.redisId, input.redisId))
.returning()
.then((res) => res[0]);
if (!updatedRedis) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move redis",
});
}
return updatedRedis;
}),
});

View File

@@ -596,25 +596,17 @@ export const settingsRouter = createTRPCRouter({
}),
readStats: adminProcedure
.input(
z
.object({
dateRange: z
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
z.object({
start: z.string().optional(),
end: z.string().optional(),
}),
)
.query(({ input }) => {
if (IS_CLOUD) {
return [];
}
const rawConfig = readMonitoringConfig(
!!input?.dateRange?.start || !!input?.dateRange?.end,
);
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
const rawConfig = readMonitoringConfig(!!input?.start || !!input?.end);
const processedLogs = processLogs(rawConfig as string, input);
return processedLogs || [];
}),
haveActivateRequests: adminProcedure.query(async () => {

View File

@@ -98,6 +98,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
});
}
} else {
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
});
}
}

View File

@@ -26,6 +26,7 @@ type DeployJob =
applicationType: "application-preview";
previewDeploymentId: string;
serverId?: string;
isExternal?: boolean;
};
export type DeploymentJob = DeployJob;

View File

@@ -129,7 +129,6 @@ export const applications = pgTable("application", {
false,
),
buildArgs: text("buildArgs"),
buildSecrets: json("buildSecrets").$type<Record<string, string>>(),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
@@ -354,7 +353,6 @@ const createSchema = createInsertSchema(applications, {
autoDeploy: z.boolean(),
env: z.string().optional(),
buildArgs: z.string().optional(),
buildSecrets: z.record(z.string(), z.string()).optional(),
name: z.string().min(1),
description: z.string().optional(),
memoryReservation: z.string().optional(),
@@ -501,12 +499,11 @@ export const apiSaveGitProvider = createSchema
}),
);
export const apiSaveEnvironmentVariables = z
.object({
applicationId: z.string(),
env: z.string().optional(),
buildArgs: z.string().optional(),
buildSecrets: z.record(z.string(), z.string()).optional(),
export const apiSaveEnvironmentVariables = createSchema
.pick({
applicationId: true,
env: true,
buildArgs: true,
})
.required();

View File

@@ -149,7 +149,6 @@ table application {
previewLimit integer [default: 3]
isPreviewDeploymentsActive boolean [default: false]
buildArgs text
buildSecrets json
memoryReservation text
memoryLimit text
cpuReservation text

View File

@@ -387,11 +387,13 @@ export const deployPreviewApplication = async ({
titleLog = "Preview Deployment",
descriptionLog = "",
previewDeploymentId,
isExternal = false,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
isExternal?: boolean;
}) => {
const application = await findApplicationById(applicationId);
@@ -417,46 +419,43 @@ export const deployPreviewApplication = async ({
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
if (!isExternal) {
const commentExists = await issueCommentExists({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
application.buildArgs = application.previewBuildArgs;
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") {
await cloneGithubRepository({
...application,
@@ -466,25 +465,31 @@ export const deployPreviewApplication = async ({
});
await buildApplication(application, deployment.logPath);
}
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
if (!isExternal) {
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
if (!isExternal) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
@@ -500,11 +505,13 @@ export const deployRemotePreviewApplication = async ({
titleLog = "Preview Deployment",
descriptionLog = "",
previewDeploymentId,
isExternal = false,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
isExternal?: boolean;
}) => {
const application = await findApplicationById(applicationId);
@@ -530,36 +537,39 @@ export const deployRemotePreviewApplication = async ({
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
if (!isExternal) {
const commentExists = await issueCommentExists({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
application.buildArgs = application.previewBuildArgs;
@@ -586,25 +596,29 @@ export const deployRemotePreviewApplication = async ({
await mechanizeDockerContainer(application);
}
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
if (!isExternal) {
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
if (!isExternal) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",

View File

@@ -136,26 +136,24 @@ export const getContainersByAppNameMatch = async (
result = stdout.trim().split("\n");
}
const containers = result
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
});
return containers || [];
} catch (_error) {}
@@ -192,30 +190,28 @@ export const getStackContainersByAppName = async (
result = stdout.trim().split("\n");
}
const containers = result
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
});
return containers || [];
} catch (_error) {}
@@ -253,31 +249,29 @@ export const getServiceContainersByAppName = async (
result = stdout.trim().split("\n");
}
const containers = result
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
});
return containers || [];
} catch (_error) {}
@@ -312,25 +306,23 @@ export const getContainersByAppLabel = async (
const lines = stdout.trim().split("\n");
const containers = lines
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
const containers = lines.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
});
return containers || [];
} catch (_error) {}

View File

@@ -151,6 +151,7 @@ export const findPreviewDeploymentsByApplicationId = async (
export const createPreviewDeployment = async (
schema: typeof apiCreatePreviewDeployment._type,
isExternal = false,
) => {
const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`;
@@ -165,27 +166,32 @@ export const createPreviewDeployment = async (
org?.ownerId || "",
);
const octokit = authGithub(application?.github as Github);
let issueId = "";
if (!isExternal) {
const octokit = authGithub(application?.github as Github);
const runningComment = getIssueComment(
application.name,
"initializing",
generateDomain,
);
const runningComment = getIssueComment(
application.name,
"initializing",
generateDomain,
);
const issue = await octokit.rest.issues.createComment({
owner: application?.owner || "",
repo: application?.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
const issue = await octokit.rest.issues.createComment({
owner: application?.owner || "",
repo: application?.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
issueId = `${issue.data.id}`;
}
const previewDeployment = await db
.insert(previewDeployments)
.values({
...schema,
appName: appName,
pullRequestCommentId: `${issue.data.id}`,
pullRequestCommentId: issueId,
})
.returning()
.then((value) => value[0]);
@@ -231,6 +237,13 @@ export const findPreviewDeploymentsByPullRequestId = async (
) => {
const previewDeploymentResult = await db.query.previewDeployments.findMany({
where: eq(previewDeployments.pullRequestId, pullRequestId),
with: {
application: {
with: {
project: true,
},
},
},
});
return previewDeploymentResult;

View File

@@ -12,14 +12,8 @@ export const buildCustomDocker = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const {
appName,
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
} = application;
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
application;
const dockerFilePath = getBuildAppDirectory(application);
try {
const image = `${appName}`;
@@ -31,10 +25,6 @@ export const buildCustomDocker = async (
application.project.env,
);
const secrets = buildSecrets
? Object.entries(buildSecrets).map(([key, value]) => `${key}=${value}`)
: [];
const dockerContextPath = getDockerContextPath(application);
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
@@ -46,12 +36,6 @@ export const buildCustomDocker = async (
for (const arg of args) {
commandArgs.push("--build-arg", arg);
}
for (const secret of secrets) {
const [key] = secret.split("=");
commandArgs.push("--secret", `id=${key},env=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -70,10 +54,6 @@ export const buildCustomDocker = async (
},
{
cwd: dockerContextPath || defaultContextPath,
env: {
...process.env,
...Object.fromEntries(secrets.map((s) => s.split("="))),
},
},
);
} catch (error) {
@@ -85,14 +65,8 @@ export const getDockerCommand = (
application: ApplicationNested,
logPath: string,
) => {
const {
appName,
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
} = application;
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
application;
const dockerFilePath = getBuildAppDirectory(application);
try {
@@ -105,10 +79,6 @@ export const getDockerCommand = (
application.project.env,
);
const secrets = buildSecrets
? Object.entries(buildSecrets).map(([key, value]) => `${key}=${value}`)
: [];
const dockerContextPath =
getDockerContextPath(application) || defaultContextPath;
@@ -122,11 +92,6 @@ export const getDockerCommand = (
commandArgs.push("--build-arg", arg);
}
for (const secret of secrets) {
const [key] = secret.split("=");
commandArgs.push("--secret", `id=${key},env=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -140,14 +105,6 @@ export const getDockerCommand = (
);
}
// Export secrets as environment variables
if (secrets.length > 0) {
command += "\n# Export build secrets\n";
for (const secret of secrets) {
command += `export ${secret}\n`;
}
}
command += `
echo "Building ${appName}" >> ${logPath};
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {