Compare commits

..

1 Commits

Author SHA1 Message Date
Mauricio Siu
a6de744f91 fix(2fa): improve error handling and display for two-factor authentication setup
- Handle potential errors when enabling two-factor authentication
- Provide more detailed error messages for password verification
- Update client-side error handling to display specific error messages
- Add base URL configuration for non-cloud environments
2025-03-08 12:37:50 -06:00
44 changed files with 965 additions and 12847 deletions

View File

@@ -47,7 +47,7 @@ const baseAdmin: User = {
letsEncryptEmail: null, letsEncryptEmail: null,
sshPrivateKey: null, sshPrivateKey: null,
enableDockerCleanup: false, enableDockerCleanup: false,
logCleanupCron: null, enableLogRotation: false,
serversQuantity: 0, serversQuantity: 0,
stripeCustomerId: "", stripeCustomerId: "",
stripeSubscriptionId: "", stripeSubscriptionId: "",

View File

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

View File

@@ -11,7 +11,6 @@ import { z } from "zod";
const addEnvironmentSchema = z.object({ const addEnvironmentSchema = z.object({
env: z.string(), env: z.string(),
buildArgs: z.string(), buildArgs: z.string(),
buildSecrets: z.record(z.string(), z.string()),
}); });
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>; type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
@@ -37,7 +36,6 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
defaultValues: { defaultValues: {
env: data?.env || "", env: data?.env || "",
buildArgs: data?.buildArgs || "", buildArgs: data?.buildArgs || "",
buildSecrets: data?.buildSecrets || {},
}, },
resolver: zodResolver(addEnvironmentSchema), resolver: zodResolver(addEnvironmentSchema),
}); });
@@ -46,7 +44,6 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
mutateAsync({ mutateAsync({
env: data.env, env: data.env,
buildArgs: data.buildArgs, buildArgs: data.buildArgs,
buildSecrets: data.buildSecrets,
applicationId, applicationId,
}) })
.then(async () => { .then(async () => {
@@ -72,63 +69,25 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")} placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
/> />
{data?.buildType === "dockerfile" && ( {data?.buildType === "dockerfile" && (
<> <Secrets
<Secrets name="buildArgs"
name="buildArgs" title="Build-time Variables"
title="Build-time Variables" description={
description={ <span>
<span> Available only at build-time. See documentation&nbsp;
Available only at build-time. See documentation&nbsp; <a
<a className="text-primary"
className="text-primary" href="https://docs.docker.com/build/guide/build-args/"
href="https://docs.docker.com/build/guide/build-args/" target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" >
> here
here </a>
</a> .
. </span>
</span> }
} placeholder="NPM_TOKEN=xyz"
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");
}}
/>
</>
)} )}
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit"> <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 { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch"; 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 { api } from "@/utils/api";
import { import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
Ban,
CheckCircle2,
Hammer,
HelpCircle,
RefreshCcw,
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";
@@ -55,188 +41,128 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle> <CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap"> <CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}> <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(() => {
toast.success("Application deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
);
}) })
.then(() => { .catch(() => {
toast.success("Application deployed successfully"); toast.error("Error deploying application");
refetch(); });
router.push( }}
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`, >
); <Button
}) variant="default"
.catch(() => { isLoading={data?.applicationStatus === "running"}
toast.error("Error deploying application");
});
}}
> >
<Button Deploy
variant="default" </Button>
isLoading={data?.applicationStatus === "running"} </DialogAction>
className="flex items-center gap-1.5" <DialogAction
> title="Reload Application"
Deploy description="Are you sure you want to reload this application?"
<Tooltip> type="default"
<TooltipTrigger asChild> onClick={async () => {
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> await reload({
</TooltipTrigger> applicationId: applicationId,
<TooltipPrimitive.Portal> appName: data?.appName || "",
<TooltipContent sideOffset={5} className="z-[60]"> })
<p> .then(() => {
Downloads the source code and performs a complete build toast.success("Application reloaded successfully");
</p> refetch();
</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 || "",
}) })
.then(() => { .catch(() => {
toast.success("Application reloaded successfully"); toast.error("Error reloading application");
refetch(); });
}) }}
.catch(() => { >
toast.error("Error reloading application"); <Button variant="secondary" isLoading={isReloading}>
}); Reload
}} <RefreshCcw className="size-4" />
> </Button>
<Button variant="secondary" isLoading={isReloading}> </DialogAction>
Reload <DialogAction
<RefreshCcw className="size-4" /> title="Rebuild Application"
</Button> description="Are you sure you want to rebuild this application?"
</DialogAction> type="default"
<DialogAction onClick={async () => {
title="Rebuild Application" await redeploy({
description="Are you sure you want to rebuild this application?" applicationId: applicationId,
type="default" })
onClick={async () => { .then(() => {
await redeploy({ toast.success("Application rebuilt successfully");
applicationId: applicationId, refetch();
}) })
.then(() => { .catch(() => {
toast.success("Application rebuilt successfully"); toast.error("Error rebuilding application");
refetch(); });
}) }}
.catch(() => { >
toast.error("Error rebuilding application"); <Button
}); variant="secondary"
}} isLoading={data?.applicationStatus === "running"}
> >
<Button Rebuild
variant="secondary" <Hammer className="size-4" />
isLoading={data?.applicationStatus === "running"} </Button>
className="flex items-center gap-1.5" </DialogAction>
>
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>
{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(() => {
toast.success("Application started successfully");
refetch();
}) })
.then(() => { .catch(() => {
toast.success("Application started successfully"); toast.error("Error starting application");
refetch(); });
}) }}
.catch(() => { >
toast.error("Error starting application"); <Button variant="secondary" isLoading={isStarting}>
}); Start
}} <CheckCircle2 className="size-4" />
> </Button>
<Button </DialogAction>
variant="secondary" ) : (
isLoading={isStarting} <DialogAction
className="flex items-center gap-1.5" title="Stop Application"
> description="Are you sure you want to stop this application?"
Start onClick={async () => {
<CheckCircle2 className="size-4" /> await stop({
<Tooltip> applicationId: applicationId,
<TooltipTrigger asChild> })
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> .then(() => {
</TooltipTrigger> toast.success("Application stopped successfully");
<TooltipPrimitive.Portal> refetch();
<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,
}) })
.then(() => { .catch(() => {
toast.success("Application stopped successfully"); toast.error("Error stopping application");
refetch(); });
}) }}
.catch(() => { >
toast.error("Error stopping application"); <Button variant="destructive" isLoading={isStopping}>
}); Stop
}} <Ban className="size-4" />
> </Button>
<Button </DialogAction>
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>
<DockerTerminalModal <DockerTerminalModal
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}

View File

@@ -1,15 +1,8 @@
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; 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 { 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 { useRouter } from "next/router";
import { toast } from "sonner"; import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -34,159 +27,103 @@ export const ComposeActions = ({ composeId }: Props) => {
api.compose.stop.useMutation(); api.compose.stop.useMutation();
return ( return (
<div className="flex flex-row gap-4 w-full flex-wrap "> <div className="flex flex-row gap-4 w-full flex-wrap ">
<TooltipProvider delayDuration={0}> <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 <DialogAction
title="Deploy Compose" title="Start Compose"
description="Are you sure you want to deploy this compose?" description="Are you sure you want to start this compose?"
type="default" type="default"
onClick={async () => { onClick={async () => {
await deploy({ await start({
composeId: composeId, composeId: composeId,
}) })
.then(() => { .then(() => {
toast.success("Compose deployed successfully"); toast.success("Compose started successfully");
refetch(); refetch();
router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
);
}) })
.catch(() => { .catch(() => {
toast.error("Error deploying compose"); toast.error("Error starting compose");
}); });
}} }}
> >
<Button <Button variant="secondary" isLoading={isStarting}>
variant="default" Start
isLoading={data?.composeStatus === "running"} <CheckCircle2 className="size-4" />
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> </Button>
</DialogAction> </DialogAction>
) : (
<DialogAction <DialogAction
title="Rebuild Compose" title="Stop Compose"
description="Are you sure you want to rebuild this compose?" description="Are you sure you want to stop this compose?"
type="default"
onClick={async () => { onClick={async () => {
await redeploy({ await stop({
composeId: composeId, composeId: composeId,
}) })
.then(() => { .then(() => {
toast.success("Compose rebuilt successfully"); toast.success("Compose stopped successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error rebuilding compose"); toast.error("Error stopping compose");
}); });
}} }}
> >
<Button <Button variant="destructive" isLoading={isStopping}>
variant="secondary" Stop
isLoading={data?.composeStatus === "running"} <Ban className="size-4" />
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> </Button>
</DialogAction> </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 <DockerTerminalModal
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}

View File

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

View File

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

View File

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

View File

@@ -2,26 +2,12 @@ import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs"; 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 {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
Ban,
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;
} }
@@ -71,179 +57,122 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
); );
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 className="pb-4">
<CardHeader> <CardTitle className="text-xl">General</CardTitle>
<CardTitle className="text-xl">Deploy Settings</CardTitle> </CardHeader>
</CardHeader> <CardContent className="flex gap-4">
<CardContent className="flex flex-row gap-4 flex-wrap"> <DialogAction
<TooltipProvider delayDuration={0}> title="Deploy Postgres"
<DialogAction description="Are you sure you want to deploy this postgres?"
title="Deploy Postgres" type="default"
description="Are you sure you want to deploy this postgres?" onClick={async () => {
type="default" setIsDeploying(true);
onClick={async () => {
setIsDeploying(true); await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000)); refetch();
refetch(); }}
}} >
> <Button
<Button variant="default"
variant="default" isLoading={data?.applicationStatus === "running"}
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 || ""}
> >
<Button variant="outline"> Deploy
<Terminal /> </Button>
Open Terminal </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> </Button>
</DockerTerminalModal> </DialogAction>
</CardContent> ) : (
</Card> <DialogAction
<DrawerLogs title="Stop Postgres"
isOpen={isDrawerOpen} description="Are you sure you want to stop this postgres?"
onClose={() => { onClick={async () => {
setIsDrawerOpen(false); await stop({
setFilteredLogs([]); postgresId: postgresId,
setIsDeploying(false); })
refetch(); .then(() => {
}} toast.success("Postgres stopped successfully");
filteredLogs={filteredLogs} refetch();
/> })
</div> .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 { 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 {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
Ban,
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;
} }
@@ -77,150 +63,94 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle> <CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap"> <CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}> <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
variant="default"
isLoading={data?.applicationStatus === "running"}
> >
<Button Deploy
variant="default" </Button>
isLoading={data?.applicationStatus === "running"} </DialogAction>
className="flex items-center gap-1.5" <DialogAction
> title="Reload Redis"
Deploy description="Are you sure you want to reload this redis?"
<Tooltip> type="default"
<TooltipTrigger asChild> onClick={async () => {
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" /> await reload({
</TooltipTrigger> redisId: redisId,
<TooltipPrimitive.Portal> appName: data?.appName || "",
<TooltipContent sideOffset={5} className="z-[60]"> })
<p>Downloads and sets up the Redis database</p> .then(() => {
</TooltipContent> toast.success("Redis reloaded successfully");
</TooltipPrimitive.Portal> refetch();
</Tooltip> })
</Button> .catch(() => {
</DialogAction> 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 <DialogAction
title="Reload Redis" title="Start Redis"
description="Are you sure you want to reload this redis?" description="Are you sure you want to start this redis?"
type="default" type="default"
onClick={async () => { onClick={async () => {
await reload({ await start({
redisId: redisId, redisId: redisId,
appName: data?.appName || "",
}) })
.then(() => { .then(() => {
toast.success("Redis reloaded successfully"); toast.success("Redis started successfully");
refetch(); refetch();
}) })
.catch(() => { .catch(() => {
toast.error("Error reloading Redis"); toast.error("Error starting Redis");
}); });
}} }}
> >
<Button <Button variant="secondary" isLoading={isStarting}>
variant="secondary" Start
isLoading={isReloading} <CheckCircle2 className="size-4" />
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> </Button>
</DialogAction> </DialogAction>
{data?.applicationStatus === "idle" ? ( ) : (
<DialogAction <DialogAction
title="Start Redis" title="Stop Redis"
description="Are you sure you want to start this redis?" description="Are you sure you want to stop this redis?"
type="default" onClick={async () => {
onClick={async () => { await stop({
await start({ redisId: redisId,
redisId: redisId, })
.then(() => {
toast.success("Redis stopped successfully");
refetch();
}) })
.then(() => { .catch(() => {
toast.success("Redis started successfully"); toast.error("Error stopping Redis");
refetch(); });
}) }}
.catch(() => { >
toast.error("Error starting Redis"); <Button variant="destructive" isLoading={isStopping}>
}); Stop
}} <Ban className="size-4" />
> </Button>
<Button </DialogAction>
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>
<DockerTerminalModal <DockerTerminalModal
appName={data?.appName || ""} appName={data?.appName || ""}
serverId={data?.serverId || ""} serverId={data?.serverId || ""}

View File

@@ -1,10 +1,10 @@
import { api } from "@/utils/api";
import { import {
type ChartConfig, type ChartConfig,
ChartContainer, ChartContainer,
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from "@/components/ui/chart"; } from "@/components/ui/chart";
import { api } from "@/utils/api";
import { import {
Area, Area,
AreaChart, AreaChart,
@@ -14,13 +14,6 @@ import {
YAxis, YAxis,
} from "recharts"; } from "recharts";
export interface RequestDistributionChartProps {
dateRange?: {
from: Date | undefined;
to: Date | undefined;
};
}
const chartConfig = { const chartConfig = {
views: { views: {
label: "Page Views", label: "Page Views",
@@ -31,22 +24,10 @@ const chartConfig = {
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
export const RequestDistributionChart = ({ export const RequestDistributionChart = () => {
dateRange, const { data: stats } = api.settings.readStats.useQuery(undefined, {
}: RequestDistributionChartProps) => { refetchInterval: 1333,
const { data: stats } = api.settings.readStats.useQuery( });
{
dateRange: dateRange
? {
start: dateRange.from?.toISOString(),
end: dateRange.to?.toISOString(),
}
: undefined,
},
{
refetchInterval: 1333,
},
);
return ( return (
<ResponsiveContainer width="100%" height={200}> <ResponsiveContainer width="100%" height={200}>

View File

@@ -79,15 +79,7 @@ export const priorities = [
icon: Server, icon: Server,
}, },
]; ];
export const RequestsTable = () => {
export interface RequestsTableProps {
dateRange?: {
from: Date | undefined;
to: Date | undefined;
};
}
export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
const [statusFilter, setStatusFilter] = useState<string[]>([]); const [statusFilter, setStatusFilter] = useState<string[]>([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [selectedRow, setSelectedRow] = useState<LogEntry>(); const [selectedRow, setSelectedRow] = useState<LogEntry>();
@@ -106,12 +98,6 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
page: pagination, page: pagination,
search, search,
status: statusFilter, status: statusFilter,
dateRange: dateRange
? {
start: dateRange.from?.toISOString(),
end: dateRange.to?.toISOString(),
}
: undefined,
}, },
{ {
refetchInterval: 1333, refetchInterval: 1333,

View File

@@ -1,7 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { import {
Card, Card,
CardContent, CardContent,
@@ -9,29 +8,9 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { type RouterOutputs, api } from "@/utils/api"; import { type RouterOutputs, api } from "@/utils/api";
import { format } from "date-fns"; import { ArrowDownUp } from "lucide-react";
import {
ArrowDownUp,
AlertCircle,
InfoIcon,
Calendar as CalendarIcon,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState, useEffect } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { RequestDistributionChart } from "./request-distribution-chart"; import { RequestDistributionChart } from "./request-distribution-chart";
import { RequestsTable } from "./requests-table"; import { RequestsTable } from "./requests-table";
@@ -41,30 +20,17 @@ export type LogEntry = NonNullable<
>[0]; >[0];
export const ShowRequests = () => { export const ShowRequests = () => {
const { data: isLogRotateActive, refetch: refetchLogRotate } =
api.settings.getLogRotateStatus.useQuery();
const { mutateAsync: toggleLogRotate } =
api.settings.toggleLogRotate.useMutation();
const { data: isActive, refetch } = const { data: isActive, refetch } =
api.settings.haveActivateRequests.useQuery(); api.settings.haveActivateRequests.useQuery();
const { mutateAsync: toggleRequests } = const { mutateAsync: toggleRequests } =
api.settings.toggleRequests.useMutation(); api.settings.toggleRequests.useMutation();
const { data: logCleanupStatus } =
api.settings.getLogCleanupStatus.useQuery();
const { mutateAsync: updateLogCleanup } =
api.settings.updateLogCleanup.useMutation();
const [cronExpression, setCronExpression] = useState<string | null>(null);
const [dateRange, setDateRange] = useState<{
from: Date | undefined;
to: Date | undefined;
}>({
from: undefined,
to: undefined,
});
useEffect(() => {
if (logCleanupStatus) {
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
}
}, [logCleanupStatus]);
return ( return (
<> <>
<div className="w-full"> <div className="w-full">
@@ -91,60 +57,7 @@ export const ShowRequests = () => {
</AlertBlock> </AlertBlock>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-8 border-t"> <CardContent className="space-y-2 py-8 border-t">
<div className="flex w-full gap-4 justify-end items-center"> <div className="flex w-full gap-4 justify-end">
<div className="flex-1 flex items-center gap-4">
<div className="flex items-center gap-2">
<Label htmlFor="cron" className="min-w-32">
Log Cleanup Schedule
</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-80">
At the scheduled time, the cleanup job will keep
only the last 1000 entries in the access log file
and signal Traefik to reopen its log files. The
default schedule is daily at midnight (0 0 * * *).
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex-1 flex gap-4">
<Input
id="cron"
placeholder="0 0 * * *"
value={cronExpression || ""}
onChange={(e) => setCronExpression(e.target.value)}
className="max-w-60"
required
/>
<Button
variant="outline"
onClick={async () => {
if (!cronExpression?.trim()) {
toast.error("Please enter a valid cron expression");
return;
}
try {
await updateLogCleanup({
cronExpression: cronExpression,
});
toast.success("Log cleanup schedule updated");
} catch (error) {
toast.error(
`Failed to update log cleanup schedule: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}}
>
Update Schedule
</Button>
</div>
</div>
<DialogAction <DialogAction
title={isActive ? "Deactivate Requests" : "Activate Requests"} title={isActive ? "Deactivate Requests" : "Activate Requests"}
description="You will also need to restart Traefik to apply the changes" description="You will also need to restart Traefik to apply the changes"
@@ -164,81 +77,53 @@ export const ShowRequests = () => {
> >
<Button>{isActive ? "Deactivate" : "Activate"}</Button> <Button>{isActive ? "Deactivate" : "Activate"}</Button>
</DialogAction> </DialogAction>
<DialogAction
title={
isLogRotateActive
? "Activate Log Rotate"
: "Deactivate Log Rotate"
}
description={
isLogRotateActive
? "This will make the logs rotate on interval 1 day and maximum size of 100 MB and maximum 6 logs"
: "The log rotation will be disabled"
}
onClick={() => {
toggleLogRotate({
enable: !isLogRotateActive,
})
.then(() => {
toast.success(
`Log rotate ${isLogRotateActive ? "activated" : "deactivated"}`,
);
refetchLogRotate();
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Button variant="secondary">
{isLogRotateActive
? "Activate Log Rotate"
: "Deactivate Log Rotate"}
</Button>
</DialogAction>
</div> </div>
{isActive ? ( <div>
<> {isActive ? (
<div className="flex justify-end mb-4 gap-2"> <RequestDistributionChart />
{(dateRange.from || dateRange.to) && ( ) : (
<Button <div className="flex items-center justify-center min-h-[25vh]">
variant="outline" <span className="text-muted-foreground py-6">
onClick={() => You need to activate requests
setDateRange({ from: undefined, to: undefined }) </span>
}
className="px-3"
>
Clear dates
</Button>
)}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-[300px] justify-start text-left font-normal"
>
<CalendarIcon className="mr-2 h-4 w-4" />
{dateRange.from ? (
dateRange.to ? (
<>
{format(dateRange.from, "LLL dd, y")} -{" "}
{format(dateRange.to, "LLL dd, y")}
</>
) : (
format(dateRange.from, "LLL dd, y")
)
) : (
<span>Pick a date range</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange.from}
selected={{
from: dateRange.from,
to: dateRange.to,
}}
onSelect={(range) => {
setDateRange({
from: range?.from,
to: range?.to,
});
}}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
</div> </div>
<RequestDistributionChart dateRange={dateRange} /> )}
<RequestsTable dateRange={dateRange} /> {isActive && <RequestsTable />}
</> </div>
) : (
<div className="flex flex-col items-center justify-center py-12 gap-4 text-muted-foreground">
<AlertCircle className="size-12 text-muted-foreground/50" />
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">
Requests are not activated
</h3>
<p className="text-sm max-w-md">
Activate requests to see incoming traffic statistics and
monitor your application's usage. After activation, you'll
need to reload Traefik for the changes to take effect.
</p>
</div>
</div>
)}
</CardContent> </CardContent>
</div> </div>
</Card> </Card>

View File

@@ -64,12 +64,12 @@ export const Enable2FA = () => {
const handlePasswordSubmit = async (formData: PasswordForm) => { const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true); setIsPasswordLoading(true);
try { try {
const { data: enableData } = await authClient.twoFactor.enable({ const { data: enableData, error } = await authClient.twoFactor.enable({
password: formData.password, password: formData.password,
}); });
if (!enableData) { if (!enableData) {
throw new Error("No data received from server"); throw new Error(error?.message || "Unknown error");
} }
if (enableData.backupCodes) { if (enableData.backupCodes) {
@@ -95,7 +95,7 @@ export const Enable2FA = () => {
error instanceof Error ? error.message : "Error setting up 2FA", error instanceof Error ? error.message : "Error setting up 2FA",
); );
passwordForm.setError("password", { passwordForm.setError("password", {
message: "Error verifying password", message: `Error verifying password: ${error instanceof Error ? error.message : "Unknown error"}`,
}); });
} finally { } finally {
setIsPasswordLoading(false); setIsPasswordLoading(false);

View File

@@ -5,12 +5,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index"; import type { IUpdateData } from "@dokploy/server/index";
import { import {
@@ -30,17 +24,9 @@ import { UpdateWebServer } from "./update-webserver";
interface Props { interface Props {
updateData?: IUpdateData; updateData?: IUpdateData;
children?: React.ReactNode;
isOpen?: boolean;
onOpenChange?: (open: boolean) => void;
} }
export const UpdateServer = ({ export const UpdateServer = ({ updateData }: Props) => {
updateData,
children,
isOpen: isOpenProp,
onOpenChange: onOpenChangeProp,
}: Props) => {
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData); const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
const [isUpdateAvailable, setIsUpdateAvailable] = useState( const [isUpdateAvailable, setIsUpdateAvailable] = useState(
!!updateData?.updateAvailable, !!updateData?.updateAvailable,
@@ -49,10 +35,10 @@ export const UpdateServer = ({
api.settings.getUpdateData.useMutation(); api.settings.getUpdateData.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: releaseTag } = api.settings.getReleaseTag.useQuery(); const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
const [isOpen, setIsOpen] = useState(false);
const [latestVersion, setLatestVersion] = useState( const [latestVersion, setLatestVersion] = useState(
updateData?.latestVersion ?? "", updateData?.latestVersion ?? "",
); );
const [isOpenInternal, setIsOpenInternal] = useState(false);
const handleCheckUpdates = async () => { const handleCheckUpdates = async () => {
try { try {
@@ -79,52 +65,28 @@ export const UpdateServer = ({
} }
}; };
const isOpen = isOpenInternal || isOpenProp;
const onOpenChange = (open: boolean) => {
setIsOpenInternal(open);
onOpenChangeProp?.(open);
};
return ( return (
<Dialog open={isOpen} onOpenChange={onOpenChange}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{children ? ( <Button
children variant={updateData ? "outline" : "secondary"}
) : ( className="gap-2"
<TooltipProvider delayDuration={0}> >
<Tooltip> {updateData ? (
<TooltipTrigger asChild> <>
<Button <span className="flex h-2 w-2">
variant={updateData ? "outline" : "secondary"} <span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
size="sm" <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
onClick={() => onOpenChange?.(true)} </span>
> Update available
<Download className="h-4 w-4 flex-shrink-0" /> </>
{updateData ? ( ) : (
<span className="font-medium truncate group-data-[collapsible=icon]:hidden"> <>
Update Available <Sparkles className="h-4 w-4" />
</span> Updates
) : ( </>
<span className="font-medium truncate group-data-[collapsible=icon]:hidden"> )}
Check for updates </Button>
</span>
)}
{updateData && (
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
)}
</Button>
</TooltipTrigger>
{updateData && (
<TooltipContent side="right" sideOffset={10}>
<p>Update Available</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-lg p-6"> <DialogContent className="max-w-lg p-6">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
@@ -255,7 +217,7 @@ export const UpdateServer = ({
<div className="space-y-4 flex items-center justify-end"> <div className="space-y-4 flex items-center justify-end">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={() => onOpenChange?.(false)}> <Button variant="outline" onClick={() => setIsOpen(false)}>
Cancel Cancel
</Button> </Button>
{isUpdateAvailable ? ( {isUpdateAvailable ? (

View File

@@ -37,6 +37,8 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { import {
Collapsible, Collapsible,
@@ -1015,16 +1017,18 @@ export default function Page({ children }: Props) {
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
{!isCloud && auth?.role === "owner" && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<UpdateServerButton />
</SidebarMenuButton>
</SidebarMenuItem>
)}
</SidebarMenu> </SidebarMenu>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<SidebarMenu className="flex flex-col gap-2"> <SidebarMenu>
{!isCloud && auth?.role === "owner" && (
<SidebarMenuItem>
<UpdateServerButton />
</SidebarMenuItem>
)}
<SidebarMenuItem> <SidebarMenuItem>
<UserNav /> <UserNav />
</SidebarMenuItem> </SidebarMenuItem>

View File

@@ -3,14 +3,7 @@ import type { IUpdateData } from "@dokploy/server/index";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import UpdateServer from "../dashboard/settings/web-server/update-server"; import UpdateServer from "../dashboard/settings/web-server/update-server";
import { Button } from "../ui/button";
import { Download } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const UpdateServerButton = () => { export const UpdateServerButton = () => {
@@ -22,7 +15,6 @@ export const UpdateServerButton = () => {
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: getUpdateData } = const { mutateAsync: getUpdateData } =
api.settings.getUpdateData.useMutation(); api.settings.getUpdateData.useMutation();
const [isOpen, setIsOpen] = useState(false);
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null); const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
@@ -77,47 +69,11 @@ export const UpdateServerButton = () => {
}; };
}, []); }, []);
return updateData.updateAvailable ? ( return (
<div className="border-t pt-4"> updateData.updateAvailable && (
<UpdateServer <div>
updateData={updateData} <UpdateServer updateData={updateData} />
isOpen={isOpen} </div>
onOpenChange={setIsOpen} )
> );
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={updateData ? "outline" : "secondary"}
className="w-full"
onClick={() => setIsOpen(true)}
>
<Download className="h-4 w-4 flex-shrink-0" />
{updateData ? (
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
Update Available
</span>
) : (
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
Check for updates
</span>
)}
{updateData && (
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
)}
</Button>
</TooltipTrigger>
{updateData && (
<TooltipContent side="right" sideOffset={10}>
<p>Update Available</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</UpdateServer>
</div>
) : null;
}; };

View File

@@ -1,68 +0,0 @@
import type * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
),
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };

View File

@@ -1 +0,0 @@
ALTER TABLE "user_temp" ADD COLUMN "logCleanupCron" text;

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

File diff suppressed because it is too large Load Diff

View File

@@ -498,20 +498,6 @@
"when": 1741322697251, "when": 1741322697251,
"tag": "0070_useful_serpent_society", "tag": "0070_useful_serpent_society",
"breakpoints": true "breakpoints": true
},
{
"idx": 71,
"version": "7",
"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, PlusIcon,
Search, Search,
X, X,
Trash2,
} from "lucide-react"; } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
@@ -73,25 +72,9 @@ import type {
} from "next"; } from "next";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; 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 { toast } from "sonner";
import superjson from "superjson"; 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 = { export type Services = {
appName: string; appName: string;
@@ -220,47 +203,10 @@ const Project = (
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
const { projectId } = props; const { projectId } = props;
const { data: auth } = api.user.get.useQuery(); 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, isLoading, refetch } = api.project.one.useQuery({ projectId });
const { data: allProjects } = api.project.all.useQuery();
const router = useRouter(); const router = useRouter();
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
const [selectedTargetProject, setSelectedTargetProject] =
useState<string>("");
const emptyServices = const emptyServices =
data?.mariadb?.length === 0 && data?.mariadb?.length === 0 &&
data?.mongo?.length === 0 && data?.mongo?.length === 0 &&
@@ -308,38 +254,6 @@ const Project = (
const composeActions = { const composeActions = {
start: api.compose.start.useMutation(), start: api.compose.start.useMutation(),
stop: api.compose.stop.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 () => { const handleBulkStart = async () => {
@@ -382,145 +296,9 @@ const Project = (
setIsBulkActionLoading(false); 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(() => { const filteredServices = useMemo(() => {
if (!applications) return []; if (!applications) return [];
const filtered = applications.filter( return applications.filter(
(service) => (service) =>
(service.name.toLowerCase().includes(searchQuery.toLowerCase()) || (service.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
service.description service.description
@@ -528,8 +306,7 @@ const Project = (
.includes(searchQuery.toLowerCase())) && .includes(searchQuery.toLowerCase())) &&
(selectedTypes.length === 0 || selectedTypes.includes(service.type)), (selectedTypes.length === 0 || selectedTypes.includes(service.type)),
); );
return sortServices(filtered); }, [applications, searchQuery, selectedTypes]);
}, [applications, searchQuery, selectedTypes, sortBy]);
return ( return (
<div> <div>
@@ -603,7 +380,7 @@ const Project = (
</div> </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-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
@@ -668,107 +445,11 @@ const Project = (
Stop Stop
</Button> </Button>
</DialogAction> </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> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </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"> <div className="w-full relative">
<Input <Input
placeholder="Filter services..." 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" /> <Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div> </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 <Popover
open={openCombobox} open={openCombobox}
onOpenChange={setOpenCombobox} onOpenChange={setOpenCombobox}

View File

@@ -298,7 +298,6 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, { await updateApplication(input.applicationId, {
env: input.env, env: input.env,
buildArgs: input.buildArgs, buildArgs: input.buildArgs,
buildSecrets: input.buildSecrets,
}); });
return true; return true;
}), }),
@@ -669,49 +668,4 @@ export const applicationRouter = createTRPCRouter({
return stats; 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, apiFindCompose,
apiRandomizeCompose, apiRandomizeCompose,
apiUpdateCompose, apiUpdateCompose,
compose as composeTable, compose,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup"; import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates"; import { templates } from "@/templates/templates";
@@ -24,7 +24,6 @@ import { dump } from "js-yaml";
import _ from "lodash"; import _ from "lodash";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
import type { DeploymentJob } from "@/server/queues/queue-types"; import type { DeploymentJob } from "@/server/queues/queue-types";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
@@ -158,8 +157,8 @@ export const composeRouter = createTRPCRouter({
4; 4;
const result = await db const result = await db
.delete(composeTable) .delete(compose)
.where(eq(composeTable.composeId, input.composeId)) .where(eq(compose.composeId, input.composeId))
.returning(); .returning();
const cleanupOperations = [ const cleanupOperations = [
@@ -502,48 +501,4 @@ export const composeRouter = createTRPCRouter({
const uniqueTags = _.uniq(allTags); const uniqueTags = _.uniq(allTags);
return uniqueTags; 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, apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB, apiSaveExternalPortMariaDB,
apiUpdateMariaDB, apiUpdateMariaDB,
mariadb as mariadbTable,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup"; import { cancelJobs } from "@/server/utils/backup";
import { import {
@@ -31,9 +30,6 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const mariadbRouter = createTRPCRouter({ export const mariadbRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -326,47 +322,4 @@ export const mariadbRouter = createTRPCRouter({
return true; 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, apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo, apiSaveExternalPortMongo,
apiUpdateMongo, apiUpdateMongo,
mongo as mongoTable,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup"; import { cancelJobs } from "@/server/utils/backup";
import { import {
@@ -31,9 +30,6 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const mongoRouter = createTRPCRouter({ export const mongoRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -340,47 +336,4 @@ export const mongoRouter = createTRPCRouter({
return true; 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, apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql, apiSaveExternalPortMySql,
apiUpdateMySql, apiUpdateMySql,
mysql as mysqlTable,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -33,9 +32,6 @@ import {
updateMySqlById, updateMySqlById,
} from "@dokploy/server"; } from "@dokploy/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const mysqlRouter = createTRPCRouter({ export const mysqlRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -336,47 +332,4 @@ export const mysqlRouter = createTRPCRouter({
return true; 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, apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres, apiSaveExternalPortPostgres,
apiUpdatePostgres, apiUpdatePostgres,
postgres as postgresTable,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup"; import { cancelJobs } from "@/server/utils/backup";
import { import {
@@ -31,9 +30,6 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const postgresRouter = createTRPCRouter({ export const postgresRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -356,49 +352,4 @@ export const postgresRouter = createTRPCRouter({
return true; 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

@@ -8,7 +8,6 @@ import {
apiSaveEnvironmentVariablesRedis, apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis, apiSaveExternalPortRedis,
apiUpdateRedis, apiUpdateRedis,
redis as redisTable,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -31,9 +30,6 @@ import {
updateRedisById, updateRedisById,
} from "@dokploy/server"; } from "@dokploy/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const redisRouter = createTRPCRouter({ export const redisRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -320,47 +316,4 @@ export const redisRouter = createTRPCRouter({
return true; 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

@@ -28,6 +28,7 @@ import {
getDokployImageTag, getDokployImageTag,
getUpdateData, getUpdateData,
initializeTraefik, initializeTraefik,
logRotationManager,
parseRawConfig, parseRawConfig,
paths, paths,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -52,9 +53,6 @@ import {
writeConfig, writeConfig,
writeMainConfig, writeMainConfig,
writeTraefikConfigInPath, writeTraefikConfigInPath,
startLogCleanup,
stopLogCleanup,
getLogCleanupStatus,
} from "@dokploy/server"; } from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
@@ -579,43 +577,48 @@ export const settingsRouter = createTRPCRouter({
totalCount: 0, totalCount: 0,
}; };
} }
const rawConfig = readMonitoringConfig( const rawConfig = readMonitoringConfig();
!!input.dateRange?.start && !!input.dateRange?.end,
);
const parsedConfig = parseRawConfig( const parsedConfig = parseRawConfig(
rawConfig as string, rawConfig as string,
input.page, input.page,
input.sort, input.sort,
input.search, input.search,
input.status, input.status,
input.dateRange,
); );
return parsedConfig; return parsedConfig;
}), }),
readStats: adminProcedure readStats: adminProcedure.query(() => {
if (IS_CLOUD) {
return [];
}
const rawConfig = readMonitoringConfig();
const processedLogs = processLogs(rawConfig as string);
return processedLogs || [];
}),
getLogRotateStatus: adminProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
return await logRotationManager.getStatus();
}),
toggleLogRotate: adminProcedure
.input( .input(
z z.object({
.object({ enable: z.boolean(),
dateRange: z }),
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
) )
.query(({ input }) => { .mutation(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return []; return true;
} }
const rawConfig = readMonitoringConfig( if (input.enable) {
!!input?.dateRange?.start || !!input?.dateRange?.end, await logRotationManager.activate();
); } else {
const processedLogs = processLogs(rawConfig as string, input?.dateRange); await logRotationManager.deactivate();
return processedLogs || []; }
return true;
}), }),
haveActivateRequests: adminProcedure.query(async () => { haveActivateRequests: adminProcedure.query(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
@@ -817,20 +820,10 @@ export const settingsRouter = createTRPCRouter({
}); });
} }
}), }),
updateLogCleanup: adminProcedure
.input(
z.object({
cronExpression: z.string().nullable(),
}),
)
.mutation(async ({ input }) => {
if (input.cronExpression) {
return startLogCleanup(input.cronExpression);
}
return stopLogCleanup();
}),
getLogCleanupStatus: adminProcedure.query(async () => {
return getLogCleanupStatus();
}),
}); });
// {
// "Parallelism": 1,
// "Delay": 10000000000,
// "FailureAction": "rollback",
// "Order": "start-first"
// }

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ export const users_temp = pgTable("user_temp", {
letsEncryptEmail: text("letsEncryptEmail"), letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"), sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false), enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
logCleanupCron: text("logCleanupCron"), enableLogRotation: boolean("enableLogRotation").notNull().default(false),
// Metrics // Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false), enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
metricsConfig: jsonb("metricsConfig") metricsConfig: jsonb("metricsConfig")
@@ -250,12 +250,6 @@ export const apiReadStatsLogs = z.object({
status: z.string().array().optional(), status: z.string().array().optional(),
search: z.string().optional(), search: z.string().optional(),
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(), sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
dateRange: z
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
}); });
export const apiUpdateWebServerMonitoring = z.object({ export const apiUpdateWebServerMonitoring = z.object({
@@ -311,5 +305,4 @@ export const apiUpdateUser = createSchema.partial().extend({
}), }),
}) })
.optional(), .optional(),
logCleanupCron: z.string().optional().nullable(),
}); });

View File

@@ -116,9 +116,3 @@ export * from "./db/validations/index";
export * from "./utils/gpu-setup"; export * from "./utils/gpu-setup";
export * from "./lib/auth"; export * from "./lib/auth";
export {
startLogCleanup,
stopLogCleanup,
getLogCleanupStatus,
} from "./utils/access-log/handler";

View File

@@ -14,6 +14,9 @@ const { handler, api } = betterAuth({
provider: "pg", provider: "pg",
schema: schema, schema: schema,
}), }),
...(!IS_CLOUD && {
baseURL: "http://localhost:3000",
}),
logger: { logger: {
disabled: process.env.NODE_ENV === "production", disabled: process.env.NODE_ENV === "production",
}, },

View File

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

View File

@@ -1,77 +1,121 @@
import { paths } from "@dokploy/server/constants"; import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { type RotatingFileStream, createStream } from "rotating-file-stream";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { findAdmin } from "@dokploy/server/services/admin"; import { findAdmin } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user"; import { updateUser } from "@dokploy/server/services/user";
import { scheduleJob, scheduledJobs } from "node-schedule";
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup"; class LogRotationManager {
private static instance: LogRotationManager;
private stream: RotatingFileStream | null = null;
export const startLogCleanup = async ( private constructor() {
cronExpression = "0 0 * * *", if (IS_CLOUD) {
): Promise<boolean> => { return;
try { }
this.initialize().catch(console.error);
}
public static getInstance(): LogRotationManager {
if (!LogRotationManager.instance) {
LogRotationManager.instance = new LogRotationManager();
}
return LogRotationManager.instance;
}
private async initialize(): Promise<void> {
const isActive = await this.getStateFromDB();
if (isActive) {
await this.activateStream();
}
}
private async getStateFromDB(): Promise<boolean> {
const admin = await findAdmin();
return admin?.user.enableLogRotation ?? false;
}
private async setStateInDB(active: boolean): Promise<void> {
const admin = await findAdmin();
if (!admin) {
return;
}
await updateUser(admin.user.id, {
enableLogRotation: active,
});
}
private async activateStream(): Promise<void> {
const { DYNAMIC_TRAEFIK_PATH } = paths(); const { DYNAMIC_TRAEFIK_PATH } = paths();
if (this.stream) {
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME]; await this.deactivateStream();
if (existingJob) {
existingJob.cancel();
} }
scheduleJob(LOG_CLEANUP_JOB_NAME, cronExpression, async () => { this.stream = createStream("access.log", {
try { size: "100M",
await execAsync( interval: "1d",
`tail -n 1000 ${DYNAMIC_TRAEFIK_PATH}/access.log > ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp && mv ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp ${DYNAMIC_TRAEFIK_PATH}/access.log`, path: DYNAMIC_TRAEFIK_PATH,
); rotate: 6,
compress: "gzip",
await execAsync("docker exec dokploy-traefik kill -USR1 1");
} catch (error) {
console.error("Error during log cleanup:", error);
}
}); });
const admin = await findAdmin(); this.stream.on("rotation", this.handleRotation.bind(this));
if (admin) {
await updateUser(admin.user.id, {
logCleanupCron: cronExpression,
});
}
return true;
} catch (_) {
return false;
} }
};
export const stopLogCleanup = async (): Promise<boolean> => { private async deactivateStream(): Promise<void> {
try { return new Promise<void>((resolve) => {
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME]; if (this.stream) {
if (existingJob) { this.stream.end(() => {
existingJob.cancel(); this.stream = null;
} resolve();
});
// Update database } else {
const admin = await findAdmin(); resolve();
if (admin) { }
await updateUser(admin.user.id, { });
logCleanupCron: null,
});
}
return true;
} catch (error) {
console.error("Error stopping log cleanup:", error);
return false;
} }
};
export const getLogCleanupStatus = async (): Promise<{ public async activate(): Promise<boolean> {
enabled: boolean; const currentState = await this.getStateFromDB();
cronExpression: string | null; if (currentState) {
}> => { return true;
const admin = await findAdmin(); }
const cronExpression = admin?.user.logCleanupCron ?? null;
return { await this.setStateInDB(true);
enabled: cronExpression !== null, await this.activateStream();
cronExpression, return true;
}; }
};
public async deactivate(): Promise<boolean> {
console.log("Deactivating log rotation...");
const currentState = await this.getStateFromDB();
if (!currentState) {
console.log("Log rotation is already inactive in DB");
return true;
}
await this.setStateInDB(false);
await this.deactivateStream();
console.log("Log rotation deactivated successfully");
return true;
}
private async handleRotation() {
try {
const status = await this.getStatus();
if (!status) {
await this.deactivateStream();
}
await execAsync(
"docker kill -s USR1 $(docker ps -q --filter name=dokploy-traefik)",
);
console.log("USR1 Signal send to Traefik");
} catch (error) {
console.error("Error sending USR1 Signal to Traefik:", error);
}
}
public async getStatus(): Promise<boolean> {
const dbState = await this.getStateFromDB();
return dbState;
}
}
export const logRotationManager = LogRotationManager.getInstance();

View File

@@ -6,21 +6,14 @@ interface HourlyData {
count: number; count: number;
} }
export function processLogs( export function processLogs(logString: string): HourlyData[] {
logString: string,
dateRange?: { start?: string; end?: string },
): HourlyData[] {
if (_.isEmpty(logString)) { if (_.isEmpty(logString)) {
return []; return [];
} }
const hourlyData = _(logString) const hourlyData = _(logString)
.split("\n") .split("\n")
.filter((line) => { .compact()
const trimmed = line.trim();
// Check if the line starts with { and ends with } to ensure it's a potential JSON object
return trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}");
})
.map((entry) => { .map((entry) => {
try { try {
const log: LogEntry = JSON.parse(entry); const log: LogEntry = JSON.parse(entry);
@@ -28,20 +21,6 @@ export function processLogs(
return null; return null;
} }
const date = new Date(log.StartUTC); const date = new Date(log.StartUTC);
if (dateRange?.start || dateRange?.end) {
const logDate = date.getTime();
const start = dateRange?.start
? new Date(dateRange.start).getTime()
: 0;
const end = dateRange?.end
? new Date(dateRange.end).getTime()
: Number.POSITIVE_INFINITY;
if (logDate < start || logDate > end) {
return null;
}
}
return `${date.toISOString().slice(0, 13)}:00:00Z`; return `${date.toISOString().slice(0, 13)}:00:00Z`;
} catch (error) { } catch (error) {
console.error("Error parsing log entry:", error); console.error("Error parsing log entry:", error);
@@ -72,46 +51,21 @@ export function parseRawConfig(
sort?: SortInfo, sort?: SortInfo,
search?: string, search?: string,
status?: string[], status?: string[],
dateRange?: { start?: string; end?: string },
): { data: LogEntry[]; totalCount: number } { ): { data: LogEntry[]; totalCount: number } {
try { try {
if (_.isEmpty(rawConfig)) { if (_.isEmpty(rawConfig)) {
return { data: [], totalCount: 0 }; return { data: [], totalCount: 0 };
} }
// Split logs into chunks to avoid memory issues
let parsedLogs = _(rawConfig) let parsedLogs = _(rawConfig)
.split("\n") .split("\n")
.filter((line) => {
const trimmed = line.trim();
return (
trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}")
);
})
.map((line) => {
try {
return JSON.parse(line) as LogEntry;
} catch (error) {
console.error("Error parsing log line:", error);
return null;
}
})
.compact() .compact()
.map((line) => JSON.parse(line) as LogEntry)
.value(); .value();
// Apply date range filter if provided parsedLogs = parsedLogs.filter(
if (dateRange?.start || dateRange?.end) { (log) => log.ServiceName !== "dokploy-service-app@file",
parsedLogs = parsedLogs.filter((log) => { );
const logDate = new Date(log.StartUTC).getTime();
const start = dateRange?.start
? new Date(dateRange.start).getTime()
: 0;
const end = dateRange?.end
? new Date(dateRange.end).getTime()
: Number.POSITIVE_INFINITY;
return logDate >= start && logDate <= end;
});
}
if (search) { if (search) {
parsedLogs = parsedLogs.filter((log) => parsedLogs = parsedLogs.filter((log) =>
@@ -124,7 +78,6 @@ export function parseRawConfig(
status.some((range) => isStatusInRange(log.DownstreamStatus, range)), status.some((range) => isStatusInRange(log.DownstreamStatus, range)),
); );
} }
const totalCount = parsedLogs.length; const totalCount = parsedLogs.length;
if (sort) { if (sort) {
@@ -148,7 +101,6 @@ export function parseRawConfig(
throw new Error("Failed to parse rawConfig"); throw new Error("Failed to parse rawConfig");
} }
} }
const isStatusInRange = (status: number, range: string) => { const isStatusInRange = (status: number, range: string) => {
switch (range) { switch (range) {
case "info": case "info":

View File

@@ -12,7 +12,6 @@ import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql"; import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres"; import { runPostgresBackup } from "./postgres";
import { findAdmin } from "../../services/admin"; import { findAdmin } from "../../services/admin";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => { export const initCronJobs = async () => {
console.log("Setting up cron jobs...."); console.log("Setting up cron jobs....");
@@ -169,8 +168,4 @@ export const initCronJobs = async () => {
} }
} }
} }
if (admin?.user.logCleanupCron) {
await startLogCleanup(admin.user.logCleanupCron);
}
}; };

View File

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

View File

@@ -137,44 +137,12 @@ export const readRemoteConfig = async (serverId: string, appName: string) => {
} }
}; };
export const readMonitoringConfig = (readAll = false) => { export const readMonitoringConfig = () => {
const { DYNAMIC_TRAEFIK_PATH } = paths(); const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log"); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
if (!readAll) { const yamlStr = fs.readFileSync(configPath, "utf8");
// Read first 500 lines return yamlStr;
let content = "";
let chunk = "";
let validCount = 0;
for (const char of fs.readFileSync(configPath, "utf8")) {
chunk += char;
if (char === "\n") {
try {
const trimmed = chunk.trim();
if (
trimmed !== "" &&
trimmed.startsWith("{") &&
trimmed.endsWith("}")
) {
const log = JSON.parse(trimmed);
if (log.ServiceName !== "dokploy-service-app@file") {
content += chunk;
validCount++;
if (validCount >= 500) {
break;
}
}
}
} catch {
// Ignore invalid JSON
}
chunk = "";
}
}
return content;
}
return fs.readFileSync(configPath, "utf8");
} }
return null; return null;
}; };

1
pnpm-lock.yaml generated
View File

@@ -6282,7 +6282,6 @@ packages:
oslo@1.2.0: oslo@1.2.0:
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==} resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
deprecated: Package is no longer supported. Please see https://oslojs.dev for the successor project.
otpauth@9.3.4: otpauth@9.3.4:
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==} resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}