mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2619cb49d1 | ||
|
|
46d12fa9d8 | ||
|
|
51ee46496c | ||
|
|
a13e24dab0 | ||
|
|
4aac3476b6 | ||
|
|
037343a796 | ||
|
|
274d80ea7c | ||
|
|
629889f1a8 | ||
|
|
3e74ce05a7 | ||
|
|
d05218e848 | ||
|
|
0fbad4f75e | ||
|
|
c3cbaf2a57 | ||
|
|
560d493d56 | ||
|
|
27b2106630 | ||
|
|
609954c366 | ||
|
|
84faa9747e | ||
|
|
4b370ef43e | ||
|
|
b94a6bff92 | ||
|
|
276b754377 | ||
|
|
f3b3798362 | ||
|
|
461acc354e | ||
|
|
dfc75a9116 | ||
|
|
e1580bad23 | ||
|
|
b567ec1d83 | ||
|
|
9c73b8dc36 | ||
|
|
7348526873 | ||
|
|
6fc83f2db3 | ||
|
|
43d22c2bd4 | ||
|
|
38a5313967 | ||
|
|
ba3645933f | ||
|
|
17a26353b6 | ||
|
|
e2c163c6d5 | ||
|
|
616e11722c | ||
|
|
91a44706df | ||
|
|
748de47a6d | ||
|
|
cbf9aef0df | ||
|
|
e2befc24a5 | ||
|
|
0f48f2c830 | ||
|
|
5dfa7645f3 | ||
|
|
7fe163dd33 | ||
|
|
19b56771b8 | ||
|
|
cff01ed438 | ||
|
|
10fa3c8cf1 | ||
|
|
6c5497ed21 | ||
|
|
380656efee | ||
|
|
c64d2245ce | ||
|
|
a985998b93 | ||
|
|
4f3ba16dfa | ||
|
|
6c788429f1 | ||
|
|
3176a9d7e3 | ||
|
|
94a6a9587e | ||
|
|
911681f389 | ||
|
|
5992688e85 | ||
|
|
425061e481 | ||
|
|
08c0bf8a21 | ||
|
|
64a2c9e0a1 | ||
|
|
21e46f5382 | ||
|
|
52b2158309 | ||
|
|
178d84d438 | ||
|
|
80016b57a8 | ||
|
|
b4b2d12f6e | ||
|
|
294378d95b | ||
|
|
c52812f9d3 | ||
|
|
82f7c5d5f3 | ||
|
|
3d2ae52259 | ||
|
|
bf115c7895 | ||
|
|
c2c29dbaba | ||
|
|
d4032f34bf | ||
|
|
136570b36c | ||
|
|
7d0075c230 | ||
|
|
19b4edee8d | ||
|
|
7f04eb856e | ||
|
|
5156b45ffc | ||
|
|
80e6f21840 | ||
|
|
5b519151e8 | ||
|
|
aa475e6123 | ||
|
|
66756c34fe | ||
|
|
946a5739dc | ||
|
|
6c817a9e5d | ||
|
|
6aea937e86 | ||
|
|
19612d4b66 | ||
|
|
47dd003461 | ||
|
|
def99225fc | ||
|
|
32405fc61a | ||
|
|
25e1a9af57 | ||
|
|
1fcb1f2c5e | ||
|
|
fdaba7e752 | ||
|
|
c1640cba29 | ||
|
|
3bd54ff61e | ||
|
|
5853d18bc1 | ||
|
|
f575317906 | ||
|
|
e6028e73ac | ||
|
|
bcbed151e8 | ||
|
|
c708f7ba62 | ||
|
|
95a538f261 | ||
|
|
f854457d69 | ||
|
|
cd998c37f1 | ||
|
|
d46a61098b | ||
|
|
8f14d854a0 | ||
|
|
388399b370 | ||
|
|
a8b4bb9c41 | ||
|
|
ebc8c2f73d | ||
|
|
1227d2b5fc | ||
|
|
314438b84c | ||
|
|
cc5574e08a | ||
|
|
11a8fcc476 | ||
|
|
c50229a33c | ||
|
|
0609d74d2b | ||
|
|
fce8eca894 | ||
|
|
3de0d674ed | ||
|
|
7faab54a65 | ||
|
|
40d9db7ccf | ||
|
|
c7c01f57d4 | ||
|
|
45cf295be0 | ||
|
|
79372527e6 | ||
|
|
edcfc7d670 |
BIN
.github/sponsors/agentdock.png
vendored
Normal file
BIN
.github/sponsors/agentdock.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
.github/sponsors/american-cloud.png
vendored
Normal file
BIN
.github/sponsors/american-cloud.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
6
.github/workflows/pull-request.yml
vendored
6
.github/workflows/pull-request.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
|
||||
@@ -52,7 +52,7 @@ feat: add new feature
|
||||
|
||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||
|
||||
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
|
||||
We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dokploy/dokploy.git
|
||||
|
||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
@@ -49,18 +49,18 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
||||
# Install Nixpacks and tsx
|
||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||
|
||||
ARG NIXPACKS_VERSION=1.35.0
|
||||
ARG NIXPACKS_VERSION=1.39.0
|
||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& chmod +x install.sh \
|
||||
&& ./install.sh \
|
||||
&& pnpm install -g tsx
|
||||
|
||||
# Install Railpack
|
||||
ARG RAILPACK_VERSION=0.0.37
|
||||
ARG RAILPACK_VERSION=0.0.64
|
||||
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||
|
||||
# Install buildpacks
|
||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
CMD [ "pnpm", "start" ]
|
||||
|
||||
36
README.md
36
README.md
@@ -80,12 +80,30 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
### Premium Supporters 🥇
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
|
||||
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
|
||||
</a>
|
||||
|
||||
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
|
||||
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
### Elite Contributors 🥈
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
|
||||
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
|
||||
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<!-- Elite Contributors 🥈 -->
|
||||
|
||||
|
||||
|
||||
<!-- Add Elite Contributors here -->
|
||||
|
||||
### Supporting Members 🥉
|
||||
@@ -97,6 +115,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
||||
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -129,19 +148,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||
</a>
|
||||
|
||||
<!-- ## Supported OS
|
||||
|
||||
- Ubuntu 24.04 LTS
|
||||
- Ubuntu 23.10
|
||||
- Ubuntu 22.04 LTS
|
||||
- Ubuntu 20.04 LTS
|
||||
- Ubuntu 18.04 LTS
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8 -->
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
||||
|
||||
@@ -1 +1 @@
|
||||
20.9.0
|
||||
20.16.0
|
||||
@@ -1,26 +0,0 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
# Build only the dokploy app
|
||||
RUN pnpm run dokploy:build
|
||||
|
||||
# Deploy only the dokploy app
|
||||
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
|
||||
|
||||
FROM base AS dokploy
|
||||
COPY --from=build /prod/dokploy /prod/dokploy
|
||||
WORKDIR /prod/dokploy
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
@@ -105,6 +105,7 @@ const baseApp: ApplicationNested = {
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
isStaticSpa: null,
|
||||
redirects: [],
|
||||
refreshToken: "",
|
||||
registry: null,
|
||||
@@ -149,67 +150,68 @@ describe("unzipDrop using real zip files", () => {
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||
baseApp.appName = "folderwithfile";
|
||||
// const appName = "folderwithfile";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with multiple root folders", async () => {
|
||||
baseApp.appName = "two-folders";
|
||||
// const appName = "two-folders";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a file", async () => {
|
||||
baseApp.appName = "nested";
|
||||
// const appName = "nested";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||
expect(files.some((f) => f.name === "folder3")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||
baseApp.appName = "folder-with-sibling-file";
|
||||
// const appName = "folder-with-sibling-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||
// baseApp.appName = "folderwithfile";
|
||||
// // const appName = "folderwithfile";
|
||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||
|
||||
// const zipBuffer = zip.toBuffer();
|
||||
// const file = new File([zipBuffer], "single.zip");
|
||||
// await unzipDrop(file, baseApp);
|
||||
|
||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
// expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||
// });
|
||||
|
||||
// it("should correctly extract a zip with multiple root folders", async () => {
|
||||
// baseApp.appName = "two-folders";
|
||||
// // const appName = "two-folders";
|
||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||
|
||||
// const zipBuffer = zip.toBuffer();
|
||||
// const file = new File([zipBuffer], "single.zip");
|
||||
// await unzipDrop(file, baseApp);
|
||||
|
||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
// expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||
// });
|
||||
|
||||
// it("should correctly extract a zip with a single root with a file", async () => {
|
||||
// baseApp.appName = "nested";
|
||||
// // const appName = "nested";
|
||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||
|
||||
// const zipBuffer = zip.toBuffer();
|
||||
// const file = new File([zipBuffer], "single.zip");
|
||||
// await unzipDrop(file, baseApp);
|
||||
|
||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
// expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||
// expect(files.some((f) => f.name === "folder3")).toBe(true);
|
||||
// });
|
||||
|
||||
// it("should correctly extract a zip with a single root with a folder", async () => {
|
||||
// baseApp.appName = "folder-with-sibling-file";
|
||||
// // const appName = "folder-with-sibling-file";
|
||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||
|
||||
// const zipBuffer = zip.toBuffer();
|
||||
// const file = new File([zipBuffer], "single.zip");
|
||||
// await unzipDrop(file, baseApp);
|
||||
|
||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
// expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -16,6 +16,8 @@ import { beforeEach, expect, test, vi } from "vitest";
|
||||
const baseAdmin: User = {
|
||||
https: false,
|
||||
enablePaidFeatures: false,
|
||||
allowImpersonation: false,
|
||||
role: "user",
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
refreshRate: 20,
|
||||
|
||||
@@ -85,6 +85,7 @@ const baseApp: ApplicationNested = {
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
isStaticSpa: null,
|
||||
redirects: [],
|
||||
refreshToken: "",
|
||||
registry: null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("normalizeS3Path", () => {
|
||||
test("should handle empty and whitespace-only prefix", () => {
|
||||
|
||||
@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
{templateInfo.template.envs.map((env, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-card p-2 font-mono text-sm"
|
||||
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
|
||||
>
|
||||
{env}
|
||||
</div>
|
||||
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
<DialogDescription>Mount File Content</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="h-[25vh] pr-4">
|
||||
<ScrollArea className="h-[45vh] pr-4">
|
||||
<CodeEditor
|
||||
language="yaml"
|
||||
value={selectedMount?.content || ""}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -62,10 +64,11 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
publishDirectory: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.static),
|
||||
buildType: z.literal(BuildType.railpack),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.railpack),
|
||||
buildType: z.literal(BuildType.static),
|
||||
isStaticSpa: z.boolean().default(false),
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -82,6 +85,7 @@ interface ApplicationData {
|
||||
dockerBuildStage?: string | null;
|
||||
herokuVersion?: string | null;
|
||||
publishDirectory?: string | null;
|
||||
isStaticSpa?: boolean | null;
|
||||
}
|
||||
|
||||
function isValidBuildType(value: string): value is BuildType {
|
||||
@@ -114,16 +118,18 @@ const resetData = (data: ApplicationData): AddTemplate => {
|
||||
case BuildType.static:
|
||||
return {
|
||||
buildType: BuildType.static,
|
||||
isStaticSpa: data.isStaticSpa ?? false,
|
||||
};
|
||||
case BuildType.railpack:
|
||||
return {
|
||||
buildType: BuildType.railpack,
|
||||
};
|
||||
default:
|
||||
default: {
|
||||
const buildType = data.buildType as BuildType;
|
||||
return {
|
||||
buildType,
|
||||
} as AddTemplate;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,6 +179,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
data.buildType === BuildType.heroku_buildpacks
|
||||
? data.herokuVersion
|
||||
: null,
|
||||
isStaticSpa:
|
||||
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Build type saved");
|
||||
@@ -200,6 +208,22 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<AlertBlock>
|
||||
Builders can consume significant memory and CPU resources
|
||||
(recommended: 4+ GB RAM and 2+ CPU cores). For production
|
||||
environments, please review our{" "}
|
||||
<a
|
||||
href="https://docs.dokploy.com/docs/core/applications/going-production"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium underline underline-offset-4"
|
||||
>
|
||||
Production Guide
|
||||
</a>{" "}
|
||||
for best practices and optimization recommendations. Builders are
|
||||
suitable for development and prototyping purposes when you have
|
||||
sufficient resources available.
|
||||
</AlertBlock>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 p-2"
|
||||
@@ -347,6 +371,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{buildType === BuildType.static && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isStaticSpa"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-x-2 p-2">
|
||||
<Checkbox
|
||||
id="checkboxIsStaticSpa"
|
||||
value={String(field.value)}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<FormLabel htmlFor="checkboxIsStaticSpa">
|
||||
Single Page Application (SPA)
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -9,12 +10,11 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { type RouterOutputs, api } from "@/utils/api";
|
||||
import { RocketIcon, Clock, Loader2 } from "lucide-react";
|
||||
import { Clock, Loader2, RocketIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CancelQueues } from "./cancel-queues";
|
||||
import { RefreshToken } from "./refresh-token";
|
||||
import { ShowDeployment } from "./show-deployment";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -86,7 +86,7 @@ export const ShowDeployments = ({
|
||||
<span>Webhook URL: </span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="break-all text-muted-foreground">
|
||||
{`${url}/api/deploy/${refreshToken}`}
|
||||
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
|
||||
</span>
|
||||
{(type === "application" || type === "compose") && (
|
||||
<RefreshToken id={id} type={type} />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -6,8 +8,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Copy, HelpCircle, Server } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -7,6 +8,12 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
CheckCircle2,
|
||||
@@ -21,17 +28,10 @@ import {
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { AddDomain } from "./handle-domain";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { toast } from "sonner";
|
||||
import { DnsHelperModal } from "./dns-helper-modal";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AddDomain } from "./handle-domain";
|
||||
|
||||
export type ValidationState = {
|
||||
isLoading: boolean;
|
||||
@@ -186,30 +186,19 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
return (
|
||||
<Card
|
||||
key={item.domainId}
|
||||
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent h-fit"
|
||||
className="relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit"
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Service & Domain Info */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
{item.serviceName && (
|
||||
<Badge variant="outline" className="w-fit">
|
||||
<Server className="size-3 mr-1" />
|
||||
{item.serviceName}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Link
|
||||
className="flex items-center gap-2 text-base font-medium hover:underline"
|
||||
target="_blank"
|
||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||
>
|
||||
{item.host}
|
||||
<ExternalLink className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center justify-between flex-wrap gap-y-2">
|
||||
{item.serviceName && (
|
||||
<Badge variant="outline" className="w-fit">
|
||||
<Server className="size-3 mr-1" />
|
||||
{item.serviceName}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{!item.host.includes("traefik.me") && (
|
||||
<DnsHelperModal
|
||||
domain={{
|
||||
@@ -266,6 +255,16 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full break-all">
|
||||
<Link
|
||||
className="flex items-center gap-2 text-base font-medium hover:underline"
|
||||
target="_blank"
|
||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||
>
|
||||
{item.host}
|
||||
<ExternalLink className="size-4 min-w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Domain Details */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
|
||||
@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules || false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.applicationId, form]);
|
||||
|
||||
const onSubmit = async (data: BitbucketProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -435,7 +435,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -454,7 +454,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||
) as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
registryURL: data.registryUrl || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.applicationId, form]);
|
||||
|
||||
const onSubmit = async (values: DockerProvider) => {
|
||||
await mutateAsync({
|
||||
|
||||
@@ -17,13 +17,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
@@ -262,7 +262,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -281,7 +281,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||
) as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
|
||||
@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules || false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.applicationId, form]);
|
||||
|
||||
const onSubmit = async (data: GiteaProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -470,7 +470,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules ?? false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.applicationId, form]);
|
||||
|
||||
const onSubmit = async (data: GithubProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -474,7 +474,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules ?? false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.applicationId, form]);
|
||||
|
||||
const onSubmit = async (data: GitlabProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -452,7 +452,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
import { AddPreviewDomain } from "./add-preview-domain";
|
||||
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
|
||||
@@ -1,40 +1,6 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Info,
|
||||
PlusCircle,
|
||||
PenBoxIcon,
|
||||
RefreshCw,
|
||||
DatabaseZap,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -42,10 +8,44 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { CacheType } from "../domains/handle-domain";
|
||||
|
||||
export const commonCronExpressions = [
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { HandleSchedules } from "./handle-schedules";
|
||||
import {
|
||||
Clock,
|
||||
Play,
|
||||
Terminal,
|
||||
Trash2,
|
||||
ClipboardList,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -16,16 +8,24 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
ClipboardList,
|
||||
Clock,
|
||||
Loader2,
|
||||
Play,
|
||||
Terminal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
import { HandleSchedules } from "./handle-schedules";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
||||
@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules ?? false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.composeId, form]);
|
||||
|
||||
const onSubmit = async (data: BitbucketProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -437,7 +437,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -456,7 +456,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||
) as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
|
||||
@@ -263,7 +263,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -282,7 +282,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||
) as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
|
||||
@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules ?? false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.composeId, form]);
|
||||
|
||||
const onSubmit = async (data: GiteaProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -437,7 +437,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules ?? false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.composeId, form]);
|
||||
|
||||
const onSubmit = async (data: GithubProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -474,7 +474,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -496,7 +496,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||
) as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
|
||||
@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules ?? false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
}, [form.reset, data?.composeId, form]);
|
||||
|
||||
const onSubmit = async (data: GitlabProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -453,7 +453,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -472,7 +472,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
||||
) as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
|
||||
@@ -71,8 +71,8 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
||||
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||
})
|
||||
.then(async (_data) => {
|
||||
randomizeCompose();
|
||||
refetch();
|
||||
await randomizeCompose();
|
||||
await refetch();
|
||||
toast.success("Compose updated");
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -84,15 +84,10 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
suffix: data?.appName || "",
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
toast.success("Compose Isolated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error isolating the compose");
|
||||
});
|
||||
}).then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -77,8 +77,8 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
randomize: formData?.randomize || false,
|
||||
})
|
||||
.then(async (_data) => {
|
||||
randomizeCompose();
|
||||
refetch();
|
||||
await randomizeCompose();
|
||||
await refetch();
|
||||
toast.success("Compose updated");
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -90,15 +90,10 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
suffix,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
toast.success("Compose randomized");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error randomizing the compose");
|
||||
});
|
||||
}).then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { Puzzle, RefreshCw } from "lucide-react";
|
||||
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -66,36 +66,50 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
Preview your docker-compose file with added domains. Note: At least
|
||||
one domain must be specified for this conversion to take effect.
|
||||
</AlertBlock>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
|
||||
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
|
||||
</div>
|
||||
) : compose?.length === 5 ? (
|
||||
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]">
|
||||
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
|
||||
<span className="text-muted-foreground">
|
||||
No converted compose data available.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
mutateAsync({ composeId })
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Fetched source type");
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error("Error fetching source type", {
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
Refresh <RefreshCw className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
mutateAsync({ composeId })
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Fetched source type");
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error("Error fetching source type", {
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
Refresh <RefreshCw className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
/>
|
||||
</pre>
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
/>
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -48,9 +54,9 @@ import {
|
||||
CheckIcon,
|
||||
ChevronsUpDown,
|
||||
Copy,
|
||||
RotateCcw,
|
||||
RefreshCw,
|
||||
DatabaseZap,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -58,12 +64,6 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
type DatabaseType =
|
||||
| Exclude<ServiceType, "application" | "redis">
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
MysqlIcon,
|
||||
PostgresqlIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -13,6 +20,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
ClipboardList,
|
||||
@@ -25,17 +33,9 @@ import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||
import { RestoreBackup } from "./restore-backup";
|
||||
import { HandleBackup } from "./handle-backup";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
MysqlIcon,
|
||||
PostgresqlIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
|
||||
import { HandleBackup } from "./handle-backup";
|
||||
import { RestoreBackup } from "./restore-backup";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
"use client";
|
||||
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Building2,
|
||||
Calendar,
|
||||
CheckIcon,
|
||||
ChevronsUpDown,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Fingerprint,
|
||||
Key,
|
||||
Server,
|
||||
Settings2,
|
||||
Shield,
|
||||
UserIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type User = typeof authClient.$Infer.Session.user;
|
||||
|
||||
export const ImpersonationBar = () => {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [isImpersonating, setIsImpersonating] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showBar, setShowBar] = useState(false);
|
||||
const { data } = api.user.get.useQuery();
|
||||
|
||||
const fetchUsers = async (search?: string) => {
|
||||
try {
|
||||
const session = await authClient.getSession();
|
||||
if (session?.data?.session?.impersonatedBy) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const response = await authClient.admin.listUsers({
|
||||
query: {
|
||||
limit: 30,
|
||||
...(search && {
|
||||
searchField: "email",
|
||||
searchOperator: "contains",
|
||||
searchValue: search,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const filteredUsers = response.data?.users.filter(
|
||||
// @ts-ignore
|
||||
(user) => user.allowImpersonation && data?.user?.email !== user.email,
|
||||
);
|
||||
|
||||
if (!response.error) {
|
||||
// @ts-ignore
|
||||
setUsers(filteredUsers || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
toast.error("Error loading users");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImpersonate = async () => {
|
||||
if (!selectedUser) return;
|
||||
|
||||
try {
|
||||
await authClient.admin.impersonateUser({
|
||||
userId: selectedUser.id,
|
||||
});
|
||||
setIsImpersonating(true);
|
||||
setOpen(false);
|
||||
|
||||
toast.success("Successfully impersonating user", {
|
||||
description: `You are now viewing as ${selectedUser.name || selectedUser.email}`,
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error("Error impersonating user:", error);
|
||||
toast.error("Error impersonating user");
|
||||
}
|
||||
};
|
||||
|
||||
const handleStopImpersonating = async () => {
|
||||
try {
|
||||
await authClient.admin.stopImpersonating();
|
||||
setIsImpersonating(false);
|
||||
setSelectedUser(null);
|
||||
setShowBar(false);
|
||||
toast.success("Stopped impersonating user");
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error("Error stopping impersonation:", error);
|
||||
toast.error("Error stopping impersonation");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkImpersonation = async () => {
|
||||
try {
|
||||
const session = await authClient.getSession();
|
||||
if (session?.data?.session?.impersonatedBy) {
|
||||
setIsImpersonating(true);
|
||||
setShowBar(true);
|
||||
// setSelectedUser(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking impersonation status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
checkImpersonation();
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"fixed bottom-4 right-4 z-50 rounded-full shadow-lg",
|
||||
isImpersonating &&
|
||||
!showBar &&
|
||||
"bg-red-100 hover:bg-red-200 border-red-200",
|
||||
)}
|
||||
onClick={() => setShowBar(!showBar)}
|
||||
>
|
||||
<Settings2
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isImpersonating && !showBar && "text-red-500",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isImpersonating ? "Impersonation Controls" : "User Impersonation"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"fixed bottom-0 left-0 right-0 bg-background border-t border-border p-4 flex items-center justify-center gap-4 z-40 transition-all duration-200 ease-in-out",
|
||||
showBar ? "translate-y-0" : "translate-y-full",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
||||
<Logo className="w-10 h-10" />
|
||||
{!isImpersonating ? (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-expanded={open}
|
||||
className="w-[300px] justify-between"
|
||||
>
|
||||
{selectedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{selectedUser.name || ""}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{selectedUser.email}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<UserIcon className="mr-2 h-4 w-4" />
|
||||
<span>Select user to impersonate</span>
|
||||
</>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search users by email or name..."
|
||||
onValueChange={(search) => {
|
||||
fetchUsers(search);
|
||||
}}
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
Loading users...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CommandEmpty>No users found.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup heading="All Users">
|
||||
{users.map((user) => (
|
||||
<CommandItem
|
||||
key={user.id}
|
||||
value={user.email}
|
||||
onSelect={() => {
|
||||
setSelectedUser(user);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2 flex-1">
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{user.name || ""}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{user.email} • {user.role}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
selectedUser?.id === user.id
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</>
|
||||
)}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
onClick={handleImpersonate}
|
||||
disabled={!selectedUser}
|
||||
variant="default"
|
||||
className="gap-2"
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
Impersonate
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4 w-full flex-wrap">
|
||||
<div className="flex items-center gap-4 flex-1 flex-wrap">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage
|
||||
src={data?.user?.image || ""}
|
||||
alt={data?.user?.name || ""}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{data?.user?.name?.slice(0, 2).toUpperCase() || "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="gap-1 py-1 text-yellow-500 bg-yellow-50/20"
|
||||
>
|
||||
<Shield className="h-3 w-3" />
|
||||
Impersonating
|
||||
</Badge>
|
||||
<span className="font-medium">
|
||||
{data?.user?.name || ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
|
||||
<span className="flex items-center gap-1">
|
||||
<UserIcon className="h-3 w-3" />
|
||||
{data?.user?.email} • {data?.role}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Key className="h-3 w-3" />
|
||||
<span className="flex items-center gap-1">
|
||||
ID: {data?.user?.id?.slice(0, 8)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
if (data?.id) {
|
||||
copy(data.id);
|
||||
toast.success("ID copied to clipboard");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span className="flex items-center gap-1">
|
||||
Org: {data?.organizationId?.slice(0, 8)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
if (data?.organizationId) {
|
||||
copy(data.organizationId);
|
||||
toast.success(
|
||||
"Organization ID copied to clipboard",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
{data?.user?.stripeCustomerId && (
|
||||
<span className="flex items-center gap-1">
|
||||
<CreditCard className="h-3 w-3" />
|
||||
<span className="flex items-center gap-1">
|
||||
Customer:
|
||||
{data?.user?.stripeCustomerId?.slice(0, 8)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
copy(data?.user?.stripeCustomerId || "");
|
||||
toast.success(
|
||||
"Stripe Customer ID copied to clipboard",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{data?.user?.stripeSubscriptionId && (
|
||||
<span className="flex items-center gap-1">
|
||||
<CreditCard className="h-3 w-3" />
|
||||
<span className="flex items-center gap-1">
|
||||
Sub: {data?.user?.stripeSubscriptionId?.slice(0, 8)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
copy(data.user.stripeSubscriptionId || "");
|
||||
toast.success(
|
||||
"Stripe Subscription ID copied to clipboard",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{data?.user?.serversQuantity !== undefined && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Server className="h-3 w-3" />
|
||||
<span>Servers: {data.user.serversQuantity}</span>
|
||||
</span>
|
||||
)}
|
||||
{data?.createdAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
Created:{" "}
|
||||
{format(new Date(data.createdAt), "MMM d, yyyy")}
|
||||
</span>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center gap-1 cursor-default">
|
||||
<Fingerprint
|
||||
className={cn(
|
||||
"h-3 w-3",
|
||||
data?.user?.twoFactorEnabled
|
||||
? "text-green-500"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
<Badge
|
||||
variant={
|
||||
data?.user?.twoFactorEnabled
|
||||
? "green"
|
||||
: "secondary"
|
||||
}
|
||||
className="text-[10px] px-1 py-0"
|
||||
>
|
||||
2FA{" "}
|
||||
{data?.user?.twoFactorEnabled
|
||||
? "Enabled"
|
||||
: "Disabled"}
|
||||
</Badge>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Two-Factor Authentication Status
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleStopImpersonating}
|
||||
variant="secondary"
|
||||
className="gap-2"
|
||||
size="sm"
|
||||
>
|
||||
<XIcon className="w-4 h-4" />
|
||||
Stop Impersonating
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { api } from "@/utils/api";
|
||||
import { Copy, Loader2 } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
|
||||
api.project.duplicate.useMutation({
|
||||
onSuccess: async (newProject) => {
|
||||
await utils.project.all.invalidate();
|
||||
toast.success("Project duplicated successfully");
|
||||
toast.success(
|
||||
duplicateType === "new-project"
|
||||
? "Project duplicated successfully"
|
||||
: "Services duplicated successfully",
|
||||
);
|
||||
setOpen(false);
|
||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||
if (duplicateType === "new-project") {
|
||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
|
||||
});
|
||||
|
||||
const handleDuplicate = async () => {
|
||||
if (!name) {
|
||||
if (duplicateType === "new-project" && !name) {
|
||||
toast.error("Project name is required");
|
||||
return;
|
||||
}
|
||||
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
|
||||
id: service.id,
|
||||
type: service.type,
|
||||
})),
|
||||
duplicateInSameProject: duplicateType === "same-project",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
|
||||
// Reset form when closing
|
||||
setName("");
|
||||
setDescription("");
|
||||
setDuplicateType("new-project");
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Duplicate Project</DialogTitle>
|
||||
<DialogTitle>Duplicate Services</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new project with the selected services
|
||||
Choose where to duplicate the selected services
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="New project name"
|
||||
/>
|
||||
<Label>Duplicate to</Label>
|
||||
<RadioGroup
|
||||
value={duplicateType}
|
||||
onValueChange={setDuplicateType}
|
||||
className="grid gap-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="new-project" id="new-project" />
|
||||
<Label htmlFor="new-project">New project</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="same-project" id="same-project" />
|
||||
<Label htmlFor="same-project">Same project</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Project description (optional)"
|
||||
/>
|
||||
</div>
|
||||
{duplicateType === "new-project" && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="New project name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Project description (optional)"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Selected services to duplicate</Label>
|
||||
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Duplicating...
|
||||
{duplicateType === "new-project"
|
||||
? "Duplicating project..."
|
||||
: "Duplicating services..."}
|
||||
</>
|
||||
) : duplicateType === "new-project" ? (
|
||||
"Duplicate project"
|
||||
) : (
|
||||
"Duplicate"
|
||||
"Duplicate services"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { generateSHA256Hash } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -34,6 +36,7 @@ const profileSchema = z.object({
|
||||
password: z.string().nullable(),
|
||||
currentPassword: z.string().nullable(),
|
||||
image: z.string().optional(),
|
||||
allowImpersonation: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
type Profile = z.infer<typeof profileSchema>;
|
||||
@@ -56,6 +59,7 @@ const randomImages = [
|
||||
export const ProfileForm = () => {
|
||||
const _utils = api.useUtils();
|
||||
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const {
|
||||
mutateAsync,
|
||||
@@ -79,6 +83,7 @@ export const ProfileForm = () => {
|
||||
password: "",
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: "",
|
||||
allowImpersonation: data?.user?.allowImpersonation || false,
|
||||
},
|
||||
resolver: zodResolver(profileSchema),
|
||||
});
|
||||
@@ -91,11 +96,13 @@ export const ProfileForm = () => {
|
||||
password: form.getValues("password") || "",
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: form.getValues("currentPassword") || "",
|
||||
allowImpersonation: data?.user?.allowImpersonation,
|
||||
},
|
||||
{
|
||||
keepValues: true,
|
||||
},
|
||||
);
|
||||
form.setValue("allowImpersonation", data?.user?.allowImpersonation);
|
||||
|
||||
if (data.user.email) {
|
||||
generateSHA256Hash(data.user.email).then((hash) => {
|
||||
@@ -111,6 +118,7 @@ export const ProfileForm = () => {
|
||||
password: values.password || undefined,
|
||||
image: values.image,
|
||||
currentPassword: values.currentPassword || undefined,
|
||||
allowImpersonation: values.allowImpersonation,
|
||||
})
|
||||
.then(async () => {
|
||||
await refetch();
|
||||
@@ -256,7 +264,34 @@ export const ProfileForm = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allowImpersonation"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Allow Impersonation</FormLabel>
|
||||
<FormDescription>
|
||||
Enable this option to allow Dokploy Cloud
|
||||
administrators to temporarily access your
|
||||
account for troubleshooting and support
|
||||
purposes. This helps them quickly identify and
|
||||
resolve any issues you may encounter.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button type="submit" isLoading={isUpdating}>
|
||||
{t("settings.common.save")}
|
||||
|
||||
@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
|
||||
remotely.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<p className="text-primary text-sm font-medium">
|
||||
You will need to purchase or rent a Virtual Private Server (VPS) to
|
||||
proceed, we recommend to use one of these providers since has been
|
||||
heavily tested.
|
||||
</p>
|
||||
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
|
||||
className="text-link underline"
|
||||
>
|
||||
Hostinger - Get 20% Discount
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href=" https://app.americancloud.com/register?ref=dokploy"
|
||||
className="text-link underline"
|
||||
>
|
||||
American Cloud - Get $20 Credits
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://m.do.co/c/db24efd43f35"
|
||||
className="text-link underline"
|
||||
>
|
||||
DigitalOcean - Get $200 Credits
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
|
||||
className="text-link underline"
|
||||
>
|
||||
Hetzner - Get €20 Credits
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.vultr.com/?ref=9679828"
|
||||
className="text-link underline"
|
||||
>
|
||||
Vultr
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.linode.com/es/pricing/#compute-shared"
|
||||
className="text-link underline"
|
||||
>
|
||||
Linode
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<AlertBlock className="mt-4 px-4">
|
||||
You are free to use whatever provider, but we recommend to use one
|
||||
of the above, to avoid issues.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
{!canCreateMoreServers && (
|
||||
<AlertBlock type="warning">
|
||||
You cannot create more servers,{" "}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
|
||||
@@ -40,10 +40,10 @@ import { HandleServers } from "./handle-servers";
|
||||
import { SetupServer } from "./setup-server";
|
||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
||||
import { ShowMonitoringModal } from "./show-monitoring-modal";
|
||||
import { ShowSchedulesModal } from "./show-schedules-modal";
|
||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||
import { ShowSchedulesModal } from "./show-schedules-modal";
|
||||
|
||||
export const ShowServers = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
@@ -177,6 +177,14 @@ export const WelcomeSuscription = () => {
|
||||
Hostinger - Get 20% Discount
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href=" https://app.americancloud.com/register?ref=dokploy"
|
||||
className="text-link underline"
|
||||
>
|
||||
American Cloud - Get $20 Credits
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://m.do.co/c/db24efd43f35"
|
||||
|
||||
@@ -185,24 +185,21 @@ export const ShowInvitations = () => {
|
||||
Cancel Invitation
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={async (_e) => {
|
||||
await removeInvitation({
|
||||
invitationId: invitation.id,
|
||||
}).then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
"Invitation removed",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Invitation
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={async (_e) => {
|
||||
await removeInvitation({
|
||||
invitationId: invitation.id,
|
||||
}).then(() => {
|
||||
refetch();
|
||||
toast.success("Invitation removed");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Invitation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
||||
import Page from "./side";
|
||||
import { ChatwootWidget } from "../shared/ChatwootWidget";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
metaName?: string;
|
||||
}
|
||||
|
||||
export const DashboardLayout = ({ children }: Props) => {
|
||||
return <Page>{children}</Page>;
|
||||
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
{isCloud === true && (
|
||||
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
||||
)}
|
||||
|
||||
{haveRootAccess === true && <ImpersonationBar />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import Page from "./side";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ProjectLayout = ({ children }: Props) => {
|
||||
return <Page>{children}</Page>;
|
||||
};
|
||||
69
apps/dokploy/components/shared/ChatwootWidget.tsx
Normal file
69
apps/dokploy/components/shared/ChatwootWidget.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import Script from "next/script";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface ChatwootWidgetProps {
|
||||
websiteToken: string;
|
||||
baseUrl?: string;
|
||||
settings?: {
|
||||
position?: "left" | "right";
|
||||
type?: "standard" | "expanded_bubble";
|
||||
launcherTitle?: string;
|
||||
darkMode?: boolean;
|
||||
hideMessageBubble?: boolean;
|
||||
placement?: "right" | "left";
|
||||
showPopoutButton?: boolean;
|
||||
widgetStyle?: "standard" | "bubble";
|
||||
};
|
||||
user?: {
|
||||
identifier: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
avatarUrl?: string;
|
||||
customAttributes?: Record<string, any>;
|
||||
identifierHash?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ChatwootWidget = ({
|
||||
websiteToken,
|
||||
baseUrl = "https://app.chatwoot.com",
|
||||
settings = {
|
||||
position: "right",
|
||||
type: "standard",
|
||||
launcherTitle: "Chat with us",
|
||||
},
|
||||
user,
|
||||
}: ChatwootWidgetProps) => {
|
||||
useEffect(() => {
|
||||
// Configurar los settings de Chatwoot
|
||||
window.chatwootSettings = {
|
||||
position: "right",
|
||||
};
|
||||
|
||||
(window as any).chatwootSDKReady = () => {
|
||||
window.chatwootSDK?.run({ websiteToken, baseUrl });
|
||||
|
||||
const trySetUser = () => {
|
||||
if (window.$chatwoot && user) {
|
||||
window.$chatwoot.setUser(user.identifier, {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar_url: user.avatarUrl,
|
||||
phone_number: user.phoneNumber,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
trySetUser();
|
||||
};
|
||||
}, [websiteToken, baseUrl, user, settings]);
|
||||
|
||||
return (
|
||||
<Script
|
||||
src={`${baseUrl}/packs/js/sdk.js`}
|
||||
strategy="lazyOnload"
|
||||
onLoad={() => (window as any).chatwootSDKReady?.()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
1
apps/dokploy/drizzle/0090_clean_wolf_cub.sql
Normal file
1
apps/dokploy/drizzle/0090_clean_wolf_cub.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user_temp" ADD COLUMN "allowImpersonation" boolean DEFAULT false NOT NULL;
|
||||
1
apps/dokploy/drizzle/0091_spotty_kulan_gath.sql
Normal file
1
apps/dokploy/drizzle/0091_spotty_kulan_gath.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user_temp" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;
|
||||
1
apps/dokploy/drizzle/0092_stiff_the_watchers.sql
Normal file
1
apps/dokploy/drizzle/0092_stiff_the_watchers.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "isStaticSpa" boolean;
|
||||
5704
apps/dokploy/drizzle/meta/0090_snapshot.json
Normal file
5704
apps/dokploy/drizzle/meta/0090_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5711
apps/dokploy/drizzle/meta/0091_snapshot.json
Normal file
5711
apps/dokploy/drizzle/meta/0091_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5717
apps/dokploy/drizzle/meta/0092_snapshot.json
Normal file
5717
apps/dokploy/drizzle/meta/0092_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -631,6 +631,27 @@
|
||||
"when": 1746392564463,
|
||||
"tag": "0089_noisy_sandman",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 90,
|
||||
"version": "7",
|
||||
"when": 1746509318678,
|
||||
"tag": "0090_clean_wolf_cub",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 91,
|
||||
"version": "7",
|
||||
"when": 1746518402168,
|
||||
"tag": "0091_spotty_kulan_gath",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 92,
|
||||
"version": "7",
|
||||
"when": 1747713229160,
|
||||
"tag": "0092_stiff_the_watchers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
import { organizationClient } from "better-auth/client/plugins";
|
||||
import { twoFactorClient } from "better-auth/client/plugins";
|
||||
import { apiKeyClient } from "better-auth/client/plugins";
|
||||
import { adminClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
// baseURL: "http://localhost:3000", // the base url of your auth server
|
||||
plugins: [organizationClient(), twoFactorClient(), apiKeyClient()],
|
||||
plugins: [
|
||||
organizationClient(),
|
||||
twoFactorClient(),
|
||||
apiKeyClient(),
|
||||
adminClient(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.22.0",
|
||||
"version": "v0.22.7",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -36,6 +36,8 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"@ai-sdk/anthropic": "^1.0.6",
|
||||
"@ai-sdk/azure": "^1.0.15",
|
||||
"@ai-sdk/cohere": "^1.0.6",
|
||||
@@ -92,7 +94,7 @@
|
||||
"adm-zip": "^0.5.14",
|
||||
"ai": "^4.0.23",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "1.2.6",
|
||||
"better-auth": "v1.2.8-beta.7",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"bullmq": "5.4.2",
|
||||
@@ -186,7 +188,7 @@
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0",
|
||||
"engines": {
|
||||
"node": "^20.9.0",
|
||||
"node": "^20.16.0",
|
||||
"pnpm": ">=9.5.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -17,7 +17,6 @@ const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
// session: Session | null;
|
||||
theme?: string;
|
||||
};
|
||||
|
||||
@@ -33,11 +32,13 @@ const MyApp = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-inter: ${inter.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
<style jsx global>
|
||||
{`
|
||||
:root {
|
||||
--font-inter: ${inter.style.fontFamily};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<Head>
|
||||
<title>Dokploy</title>
|
||||
</Head>
|
||||
|
||||
@@ -80,7 +80,13 @@ export default function Custom404({ statusCode, error }: Props) {
|
||||
<footer className="mt-auto text-center py-5">
|
||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<p className="text-sm text-gray-500">
|
||||
Submit Log in issue on Github
|
||||
<Link
|
||||
href="https://github.com/Dokploy/dokploy/issues"
|
||||
target="_blank"
|
||||
className="underline hover:text-primary transition-colors"
|
||||
>
|
||||
Submit Log in issue on Github
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PostgresqlIcon,
|
||||
RedisIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -18,6 +18,7 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant";
|
||||
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -93,7 +94,6 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -1064,7 +1064,7 @@ const Project = (
|
||||
|
||||
export default Project;
|
||||
Project.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -17,7 +17,7 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -363,7 +363,7 @@ const Service = (
|
||||
|
||||
export default Service;
|
||||
Service.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -13,7 +13,7 @@ import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
|
||||
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -217,12 +217,12 @@ const Service = (
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
||||
"xl:grid xl:w-fit max-md:overflow-y-scroll justify-start",
|
||||
isCloud && data?.serverId
|
||||
? "lg:grid-cols-9"
|
||||
? "xl:grid-cols-9"
|
||||
: data?.serverId
|
||||
? "lg:grid-cols-8"
|
||||
: "lg:grid-cols-9",
|
||||
? "xl:grid-cols-8"
|
||||
: "xl:grid-cols-9",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
@@ -366,7 +366,7 @@ const Service = (
|
||||
|
||||
export default Service;
|
||||
Service.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
|
||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -294,7 +294,7 @@ const Mariadb = (
|
||||
|
||||
export default Mariadb;
|
||||
Mariadb.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
|
||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -296,7 +296,7 @@ const Mongo = (
|
||||
|
||||
export default Mongo;
|
||||
Mongo.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/gener
|
||||
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -280,7 +280,7 @@ const MySql = (
|
||||
|
||||
export default MySql;
|
||||
MySql.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres
|
||||
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -278,7 +278,7 @@ const Postgresql = (
|
||||
|
||||
export default Postgresql;
|
||||
Postgresql.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/gener
|
||||
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -285,7 +285,7 @@ const Redis = (
|
||||
|
||||
export default Redis;
|
||||
Redis.getLayout = (page: ReactElement) => {
|
||||
return <ProjectLayout>{page}</ProjectLayout>;
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import type { ReactElement } from "react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
function SchedulesPage() {
|
||||
const { data: user } = api.user.get.useQuery();
|
||||
return (
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
import { Card } from "@/components/ui/card";
|
||||
const Page = () => {
|
||||
const { data: user } = api.user.get.useQuery();
|
||||
return (
|
||||
|
||||
@@ -28,6 +28,7 @@ import { projectRouter } from "./routers/project";
|
||||
import { redirectsRouter } from "./routers/redirects";
|
||||
import { redisRouter } from "./routers/redis";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
import { scheduleRouter } from "./routers/schedule";
|
||||
import { securityRouter } from "./routers/security";
|
||||
import { serverRouter } from "./routers/server";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
@@ -35,7 +36,6 @@ import { sshRouter } from "./routers/ssh-key";
|
||||
import { stripeRouter } from "./routers/stripe";
|
||||
import { swarmRouter } from "./routers/swarm";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { scheduleRouter } from "./routers/schedule";
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
|
||||
@@ -330,6 +330,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
dockerContextPath: input.dockerContextPath,
|
||||
dockerBuildStage: input.dockerBuildStage,
|
||||
herokuVersion: input.herokuVersion,
|
||||
isStaticSpa: input.isStaticSpa,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -51,9 +51,9 @@ import { processTemplate } from "@dokploy/server/templates/processors";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { dump } from "js-yaml";
|
||||
import { parse } from "toml";
|
||||
import _ from "lodash";
|
||||
import { nanoid } from "nanoid";
|
||||
import { parse } from "toml";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
|
||||
@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const projectName = slugify(`${project.name} ${input.id}`);
|
||||
const generate = processTemplate(template.config, {
|
||||
const appName = `${projectName}-${generatePassword(6)}`;
|
||||
const config = {
|
||||
...template.config,
|
||||
variables: {
|
||||
APP_NAME: appName,
|
||||
...template.config.variables,
|
||||
},
|
||||
};
|
||||
const generate = processTemplate(config, {
|
||||
serverIp: serverIp,
|
||||
projectName: projectName,
|
||||
});
|
||||
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
|
||||
serverId: input.serverId,
|
||||
name: input.id,
|
||||
sourceType: "raw",
|
||||
appName: `${projectName}-${generatePassword(6)}`,
|
||||
appName: appName,
|
||||
isolatedDeployment: true,
|
||||
});
|
||||
|
||||
@@ -605,7 +613,15 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const processedTemplate = processTemplate(config, {
|
||||
const configModified = {
|
||||
...config,
|
||||
variables: {
|
||||
APP_NAME: compose.appName,
|
||||
...config.variables,
|
||||
},
|
||||
};
|
||||
|
||||
const processedTemplate = processTemplate(configModified, {
|
||||
serverIp: serverIp,
|
||||
projectName: compose.appName,
|
||||
});
|
||||
@@ -675,7 +691,15 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const processedTemplate = processTemplate(config, {
|
||||
const configModified = {
|
||||
...config,
|
||||
variables: {
|
||||
APP_NAME: compose.appName,
|
||||
...config.variables,
|
||||
},
|
||||
};
|
||||
|
||||
const processedTemplate = processTemplate(configModified, {
|
||||
serverIp: serverIp,
|
||||
projectName: compose.appName,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiFindAllByApplication,
|
||||
apiFindAllByCompose,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { db } from "@/server/db";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
|
||||
@@ -21,32 +21,32 @@ import {
|
||||
addNewProject,
|
||||
checkProjectAccess,
|
||||
createApplication,
|
||||
createBackup,
|
||||
createCompose,
|
||||
createDomain,
|
||||
createMariadb,
|
||||
createMongo,
|
||||
createMount,
|
||||
createMysql,
|
||||
createPort,
|
||||
createPostgres,
|
||||
createPreviewDeployment,
|
||||
createProject,
|
||||
createRedirect,
|
||||
createRedis,
|
||||
createSecurity,
|
||||
deleteProject,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findMongoById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findRedisById,
|
||||
findMongoById,
|
||||
findMySqlById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
findRedisById,
|
||||
findUserById,
|
||||
updateProjectById,
|
||||
findPostgresById,
|
||||
findMariadbById,
|
||||
findMySqlById,
|
||||
createDomain,
|
||||
createPort,
|
||||
createMount,
|
||||
createRedirect,
|
||||
createPreviewDeployment,
|
||||
createBackup,
|
||||
createSecurity,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, sql } from "drizzle-orm";
|
||||
@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
duplicateInSameProject: z.boolean().default(false),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Create new project
|
||||
const newProject = await createProject(
|
||||
{
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
env: sourceProject.env,
|
||||
},
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
// Create new project or use existing one
|
||||
const targetProject = input.duplicateInSameProject
|
||||
? sourceProject
|
||||
: await createProject(
|
||||
{
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
env: sourceProject.env,
|
||||
},
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (input.includeServices) {
|
||||
const servicesToDuplicate = input.selectedServices || [];
|
||||
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
|
||||
|
||||
const newApplication = await createApplication({
|
||||
...application,
|
||||
projectId: newProject.projectId,
|
||||
name: input.duplicateInSameProject
|
||||
? `${application.name} (copy)`
|
||||
: application.name,
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const domain of domains) {
|
||||
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
|
||||
|
||||
const newPostgres = await createPostgres({
|
||||
...postgres,
|
||||
projectId: newProject.projectId,
|
||||
name: input.duplicateInSameProject
|
||||
? `${postgres.name} (copy)`
|
||||
: postgres.name,
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
|
||||
await findMariadbById(id);
|
||||
const newMariadb = await createMariadb({
|
||||
...mariadb,
|
||||
projectId: newProject.projectId,
|
||||
name: input.duplicateInSameProject
|
||||
? `${mariadb.name} (copy)`
|
||||
: mariadb.name,
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
|
||||
await findMongoById(id);
|
||||
const newMongo = await createMongo({
|
||||
...mongo,
|
||||
projectId: newProject.projectId,
|
||||
name: input.duplicateInSameProject
|
||||
? `${mongo.name} (copy)`
|
||||
: mongo.name,
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
|
||||
await findMySqlById(id);
|
||||
const newMysql = await createMysql({
|
||||
...mysql,
|
||||
projectId: newProject.projectId,
|
||||
name: input.duplicateInSameProject
|
||||
? `${mysql.name} (copy)`
|
||||
: mysql.name,
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
|
||||
const { redisId, mounts, ...redis } = await findRedisById(id);
|
||||
const newRedis = await createRedis({
|
||||
...redis,
|
||||
projectId: newProject.projectId,
|
||||
name: input.duplicateInSameProject
|
||||
? `${redis.name} (copy)`
|
||||
: redis.name,
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
|
||||
await findComposeById(id);
|
||||
const newCompose = await createCompose({
|
||||
...compose,
|
||||
projectId: newProject.projectId,
|
||||
name: input.duplicateInSameProject
|
||||
? `${compose.name} (copy)`
|
||||
: compose.name,
|
||||
projectId: targetProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
|
||||
};
|
||||
|
||||
// Duplicate selected services
|
||||
|
||||
for (const service of servicesToDuplicate) {
|
||||
await duplicateService(service.id, service.type);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
newProject.projectId,
|
||||
targetProject.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
|
||||
return newProject;
|
||||
return targetProject;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
|
||||
import { removeScheduleJob } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { deployments } from "@dokploy/server/db/schema/deployment";
|
||||
import {
|
||||
createScheduleSchema,
|
||||
schedules,
|
||||
updateScheduleSchema,
|
||||
} from "@dokploy/server/db/schema/schedule";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { runCommand } from "@dokploy/server/index";
|
||||
import { deployments } from "@dokploy/server/db/schema/deployment";
|
||||
import {
|
||||
createSchedule,
|
||||
deleteSchedule,
|
||||
findScheduleById,
|
||||
createSchedule,
|
||||
updateSchedule,
|
||||
} from "@dokploy/server/services/schedule";
|
||||
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
|
||||
import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import { removeScheduleJob } from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
export const scheduleRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(createScheduleSchema)
|
||||
|
||||
@@ -91,6 +91,18 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
process.env.USER_ADMIN_ID === ctx.user.id ||
|
||||
ctx.session?.impersonatedBy === process.env.USER_ADMIN_ID
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
getBackups: adminProcedure.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
|
||||
@@ -31,7 +31,9 @@ import { ZodError } from "zod";
|
||||
|
||||
interface CreateContextOptions {
|
||||
user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null;
|
||||
session: (Session & { activeOrganizationId: string }) | null;
|
||||
session:
|
||||
| (Session & { activeOrganizationId: string; impersonatedBy?: string })
|
||||
| null;
|
||||
req: CreateNextContextOptions["req"];
|
||||
res: CreateNextContextOptions["res"];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type http from "node:http";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { appRouter } from "../api/root";
|
||||
import { createTRPCContext } from "../api/trpc";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
|
||||
export const setupDrawerLogsWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
|
||||
39
apps/dokploy/types/chatwoot.d.ts
vendored
Normal file
39
apps/dokploy/types/chatwoot.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
chatwootSettings?: {
|
||||
hideMessageBubble?: boolean;
|
||||
showUnreadMessagesDialog?: boolean;
|
||||
position?: "left" | "right";
|
||||
locale?: string;
|
||||
useBrowserLanguage?: boolean;
|
||||
type?: "standard" | "expanded_bubble";
|
||||
darkMode?: "light" | "auto";
|
||||
launcherTitle?: string;
|
||||
showPopoutButton?: boolean;
|
||||
baseDomain?: string;
|
||||
};
|
||||
chatwootSDK?: {
|
||||
run: (config: {
|
||||
websiteToken: string;
|
||||
baseUrl: string;
|
||||
}) => void;
|
||||
};
|
||||
$chatwoot?: {
|
||||
setUser: (
|
||||
identifier: string,
|
||||
userAttributes: Record<string, any>,
|
||||
) => void;
|
||||
setCustomAttributes: (attributes: Record<string, any>) => void;
|
||||
reset: () => void;
|
||||
toggle: (state?: "open" | "close") => void;
|
||||
popoutChatWindow: () => void;
|
||||
toggleBubbleVisibility: (visibility: "show" | "hide") => void;
|
||||
setLocale: (locale: string) => void;
|
||||
setLabel: (label: string) => void;
|
||||
removeLabel: (label: string) => void;
|
||||
};
|
||||
chatwootSDKReady?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
15
apps/dokploy/utils/hooks/use-debounce.ts
Normal file
15
apps/dokploy/utils/hooks/use-debounce.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useDebounce<T>(value: T, delay?: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
findServerById,
|
||||
keepLatestNBackups,
|
||||
runCommand,
|
||||
runComposeBackup,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
runPostgresBackup,
|
||||
runComposeBackup,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/dist/db";
|
||||
import { backups, schedules, server } from "@dokploy/server/dist/db/schema";
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0",
|
||||
"engines": {
|
||||
"node": "^20.9.0",
|
||||
"node": "^20.16.0",
|
||||
"pnpm": ">=9.5.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"micromatch": "4.0.8",
|
||||
"@ai-sdk/anthropic": "^1.0.6",
|
||||
"@ai-sdk/azure": "^1.0.15",
|
||||
@@ -40,7 +42,7 @@
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"better-auth": "1.2.6",
|
||||
"better-auth": "v1.2.8-beta.7",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
"@react-email/components": "^0.0.21",
|
||||
|
||||
@@ -206,6 +206,7 @@ export const applications = pgTable("application", {
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
isStaticSpa: boolean("isStaticSpa"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -409,6 +410,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
]),
|
||||
herokuVersion: z.string().optional(),
|
||||
publishDirectory: z.string().optional(),
|
||||
isStaticSpa: z.boolean().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
@@ -461,7 +463,7 @@ export const apiSaveBuildType = createSchema
|
||||
herokuVersion: true,
|
||||
})
|
||||
.required()
|
||||
.merge(createSchema.pick({ publishDirectory: true }));
|
||||
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));
|
||||
|
||||
export const apiSaveGithubProvider = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -11,15 +11,15 @@ import {
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { generateAppName } from ".";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { users_temp } from "./user";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { generateAppName } from ".";
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { backups } from "./backups";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
@@ -15,7 +16,6 @@ import { server } from "./server";
|
||||
import { applicationStatus, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { backups } from "./backups";
|
||||
|
||||
import { schedules } from "./schedule";
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
|
||||
@@ -10,11 +10,11 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { backups } from "./backups";
|
||||
import { compose } from "./compose";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { server } from "./server";
|
||||
import { schedules } from "./schedule";
|
||||
import { backups } from "./backups";
|
||||
import { server } from "./server";
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
|
||||
@@ -4,11 +4,11 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { deployments } from "./deployment";
|
||||
import { generateAppName } from "./utils";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { generateAppName } from "./utils";
|
||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||
|
||||
export const scheduleType = pgEnum("scheduleType", [
|
||||
|
||||
@@ -20,9 +20,9 @@ import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
import { schedules } from "./schedule";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { schedules } from "./schedule";
|
||||
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||
|
||||
export const server = pgTable("server", {
|
||||
|
||||
@@ -11,10 +11,10 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { account, apikey, organization } from "./account";
|
||||
import { projects } from "./project";
|
||||
import { certificateType } from "./shared";
|
||||
import { backups } from "./backups";
|
||||
import { projects } from "./project";
|
||||
import { schedules } from "./schedule";
|
||||
import { certificateType } from "./shared";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
@@ -57,8 +57,10 @@ export const users_temp = pgTable("user_temp", {
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
logCleanupCron: text("logCleanupCron"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||
metricsConfig: jsonb("metricsConfig")
|
||||
.$type<{
|
||||
server: {
|
||||
@@ -134,6 +136,8 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
||||
const createSchema = createInsertSchema(users_temp, {
|
||||
id: z.string().min(1),
|
||||
isRegistered: z.boolean().optional(),
|
||||
}).omit({
|
||||
role: true,
|
||||
});
|
||||
|
||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||
|
||||
@@ -131,3 +131,5 @@ export {
|
||||
|
||||
export * from "./utils/schedules/utils";
|
||||
export * from "./utils/schedules/index";
|
||||
|
||||
export * from "./lib/logger";
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { apiKey, organization, twoFactor } from "better-auth/plugins";
|
||||
import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { db } from "../db";
|
||||
@@ -187,9 +187,13 @@ const { handler, api } = betterAuth({
|
||||
// required: true,
|
||||
input: false,
|
||||
},
|
||||
allowImpersonation: {
|
||||
fieldName: "allowImpersonation",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableMetadata: true,
|
||||
@@ -214,6 +218,13 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
},
|
||||
}),
|
||||
...(IS_CLOUD
|
||||
? [
|
||||
admin({
|
||||
adminUserIds: [process.env.USER_ADMIN_ID as string],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
11
packages/server/src/lib/logger.ts
Normal file
11
packages/server/src/lib/logger.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -24,13 +24,13 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
|
||||
import { type Server, findServerById } from "./server";
|
||||
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { findBackupById } from "./backup";
|
||||
import {
|
||||
type PreviewDeployment,
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { findScheduleById } from "./schedule";
|
||||
import { findBackupById } from "./backup";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
||||
@@ -7,8 +9,6 @@ import { type apiCreateDomain, domains } from "../db/schema";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { findServerById } from "./server";
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { type Schedule, schedules } from "../db/schema/schedule";
|
||||
import { db } from "../db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import path from "node:path";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { paths } from "../constants";
|
||||
import { db } from "../db";
|
||||
import { type Schedule, schedules } from "../db/schema/schedule";
|
||||
import type {
|
||||
createScheduleSchema,
|
||||
updateScheduleSchema,
|
||||
} from "../db/schema/schedule";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import { paths } from "../constants";
|
||||
import path from "node:path";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
|
||||
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;
|
||||
|
||||
|
||||
@@ -356,20 +356,20 @@ const installUtilities = () => `
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl sudo unzip tar >/dev/null
|
||||
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
@@ -377,12 +377,12 @@ const installUtilities = () => `
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl unzip >/dev/null
|
||||
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
zypper install -y curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
@@ -577,7 +577,7 @@ const installNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
export NIXPACKS_VERSION=1.35.0
|
||||
export NIXPACKS_VERSION=1.39.0
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||
fi
|
||||
@@ -587,7 +587,7 @@ const installRailpack = () => `
|
||||
if command_exists railpack; then
|
||||
echo "Railpack already installed ✅"
|
||||
else
|
||||
export RAILPACK_VERSION=0.0.37
|
||||
export RAILPACK_VERSION=0.0.64
|
||||
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
echo "Railpack version $RAILPACK_VERSION installed ✅"
|
||||
fi
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { randomBytes, createHmac } from "node:crypto";
|
||||
import { createHmac, randomBytes } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
@@ -35,7 +35,7 @@ export const generateRandomDomain = ({
|
||||
projectName,
|
||||
}: Schema): string => {
|
||||
const hash = randomBytes(3).toString("hex");
|
||||
const slugIp = serverIp.replaceAll(".", "-");
|
||||
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
|
||||
|
||||
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runComposeBackup = async (
|
||||
compose: Compose,
|
||||
@@ -38,7 +38,9 @@ export const runComposeBackup = async (
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, backupCommand);
|
||||
} else {
|
||||
await execAsync(backupCommand);
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
|
||||
@@ -11,10 +11,10 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
||||
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
|
||||
export const initCronJobs = async () => {
|
||||
console.log("Setting up cron jobs....");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user