mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 14:15:21 +02:00
Compare commits
57 Commits
v0.19.1
...
407-featur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7f44f65bc | ||
|
|
62bd8e3c95 | ||
|
|
85734c0a24 | ||
|
|
8d18aeda45 | ||
|
|
45923d3a1f | ||
|
|
043843f714 | ||
|
|
7dda252b7c | ||
|
|
bf0668c319 | ||
|
|
fc1dbcf51a | ||
|
|
b34987530e | ||
|
|
ff8d922f2b | ||
|
|
01c33ad98b | ||
|
|
9816ecaea1 | ||
|
|
832fa526dd | ||
|
|
2a5eceb555 | ||
|
|
08d7c4e1c3 | ||
|
|
c89f957133 | ||
|
|
8ba3a42c1e | ||
|
|
2c3ff5794d | ||
|
|
673e0a6880 | ||
|
|
b64ddf1119 | ||
|
|
2f074ac734 | ||
|
|
96e3721b4b | ||
|
|
b8e5cae88f | ||
|
|
fa20444a14 | ||
|
|
668ccabec8 | ||
|
|
aa07a0c574 | ||
|
|
0b64b43376 | ||
|
|
5c65dc9a21 | ||
|
|
58262606d4 | ||
|
|
f73959db41 | ||
|
|
e6c664e65f | ||
|
|
36cc157566 | ||
|
|
7e070623cc | ||
|
|
b2c0a685f8 | ||
|
|
c14528886d | ||
|
|
29eb490e2d | ||
|
|
6166963b00 | ||
|
|
f544efed35 | ||
|
|
598d095241 | ||
|
|
457a8e05fd | ||
|
|
3ca057c44a | ||
|
|
ad3a0198e9 | ||
|
|
ab5f62604c | ||
|
|
bf9e886b9a | ||
|
|
f5cd0fbdd8 | ||
|
|
8859cc97b4 | ||
|
|
3bdd5e4dd0 | ||
|
|
b0c710aa92 | ||
|
|
c83d0a95b7 | ||
|
|
71ca5babfd | ||
|
|
f342613503 | ||
|
|
efd176451f | ||
|
|
a7fd64e019 | ||
|
|
21c8b98f9c | ||
|
|
6846e0e5a3 | ||
|
|
49d4cea06f |
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a bug report
|
description: Create a bug report
|
||||||
labels: ["bug"]
|
labels: ["needs-triage🔍"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -62,6 +62,7 @@ body:
|
|||||||
- "Docker"
|
- "Docker"
|
||||||
- "Remote server"
|
- "Remote server"
|
||||||
- "Local Development"
|
- "Local Development"
|
||||||
|
- "Cloud Version"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const baseAdmin: User = {
|
|||||||
letsEncryptEmail: null,
|
letsEncryptEmail: null,
|
||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
logCleanupCron: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
|
|||||||
@@ -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 className="w-full">
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl className="">
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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>;
|
||||||
@@ -36,6 +37,7 @@ 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),
|
||||||
});
|
});
|
||||||
@@ -44,6 +46,7 @@ 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 () => {
|
||||||
@@ -69,25 +72,63 @@ 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
|
<>
|
||||||
name="buildArgs"
|
<Secrets
|
||||||
title="Build-time Variables"
|
name="buildArgs"
|
||||||
description={
|
title="Build-time Variables"
|
||||||
<span>
|
description={
|
||||||
Available only at build-time. See documentation
|
<span>
|
||||||
<a
|
Available only at build-time. See documentation
|
||||||
className="text-primary"
|
<a
|
||||||
href="https://docs.docker.com/build/guide/build-args/"
|
className="text-primary"
|
||||||
target="_blank"
|
href="https://docs.docker.com/build/guide/build-args/"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
here
|
>
|
||||||
</a>
|
here
|
||||||
.
|
</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
|
||||||
|
<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">
|
||||||
|
|||||||
@@ -4,8 +4,22 @@ 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 { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
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";
|
||||||
@@ -41,128 +55,188 @@ 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">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Application"
|
|
||||||
description="Are you sure you want to deploy this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await deploy({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Application"
|
|
||||||
description="Are you sure you want to reload this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
applicationId: applicationId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Rebuild Application"
|
|
||||||
description="Are you sure you want to rebuild this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await redeploy({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application rebuilt successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error rebuilding application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Application"
|
title="Deploy Application"
|
||||||
description="Are you sure you want to start this application?"
|
description="Are you sure you want to deploy this application?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await deploy({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application started successfully");
|
toast.success("Application deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting application");
|
toast.error("Error deploying application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Downloads the source code and performs a complete build
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Application"
|
title="Reload Application"
|
||||||
description="Are you sure you want to stop this application?"
|
description="Are you sure you want to reload this application?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application stopped successfully");
|
toast.success("Application reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping application");
|
toast.error("Error reloading application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button variant="secondary" isLoading={isReloading}>
|
||||||
Stop
|
Reload
|
||||||
<Ban className="size-4" />
|
<RefreshCcw className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
<DialogAction
|
||||||
|
title="Rebuild Application"
|
||||||
|
description="Are you sure you want to rebuild this application?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application rebuilt successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error rebuilding application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Only rebuilds the application without downloading new
|
||||||
|
code
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Application"
|
||||||
|
description="Are you sure you want to start this application?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the application (requires a previous successful
|
||||||
|
build)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Application"
|
||||||
|
description="Are you sure you want to stop this application?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running application</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
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, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
@@ -27,103 +34,159 @@ 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 ">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
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="Start Compose"
|
title="Deploy Compose"
|
||||||
description="Are you sure you want to start this compose?"
|
description="Are you sure you want to deploy this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await deploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose started successfully");
|
toast.success("Compose deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting compose");
|
toast.error("Error deploying compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads the source code and performs a complete build</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Compose"
|
title="Rebuild Compose"
|
||||||
description="Are you sure you want to stop this compose?"
|
description="Are you sure you want to rebuild this compose?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose stopped successfully");
|
toast.success("Compose rebuilt successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping compose");
|
toast.error("Error rebuilding compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Only rebuilds the compose without downloading new code</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</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 || ""}
|
||||||
|
|||||||
@@ -2,8 +2,21 @@ 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 { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
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";
|
||||||
@@ -65,92 +78,150 @@ 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">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Mariadb"
|
|
||||||
description="Are you sure you want to deploy this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mariadb"
|
|
||||||
description="Are you sure you want to reload this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mariadbId: mariadbId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mariadb"
|
title="Deploy Mariadb"
|
||||||
description="Are you sure you want to start this mariadb?"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mariadbId: mariadbId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the MariaDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mariadb"
|
title="Reload Mariadb"
|
||||||
description="Are you sure you want to stop this mariadb?"
|
description="Are you sure you want to reload this mariadb?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mariadb stopped successfully");
|
toast.success("Mariadb reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Mariadb");
|
toast.error("Error reloading Mariadb");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MariaDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Mariadb"
|
||||||
|
description="Are you sure you want to start this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MariaDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mariadb"
|
||||||
|
description="Are you sure you want to stop this mariadb?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MariaDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -2,8 +2,21 @@ 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 { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
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";
|
||||||
@@ -64,93 +77,150 @@ 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">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Mongo"
|
|
||||||
description="Are you sure you want to deploy this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mongo"
|
|
||||||
description="Are you sure you want to reload this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mongoId: mongoId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mongo");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to start this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the MongoDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mongo"
|
||||||
|
description="Are you sure you want to reload this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mongo started successfully");
|
toast.success("Mongo reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting Mongo");
|
toast.error("Error reloading Mongo");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="secondary"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MongoDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mongo"
|
title="Start Mongo"
|
||||||
description="Are you sure you want to stop this mongo?"
|
description="Are you sure you want to start this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await start({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error stopping Mongo");
|
toast.success("Mongo started successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
toast.error("Error starting Mongo");
|
||||||
Stop
|
});
|
||||||
<Ban className="size-4" />
|
}}
|
||||||
</Button>
|
>
|
||||||
</DialogAction>
|
<Button
|
||||||
)}
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MongoDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mongo"
|
||||||
|
description="Are you sure you want to stop this mongo?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mongoId: mongoId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MongoDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
}, [appName]);
|
}, [appName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
|
<div className="rounded-xl bg-background flex flex-col gap-4">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
||||||
|
|||||||
@@ -2,8 +2,21 @@ 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 { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
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";
|
||||||
@@ -62,93 +75,150 @@ 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">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Mysql"
|
|
||||||
description="Are you sure you want to deploy this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mysql"
|
|
||||||
description="Are you sure you want to reload this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mysqlId: mysqlId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mysql reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mysql");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mysql"
|
title="Deploy Mysql"
|
||||||
description="Are you sure you want to start this mysql?"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mysqlId: mysqlId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mysql started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mysql");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the MySQL database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Mysql"
|
title="Reload Mysql"
|
||||||
description="Are you sure you want to stop this mysql?"
|
description="Are you sure you want to reload this mysql?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mysql stopped successfully");
|
toast.success("Mysql reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Mysql");
|
toast.error("Error reloading Mysql");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MySQL service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Mysql"
|
||||||
|
description="Are you sure you want to start this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mysql started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mysql");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MySQL database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mysql"
|
||||||
|
description="Are you sure you want to stop this mysql?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mysql stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mysql");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MySQL database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ 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 { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -57,122 +71,179 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<>
|
||||||
<Card className="bg-background">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<CardHeader className="pb-4">
|
<Card className="bg-background">
|
||||||
<CardTitle className="text-xl">General</CardTitle>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
<CardContent className="flex gap-4">
|
</CardHeader>
|
||||||
<DialogAction
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
title="Deploy Postgres"
|
<TooltipProvider delayDuration={0}>
|
||||||
description="Are you sure you want to deploy this postgres?"
|
<DialogAction
|
||||||
type="default"
|
title="Deploy Postgres"
|
||||||
onClick={async () => {
|
description="Are you sure you want to deploy this postgres?"
|
||||||
setIsDeploying(true);
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
setIsDeploying(true);
|
||||||
refetch();
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Postgres"
|
|
||||||
description="Are you sure you want to reload this postgres?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
postgresId: postgresId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Postgres reloaded successfully");
|
|
||||||
refetch();
|
refetch();
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error("Error reloading Postgres");
|
<Button
|
||||||
});
|
variant="default"
|
||||||
}}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
>
|
className="flex items-center gap-1.5"
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
>
|
||||||
Reload
|
Deploy
|
||||||
<RefreshCcw className="size-4" />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
</DialogAction>
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
{data?.applicationStatus === "idle" ? (
|
</TooltipTrigger>
|
||||||
<DialogAction
|
<TooltipPrimitive.Portal>
|
||||||
title="Start Postgres"
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
description="Are you sure you want to start this postgres?"
|
<p>Downloads and sets up the PostgreSQL database</p>
|
||||||
type="default"
|
</TooltipContent>
|
||||||
onClick={async () => {
|
</TooltipPrimitive.Portal>
|
||||||
await start({
|
</Tooltip>
|
||||||
postgresId: postgresId,
|
</Button>
|
||||||
})
|
</DialogAction>
|
||||||
.then(() => {
|
<DialogAction
|
||||||
toast.success("Postgres started successfully");
|
title="Reload Postgres"
|
||||||
refetch();
|
description="Are you sure you want to reload this postgres?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
postgresId: postgresId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error starting Postgres");
|
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="secondary" isLoading={isStarting}>
|
<Button variant="outline">
|
||||||
Start
|
<Terminal />
|
||||||
<CheckCircle2 className="size-4" />
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DockerTerminalModal>
|
||||||
) : (
|
</CardContent>
|
||||||
<DialogAction
|
</Card>
|
||||||
title="Stop Postgres"
|
<DrawerLogs
|
||||||
description="Are you sure you want to stop this postgres?"
|
isOpen={isDrawerOpen}
|
||||||
onClick={async () => {
|
onClose={() => {
|
||||||
await stop({
|
setIsDrawerOpen(false);
|
||||||
postgresId: postgresId,
|
setFilteredLogs([]);
|
||||||
})
|
setIsDeploying(false);
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Postgres stopped successfully");
|
}}
|
||||||
refetch();
|
filteredLogs={filteredLogs}
|
||||||
})
|
/>
|
||||||
.catch(() => {
|
</div>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -383,9 +383,8 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
side="top"
|
side="top"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
If ot server is selected, the application
|
If no server is selected, the application will be
|
||||||
will be deployed on the server where the
|
deployed on the server where the user is logged in.
|
||||||
user is logged in.
|
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ 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 { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -63,94 +77,150 @@ 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">
|
||||||
<DialogAction
|
<TooltipProvider delayDuration={0}>
|
||||||
title="Deploy Redis"
|
|
||||||
description="Are you sure you want to deploy this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Redis"
|
|
||||||
description="Are you sure you want to reload this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
redisId: redisId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{/* <ResetRedis redisId={redisId} appName={data?.appName || ""} /> */}
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Redis"
|
title="Deploy Redis"
|
||||||
description="Are you sure you want to start this redis?"
|
description="Are you sure you want to deploy this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
redisId: redisId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Redis");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
<Button
|
||||||
Start
|
variant="default"
|
||||||
<CheckCircle2 className="size-4" />
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads and sets up the Redis database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Redis"
|
title="Reload Redis"
|
||||||
description="Are you sure you want to stop this redis?"
|
description="Are you sure you want to reload this redis?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await reload({
|
||||||
redisId: redisId,
|
redisId: redisId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Redis stopped successfully");
|
toast.success("Redis reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping Redis");
|
toast.error("Error reloading Redis");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
<Button
|
||||||
Stop
|
variant="secondary"
|
||||||
<Ban className="size-4" />
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the Redis service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Redis"
|
||||||
|
description="Are you sure you want to start this redis?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
redisId: redisId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the Redis database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Redis"
|
||||||
|
description="Are you sure you want to stop this redis?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
redisId: redisId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running Redis database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -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,6 +14,13 @@ 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",
|
||||||
@@ -24,10 +31,22 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const RequestDistributionChart = () => {
|
export const RequestDistributionChart = ({
|
||||||
const { data: stats } = api.settings.readStats.useQuery(undefined, {
|
dateRange,
|
||||||
refetchInterval: 1333,
|
}: RequestDistributionChartProps) => {
|
||||||
});
|
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}>
|
||||||
|
|||||||
@@ -79,7 +79,15 @@ 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>();
|
||||||
@@ -98,6 +106,12 @@ export const RequestsTable = () => {
|
|||||||
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,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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,
|
||||||
@@ -8,9 +9,29 @@ 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 { ArrowDownUp } from "lucide-react";
|
import { format } from "date-fns";
|
||||||
|
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";
|
||||||
@@ -20,17 +41,30 @@ 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">
|
||||||
@@ -57,7 +91,60 @@ 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">
|
<div className="flex w-full gap-4 justify-end items-center">
|
||||||
|
<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"
|
||||||
@@ -77,53 +164,81 @@ 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>
|
||||||
|
|
||||||
<div>
|
{isActive ? (
|
||||||
{isActive ? (
|
<>
|
||||||
<RequestDistributionChart />
|
<div className="flex justify-end mb-4 gap-2">
|
||||||
) : (
|
{(dateRange.from || dateRange.to) && (
|
||||||
<div className="flex items-center justify-center min-h-[25vh]">
|
<Button
|
||||||
<span className="text-muted-foreground py-6">
|
variant="outline"
|
||||||
You need to activate requests
|
onClick={() =>
|
||||||
</span>
|
setDateRange({ from: undefined, to: undefined })
|
||||||
|
}
|
||||||
|
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} />
|
||||||
{isActive && <RequestsTable />}
|
<RequestsTable dateRange={dateRange} />
|
||||||
</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>
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ import { S3_PROVIDERS } from "./constants";
|
|||||||
|
|
||||||
const addDestination = z.object({
|
const addDestination = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
provider: z.string().optional(),
|
provider: z.string().min(1, "Provider is required"),
|
||||||
accessKeyId: z.string(),
|
accessKeyId: z.string().min(1, "Access Key Id is required"),
|
||||||
secretAccessKey: z.string(),
|
secretAccessKey: z.string().min(1, "Secret Access Key is required"),
|
||||||
bucket: z.string(),
|
bucket: z.string().min(1, "Bucket is required"),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string().min(1, "Endpoint is required"),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +129,63 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async (serverId?: string) => {
|
||||||
|
const result = await form.trigger([
|
||||||
|
"provider",
|
||||||
|
"accessKeyId",
|
||||||
|
"secretAccessKey",
|
||||||
|
"bucket",
|
||||||
|
"endpoint",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const errors = form.formState.errors;
|
||||||
|
const errorFields = Object.entries(errors)
|
||||||
|
.map(([field, error]) => `${field}: ${error?.message}`)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
toast.error("Please fill all required fields", {
|
||||||
|
description: errorFields,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloud && !serverId) {
|
||||||
|
toast.error("Please select a server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = form.getValues("provider");
|
||||||
|
const accessKey = form.getValues("accessKeyId");
|
||||||
|
const secretKey = form.getValues("secretAccessKey");
|
||||||
|
const bucket = form.getValues("bucket");
|
||||||
|
const endpoint = form.getValues("endpoint");
|
||||||
|
const region = form.getValues("region");
|
||||||
|
|
||||||
|
const connectionString = `:s3,provider=${provider},access_key_id=${accessKey},secret_access_key=${secretKey},endpoint=${endpoint}${region ? `,region=${region}` : ""}:${bucket}`;
|
||||||
|
|
||||||
|
await testConnection({
|
||||||
|
provider,
|
||||||
|
accessKey,
|
||||||
|
bucket,
|
||||||
|
endpoint,
|
||||||
|
name: "Test",
|
||||||
|
region,
|
||||||
|
secretAccessKey: secretKey,
|
||||||
|
serverId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error connecting to provider", {
|
||||||
|
description: `${e.message}\n\nTry manually: rclone ls ${connectionString}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
@@ -349,26 +406,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
isLoading={isLoading}
|
isLoading={isLoadingConnection}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await handleTestConnection(form.getValues("serverId"));
|
||||||
provider: form.getValues("provider") || "",
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
serverId: form.getValues("serverId"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error("Error connecting the provider", {
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Test Connection
|
Test Connection
|
||||||
@@ -380,21 +420,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await handleTestConnection();
|
||||||
provider: form.getValues("provider") || "",
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error connecting the provider");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Test connection
|
Test connection
|
||||||
|
|||||||
@@ -56,9 +56,17 @@ export const ShowDestinations = () => {
|
|||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<span className="text-sm">
|
<div className="flex flex-col gap-1">
|
||||||
{index + 1}. {destination.name}
|
<span className="text-sm">
|
||||||
</span>
|
{index + 1}. {destination.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Created at:{" "}
|
||||||
|
{new Date(
|
||||||
|
destination.createdAt,
|
||||||
|
).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<HandleDestinations
|
<HandleDestinations
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
|
|||||||
@@ -45,21 +45,12 @@ const Schema = z.object({
|
|||||||
|
|
||||||
type Schema = z.infer<typeof Schema>;
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
interface Model {
|
|
||||||
id: string;
|
|
||||||
object: string;
|
|
||||||
created: number;
|
|
||||||
owned_by: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
aiId?: string;
|
aiId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandleAi = ({ aiId }: Props) => {
|
export const HandleAi = ({ aiId }: Props) => {
|
||||||
const [models, setModels] = useState<Model[]>([]);
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { data, refetch } = api.ai.one.useQuery(
|
const { data, refetch } = api.ai.one.useQuery(
|
||||||
@@ -73,6 +64,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
const { mutateAsync, isLoading } = aiId
|
const { mutateAsync, isLoading } = aiId
|
||||||
? api.ai.update.useMutation()
|
? api.ai.update.useMutation()
|
||||||
: api.ai.create.useMutation();
|
: api.ai.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -94,50 +86,33 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
});
|
});
|
||||||
}, [aiId, form, data]);
|
}, [aiId, form, data]);
|
||||||
|
|
||||||
const fetchModels = async (apiUrl: string, apiKey: string) => {
|
const apiUrl = form.watch("apiUrl");
|
||||||
setIsLoadingModels(true);
|
const apiKey = form.watch("apiKey");
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${apiUrl}/models`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch models");
|
|
||||||
}
|
|
||||||
const res = await response.json();
|
|
||||||
setModels(res.data);
|
|
||||||
|
|
||||||
// Set default model to gpt-4 if present
|
const { data: models, isLoading: isLoadingServerModels } =
|
||||||
const defaultModel = res.data.find(
|
api.ai.getModels.useQuery(
|
||||||
(model: Model) => model.id === "gpt-4",
|
{
|
||||||
);
|
apiUrl: apiUrl ?? "",
|
||||||
if (defaultModel) {
|
apiKey: apiKey ?? "",
|
||||||
form.setValue("model", defaultModel.id);
|
},
|
||||||
return defaultModel.id;
|
{
|
||||||
}
|
enabled: !!apiUrl && !!apiKey,
|
||||||
} catch (error) {
|
onError: (error) => {
|
||||||
setError("Failed to fetch models. Please check your API URL and Key.");
|
setError(`Failed to fetch models: ${error.message}`);
|
||||||
setModels([]);
|
},
|
||||||
} finally {
|
},
|
||||||
setIsLoadingModels(false);
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const apiUrl = form.watch("apiUrl");
|
const apiUrl = form.watch("apiUrl");
|
||||||
const apiKey = form.watch("apiKey");
|
const apiKey = form.watch("apiKey");
|
||||||
if (apiUrl && apiKey) {
|
if (apiUrl && apiKey) {
|
||||||
form.setValue("model", "");
|
form.setValue("model", "");
|
||||||
fetchModels(apiUrl, apiKey);
|
|
||||||
}
|
}
|
||||||
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
try {
|
try {
|
||||||
console.log("Form data:", data);
|
|
||||||
console.log("Current model value:", form.getValues("model"));
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
...data,
|
...data,
|
||||||
aiId: aiId || "",
|
aiId: aiId || "",
|
||||||
@@ -148,8 +123,9 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Submit error:", error);
|
toast.error("Failed to save AI settings", {
|
||||||
toast.error("Failed to save AI settings");
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,13 +208,13 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoadingModels && (
|
{isLoadingServerModels && (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Loading models...
|
Loading models...
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoadingModels && models.length > 0 && (
|
{!isLoadingServerModels && models && models.length > 0 && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="model"
|
name="model"
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ 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 {
|
||||||
@@ -24,9 +30,17 @@ 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 = ({ updateData }: Props) => {
|
export const UpdateServer = ({
|
||||||
|
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,
|
||||||
@@ -35,10 +49,10 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
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 {
|
||||||
@@ -65,28 +79,52 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOpen = isOpenInternal || isOpenProp;
|
||||||
|
const onOpenChange = (open: boolean) => {
|
||||||
|
setIsOpenInternal(open);
|
||||||
|
onOpenChangeProp?.(open);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
{children ? (
|
||||||
variant={updateData ? "outline" : "secondary"}
|
children
|
||||||
className="gap-2"
|
) : (
|
||||||
>
|
<TooltipProvider delayDuration={0}>
|
||||||
{updateData ? (
|
<Tooltip>
|
||||||
<>
|
<TooltipTrigger asChild>
|
||||||
<span className="flex h-2 w-2">
|
<Button
|
||||||
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
|
variant={updateData ? "outline" : "secondary"}
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
size="sm"
|
||||||
</span>
|
onClick={() => onOpenChange?.(true)}
|
||||||
Update available
|
>
|
||||||
</>
|
<Download className="h-4 w-4 flex-shrink-0" />
|
||||||
) : (
|
{updateData ? (
|
||||||
<>
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
<Sparkles className="h-4 w-4" />
|
Update Available
|
||||||
Updates
|
</span>
|
||||||
</>
|
) : (
|
||||||
)}
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
</Button>
|
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>
|
||||||
|
)}
|
||||||
</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">
|
||||||
@@ -217,7 +255,7 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
|
|
||||||
<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={() => setIsOpen(false)}>
|
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{isUpdateAvailable ? (
|
{isUpdateAvailable ? (
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@@ -1017,18 +1015,16 @@ 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>
|
<SidebarMenu className="flex flex-col gap-2">
|
||||||
|
{!isCloud && auth?.role === "owner" && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<UpdateServerButton />
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)}
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -1055,10 +1051,6 @@ export default function Page({ children }: Props) {
|
|||||||
</Link>
|
</Link>
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="block" />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>{activeItem?.title}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ 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 = () => {
|
||||||
@@ -15,6 +22,7 @@ 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);
|
||||||
|
|
||||||
@@ -69,11 +77,47 @@ export const UpdateServerButton = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return updateData.updateAvailable ? (
|
||||||
updateData.updateAvailable && (
|
<div className="border-t pt-4">
|
||||||
<div>
|
<UpdateServer
|
||||||
<UpdateServer updateData={updateData} />
|
updateData={updateData}
|
||||||
</div>
|
isOpen={isOpen}
|
||||||
)
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="block" />
|
{_index + 1 < list.length && <BreadcrumbSeparator className="block" />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
|
|||||||
68
apps/dokploy/components/ui/calendar.tsx
Normal file
68
apps/dokploy/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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 };
|
||||||
1
apps/dokploy/drizzle/0070_useful_serpent_society.sql
Normal file
1
apps/dokploy/drizzle/0070_useful_serpent_society.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "destination" ADD COLUMN "createdAt" timestamp DEFAULT now() NOT NULL;
|
||||||
1
apps/dokploy/drizzle/0071_flaky_black_queen.sql
Normal file
1
apps/dokploy/drizzle/0071_flaky_black_queen.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "logCleanupCron" text;
|
||||||
2
apps/dokploy/drizzle/0072_milky_lyja.sql
Normal file
2
apps/dokploy/drizzle/0072_milky_lyja.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "buildSecrets" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "user_temp" DROP COLUMN "enableLogRotation";
|
||||||
5126
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
5126
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5132
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
5132
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5131
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
5131
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -491,6 +491,27 @@
|
|||||||
"when": 1741152916611,
|
"when": 1741152916611,
|
||||||
"tag": "0069_legal_bill_hollister",
|
"tag": "0069_legal_bill_hollister",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741322697251,
|
||||||
|
"tag": "0070_useful_serpent_society",
|
||||||
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,7 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
@@ -72,9 +73,25 @@ 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 } from "react";
|
import { type ReactElement, useMemo, useState, useEffect } 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;
|
||||||
@@ -203,10 +220,47 @@ 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 &&
|
||||||
@@ -254,6 +308,38 @@ 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 () => {
|
||||||
@@ -296,9 +382,145 @@ 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 [];
|
||||||
return applications.filter(
|
const filtered = applications.filter(
|
||||||
(service) =>
|
(service) =>
|
||||||
(service.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(service.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
service.description
|
service.description
|
||||||
@@ -306,7 +528,8 @@ const Project = (
|
|||||||
.includes(searchQuery.toLowerCase())) &&
|
.includes(searchQuery.toLowerCase())) &&
|
||||||
(selectedTypes.length === 0 || selectedTypes.includes(service.type)),
|
(selectedTypes.length === 0 || selectedTypes.includes(service.type)),
|
||||||
);
|
);
|
||||||
}, [applications, searchQuery, selectedTypes]);
|
return sortServices(filtered);
|
||||||
|
}, [applications, searchQuery, selectedTypes, sortBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -380,7 +603,7 @@ const Project = (
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl: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
|
||||||
@@ -445,11 +668,107 @@ 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 sm:flex-row sm:gap-4 sm:items-center">
|
<div className="flex flex-col gap-2 lg:flex-row lg:gap-4 lg:items-center">
|
||||||
<div className="w-full relative">
|
<div className="w-full relative">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter services..."
|
placeholder="Filter services..."
|
||||||
@@ -459,6 +778,23 @@ 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}
|
||||||
|
|||||||
@@ -228,15 +228,15 @@ const Service = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="preview-deployments">
|
<TabsTrigger value="preview-deployments">
|
||||||
Preview Deployments
|
Preview Deployments
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,12 +224,12 @@ const Service = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,11 +197,11 @@ const Mariadb = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -198,11 +198,11 @@ const Mongo = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -200,13 +200,13 @@ const MySql = (
|
|||||||
<TabsTrigger value="environment">
|
<TabsTrigger value="environment">
|
||||||
Environment
|
Environment
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">
|
<TabsTrigger value="monitoring">
|
||||||
Monitoring
|
Monitoring
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,11 +197,11 @@ const Postgresql = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,10 +197,10 @@ const Redis = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
28
apps/dokploy/public/templates/datalens.svg
Normal file
28
apps/dokploy/public/templates/datalens.svg
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="163" height="32" fill="none" viewBox="0 0 163 32">
|
||||||
|
<g clip-path="url(#a)">
|
||||||
|
<rect width="32" height="32" fill="#FF6928" rx="16" />
|
||||||
|
<path fill="#fff" fill-rule="evenodd" d="M7.556 29.778C-3 29.778-11.556 23.609-11.556 16c0-7.61 8.557-13.778 19.112-13.778 11.17 0 19.11 5.905 19.11 13.778s-7.94 13.778-19.11 13.778Zm0-8c-10.17 0-17.334-2.587-17.334-5.778 0-3.191 7.163-5.778 17.334-5.778 10.17 0 17.333 2.128 17.333 5.778 0 3.65-7.163 5.778-17.333 5.778Z" clip-rule="evenodd" />
|
||||||
|
<g filter="url(#b)" opacity=".3">
|
||||||
|
<path fill="#FF6928" d="M1.893 11.25C4.192 6.285 5.753 3.667 10.495.177L7.9-5.684-5.308-2.17l-.438 15.1 7.639-1.678Z" />
|
||||||
|
</g>
|
||||||
|
<g filter="url(#c)" opacity=".3">
|
||||||
|
<path fill="#FF6928" d="M12.434 29.6c-4.607-2.955-6.988-4.86-9.798-10.033l-6.16 1.773 1.679 13.563 14.898 2.493-.62-7.796Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path fill="currentColor" d="M46.18 22.416c4.02 0 6.03-2.171 6.03-6.514v-.227c0-2.143-.493-3.745-1.48-4.807-.966-1.081-2.502-1.622-4.607-1.622h-1.82v13.17h1.877ZM39.694 5.662h6.656c3.47 0 6.144.892 8.022 2.674 1.763 1.764 2.645 4.19 2.645 7.282v.227c0 3.13-.891 5.585-2.674 7.367C52.485 25.071 49.811 26 46.322 26h-6.628V5.662Zm23.788 20.65c-1.441 0-2.608-.35-3.499-1.052-.986-.777-1.479-1.905-1.479-3.384 0-1.65.72-2.883 2.162-3.698 1.29-.72 3.148-1.081 5.575-1.081h1.678V16.5c0-.949-.18-1.64-.54-2.077-.342-.436-.968-.654-1.878-.654-1.46 0-2.304.701-2.531 2.105h-3.897c.114-1.669.806-2.958 2.076-3.869 1.157-.815 2.693-1.223 4.608-1.223 1.916 0 3.414.417 4.494 1.252 1.157.91 1.736 2.332 1.736 4.266V26h-4.011v-1.792c-1.005 1.403-2.503 2.105-4.494 2.105Zm1.223-2.872c.93 0 1.697-.237 2.304-.711.607-.474.91-1.119.91-1.934v-1.252h-1.593c-1.251 0-2.2.161-2.844.484-.626.322-.939.863-.939 1.621 0 1.195.72 1.792 2.162 1.792Zm15.734 2.844c-3.204 0-4.807-1.564-4.807-4.693v-7.538h-1.905v-2.93h1.905V7.91h4.096v3.215h3.13v2.93h-3.13v7.167c0 1.176.55 1.764 1.65 1.764.607 0 1.129-.095 1.565-.285v3.186c-.759.266-1.593.398-2.504.398Zm8.92.029c-1.442 0-2.608-.35-3.5-1.053-.985-.777-1.478-1.905-1.478-3.384 0-1.65.72-2.883 2.161-3.698 1.29-.72 3.148-1.081 5.576-1.081h1.678V16.5c0-.949-.18-1.64-.54-2.077-.342-.436-.968-.654-1.878-.654-1.46 0-2.304.701-2.532 2.105H84.95c.114-1.669.806-2.958 2.077-3.869 1.157-.815 2.693-1.223 4.608-1.223 1.915 0 3.413.417 4.494 1.252 1.157.91 1.735 2.332 1.735 4.266V26h-4.01v-1.792c-1.005 1.403-2.503 2.105-4.495 2.105Zm1.223-2.873c.929 0 1.697-.237 2.303-.711.607-.474.91-1.119.91-1.934v-1.252h-1.592c-1.252 0-2.2.161-2.845.484-.625.322-.938.863-.938 1.621 0 1.195.72 1.792 2.162 1.792Zm10.671-17.778h4.608v16.726h8.277V26h-12.885V5.662Zm21.632 20.65c-2.313 0-4.162-.672-5.547-2.019-1.479-1.365-2.218-3.214-2.218-5.546v-.228c0-2.313.739-4.19 2.218-5.632 1.442-1.403 3.252-2.105 5.433-2.105 2.067 0 3.755.598 5.063 1.792 1.46 1.328 2.191 3.252 2.191 5.775v1.137h-10.724c.057 1.252.398 2.219 1.024 2.902.645.663 1.536.995 2.674.995 1.782 0 2.816-.692 3.1-2.076h3.897c-.246 1.612-.986 2.854-2.219 3.726-1.213.853-2.844 1.28-4.892 1.28Zm3.129-9.357c-.133-2.219-1.214-3.328-3.243-3.328-.929 0-1.697.294-2.304.881-.588.57-.957 1.385-1.109 2.447h6.656Zm6.19-5.831h4.125v2.36c.929-1.801 2.541-2.702 4.835-2.702 1.498 0 2.703.455 3.613 1.366.929.986 1.394 2.446 1.394 4.38V26h-4.125v-8.875c0-1.024-.209-1.773-.626-2.247-.417-.493-1.081-.74-1.991-.74-.929 0-1.678.285-2.247.854-.569.55-.853 1.356-.853 2.418V26h-4.125V11.124Zm22.385 15.189c-2.01 0-3.575-.427-4.694-1.28-1.118-.853-1.716-2.086-1.792-3.698h3.84c.114.72.361 1.252.74 1.593.379.341 1.005.512 1.877.512 1.555 0 2.333-.54 2.333-1.621 0-.512-.228-.892-.683-1.138-.455-.266-1.233-.474-2.332-.626-2.01-.322-3.414-.806-4.21-1.45-.853-.664-1.28-1.726-1.28-3.186s.607-2.627 1.82-3.499c1.043-.758 2.399-1.138 4.068-1.138 1.725 0 3.119.342 4.181 1.024 1.119.759 1.773 1.953 1.963 3.584h-3.783c-.114-.626-.351-1.08-.711-1.365-.361-.284-.901-.427-1.622-.427-.663 0-1.185.143-1.564.427a1.35 1.35 0 0 0-.541 1.11c0 .473.2.824.598 1.052.417.227 1.175.417 2.275.569 1.953.265 3.385.71 4.295 1.337.986.739 1.48 1.848 1.48 3.328 0 1.592-.55 2.806-1.65 3.64-1.081.835-2.617 1.252-4.608 1.252Z" />
|
||||||
|
<defs>
|
||||||
|
<filter id="b" width="37.575" height="39.945" x="-16.413" y="-16.35" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feGaussianBlur result="effect1_foregroundBlur_219_14417" stdDeviation="5.333" />
|
||||||
|
</filter>
|
||||||
|
<filter id="c" width="37.91" height="39.162" x="-14.19" y="8.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feGaussianBlur result="effect1_foregroundBlur_219_14417" stdDeviation="5.333" />
|
||||||
|
</filter>
|
||||||
|
<clipPath id="a">
|
||||||
|
<rect width="32" height="32" fill="#fff" rx="16" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
7
apps/dokploy/public/templates/hoarder.svg
Normal file
7
apps/dokploy/public/templates/hoarder.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 355 354" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-232,-118)">
|
||||||
|
<path d="M565.33,118.79L253.02,118.79C241.46,118.79 232.09,128.16 232.09,139.72L232.09,450.44C232.09,462 241.46,471.37 253.02,471.37L565.33,471.37C576.89,471.37 586.26,462 586.26,450.44L586.26,139.72C586.26,128.16 576.89,118.79 565.33,118.79ZM386.85,419.57C386.85,422.58 384.4,425.03 381.39,425.03L285.11,425.03C282.1,425.03 279.65,422.58 279.65,419.57L279.65,169.43C279.65,166.42 282.1,163.96 285.11,163.96L379.87,163.96C382.88,163.96 385.33,166.41 385.33,169.43L385.33,264.64C385.33,264.64 384.81,305.61 386.85,337.76L386.85,419.58L386.85,419.57ZM537.19,419.57C537.19,423.92 532.36,426.52 528.75,424.14L484.85,395.44C482.95,394.18 480.5,394.25 478.64,395.59L440.16,423.4C438.56,424.59 436.67,424.66 435.07,424.07C433.66,423.07 432.73,421.43 432.73,419.57L432.73,225.34C437.94,224.34 443.73,223.78 450.43,223.78C483.29,223.78 537.2,242.37 537.2,294.78L537.2,419.58L537.19,419.57Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -25,6 +25,10 @@ import {
|
|||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/user";
|
||||||
|
import {
|
||||||
|
getProviderHeaders,
|
||||||
|
type Model,
|
||||||
|
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -41,6 +45,58 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return aiSetting;
|
return aiSetting;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getModels: protectedProcedure
|
||||||
|
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
||||||
|
const response = await fetch(`${input.apiUrl}/models`, { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Failed to fetch models: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
return res.map((model) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.models) {
|
||||||
|
return res.models.map((model: any) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
})) as Model[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data) {
|
||||||
|
return res.data as Model[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const possibleModels =
|
||||||
|
(Object.values(res).find(Array.isArray) as any[]) || [];
|
||||||
|
return possibleModels.map((model) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
})) as Model[];
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ 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;
|
||||||
}),
|
}),
|
||||||
@@ -668,4 +669,49 @@ 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
apiRandomizeCompose,
|
apiRandomizeCompose,
|
||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
compose,
|
compose as composeTable,
|
||||||
} 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,6 +24,7 @@ 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";
|
||||||
@@ -157,8 +158,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
4;
|
4;
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.delete(compose)
|
.delete(composeTable)
|
||||||
.where(eq(compose.composeId, input.composeId))
|
.where(eq(composeTable.composeId, input.composeId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
@@ -501,4 +502,48 @@ 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
updateDestinationById,
|
updateDestinationById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, desc } from "drizzle-orm";
|
||||||
|
|
||||||
export const destinationRouter = createTRPCRouter({
|
export const destinationRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
@@ -98,6 +98,7 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.destinations.findMany({
|
return await db.query.destinations.findMany({
|
||||||
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
orderBy: [desc(destinations.createdAt)],
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: adminProcedure
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 {
|
||||||
@@ -30,6 +31,9 @@ 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
|
||||||
@@ -322,4 +326,47 @@ 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 {
|
||||||
@@ -30,6 +31,9 @@ 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
|
||||||
@@ -336,4 +340,47 @@ 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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";
|
||||||
@@ -32,6 +33,9 @@ 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
|
||||||
@@ -332,4 +336,47 @@ 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 {
|
||||||
@@ -30,6 +31,9 @@ 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
|
||||||
@@ -352,4 +356,49 @@ 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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";
|
||||||
@@ -30,6 +31,9 @@ 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
|
||||||
@@ -316,4 +320,47 @@ 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
getUpdateData,
|
getUpdateData,
|
||||||
initializeTraefik,
|
initializeTraefik,
|
||||||
logRotationManager,
|
|
||||||
parseRawConfig,
|
parseRawConfig,
|
||||||
paths,
|
paths,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
@@ -53,6 +52,9 @@ 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";
|
||||||
@@ -577,48 +579,43 @@ 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.query(() => {
|
readStats: adminProcedure
|
||||||
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.object({
|
z
|
||||||
enable: z.boolean(),
|
.object({
|
||||||
}),
|
dateRange: z
|
||||||
|
.object({
|
||||||
|
start: z.string().optional(),
|
||||||
|
end: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.query(({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return [];
|
||||||
}
|
}
|
||||||
if (input.enable) {
|
const rawConfig = readMonitoringConfig(
|
||||||
await logRotationManager.activate();
|
!!input?.dateRange?.start || !!input?.dateRange?.end,
|
||||||
} else {
|
);
|
||||||
await logRotationManager.deactivate();
|
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
|
||||||
}
|
return processedLogs || [];
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
}),
|
||||||
haveActivateRequests: adminProcedure.query(async () => {
|
haveActivateRequests: adminProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
@@ -820,10 +817,20 @@ 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"
|
|
||||||
// }
|
|
||||||
|
|||||||
96
apps/dokploy/templates/datalens/docker-compose.yml
Normal file
96
apps/dokploy/templates/datalens/docker-compose.yml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
services:
|
||||||
|
pg-compeng:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: "postgres"
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
|
||||||
|
control-api:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-control-api:0.2192.0
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
BI_API_UWSGI_WORKERS_COUNT: 4
|
||||||
|
CONNECTOR_AVAILABILITY_VISIBLE: "clickhouse,postgres,chyt,ydb,mysql,greenplum,mssql,appmetrica_api,metrika_api"
|
||||||
|
RQE_FORCE_OFF: 1
|
||||||
|
DL_CRY_ACTUAL_KEY_ID: key_1
|
||||||
|
DL_CRY_KEY_VAL_ID_key_1: "h1ZpilcYLYRdWp7Nk8X1M1kBPiUi8rdjz9oBfHyUKIk="
|
||||||
|
RQE_SECRET_KEY: ""
|
||||||
|
US_HOST: "http://us:8083"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
|
||||||
|
data-api:
|
||||||
|
container_name: datalens-data-api
|
||||||
|
image: ghcr.io/datalens-tech/datalens-data-api:0.2192.0
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
GUNICORN_WORKERS_COUNT: 5
|
||||||
|
RQE_FORCE_OFF: 1
|
||||||
|
CACHES_ON: 0
|
||||||
|
MUTATIONS_CACHES_ON: 0
|
||||||
|
RQE_SECRET_KEY: ""
|
||||||
|
DL_CRY_ACTUAL_KEY_ID: key_1
|
||||||
|
DL_CRY_KEY_VAL_ID_key_1: "h1ZpilcYLYRdWp7Nk8X1M1kBPiUi8rdjz9oBfHyUKIk="
|
||||||
|
BI_COMPENG_PG_ON: 1
|
||||||
|
BI_COMPENG_PG_URL: "postgresql://postgres:postgres@pg-compeng:5432/postgres"
|
||||||
|
US_HOST: "http://us:8083"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
- pg-compeng
|
||||||
|
|
||||||
|
pg-us:
|
||||||
|
container_name: datalens-pg-us
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: us-db-ci_purgeable
|
||||||
|
POSTGRES_USER: us
|
||||||
|
POSTGRES_PASSWORD: us
|
||||||
|
volumes:
|
||||||
|
- ${VOLUME_US:-./metadata}:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
us:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-us:0.310.0
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- pg-us
|
||||||
|
environment:
|
||||||
|
APP_INSTALLATION: "opensource"
|
||||||
|
APP_ENV: "prod"
|
||||||
|
MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
POSTGRES_DSN_LIST: ${METADATA_POSTGRES_DSN_LIST:-postgres://us:us@pg-us:5432/us-db-ci_purgeable}
|
||||||
|
SKIP_INSTALL_DB_EXTENSIONS: ${METADATA_SKIP_INSTALL_DB_EXTENSIONS:-0}
|
||||||
|
USE_DEMO_DATA: ${USE_DEMO_DATA:-0}
|
||||||
|
HC: ${HC:-0}
|
||||||
|
NODE_EXTRA_CA_CERTS: /certs/root.crt
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./certs:/certs
|
||||||
|
|
||||||
|
datalens:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-ui:0.2601.0
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- ${UI_PORT:-8080}:8080
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
- control-api
|
||||||
|
- data-api
|
||||||
|
environment:
|
||||||
|
APP_MODE: "full"
|
||||||
|
APP_ENV: "production"
|
||||||
|
APP_INSTALLATION: "opensource"
|
||||||
|
AUTH_POLICY: "disabled"
|
||||||
|
US_ENDPOINT: "http://us:8083"
|
||||||
|
BI_API_ENDPOINT: "http://control-api:8080"
|
||||||
|
BI_DATA_ENDPOINT: "http://data-api:8080"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
NODE_EXTRA_CA_CERTS: "/usr/local/share/ca-certificates/cert.pem"
|
||||||
|
HC: ${HC:-0}
|
||||||
|
YANDEX_MAP_ENABLED: ${YANDEX_MAP_ENABLED:-0}
|
||||||
|
YANDEX_MAP_TOKEN: ${YANDEX_MAP_TOKEN:-0}
|
||||||
23
apps/dokploy/templates/datalens/index.ts
Normal file
23
apps/dokploy/templates/datalens/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: generateRandomDomain(schema),
|
||||||
|
port: 8080,
|
||||||
|
serviceName: "datalens",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = ["HC=1"];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
apps/dokploy/templates/hoarder/docker-compose.yml
Normal file
45
apps/dokploy/templates/hoarder/docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: ghcr.io/hoarder-app/hoarder:0.22.0
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- hoarder-data:/data
|
||||||
|
ports:
|
||||||
|
- 3000
|
||||||
|
environment:
|
||||||
|
- DISABLE_SIGNUPS
|
||||||
|
- NEXTAUTH_URL
|
||||||
|
- NEXTAUTH_SECRET
|
||||||
|
- MEILI_ADDR=http://meilisearch:7700
|
||||||
|
- BROWSER_WEB_URL=http://chrome:9222
|
||||||
|
- DATA_DIR=/data
|
||||||
|
chrome:
|
||||||
|
image: gcr.io/zenika-hub/alpine-chrome:124
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --no-sandbox
|
||||||
|
- --disable-gpu
|
||||||
|
- --disable-dev-shm-usage
|
||||||
|
- --remote-debugging-address=0.0.0.0
|
||||||
|
- --remote-debugging-port=9222
|
||||||
|
- --hide-scrollbars
|
||||||
|
meilisearch:
|
||||||
|
image: getmeili/meilisearch:v1.6
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MEILI_MASTER_KEY
|
||||||
|
- MEILI_NO_ANALYTICS="true"
|
||||||
|
volumes:
|
||||||
|
- meilisearch-data:/meili_data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://127.0.0.1:7700/health'
|
||||||
|
interval: 2s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 15
|
||||||
|
volumes:
|
||||||
|
meilisearch-data:
|
||||||
|
hoarder-data:
|
||||||
34
apps/dokploy/templates/hoarder/index.ts
Normal file
34
apps/dokploy/templates/hoarder/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateBase64,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const postgresPassword = generatePassword();
|
||||||
|
const nextSecret = generateBase64(32);
|
||||||
|
const meiliMasterKey = generateBase64(32);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 3000,
|
||||||
|
serviceName: "web",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`NEXTAUTH_SECRET=${nextSecret}`,
|
||||||
|
`MEILI_MASTER_KEY=${meiliMasterKey}`,
|
||||||
|
`NEXTAUTH_URL=http://${mainDomain}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# This is an UNOFFICIAL production docker image build for Superset:
|
# This is an UNOFFICIAL production docker image build for Superset:
|
||||||
# - https://github.com/amancevice/docker-superset
|
# - https://github.com/amancevice/docker-superset
|
||||||
|
|
||||||
|
|
||||||
# ## SETUP INSTRUCTIONS
|
# ## SETUP INSTRUCTIONS
|
||||||
#
|
#
|
||||||
# After deploying this image, you will need to run one of the two
|
# After deploying this image, you will need to run one of the two
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
# $ superset-init # Initialise database only
|
# $ superset-init # Initialise database only
|
||||||
#
|
#
|
||||||
# You will be prompted to enter the credentials for the admin user.
|
# You will be prompted to enter the credentials for the admin user.
|
||||||
|
|
||||||
|
|
||||||
# ## NETWORK INSTRUCTIONS
|
# ## NETWORK INSTRUCTIONS
|
||||||
#
|
#
|
||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
superset_redis:
|
superset_redis:
|
||||||
image: redis
|
image: redis
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -108,6 +108,20 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["monitoring"],
|
tags: ["monitoring"],
|
||||||
load: () => import("./grafana/index").then((m) => m.generate),
|
load: () => import("./grafana/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "datalens",
|
||||||
|
name: "DataLens",
|
||||||
|
version: "1.23.0",
|
||||||
|
description: "A modern, scalable business intelligence and data visualization system.",
|
||||||
|
logo: "datalens.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/datalens-tech/datalens",
|
||||||
|
website: "https://datalens.tech/",
|
||||||
|
docs: "https://datalens.tech/docs/",
|
||||||
|
},
|
||||||
|
tags: ["analytics", "self-hosted", "bi", "monitoring"],
|
||||||
|
load: () => import("./datalens/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "directus",
|
id: "directus",
|
||||||
name: "Directus",
|
name: "Directus",
|
||||||
@@ -707,6 +721,21 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["self-hosted", "open-source", "manager"],
|
tags: ["self-hosted", "open-source", "manager"],
|
||||||
load: () => import("./hi-events/index").then((m) => m.generate),
|
load: () => import("./hi-events/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "hoarder",
|
||||||
|
name: "Hoarder",
|
||||||
|
version: "0.22.0",
|
||||||
|
description:
|
||||||
|
'Hoarder is an open source "Bookmark Everything" app that uses AI for automatically tagging the content you throw at it.',
|
||||||
|
logo: "hoarder.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/hoarder/hoarder",
|
||||||
|
website: "https://hoarder.app/",
|
||||||
|
docs: "https://docs.hoarder.app/",
|
||||||
|
},
|
||||||
|
tags: ["self-hosted", "bookmarks", "link-sharing"],
|
||||||
|
load: () => import("./hoarder/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "windows",
|
id: "windows",
|
||||||
name: "Windows (dockerized)",
|
name: "Windows (dockerized)",
|
||||||
|
|||||||
@@ -17,15 +17,17 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
zipline:
|
zipline:
|
||||||
image: ghcr.io/diced/zipline:3.7.9
|
image: ghcr.io/diced/zipline:4
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- CORE_RETURN_HTTPS=${ZIPLINE_RETURN_HTTPS}
|
- CORE_RETURN_HTTPS=${ZIPLINE_RETURN_HTTPS}
|
||||||
- CORE_SECRET=${ZIPLINE_SECRET}
|
- CORE_SECRET=${ZIPLINE_SECRET}
|
||||||
- CORE_HOST=0.0.0.0
|
- CORE_HOSTNAME=0.0.0.0
|
||||||
- CORE_PORT=${ZIPLINE_PORT}
|
- CORE_PORT=${ZIPLINE_PORT}
|
||||||
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
|
- DATABASE_URL=postgres://postgres:postgres@postgres/postgres
|
||||||
- CORE_LOGGER=${ZIPLINE_LOGGER}
|
- CORE_LOGGER=${ZIPLINE_LOGGER}
|
||||||
|
- DATASOURCE_TYPE=local
|
||||||
|
- DATASOURCE_LOCAL_DIRECTORY=./uploads
|
||||||
volumes:
|
volumes:
|
||||||
- "../files/uploads:/zipline/uploads"
|
- "../files/uploads:/zipline/uploads"
|
||||||
- "../files/public:/zipline/public"
|
- "../files/public:/zipline/public"
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ 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"),
|
||||||
@@ -353,6 +354,7 @@ 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(),
|
||||||
@@ -499,11 +501,12 @@ export const apiSaveGitProvider = createSchema
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const apiSaveEnvironmentVariables = createSchema
|
export const apiSaveEnvironmentVariables = z
|
||||||
.pick({
|
.object({
|
||||||
applicationId: true,
|
applicationId: z.string(),
|
||||||
env: true,
|
env: z.string().optional(),
|
||||||
buildArgs: true,
|
buildArgs: z.string().optional(),
|
||||||
|
buildSecrets: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -21,6 +21,7 @@ export const destinations = pgTable("destination", {
|
|||||||
organizationId: text("organizationId")
|
organizationId: text("organizationId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const destinationsRelations = relations(
|
export const destinationsRelations = relations(
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ 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
|
||||||
|
|||||||
@@ -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),
|
||||||
enableLogRotation: boolean("enableLogRotation").notNull().default(false),
|
logCleanupCron: text("logCleanupCron"),
|
||||||
// Metrics
|
// Metrics
|
||||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
metricsConfig: jsonb("metricsConfig")
|
||||||
@@ -250,6 +250,12 @@ 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({
|
||||||
@@ -305,4 +311,5 @@ export const apiUpdateUser = createSchema.partial().extend({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
logCleanupCron: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,3 +116,9 @@ 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";
|
||||||
|
|||||||
@@ -136,24 +136,26 @@ export const getContainersByAppNameMatch = async (
|
|||||||
result = stdout.trim().split("\n");
|
result = stdout.trim().split("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = result.map((line) => {
|
const containers = result
|
||||||
const parts = line.split(" | ");
|
.map((line) => {
|
||||||
const containerId = parts[0]
|
const parts = line.split(" | ");
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
const containerId = parts[0]
|
||||||
: "No container id";
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
const name = parts[1]
|
: "No container id";
|
||||||
? parts[1].replace("Name: ", "").trim()
|
const name = parts[1]
|
||||||
: "No container name";
|
? parts[1].replace("Name: ", "").trim()
|
||||||
|
: "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) {}
|
||||||
@@ -190,28 +192,30 @@ export const getStackContainersByAppName = async (
|
|||||||
result = stdout.trim().split("\n");
|
result = stdout.trim().split("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = result.map((line) => {
|
const containers = result
|
||||||
const parts = line.split(" | ");
|
.map((line) => {
|
||||||
const containerId = parts[0]
|
const parts = line.split(" | ");
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
const containerId = parts[0]
|
||||||
: "No container id";
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
const name = parts[1]
|
: "No container id";
|
||||||
? parts[1].replace("Name: ", "").trim()
|
const name = parts[1]
|
||||||
: "No container name";
|
? parts[1].replace("Name: ", "").trim()
|
||||||
|
: "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) {}
|
||||||
@@ -249,29 +253,31 @@ export const getServiceContainersByAppName = async (
|
|||||||
result = stdout.trim().split("\n");
|
result = stdout.trim().split("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = result.map((line) => {
|
const containers = result
|
||||||
const parts = line.split(" | ");
|
.map((line) => {
|
||||||
const containerId = parts[0]
|
const parts = line.split(" | ");
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
const containerId = parts[0]
|
||||||
: "No container id";
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
const name = parts[1]
|
: "No container id";
|
||||||
? parts[1].replace("Name: ", "").trim()
|
const name = parts[1]
|
||||||
: "No container name";
|
? parts[1].replace("Name: ", "").trim()
|
||||||
|
: "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) {}
|
||||||
@@ -306,23 +312,25 @@ export const getContainersByAppLabel = async (
|
|||||||
|
|
||||||
const lines = stdout.trim().split("\n");
|
const lines = stdout.trim().split("\n");
|
||||||
|
|
||||||
const containers = lines.map((line) => {
|
const containers = lines
|
||||||
const parts = line.split(" | ");
|
.map((line) => {
|
||||||
const containerId = parts[0]
|
const parts = line.split(" | ");
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
const containerId = parts[0]
|
||||||
: "No container id";
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
const name = parts[1]
|
: "No container id";
|
||||||
? parts[1].replace("Name: ", "").trim()
|
const name = parts[1]
|
||||||
: "No container name";
|
? parts[1].replace("Name: ", "").trim()
|
||||||
const state = parts[2]
|
: "No container name";
|
||||||
? parts[2].replace("State: ", "").trim()
|
const state = parts[2]
|
||||||
: "No state";
|
? parts[2].replace("State: ", "").trim()
|
||||||
return {
|
: "No state";
|
||||||
containerId,
|
return {
|
||||||
name,
|
containerId,
|
||||||
state,
|
name,
|
||||||
};
|
state,
|
||||||
});
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
|
|||||||
@@ -1,121 +1,77 @@
|
|||||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
import { 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";
|
||||||
|
|
||||||
class LogRotationManager {
|
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";
|
||||||
private static instance: LogRotationManager;
|
|
||||||
private stream: RotatingFileStream | null = null;
|
|
||||||
|
|
||||||
private constructor() {
|
export const startLogCleanup = async (
|
||||||
if (IS_CLOUD) {
|
cronExpression = "0 0 * * *",
|
||||||
return;
|
): Promise<boolean> => {
|
||||||
}
|
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) {
|
|
||||||
await this.deactivateStream();
|
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
||||||
|
if (existingJob) {
|
||||||
|
existingJob.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stream = createStream("access.log", {
|
scheduleJob(LOG_CLEANUP_JOB_NAME, cronExpression, async () => {
|
||||||
size: "100M",
|
try {
|
||||||
interval: "1d",
|
await execAsync(
|
||||||
path: DYNAMIC_TRAEFIK_PATH,
|
`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`,
|
||||||
rotate: 6,
|
);
|
||||||
compress: "gzip",
|
|
||||||
});
|
|
||||||
|
|
||||||
this.stream.on("rotation", this.handleRotation.bind(this));
|
await execAsync("docker exec dokploy-traefik kill -USR1 1");
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("Error during log cleanup:", error);
|
||||||
private async deactivateStream(): Promise<void> {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
if (this.stream) {
|
|
||||||
this.stream.end(() => {
|
|
||||||
this.stream = null;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
public async activate(): Promise<boolean> {
|
const admin = await findAdmin();
|
||||||
const currentState = await this.getStateFromDB();
|
if (admin) {
|
||||||
if (currentState) {
|
await updateUser(admin.user.id, {
|
||||||
return true;
|
logCleanupCron: cronExpression,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setStateInDB(true);
|
|
||||||
await this.activateStream();
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public async deactivate(): Promise<boolean> {
|
export const stopLogCleanup = async (): Promise<boolean> => {
|
||||||
console.log("Deactivating log rotation...");
|
try {
|
||||||
const currentState = await this.getStateFromDB();
|
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
||||||
if (!currentState) {
|
if (existingJob) {
|
||||||
console.log("Log rotation is already inactive in DB");
|
existingJob.cancel();
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
const admin = await findAdmin();
|
||||||
|
if (admin) {
|
||||||
|
await updateUser(admin.user.id, {
|
||||||
|
logCleanupCron: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setStateInDB(false);
|
|
||||||
await this.deactivateStream();
|
|
||||||
console.log("Log rotation deactivated successfully");
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error stopping log cleanup:", error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private async handleRotation() {
|
export const getLogCleanupStatus = async (): Promise<{
|
||||||
try {
|
enabled: boolean;
|
||||||
const status = await this.getStatus();
|
cronExpression: string | null;
|
||||||
if (!status) {
|
}> => {
|
||||||
await this.deactivateStream();
|
const admin = await findAdmin();
|
||||||
}
|
const cronExpression = admin?.user.logCleanupCron ?? null;
|
||||||
await execAsync(
|
return {
|
||||||
"docker kill -s USR1 $(docker ps -q --filter name=dokploy-traefik)",
|
enabled: cronExpression !== null,
|
||||||
);
|
cronExpression,
|
||||||
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();
|
|
||||||
|
|||||||
@@ -6,14 +6,21 @@ interface HourlyData {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processLogs(logString: string): HourlyData[] {
|
export function processLogs(
|
||||||
|
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")
|
||||||
.compact()
|
.filter((line) => {
|
||||||
|
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);
|
||||||
@@ -21,6 +28,20 @@ export function processLogs(logString: string): HourlyData[] {
|
|||||||
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);
|
||||||
@@ -51,21 +72,46 @@ 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();
|
||||||
|
|
||||||
parsedLogs = parsedLogs.filter(
|
// Apply date range filter if provided
|
||||||
(log) => log.ServiceName !== "dokploy-service-app@file",
|
if (dateRange?.start || dateRange?.end) {
|
||||||
);
|
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) =>
|
||||||
@@ -78,6 +124,7 @@ 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) {
|
||||||
@@ -101,6 +148,7 @@ 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":
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function getProviderName(apiUrl: string) {
|
|||||||
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
|
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
|
||||||
return "ollama";
|
return "ollama";
|
||||||
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
||||||
throw new Error(`Unsupported AI provider for URL: ${apiUrl}`);
|
return "custom";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||||
@@ -67,7 +67,46 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
|||||||
baseURL: config.apiUrl,
|
baseURL: config.apiUrl,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
});
|
});
|
||||||
|
case "custom":
|
||||||
|
return createOpenAICompatible({
|
||||||
|
name: "custom",
|
||||||
|
baseURL: config.apiUrl,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported AI provider: ${providerName}`);
|
throw new Error(`Unsupported AI provider: ${providerName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getProviderHeaders = (
|
||||||
|
apiUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
): Record<string, string> => {
|
||||||
|
// Anthropic
|
||||||
|
if (apiUrl.includes("anthropic")) {
|
||||||
|
return {
|
||||||
|
"x-api-key": apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mistral
|
||||||
|
if (apiUrl.includes("mistral")) {
|
||||||
|
return {
|
||||||
|
Authorization: apiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default (OpenAI style)
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export interface Model {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created: number;
|
||||||
|
owned_by: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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....");
|
||||||
@@ -168,4 +169,8 @@ export const initCronJobs = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (admin?.user.logCleanupCron) {
|
||||||
|
await startLogCleanup(admin.user.logCleanupCron);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ export const buildCustomDocker = async (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
writeStream: WriteStream,
|
writeStream: WriteStream,
|
||||||
) => {
|
) => {
|
||||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
const {
|
||||||
application;
|
appName,
|
||||||
|
env,
|
||||||
|
publishDirectory,
|
||||||
|
buildArgs,
|
||||||
|
buildSecrets,
|
||||||
|
dockerBuildStage,
|
||||||
|
} = application;
|
||||||
const dockerFilePath = getBuildAppDirectory(application);
|
const dockerFilePath = getBuildAppDirectory(application);
|
||||||
try {
|
try {
|
||||||
const image = `${appName}`;
|
const image = `${appName}`;
|
||||||
@@ -25,6 +31,10 @@ 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, "."];
|
||||||
@@ -36,6 +46,12 @@ 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.
|
||||||
@@ -54,6 +70,10 @@ export const buildCustomDocker = async (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
cwd: dockerContextPath || defaultContextPath,
|
cwd: dockerContextPath || defaultContextPath,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...Object.fromEntries(secrets.map((s) => s.split("="))),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -65,8 +85,14 @@ export const getDockerCommand = (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
) => {
|
) => {
|
||||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
const {
|
||||||
application;
|
appName,
|
||||||
|
env,
|
||||||
|
publishDirectory,
|
||||||
|
buildArgs,
|
||||||
|
buildSecrets,
|
||||||
|
dockerBuildStage,
|
||||||
|
} = application;
|
||||||
const dockerFilePath = getBuildAppDirectory(application);
|
const dockerFilePath = getBuildAppDirectory(application);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -79,6 +105,10 @@ 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;
|
||||||
|
|
||||||
@@ -92,6 +122,11 @@ 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.
|
||||||
@@ -105,6 +140,14 @@ 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} || {
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
|||||||
if (groupName) {
|
if (groupName) {
|
||||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||||
}
|
}
|
||||||
return kind === "member";
|
return kind === "user";
|
||||||
});
|
});
|
||||||
const mappedRepositories = filteredRepos.map((repo: any) => {
|
const mappedRepositories = filteredRepos.map((repo: any) => {
|
||||||
return {
|
return {
|
||||||
@@ -433,7 +433,7 @@ export const testGitlabConnection = async (
|
|||||||
if (groupName) {
|
if (groupName) {
|
||||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||||
}
|
}
|
||||||
return kind === "member";
|
return kind === "user";
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredRepos.length;
|
return filteredRepos.length;
|
||||||
|
|||||||
@@ -137,12 +137,44 @@ export const readRemoteConfig = async (serverId: string, appName: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readMonitoringConfig = () => {
|
export const readMonitoringConfig = (readAll = false) => {
|
||||||
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)) {
|
||||||
const yamlStr = fs.readFileSync(configPath, "utf8");
|
if (!readAll) {
|
||||||
return yamlStr;
|
// Read first 500 lines
|
||||||
|
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
1
pnpm-lock.yaml
generated
@@ -6282,6 +6282,7 @@ 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==}
|
||||||
|
|||||||
Reference in New Issue
Block a user