Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8721406a | ||
|
|
5db5336ec8 | ||
|
|
a6e7edd4d9 | ||
|
|
ddbb414225 | ||
|
|
b73889dd4f | ||
|
|
ce2dce3401 | ||
|
|
173110a415 | ||
|
|
2307346ae3 | ||
|
|
2f175f0e44 | ||
|
|
7003fe77c9 | ||
|
|
04235fb6c9 | ||
|
|
f138b0917f | ||
|
|
82367213ea | ||
|
|
3c490ba2d0 | ||
|
|
4bf5e5ca06 | ||
|
|
6af5742702 | ||
|
|
3015d69adc | ||
|
|
f7fa8e74af | ||
|
|
2835c997e9 | ||
|
|
bd55e3751f | ||
|
|
74374bd643 | ||
|
|
4e929c12f2 | ||
|
|
56ea356723 | ||
|
|
b1f7d05743 | ||
|
|
58338380ab | ||
|
|
2487e3e062 | ||
|
|
01a882497f | ||
|
|
036313e3c3 | ||
|
|
d3304052b0 | ||
|
|
9efd2e3d5c | ||
|
|
1e7d9fc3aa | ||
|
|
70ca219c0a | ||
|
|
3a07d8de2e | ||
|
|
f4f1fc28a0 | ||
|
|
4c71d3a95f | ||
|
|
af84942d22 | ||
|
|
1aaff0594d | ||
|
|
69a4a87079 | ||
|
|
3eef4aa016 | ||
|
|
2492581bde | ||
|
|
6961ee1fc0 | ||
|
|
8e532d5a60 | ||
|
|
fa791706a0 | ||
|
|
a5c1f8ef49 | ||
|
|
6866c3b116 | ||
|
|
444302e7b9 | ||
|
|
9b77573269 | ||
|
|
8157dd9eaa | ||
|
|
f1fc3f161a | ||
|
|
06af2042ee | ||
|
|
8900e30ae7 | ||
|
|
b5fa411093 | ||
|
|
cab9443d25 | ||
|
|
de315124c3 | ||
|
|
7bef3a0c29 | ||
|
|
82afd486da | ||
|
|
69c9e86a13 | ||
|
|
3d59e289be | ||
|
|
8d33ff5fb5 | ||
|
|
46219e1b3d | ||
|
|
3457de4f36 | ||
|
|
e57efa2e31 | ||
|
|
fb0308fd60 | ||
|
|
b376ead7b5 | ||
|
|
deeea11428 | ||
|
|
5c17797749 | ||
|
|
7b06fd47b8 | ||
|
|
faceed12b0 | ||
|
|
814580ff2c | ||
|
|
4f092b2fb3 | ||
|
|
0799f8e04c | ||
|
|
59c050b519 | ||
|
|
88f969917f | ||
|
|
ed470ee827 | ||
|
|
96584e5b32 | ||
|
|
a7165bef20 | ||
|
|
b0f5e7dad3 | ||
|
|
fa083257f1 | ||
|
|
5cf12e51d1 | ||
|
|
b6fd410af2 | ||
|
|
c5c3ca39cd | ||
|
|
5f6d041248 | ||
|
|
b817b4b6ee | ||
|
|
c09ff25360 | ||
|
|
046f0a5c20 | ||
|
|
66c4d8f118 | ||
|
|
7f0a92f224 | ||
|
|
0ca8ee17be | ||
|
|
c765d7d9eb | ||
|
|
a331020bf8 | ||
|
|
2e6d9c34c0 | ||
|
|
b53da82204 | ||
|
|
3b5e8921d0 | ||
|
|
7306d8c513 | ||
|
|
bc097c7667 | ||
|
|
ed7150fac1 | ||
|
|
1b6d8d803b | ||
|
|
3e467959c9 | ||
|
|
b3092691b7 | ||
|
|
5a440d934d | ||
|
|
96c5176984 | ||
|
|
433430118f | ||
|
|
6d3ea8df59 | ||
|
|
e52a0fc9d4 | ||
|
|
15a76d2639 |
@@ -18,8 +18,10 @@ jobs:
|
|||||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
else
|
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
|
else
|
||||||
|
TAG="feature"
|
||||||
fi
|
fi
|
||||||
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
|
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
|
||||||
docker push dokploy/dokploy:${TAG}-amd64
|
docker push dokploy/dokploy:${TAG}-amd64
|
||||||
@@ -41,8 +43,10 @@ jobs:
|
|||||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
else
|
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
|
else
|
||||||
|
TAG="feature"
|
||||||
fi
|
fi
|
||||||
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
|
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
|
||||||
docker push dokploy/dokploy:${TAG}-arm64
|
docker push dokploy/dokploy:${TAG}-arm64
|
||||||
@@ -72,12 +76,18 @@ jobs:
|
|||||||
dokploy/dokploy:${TAG}-amd64 \
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
dokploy/dokploy:${TAG}-arm64
|
dokploy/dokploy:${TAG}-arm64
|
||||||
docker manifest push dokploy/dokploy:${VERSION}
|
docker manifest push dokploy/dokploy:${VERSION}
|
||||||
else
|
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
docker manifest create dokploy/dokploy:${TAG} \
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
dokploy/dokploy:${TAG}-amd64 \
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
dokploy/dokploy:${TAG}-arm64
|
dokploy/dokploy:${TAG}-arm64
|
||||||
docker manifest push dokploy/dokploy:${TAG}
|
docker manifest push dokploy/dokploy:${TAG}
|
||||||
|
else
|
||||||
|
TAG="feature"
|
||||||
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
|
dokploy/dokploy:${TAG}-arm64
|
||||||
|
docker manifest push dokploy/dokploy:${TAG}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
@@ -89,12 +99,14 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- fix/build-i18n
|
||||||
- build-arm64:
|
- build-arm64:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- fix/build-i18n
|
||||||
- combine-manifests:
|
- combine-manifests:
|
||||||
requires:
|
requires:
|
||||||
- build-amd64
|
- build-amd64
|
||||||
@@ -104,3 +116,4 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- fix/build-i18n
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ We have a few guidelines to follow when contributing to this project:
|
|||||||
|
|
||||||
## Commit Convention
|
## Commit Convention
|
||||||
|
|
||||||
|
|
||||||
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||||
|
|
||||||
### Commit Message Format
|
### Commit Message Format
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<type>[optional scope]: <description>
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
@@ -235,7 +237,7 @@ export function generate(schema: Schema): Template {
|
|||||||
|
|
||||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
||||||
|
|
||||||
### Recomendations
|
### Recommendations
|
||||||
|
|
||||||
- Use the same name of the folder as the id of the template.
|
- Use the same name of the folder as the id of the template.
|
||||||
- The logo should be in the public folder.
|
- The logo should be in the public folder.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
|
|||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
COPY --from=build /prod/dokploy/dist ./dist
|
COPY --from=build /prod/dokploy/dist ./dist
|
||||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||||
|
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
|
||||||
COPY --from=build /prod/dokploy/public ./public
|
COPY --from=build /prod/dokploy/public ./public
|
||||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
|
|||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
COPY --from=build /prod/dokploy/dist ./dist
|
COPY --from=build /prod/dokploy/dist ./dist
|
||||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||||
|
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
|
||||||
COPY --from=build /prod/dokploy/public ./public
|
COPY --from=build /prod/dokploy/public ./public
|
||||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ const baseApp: ApplicationNested = {
|
|||||||
serverId: "",
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
adminId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
|
|||||||
179
apps/dokploy/__test__/env/shared.test.ts
vendored
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||||
|
PORT=3000
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
SERVICE_PORT=4000
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariables", () => {
|
||||||
|
it("resolves project variables correctly", () => {
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
"SERVICE_PORT=4000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles undefined project variables", () => {
|
||||||
|
const incompleteProjectEnv = `
|
||||||
|
NODE_ENV=production
|
||||||
|
`;
|
||||||
|
|
||||||
|
const invalidServiceEnv = `
|
||||||
|
UNDEFINED_VAR=\${{project.UNDEFINED_VAR}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
prepareEnvironmentVariables(invalidServiceEnv, incompleteProjectEnv), // Cambiado el orden
|
||||||
|
).toThrow("Invalid project environment variable: project.UNDEFINED_VAR");
|
||||||
|
});
|
||||||
|
it("allows service-specific variables to override project variables", () => {
|
||||||
|
const serviceSpecificEnv = `
|
||||||
|
ENVIRONMENT=production
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceSpecificEnv,
|
||||||
|
projectEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=production", // Overrides project variable
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves complex references for dynamic endpoints", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
BASE_URL=https://api.example.com
|
||||||
|
API_VERSION=v1
|
||||||
|
PORT=8000
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
API_ENDPOINT=\${{project.BASE_URL}}/\${{project.API_VERSION}}/endpoint
|
||||||
|
SERVICE_PORT=9000
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"API_ENDPOINT=https://api.example.com/v1/endpoint",
|
||||||
|
"SERVICE_PORT=9000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing project variables gracefully", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
PORT=8080
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
MISSING_VAR=\${{project.MISSING_KEY}}
|
||||||
|
SERVICE_PORT=3000
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(() => prepareEnvironmentVariables(serviceEnv, projectEnv)).toThrow(
|
||||||
|
"Invalid project environment variable: project.MISSING_KEY",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides project variables with service-specific values", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://project:project@localhost:5432/project_db
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
DATABASE_URL=postgres://service:service@localhost:5432/service_db
|
||||||
|
SERVICE_NAME=my-service
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
"DATABASE_URL=postgres://service:service@localhost:5432/service_db",
|
||||||
|
"SERVICE_NAME=my-service",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles project variables with normal and unusual characters", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=PRODUCTION
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Needs to be in quotes
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||||
|
SPECIAL_VAR="$^@$^@#$^@!#$@#$-\${{project.ENVIRONMENT}}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=PRODUCTION",
|
||||||
|
"SPECIAL_VAR=$^@$^@#$^@!#$@#$-PRODUCTION",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles complex cases with multiple references, special characters, and spaces", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=STAGING
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||||
|
COMPLEX_VAR="Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix "
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=STAGING",
|
||||||
|
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix ",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles references enclosed in single quotes", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=STAGING
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV='\${{project.ENVIRONMENT}}'
|
||||||
|
COMPLEX_VAR='Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix'
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV=STAGING",
|
||||||
|
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles double and single quotes combined", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=PRODUCTION
|
||||||
|
APP_NAME=MyApp
|
||||||
|
`;
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV="'\${{project.ENVIRONMENT}}'"
|
||||||
|
COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'"
|
||||||
|
`;
|
||||||
|
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV='PRODUCTION'",
|
||||||
|
"COMPLEX_VAR='Prefix \"DoubleQuoted\" and MyApp'",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,14 @@ const baseApp: ApplicationNested = {
|
|||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
adminId: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
createdAt: "",
|
||||||
|
projectId: "",
|
||||||
|
},
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
buildType: "nixpacks",
|
buildType: "nixpacks",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const redirectPresets = [
|
|||||||
redirect: {
|
redirect: {
|
||||||
regex: "^https?://(?:www.)?(.+)",
|
regex: "^https?://(?:www.)?(.+)",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
replacement: "https://www.$${1}",
|
replacement: "https://www.${1}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,7 @@ const redirectPresets = [
|
|||||||
redirect: {
|
redirect: {
|
||||||
regex: "^https?://www.(.+)",
|
regex: "^https?://www.(.+)",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
replacement: "https://$${1}",
|
replacement: "https://${1}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
162
apps/dokploy/components/dashboard/projects/add-env.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AlertTriangle, FileIcon, SquarePen } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const updateProjectSchema = z.object({
|
||||||
|
env: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type UpdateProject = z.infer<typeof updateProjectSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddEnv = ({ projectId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
|
api.project.update.useMutation();
|
||||||
|
const { data } = api.project.one.useQuery(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!projectId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
const form = useForm<UpdateProject>({
|
||||||
|
defaultValues: {
|
||||||
|
env: data?.env ?? "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(updateProjectSchema),
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
env: data.env ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: UpdateProject) => {
|
||||||
|
await mutateAsync({
|
||||||
|
env: formData.env || "",
|
||||||
|
projectId: projectId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Project env updated succesfully");
|
||||||
|
utils.project.all.invalidate();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the env");
|
||||||
|
})
|
||||||
|
.finally(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<FileIcon className="size-4" />
|
||||||
|
<span>Add Env</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Modify Shared Env</DialogTitle>
|
||||||
|
<DialogDescription>Update the env variables</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<AlertBlock type="info">
|
||||||
|
To use a shared env, in one of your services, you need to use like
|
||||||
|
this: Let's say you have a shared env ENVIROMENT="development" and you
|
||||||
|
want to use it in your service, you need to use like this:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>ENVIRONMENT=${"{{project.ENVIRONMENT}}"}</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
|
||||||
|
</li>
|
||||||
|
</ul>{" "}
|
||||||
|
This allows the service to inherit and use the shared variables from
|
||||||
|
the project level, ensuring consistency across services.
|
||||||
|
</AlertBlock>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid items-center gap-4">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4 "
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="env"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Enviroment variables</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
language="properties"
|
||||||
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
|
placeholder={`NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { AddEnv } from "./add-env";
|
||||||
import { UpdateProject } from "./update";
|
import { UpdateProject } from "./update";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
@@ -190,7 +191,9 @@ export const ShowProjects = () => {
|
|||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<AddEnv projectId={project.projectId} />
|
||||||
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<UpdateProject projectId={project.projectId} />
|
<UpdateProject projectId={project.projectId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import useLocale from "@/utils/hooks/use-locale";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -28,6 +37,9 @@ const appearanceFormSchema = z.object({
|
|||||||
theme: z.enum(["light", "dark", "system"], {
|
theme: z.enum(["light", "dark", "system"], {
|
||||||
required_error: "Please select a theme.",
|
required_error: "Please select a theme.",
|
||||||
}),
|
}),
|
||||||
|
language: z.enum(["en", "pl", "zh-Hans"], {
|
||||||
|
required_error: "Please select a language.",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
||||||
@@ -35,10 +47,14 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
|||||||
// This can come from your database or API.
|
// This can come from your database or API.
|
||||||
const defaultValues: Partial<AppearanceFormValues> = {
|
const defaultValues: Partial<AppearanceFormValues> = {
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
language: "en",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppearanceForm() {
|
export function AppearanceForm() {
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme } = useTheme();
|
||||||
|
const { locale, setLocale } = useLocale();
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
const form = useForm<AppearanceFormValues>({
|
const form = useForm<AppearanceFormValues>({
|
||||||
resolver: zodResolver(appearanceFormSchema),
|
resolver: zodResolver(appearanceFormSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -47,19 +63,23 @@ export function AppearanceForm() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
theme: (theme ?? "system") as AppearanceFormValues["theme"],
|
theme: (theme ?? "system") as AppearanceFormValues["theme"],
|
||||||
|
language: locale,
|
||||||
});
|
});
|
||||||
}, [form, theme]);
|
}, [form, theme, locale]);
|
||||||
function onSubmit(data: AppearanceFormValues) {
|
function onSubmit(data: AppearanceFormValues) {
|
||||||
setTheme(data.theme);
|
setTheme(data.theme);
|
||||||
|
setLocale(data.language);
|
||||||
toast.success("Preferences Updated");
|
toast.success("Preferences Updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-transparent">
|
<Card className="bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Appearance</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
|
{t("settings.appearance.title")}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Customize the theme of your dashboard.
|
{t("settings.appearance.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
@@ -72,9 +92,9 @@ export function AppearanceForm() {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className="space-y-1 ">
|
<FormItem className="space-y-1 ">
|
||||||
<FormLabel>Theme</FormLabel>
|
<FormLabel>{t("settings.appearance.theme")}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Select a theme for your dashboard
|
{t("settings.appearance.themeDescription")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -92,7 +112,7 @@ export function AppearanceForm() {
|
|||||||
<img src="/images/theme-light.svg" alt="light" />
|
<img src="/images/theme-light.svg" alt="light" />
|
||||||
</div>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
Light
|
{t("settings.appearance.themes.light")}
|
||||||
</span>
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -105,7 +125,7 @@ export function AppearanceForm() {
|
|||||||
<img src="/images/theme-dark.svg" alt="dark" />
|
<img src="/images/theme-dark.svg" alt="dark" />
|
||||||
</div>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
Dark
|
{t("settings.appearance.themes.dark")}
|
||||||
</span>
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -121,7 +141,7 @@ export function AppearanceForm() {
|
|||||||
<img src="/images/theme-system.svg" alt="system" />
|
<img src="/images/theme-system.svg" alt="system" />
|
||||||
</div>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
System
|
{t("settings.appearance.themes.system")}
|
||||||
</span>
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -131,7 +151,44 @@ export function AppearanceForm() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit">Save</Button>
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="language"
|
||||||
|
defaultValue={form.control._defaultValues.language}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="space-y-1">
|
||||||
|
<FormLabel>{t("settings.appearance.language")}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("settings.appearance.languageDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="No preset selected" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{[
|
||||||
|
{ label: "English", value: "en" },
|
||||||
|
{ label: "Polski", value: "pl" },
|
||||||
|
{ label: "简体中文", value: "zh-Hans" },
|
||||||
|
].map((preset) => (
|
||||||
|
<SelectItem key={preset.label} value={preset.value}>
|
||||||
|
{preset.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">{t("settings.common.save")}</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -51,6 +52,7 @@ const randomImages = [
|
|||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.auth.get.useQuery();
|
||||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -91,10 +93,10 @@ export const ProfileForm = () => {
|
|||||||
<Card className="bg-transparent">
|
<Card className="bg-transparent">
|
||||||
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Account</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
<CardDescription>
|
{t("settings.profile.title")}
|
||||||
Change the details of your profile here.
|
</CardTitle>
|
||||||
</CardDescription>
|
<CardDescription>{t("settings.profile.description")}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -107,9 +109,12 @@ export const ProfileForm = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>{t("settings.profile.email")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Email" {...field} />
|
<Input
|
||||||
|
placeholder={t("settings.profile.email")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -120,11 +125,11 @@ export const ProfileForm = () => {
|
|||||||
name="password"
|
name="password"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>{t("settings.profile.password")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder={t("settings.profile.password")}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
@@ -139,7 +144,7 @@ export const ProfileForm = () => {
|
|||||||
name="image"
|
name="image"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Avatar</FormLabel>
|
<FormLabel>{t("settings.profile.avatar")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
@@ -177,7 +182,7 @@ export const ProfileForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
{t("settings.common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { UpdateServerIp } from "@/components/dashboard/settings/web-server/update-server-ip";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -11,10 +12,13 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
import { GPUSupportModal } from "../gpu-support-modal";
|
||||||
|
|
||||||
export const ShowDokployActions = () => {
|
export const ShowDokployActions = () => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { mutateAsync: reloadServer, isLoading } =
|
const { mutateAsync: reloadServer, isLoading } =
|
||||||
api.settings.reloadServer.useMutation();
|
api.settings.reloadServer.useMutation();
|
||||||
|
|
||||||
@@ -22,11 +26,13 @@ export const ShowDokployActions = () => {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||||
<Button isLoading={isLoading} variant="outline">
|
<Button isLoading={isLoading} variant="outline">
|
||||||
Server
|
{t("settings.server.webServer.server.label")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
<DropdownMenuContent className="w-56" align="start">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
{t("settings.server.webServer.actions")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -39,12 +45,27 @@ export const ShowDokployActions = () => {
|
|||||||
toast.success("Server Reloaded");
|
toast.success("Server Reloaded");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>Reload</span>
|
<span>{t("settings.server.webServer.reload")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<ShowModalLogs appName="dokploy">
|
<ShowModalLogs appName="dokploy">
|
||||||
<span>Watch logs</span>
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("settings.server.webServer.watchLogs")}
|
||||||
|
</DropdownMenuItem>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
|
<GPUSupportModal />
|
||||||
|
<UpdateServerIp>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("settings.server.webServer.updateServerIp")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</UpdateServerIp>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CardDescription, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -21,13 +20,13 @@ export const ShowServerActions = ({ serverId }: Props) => {
|
|||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer "
|
className="w-full cursor-pointer"
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
View Actions
|
View Actions
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
||||||
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ShowStorageActions = ({ serverId }: Props) => {
|
export const ShowStorageActions = ({ serverId }: Props) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||||
api.settings.cleanAll.useMutation();
|
api.settings.cleanAll.useMutation();
|
||||||
|
|
||||||
@@ -64,11 +66,13 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Space
|
{t("settings.server.webServer.storage.label")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-64" align="start">
|
<DropdownMenuContent className="w-64" align="start">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
{t("settings.server.webServer.actions")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -85,7 +89,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean unused images</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanUnusedImages")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer"
|
className="w-full cursor-pointer"
|
||||||
@@ -101,7 +107,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean unused volumes</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanUnusedVolumes")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -118,7 +126,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean stopped containers</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanStoppedContainers")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -135,7 +145,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean Docker Builder & System</span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanDockerBuilder")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{!serverId && (
|
{!serverId && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -150,7 +162,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean Monitoring </span>
|
<span>
|
||||||
|
{t("settings.server.webServer.storage.cleanMonitoring")}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -168,7 +182,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Clean all</span>
|
<span>{t("settings.server.webServer.storage.cleanAll")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { api } from "@/utils/api";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ interface Props {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ShowTraefikActions = ({ serverId }: Props) => {
|
export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||||
api.settings.reloadTraefik.useMutation();
|
api.settings.reloadTraefik.useMutation();
|
||||||
|
|
||||||
@@ -51,11 +53,13 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Traefik
|
{t("settings.server.webServer.traefik.label")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
<DropdownMenuContent className="w-56" align="start">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
{t("settings.server.webServer.actions")}
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -70,18 +74,24 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
toast.error("Error to reload the traefik");
|
toast.error("Error to reload the traefik");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>Reload</span>
|
<span>{t("settings.server.webServer.reload")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
|
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
|
||||||
<span>Watch logs</span>
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("settings.server.webServer.watchLogs")}
|
||||||
|
</DropdownMenuItem>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
<EditTraefikEnv serverId={serverId}>
|
<EditTraefikEnv serverId={serverId}>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>Modify Env</span>
|
<span>{t("settings.server.webServer.traefik.modifyEnv")}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</EditTraefikEnv>
|
</EditTraefikEnv>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { GPUSupport } from "./gpu-support";
|
||||||
|
|
||||||
|
export const GPUSupportModal = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<span>GPU Setup</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-4xl overflow-y-auto max-h-screen">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
Dokploy Server GPU Setup
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<GPUSupport serverId="" />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TRPCClientError } from "@trpc/client";
|
||||||
|
import { CheckCircle2, Cpu, Loader2, RefreshCw, XCircle } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface GPUSupportProps {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GPUSupport({ serverId }: GPUSupportProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: gpuStatus,
|
||||||
|
isLoading: isChecking,
|
||||||
|
refetch,
|
||||||
|
} = api.settings.checkGPUStatus.useQuery(
|
||||||
|
{ serverId },
|
||||||
|
{
|
||||||
|
enabled: serverId !== undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setupGPU = api.settings.setupGPU.useMutation({
|
||||||
|
onMutate: () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.success("GPU support enabled successfully");
|
||||||
|
setIsLoading(false);
|
||||||
|
await utils.settings.checkGPUStatus.invalidate({ serverId });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(
|
||||||
|
error.message ||
|
||||||
|
"Failed to enable GPU support. Please check server logs.",
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await utils.settings.checkGPUStatus.invalidate({ serverId });
|
||||||
|
await refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to refresh GPU status");
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
handleRefresh();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEnableGPU = async () => {
|
||||||
|
if (serverId === undefined) {
|
||||||
|
toast.error("No server selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setupGPU.mutateAsync({ serverId });
|
||||||
|
} catch (error) {
|
||||||
|
// Error handling is done in mutation's onError
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Cpu className="size-5" />
|
||||||
|
<CardTitle className="text-xl">GPU Configuration</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Configure and monitor GPU support
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DialogAction
|
||||||
|
title="Enable GPU Support?"
|
||||||
|
description="This will enable GPU support for Docker Swarm on this server. Make sure you have the required hardware and drivers installed."
|
||||||
|
onClick={handleEnableGPU}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={isLoading || serverId === undefined || isChecking}
|
||||||
|
>
|
||||||
|
{isLoading
|
||||||
|
? "Enabling GPU..."
|
||||||
|
: gpuStatus?.swarmEnabled
|
||||||
|
? "Reconfigure GPU"
|
||||||
|
: "Enable GPU"}
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isChecking || isRefreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`h-5 w-5 ${isChecking || isRefreshing ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="info">
|
||||||
|
<div className="font-medium mb-2">System Requirements:</div>
|
||||||
|
<ul className="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>NVIDIA GPU hardware must be physically installed</li>
|
||||||
|
<li>
|
||||||
|
NVIDIA drivers must be installed and running (check with
|
||||||
|
nvidia-smi)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
NVIDIA Container Runtime must be installed
|
||||||
|
(nvidia-container-runtime)
|
||||||
|
</li>
|
||||||
|
<li>User must have sudo/administrative privileges</li>
|
||||||
|
<li>System must support CUDA for GPU acceleration</li>
|
||||||
|
</ul>
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
|
{isChecking ? (
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground py-4">
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
<span>Checking GPU status...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{/* Prerequisites Section */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">Prerequisites</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Shows all software checks and available hardware
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="NVIDIA Driver"
|
||||||
|
isEnabled={gpuStatus?.driverInstalled}
|
||||||
|
description={
|
||||||
|
gpuStatus?.driverVersion
|
||||||
|
? `Installed (v${gpuStatus.driverVersion})`
|
||||||
|
: "Not Installed"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="GPU Model"
|
||||||
|
value={gpuStatus?.gpuModel || "Not Detected"}
|
||||||
|
showIcon={false}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="GPU Memory"
|
||||||
|
value={gpuStatus?.memoryInfo || "Not Available"}
|
||||||
|
showIcon={false}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Available GPUs"
|
||||||
|
value={gpuStatus?.availableGPUs || 0}
|
||||||
|
showIcon={false}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="CUDA Support"
|
||||||
|
isEnabled={gpuStatus?.cudaSupport}
|
||||||
|
description={
|
||||||
|
gpuStatus?.cudaVersion
|
||||||
|
? `Available (v${gpuStatus.cudaVersion})`
|
||||||
|
: "Not Available"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="NVIDIA Container Runtime"
|
||||||
|
isEnabled={gpuStatus?.runtimeInstalled}
|
||||||
|
description={
|
||||||
|
gpuStatus?.runtimeInstalled
|
||||||
|
? "Installed"
|
||||||
|
: "Not Installed"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Configuration Status */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">
|
||||||
|
Docker Swarm GPU Status
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Shows the configuration state that changes with the Enable
|
||||||
|
GPU
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="Runtime Configuration"
|
||||||
|
isEnabled={gpuStatus?.runtimeConfigured}
|
||||||
|
description={
|
||||||
|
gpuStatus?.runtimeConfigured
|
||||||
|
? "Default Runtime"
|
||||||
|
: "Not Default Runtime"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Swarm GPU Support"
|
||||||
|
isEnabled={gpuStatus?.swarmEnabled}
|
||||||
|
description={
|
||||||
|
gpuStatus?.swarmEnabled
|
||||||
|
? `Enabled (${gpuStatus.gpuResources} GPU${gpuStatus.gpuResources !== 1 ? "s" : ""})`
|
||||||
|
: "Not Enabled"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusRowProps {
|
||||||
|
label: string;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
description?: string;
|
||||||
|
value?: string | number;
|
||||||
|
showIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusRow({
|
||||||
|
label,
|
||||||
|
isEnabled,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
showIcon = true,
|
||||||
|
}: StatusRowProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">{label}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{showIcon ? (
|
||||||
|
<>
|
||||||
|
{isEnabled ? (
|
||||||
|
<CheckCircle2 className="size-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="size-4 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`text-sm ${isEnabled ? "text-green-500" : "text-red-500"}`}
|
||||||
|
>
|
||||||
|
{description || (isEnabled ? "Installed" : "Not Installed")}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">{value}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||||
|
import { GPUSupport } from "./gpu-support";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -89,9 +90,10 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||||
<Tabs defaultValue="ssh-keys">
|
<Tabs defaultValue="ssh-keys">
|
||||||
<TabsList className="grid grid-cols-2 w-[400px]">
|
<TabsList className="grid grid-cols-3 w-[400px]">
|
||||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="ssh-keys"
|
value="ssh-keys"
|
||||||
@@ -291,6 +293,14 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="gpu-setup"
|
||||||
|
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||||
|
<GPUSupport serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -49,6 +50,7 @@ const addServerDomain = z
|
|||||||
type AddServerDomain = z.infer<typeof addServerDomain>;
|
type AddServerDomain = z.infer<typeof addServerDomain>;
|
||||||
|
|
||||||
export const WebDomain = () => {
|
export const WebDomain = () => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { data: user, refetch } = api.admin.one.useQuery();
|
const { data: user, refetch } = api.admin.one.useQuery();
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.settings.assignDomainServer.useMutation();
|
api.settings.assignDomainServer.useMutation();
|
||||||
@@ -89,9 +91,11 @@ export const WebDomain = () => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="bg-transparent">
|
<Card className="bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Server Domain</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
|
{t("settings.server.domain.title")}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add a domain to your server application.
|
{t("settings.server.domain.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
@@ -106,7 +110,9 @@ export const WebDomain = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Domain</FormLabel>
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.domain")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -126,7 +132,9 @@ export const WebDomain = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Letsencrypt Email</FormLabel>
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.letsEncryptEmail")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -145,20 +153,32 @@ export const WebDomain = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className="md:col-span-2">
|
<FormItem className="md:col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.certificate.label")}
|
||||||
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"settings.server.domain.form.certificate.placeholder",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
<SelectItem value={"none"}>
|
||||||
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.none",
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt",
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -169,7 +189,7 @@ export const WebDomain = () => {
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button isLoading={isLoading} type="submit">
|
<Button isLoading={isLoading} type="submit">
|
||||||
Save
|
{t("settings.common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||||
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||||
@@ -18,6 +19,7 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
export const WebServer = ({ className }: Props) => {
|
export const WebServer = ({ className }: Props) => {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
const { data } = api.admin.one.useQuery();
|
const { data } = api.admin.one.useQuery();
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
@@ -25,8 +27,12 @@ export const WebServer = ({ className }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Web server settings</CardTitle>
|
<CardTitle className="text-xl">
|
||||||
<CardDescription>Reload or clean the web server.</CardDescription>
|
{t("settings.server.webServer.title")}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t("settings.server.webServer.description")}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4 ">
|
<CardContent className="flex flex-col gap-4 ">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -58,14 +58,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
|
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>View Logs</DialogTitle>
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
serverIp: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateServerIp = ({ children, serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data } = api.admin.one.useQuery();
|
||||||
|
const { data: ip } = api.server.publicIp.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.admin.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
serverIp: data?.serverIp || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
serverIp: data.serverIp || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const setCurrentIp = () => {
|
||||||
|
if (!ip) return;
|
||||||
|
form.setValue("serverIp", ip);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
await mutateAsync({
|
||||||
|
serverIp: data.serverIp,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Server IP Updated");
|
||||||
|
await utils.admin.one.invalidate();
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the IP of the server");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Server IP</DialogTitle>
|
||||||
|
<DialogDescription>Update the IP of the server</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-update-server-ip"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverIp"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server IP</FormLabel>
|
||||||
|
<FormControl className="flex gap-2">
|
||||||
|
<div>
|
||||||
|
<Input {...field} />
|
||||||
|
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={setCurrentIp}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[11rem]"
|
||||||
|
>
|
||||||
|
<p>Set current public IP</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={isLoading}
|
||||||
|
form="hook-form-update-server-ip"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -87,7 +87,7 @@ export const UpdateServer = () => {
|
|||||||
}}
|
}}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
>
|
>
|
||||||
Check updates
|
Check Updates
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
apps/dokploy/drizzle/0043_closed_naoko.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "admin" ADD COLUMN "env" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "project" ADD COLUMN "env" text DEFAULT '' NOT NULL;
|
||||||
1
apps/dokploy/drizzle/0044_sour_true_believers.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "admin" DROP COLUMN IF EXISTS "env";
|
||||||
3982
apps/dokploy/drizzle/meta/0043_snapshot.json
Normal file
3975
apps/dokploy/drizzle/meta/0044_snapshot.json
Normal file
@@ -302,6 +302,20 @@
|
|||||||
"when": 1729984439862,
|
"when": 1729984439862,
|
||||||
"tag": "0042_fancy_havok",
|
"tag": "0042_fancy_havok",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 43,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731873965888,
|
||||||
|
"tag": "0043_closed_naoko",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 44,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731875539532,
|
||||||
|
"tag": "0044_sour_true_believers",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
10
apps/dokploy/next-i18next.config.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('next-i18next').UserConfig} */
|
||||||
|
module.exports = {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: "en",
|
||||||
|
locales: ["en", "pl", "zh-Hans"],
|
||||||
|
localeDetection: false,
|
||||||
|
},
|
||||||
|
fallbackLng: "en",
|
||||||
|
keySeparator: false,
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.11.2",
|
"version": "v0.12.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"build-next": "next build",
|
"build-next": "next build",
|
||||||
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
||||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||||
@@ -84,13 +84,16 @@
|
|||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"drizzle-orm": "^0.30.8",
|
"drizzle-orm": "^0.30.8",
|
||||||
"drizzle-zod": "0.5.1",
|
"drizzle-zod": "0.5.1",
|
||||||
|
"i18next": "^23.16.4",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lucia": "^3.0.1",
|
"lucia": "^3.0.1",
|
||||||
"lucide-react": "^0.312.0",
|
"lucide-react": "^0.312.0",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"next": "^15.0.1",
|
"next": "^15.0.1",
|
||||||
|
"next-i18next": "^15.3.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
@@ -100,6 +103,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.49.3",
|
||||||
|
"react-i18next": "^15.1.0",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
@@ -119,6 +123,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/lodash": "4.17.4",
|
"@types/lodash": "4.17.4",
|
||||||
"@types/node": "^18.17.0",
|
"@types/node": "^18.17.0",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "@/styles/globals.css";
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
import { appWithTranslation } from "next-i18next";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
@@ -27,6 +28,7 @@ const MyApp = ({
|
|||||||
pageProps: { ...pageProps },
|
pageProps: { ...pageProps },
|
||||||
}: AppPropsWithLayout) => {
|
}: AppPropsWithLayout) => {
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
@@ -59,4 +61,21 @@ const MyApp = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api.withTRPC(MyApp);
|
export default api.withTRPC(
|
||||||
|
appWithTranslation(
|
||||||
|
MyApp,
|
||||||
|
// keep this in sync with next-i18next.config.js
|
||||||
|
// if you want to know why don't just import the config file, this because next-i18next.config.js must be a CJS, but the rest of the code is ESM.
|
||||||
|
// Add the config here is due to the issue: https://github.com/i18next/next-i18next/issues/2259
|
||||||
|
// if one day every page is translated, we can safely remove this config.
|
||||||
|
{
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: "en",
|
||||||
|
locales: ["en", "pl", "zh-Hans"],
|
||||||
|
localeDetection: false,
|
||||||
|
},
|
||||||
|
fallbackLng: "en",
|
||||||
|
keySeparator: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { AppearanceForm } from "@/components/dashboard/settings/appearance-form"
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import nextI18NextConfig from "../../../next-i18next.config.cjs";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -30,6 +32,7 @@ export async function getServerSideProps(
|
|||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
const locale = getLocale(req.cookies);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
@@ -63,6 +66,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
|
...(await serverSideTranslations(locale, ["settings"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -41,6 +42,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
|
const locale = getLocale(req.cookies);
|
||||||
const { user, session } = await validateRequest(req, res);
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
const helpers = createServerSideHelpers({
|
||||||
@@ -75,6 +77,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
|
...(await serverSideTranslations(locale, ["settings"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { WebServer } from "@/components/dashboard/settings/web-server";
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -31,6 +32,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
|
const locale = await getLocale(req.cookies);
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -73,6 +75,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: helpers.dehydrate(),
|
trpcState: helpers.dehydrate(),
|
||||||
|
...(await serverSideTranslations(locale, ["settings"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
1
apps/dokploy/public/locales/en/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
44
apps/dokploy/public/locales/en/settings.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Save",
|
||||||
|
"settings.server.domain.title": "Server Domain",
|
||||||
|
"settings.server.domain.description": "Add a domain to your server application.",
|
||||||
|
"settings.server.domain.form.domain": "Domain",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
|
||||||
|
"settings.server.domain.form.certificate.label": "Certificate",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Select a certificate",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "None",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Default)",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Web Server",
|
||||||
|
"settings.server.webServer.description": "Reload or clean the web server.",
|
||||||
|
"settings.server.webServer.actions": "Actions",
|
||||||
|
"settings.server.webServer.reload": "Reload",
|
||||||
|
"settings.server.webServer.watchLogs": "Watch logs",
|
||||||
|
"settings.server.webServer.updateServerIp": "Update Server IP",
|
||||||
|
"settings.server.webServer.server.label": "Server",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Modify Env",
|
||||||
|
"settings.server.webServer.storage.label": "Space",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Clean unused images",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Clean stopped containers",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Clean Docker Builder & System",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Clean Monitoring",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Clean all",
|
||||||
|
|
||||||
|
"settings.profile.title": "Account",
|
||||||
|
"settings.profile.description": "Change the details of your profile here.",
|
||||||
|
"settings.profile.email": "Email",
|
||||||
|
"settings.profile.password": "Password",
|
||||||
|
"settings.profile.avatar": "Avatar",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Appearance",
|
||||||
|
"settings.appearance.description": "Customize the theme of your dashboard.",
|
||||||
|
"settings.appearance.theme": "Theme",
|
||||||
|
"settings.appearance.themeDescription": "Select a theme for your dashboard",
|
||||||
|
"settings.appearance.themes.light": "Light",
|
||||||
|
"settings.appearance.themes.dark": "Dark",
|
||||||
|
"settings.appearance.themes.system": "System",
|
||||||
|
"settings.appearance.language": "Language",
|
||||||
|
"settings.appearance.languageDescription": "Select a language for your dashboard"
|
||||||
|
}
|
||||||
1
apps/dokploy/public/locales/pl/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
44
apps/dokploy/public/locales/pl/settings.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Zapisz",
|
||||||
|
"settings.server.domain.title": "Domena",
|
||||||
|
"settings.server.domain.description": "Dodaj domenę do aplikacji",
|
||||||
|
"settings.server.domain.form.domain": "Domena",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Email Let's Encrypt",
|
||||||
|
"settings.server.domain.form.certificate.label": "Certyfikat",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Wybierz certyfikat",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "Brak",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Domyślny)",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Serwer",
|
||||||
|
"settings.server.webServer.description": "Przeładuj lub wyczyść serwer",
|
||||||
|
"settings.server.webServer.actions": "Akcje",
|
||||||
|
"settings.server.webServer.reload": "Przeładuj",
|
||||||
|
"settings.server.webServer.watchLogs": "Obserwuj logi",
|
||||||
|
"settings.server.webServer.updateServerIp": "Zaktualizuj IP serwera",
|
||||||
|
"settings.server.webServer.server.label": "Serwer",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko",
|
||||||
|
"settings.server.webServer.storage.label": "Przestrzeń",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Wyczyść zatrzymane kontenery",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Wyczyść Docker Builder i System",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Wyczyść monitorowanie",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Wyczyść wszystko",
|
||||||
|
|
||||||
|
"settings.profile.title": "Konto",
|
||||||
|
"settings.profile.description": "Zmień szczegóły swojego profilu",
|
||||||
|
"settings.profile.email": "Email",
|
||||||
|
"settings.profile.password": "Hasło",
|
||||||
|
"settings.profile.avatar": "Avatar",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Wygląd",
|
||||||
|
"settings.appearance.description": "Dostosuj motyw swojego pulpitu",
|
||||||
|
"settings.appearance.theme": "Motyw",
|
||||||
|
"settings.appearance.themeDescription": "Wybierz motyw swojego pulpitu",
|
||||||
|
"settings.appearance.themes.light": "Jasny",
|
||||||
|
"settings.appearance.themes.dark": "Ciemny",
|
||||||
|
"settings.appearance.themes.system": "System",
|
||||||
|
"settings.appearance.language": "Język",
|
||||||
|
"settings.appearance.languageDescription": "Wybierz język swojego pulpitu"
|
||||||
|
}
|
||||||
1
apps/dokploy/public/locales/zh-Hans/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
40
apps/dokploy/public/locales/zh-Hans/settings.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "保存",
|
||||||
|
"settings.server.domain.title": "服务器域名",
|
||||||
|
"settings.server.domain.description": "添加一个域名到您的服务器。",
|
||||||
|
"settings.server.domain.form.domain": "域名",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱",
|
||||||
|
"settings.server.domain.form.certificate.label": "证书",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "选择一个证书",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "无",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (默认)",
|
||||||
|
"settings.server.webServer.title": "Web 服务器",
|
||||||
|
"settings.server.webServer.description": "管理 Web 服务器。",
|
||||||
|
"settings.server.webServer.actions": "操作",
|
||||||
|
"settings.server.webServer.reload": "重新加载",
|
||||||
|
"settings.server.webServer.watchLogs": "查看日志",
|
||||||
|
"settings.server.webServer.server.label": "服务器",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "修改环境变量",
|
||||||
|
"settings.server.webServer.storage.label": "磁盘空间",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "清理停止的容器",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统缓存",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Clean Monitoring",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "清理所有",
|
||||||
|
"settings.profile.title": "账户偏好",
|
||||||
|
"settings.profile.description": "更改您的个人资料详情。",
|
||||||
|
"settings.profile.email": "电子邮件",
|
||||||
|
"settings.profile.password": "密码",
|
||||||
|
"settings.profile.avatar": "头像",
|
||||||
|
"settings.appearance.title": "外观",
|
||||||
|
"settings.appearance.description": "自定义仪表板主题。",
|
||||||
|
"settings.appearance.theme": "主题",
|
||||||
|
"settings.appearance.themeDescription": "选择仪表板主题",
|
||||||
|
"settings.appearance.themes.light": "亮",
|
||||||
|
"settings.appearance.themes.dark": "暗",
|
||||||
|
"settings.appearance.themes.system": "系统",
|
||||||
|
"settings.appearance.language": "语言",
|
||||||
|
"settings.appearance.languageDescription": "选择仪表板语言"
|
||||||
|
}
|
||||||
4
apps/dokploy/public/templates/activepieces.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
|
||||||
|
<path d="M 175.034 156.727 C 154.522 121.333 162.546 73.285 192.958 49.41 C 223.367 25.535 264.651 34.874 285.163 70.271 L 423.708 309.332 C 444.22 344.732 436.198 392.78 405.783 416.655 C 375.371 440.532 334.094 431.191 313.579 395.794 L 253.513 292.145 C 245.791 280.823 230.072 282.584 220.633 293.569 C 212.808 302.678 210.245 325.982 208.027 346.159 C 207.703 349.123 207.386 352.011 207.057 354.782 C 205.853 367.988 201.934 381.052 195.111 392.832 C 172.809 431.313 127.916 441.458 94.849 415.502 C 61.788 389.543 53.051 337.299 75.353 298.811 C 86.917 278.851 104.563 266.513 123.48 262.884 L 123.455 262.852 C 178.116 253.627 188.248 181.826 178.247 162.266 L 175.034 156.727 Z" fill="#8142E3" style=""/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 824 B |
153
apps/dokploy/public/templates/blender.svg
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
width="11.567343mm"
|
||||||
|
height="15.032981mm"
|
||||||
|
viewBox="0 0 11.567343 15.03298"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="community_logo_black.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#c8c8c8"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="true"
|
||||||
|
borderlayer="true"
|
||||||
|
fit-margin-top="1"
|
||||||
|
fit-margin-left="1"
|
||||||
|
fit-margin-right="1"
|
||||||
|
fit-margin-bottom="1"/>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-115.93625,-150.07138)">
|
||||||
|
<g
|
||||||
|
transform="translate(-3.8788837,214.53487)"
|
||||||
|
id="g1369">
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#000000;fill-opacity:0.07058824;stroke:none;stroke-width:0.31555739;stroke-miterlimit:1.41420996;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
|
||||||
|
d="m 121.59341,-62.933898 c -0.43151,0 -0.77882,0.347312 -0.77882,0.778817 v 7.918777 c 0,0.04214 0.004,0.08316 0.0106,0.12345 7.5e-4,0.0053 10e-4,0.01041 0.002,0.01567 0.001,0.0073 0.002,0.01466 0.004,0.02186 0.10284,0.693169 0.73757,1.119278 2.19888,2.190555 2.64127,1.936306 2.45943,1.935512 5.11716,0.02186 1.68877,-1.215962 2.28048,-1.590346 2.23197,-2.501308 v -7.790874 c 0,-0.431505 -0.34751,-0.778817 -0.77902,-0.778817 z"
|
||||||
|
id="path1373"/>
|
||||||
|
<path
|
||||||
|
id="path1323"
|
||||||
|
d="m 121.59341,-63.463065 c -0.43151,0 -0.77882,0.347312 -0.77882,0.778817 v 7.918777 c 0,0.04214 0.004,0.08316 0.0106,0.12345 7.5e-4,0.0053 10e-4,0.01041 0.002,0.01567 0.001,0.0073 0.002,0.01466 0.004,0.02186 0.10284,0.693169 0.73757,1.119278 2.19888,2.190555 2.64127,1.936306 2.45943,1.935512 5.11716,0.02186 1.68877,-1.215962 2.28048,-1.590346 2.23197,-2.501308 v -7.790874 c 0,-0.431505 -0.34751,-0.778817 -0.77902,-0.778817 z"
|
||||||
|
style="opacity:1;fill:#363636;fill-opacity:1;stroke:none;stroke-width:0.31555739;stroke-miterlimit:1.41420996;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" />
|
||||||
|
<g
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996"
|
||||||
|
id="g1353"
|
||||||
|
transform="matrix(0.02054188,0,0,0.02054188,97.15326,-61.563495)">
|
||||||
|
<g
|
||||||
|
id="g1327"
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
|
||||||
|
<path
|
||||||
|
id="path1325"
|
||||||
|
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
d="m 364.467,-333.746 c 0.171,-1.908 1.646,-3.118 3.899,-3.118 2.256,0 3.73,1.21 3.901,3.118 z m 7.569,4.711 c -0.577,1.414 -1.937,2.251 -3.784,2.251 -2.313,0 -3.87,-1.444 -3.933,-3.725 h 13.297 c 0,-0.237 0,-0.435 0,-0.671 0,-5.714 -3.354,-8.925 -9.364,-8.925 -5.836,0 -9.365,3.241 -9.365,8.324 0,5.114 3.584,8.35 9.365,8.35 3.469,0 6.159,-1.189 7.817,-3.279 z"/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1331"
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
|
||||||
|
<path
|
||||||
|
id="path1329"
|
||||||
|
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
d="m 305.468,-333.737 c 0.176,-1.908 1.651,-3.118 3.906,-3.118 2.252,0 3.726,1.21 3.899,3.118 z m 7.574,4.711 c -0.578,1.418 -1.937,2.255 -3.788,2.255 -2.309,0 -3.87,-1.448 -3.931,-3.73 h 13.294 c 0,-0.234 0,-0.431 0,-0.667 0,-5.717 -3.353,-8.929 -9.363,-8.929 -5.839,0 -9.361,3.242 -9.361,8.325 0,5.114 3.582,8.35 9.361,8.35 3.468,0 6.16,-1.185 7.821,-3.278 z"/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1335"
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
|
||||||
|
<rect
|
||||||
|
id="rect1333"
|
||||||
|
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
height="19.617001"
|
||||||
|
width="4.7950001"
|
||||||
|
y="-343.56"
|
||||||
|
x="293.90701" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1339"
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
|
||||||
|
<path
|
||||||
|
id="path1337"
|
||||||
|
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
d="m 319.81,-338.348 h 4.822 v 1.168 c 1.707,-1.822 3.757,-2.743 6.069,-2.743 2.663,0 4.679,0.921 5.72,2.489 0.869,1.295 0.926,2.858 0.926,4.912 v 8.579 h -4.829 v -7.538 c 0,-3.128 -0.629,-4.572 -3.375,-4.572 -2.775,0 -4.511,1.653 -4.511,4.428 v 7.682 h -4.822 z"/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1343"
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
|
||||||
|
<path
|
||||||
|
id="path1341"
|
||||||
|
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
d="m 352.876,-331.538 c 0,2.685 -1.794,4.446 -4.57,4.446 -2.778,0 -4.572,-1.701 -4.572,-4.415 0,-2.754 1.77,-4.454 4.572,-4.454 2.776,0 4.57,1.73 4.57,4.423 z m 0,-6.157 c -1.219,-1.307 -2.983,-2.024 -5.435,-2.024 -5.29,0 -8.902,3.262 -8.902,8.151 0,4.793 3.587,8.146 8.815,8.146 2.397,0 4.157,-0.606 5.522,-1.965 v 1.444 h 4.825 v -20.861 l -4.825,1.244 z"/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1347"
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
|
||||||
|
<path
|
||||||
|
id="path1345"
|
||||||
|
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
d="m 282.947,-335.961 c 2.804,0 4.567,1.7 4.567,4.454 0,2.714 -1.791,4.415 -4.567,4.415 -2.774,0 -4.566,-1.761 -4.566,-4.446 0,-2.693 1.792,-4.423 4.566,-4.423 z m -4.566,-7.599 -4.827,-1.244 v 20.861 h 4.827 v -1.444 c 1.358,1.359 3.121,1.965 5.52,1.965 5.231,0 8.813,-3.353 8.813,-8.146 0,-4.889 -3.613,-8.151 -8.9,-8.151 -2.457,0 -4.22,0.717 -5.433,2.024 z"/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g1351"
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
|
||||||
|
<path
|
||||||
|
id="path1349"
|
||||||
|
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
d="m 378.806,-323.943 v -14.405 h 4.825 v 0.89 c 1.445,-1.74 2.974,-2.606 4.713,-2.606 0.345,0 0.779,0.056 1.356,0.113 v 4.107 c -0.465,-0.061 -0.983,-0.061 -1.533,-0.061 -2.805,0 -4.536,1.85 -4.536,4.996 v 6.966 z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(0.04039667,0,0,0.04039667,81.604348,-55.892386)"
|
||||||
|
style="clip-rule:evenodd;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996"
|
||||||
|
id="g1367">
|
||||||
|
<g
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
id="g1361"
|
||||||
|
style="fill:#ffffff;fill-opacity:1">
|
||||||
|
<path
|
||||||
|
d="m 243.13,-333.715 c 0.106,-1.891 1.032,-3.557 2.429,-4.738 1.37,-1.16 3.214,-1.869 5.226,-1.869 2.01,0 3.854,0.709 5.225,1.869 1.396,1.181 2.322,2.847 2.429,4.736 0.106,1.943 -0.675,3.748 -2.045,5.086 -1.397,1.361 -3.384,2.215 -5.609,2.215 -2.225,0 -4.216,-0.854 -5.612,-2.215 -1.371,-1.338 -2.15,-3.143 -2.043,-5.084 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
id="path1359" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
|
||||||
|
id="g1365"
|
||||||
|
style="fill:#ffffff;fill-opacity:1">
|
||||||
|
<path
|
||||||
|
d="m 230.94,-329.894 c 0.013,0.74 0.249,2.178 0.603,3.301 0.744,2.377 2.006,4.576 3.762,6.514 1.802,1.992 4.021,3.592 6.584,4.728 2.694,1.193 5.613,1.801 8.645,1.796 3.027,-0.004 5.946,-0.624 8.64,-1.826 2.563,-1.147 4.78,-2.754 6.579,-4.747 1.755,-1.946 3.015,-4.149 3.761,-6.526 0.375,-1.201 0.612,-2.42 0.707,-3.643 0.093,-1.205 0.054,-2.412 -0.117,-3.618 -0.334,-2.35 -1.147,-4.555 -2.399,-6.565 -1.145,-1.847 -2.621,-3.464 -4.376,-4.825 l 0.004,-0.003 -17.711,-13.599 c -0.016,-0.012 -0.029,-0.025 -0.046,-0.036 -1.162,-0.892 -3.116,-0.889 -4.394,0.005 -1.292,0.904 -1.44,2.399 -0.29,3.342 l -0.005,0.005 7.387,6.007 -22.515,0.024 c -0.011,0 -0.022,0 -0.03,0 -1.861,0.002 -3.65,1.223 -4.004,2.766 -0.364,1.572 0.9,2.876 2.835,2.883 l -0.003,0.007 11.412,-0.022 -20.364,15.631 c -0.026,0.019 -0.054,0.039 -0.078,0.058 -1.921,1.471 -2.542,3.917 -1.332,5.465 1.228,1.574 3.839,1.577 5.78,0.009 l 11.114,-9.096 c 0,0 -0.162,1.228 -0.149,1.965 z m 28.559,4.112 c -2.29,2.333 -5.496,3.656 -8.965,3.663 -3.474,0.006 -6.68,-1.305 -8.97,-3.634 -1.119,-1.135 -1.941,-2.441 -2.448,-3.832 -0.497,-1.367 -0.69,-2.818 -0.562,-4.282 0.121,-1.431 0.547,-2.796 1.227,-4.031 0.668,-1.214 1.588,-2.311 2.724,-3.239 2.226,-1.814 5.06,-2.796 8.024,-2.8 2.967,-0.004 5.799,0.969 8.027,2.777 1.134,0.924 2.053,2.017 2.721,3.229 0.683,1.234 1.106,2.594 1.232,4.029 0.126,1.462 -0.067,2.911 -0.564,4.279 -0.508,1.395 -1.327,2.701 -2.446,3.841 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero"
|
||||||
|
id="path1363" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/dokploy/public/templates/discord-tickets.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/dokploy/public/templates/discordtickets.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/dokploy/public/templates/invoiceshelf.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
1
apps/dokploy/public/templates/nextcloud-aio.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="256" height="128" version="1.1" viewBox="0 0 256 128" xmlns="http://www.w3.org/2000/svg"><path d="m128 7c-25.871 0-47.817 17.485-54.713 41.209-5.9795-12.461-18.642-21.209-33.287-21.209-20.304 0-37 16.696-37 37s16.696 37 37 37c14.645 0 27.308-8.7481 33.287-21.209 6.8957 23.724 28.842 41.209 54.713 41.209s47.817-17.485 54.713-41.209c5.9795 12.461 18.642 21.209 33.287 21.209 20.304 0 37-16.696 37-37s-16.696-37-37-37c-14.645 0-27.308 8.7481-33.287 21.209-6.8957-23.724-28.842-41.209-54.713-41.209zm0 22c19.46 0 35 15.54 35 35s-15.54 35-35 35-35-15.54-35-35 15.54-35 35-35zm-88 20c8.4146 0 15 6.5854 15 15s-6.5854 15-15 15-15-6.5854-15-15 6.5854-15 15-15zm176 0c8.4146 0 15 6.5854 15 15s-6.5854 15-15 15-15-6.5854-15-15 6.5854-15 15-15z" color="#000000" fill="#fff" style="-inkscape-stroke:none"/></svg>
|
||||||
|
After Width: | Height: | Size: 814 B |
13
apps/dokploy/public/templates/peppermint.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/dokploy/public/templates/postiz.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
apps/dokploy/public/templates/slash.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
29
apps/dokploy/public/templates/windmill.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 256 256" style="enable-background:new 0 0 256 256;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{opacity:0.4;fill:#FFFFFF;}
|
||||||
|
.st2{fill:#BCD4FC;}
|
||||||
|
.st3{fill:#3B82F6;}
|
||||||
|
.st4{fill:#B3B3B3;}
|
||||||
|
.st5{fill:url(#SVGID_1_);}
|
||||||
|
.st6{fill:url(#SVGID_00000021089067129159788970000008246765442136188072_);}
|
||||||
|
.st7{fill:url(#SVGID_00000117639240116366130650000015074833605515028638_);}
|
||||||
|
.st8{opacity:0.4;fill:url(#SVGID_00000101781798616409025840000016567063639337360777_);}
|
||||||
|
.st9{opacity:0.4;fill:url(#SVGID_00000052086836598721292040000002033117744178971046_);}
|
||||||
|
.st10{opacity:0.4;fill:url(#SVGID_00000159460939004760751800000002448009281983951536_);}
|
||||||
|
.st11{opacity:0.4;fill:url(#SVGID_00000013177830667419993080000017721442101626521532_);}
|
||||||
|
.st12{opacity:0.4;fill:url(#SVGID_00000152235521444854938490000006526001119318383285_);}
|
||||||
|
.st13{opacity:0.4;fill:url(#SVGID_00000119823135212293698520000012774889010992664993_);}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<polygon class="st2" points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22 "/>
|
||||||
|
<polygon class="st3" points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02 "/>
|
||||||
|
<polygon class="st3" points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46
|
||||||
|
"/>
|
||||||
|
<polygon class="st3" points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22 "/>
|
||||||
|
<polygon class="st2" points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21 "/>
|
||||||
|
<polygon class="st2" points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -4,6 +4,7 @@ import {
|
|||||||
apiCreateUserInvitation,
|
apiCreateUserInvitation,
|
||||||
apiFindOneToken,
|
apiFindOneToken,
|
||||||
apiRemoveUser,
|
apiRemoveUser,
|
||||||
|
apiUpdateAdmin,
|
||||||
users,
|
users,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
findUserById,
|
findUserById,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
removeUserByAuthId,
|
removeUserByAuthId,
|
||||||
|
updateAdmin,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -26,6 +28,18 @@ export const adminRouter = createTRPCRouter({
|
|||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
update: adminProcedure
|
||||||
|
.input(apiUpdateAdmin)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (ctx.user.rol === "user") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to update this admin",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { authId } = await findAdminById(ctx.user.adminId);
|
||||||
|
return updateAdmin(authId, input);
|
||||||
|
}),
|
||||||
createUserInvitation: adminProcedure
|
createUserInvitation: adminProcedure
|
||||||
.input(apiCreateUserInvitation)
|
.input(apiCreateUserInvitation)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
findAdminById,
|
findAdminById,
|
||||||
findServerById,
|
findServerById,
|
||||||
findServersByAdminId,
|
findServersByAdminId,
|
||||||
|
getPublicIpWithFallback,
|
||||||
haveActiveServices,
|
haveActiveServices,
|
||||||
removeDeploymentsByServerId,
|
removeDeploymentsByServerId,
|
||||||
serverSetup,
|
serverSetup,
|
||||||
@@ -181,4 +182,11 @@ export const serverRouter = createTRPCRouter({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
publicIp: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const ip = await getPublicIpWithFallback();
|
||||||
|
return ip;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
writeMainConfig,
|
writeMainConfig,
|
||||||
writeTraefikConfigInPath,
|
writeTraefikConfigInPath,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
||||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
@@ -657,6 +658,54 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return { status: "not_cloud" };
|
return { status: "not_cloud" };
|
||||||
}),
|
}),
|
||||||
|
setupGPU: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD && !input.serverId) {
|
||||||
|
throw new Error("Select a server to enable the GPU Setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setupGPUSupport(input.serverId);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("GPU Setup Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
checkGPUStatus: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
if (IS_CLOUD && !input.serverId) {
|
||||||
|
return {
|
||||||
|
driverInstalled: false,
|
||||||
|
driverVersion: undefined,
|
||||||
|
gpuModel: undefined,
|
||||||
|
runtimeInstalled: false,
|
||||||
|
runtimeConfigured: false,
|
||||||
|
cudaSupport: undefined,
|
||||||
|
cudaVersion: undefined,
|
||||||
|
memoryInfo: undefined,
|
||||||
|
availableGPUs: 0,
|
||||||
|
swarmEnabled: false,
|
||||||
|
gpuResources: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await checkGPUStatus(input.serverId || "");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("Failed to check GPU status");
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
// {
|
// {
|
||||||
// "Parallelism": 1,
|
// "Parallelism": 1,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import type { Session, User } from "lucia";
|
import type { Session, User } from "lucia";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { setupTerminalWebSocketServer } from "./wss/terminal";
|
|||||||
config({ path: ".env" });
|
config({ path: ".env" });
|
||||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
const app = next({ dev });
|
const app = next({ dev, turbopack: dev });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
void app.prepare().then(async () => {
|
void app.prepare().then(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
67
apps/dokploy/templates/activepieces/docker-compose.yml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
activepieces:
|
||||||
|
image: activepieces/activepieces:0.35.0
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
AP_ENGINE_EXECUTABLE_PATH: dist/packages/engine/main.js
|
||||||
|
AP_API_KEY: ${AP_API_KEY}
|
||||||
|
AP_ENCRYPTION_KEY: ${AP_ENCRYPTION_KEY}
|
||||||
|
AP_JWT_SECRET: ${AP_JWT_SECRET}
|
||||||
|
AP_ENVIRONMENT: prod
|
||||||
|
AP_FRONTEND_URL: https://${AP_HOST}
|
||||||
|
AP_WEBHOOK_TIMEOUT_SECONDS: 30
|
||||||
|
AP_TRIGGER_DEFAULT_POLL_INTERVAL: 5
|
||||||
|
AP_POSTGRES_DATABASE: activepieces
|
||||||
|
AP_POSTGRES_HOST: postgres
|
||||||
|
AP_POSTGRES_PORT: 5432
|
||||||
|
AP_POSTGRES_USERNAME: activepieces
|
||||||
|
AP_POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
|
||||||
|
AP_EXECUTION_MODE: UNSANDBOXED
|
||||||
|
AP_REDIS_HOST: redis
|
||||||
|
AP_REDIS_PORT: 6379
|
||||||
|
AP_SANDBOX_RUN_TIME_SECONDS: 600
|
||||||
|
AP_TELEMETRY_ENABLED: "false"
|
||||||
|
AP_TEMPLATES_SOURCE_URL: https://cloud.activepieces.com/api/v1/flow-templates
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:14
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: activepieces
|
||||||
|
POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_USER: activepieces
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U activepieces -d activepieces"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 30s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 30s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
44
apps/dokploy/templates/activepieces/index.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
|
||||||
|
const apiKey = Array.from({ length: 32 }, () =>
|
||||||
|
Math.floor(Math.random() * 16).toString(16),
|
||||||
|
).join("");
|
||||||
|
const encryptionKey = Array.from({ length: 32 }, () =>
|
||||||
|
Math.floor(Math.random() * 16).toString(16),
|
||||||
|
).join("");
|
||||||
|
const jwtSecret = Array.from({ length: 32 }, () =>
|
||||||
|
Math.floor(Math.random() * 16).toString(16),
|
||||||
|
).join("");
|
||||||
|
const postgresPassword = Array.from({ length: 32 }, () =>
|
||||||
|
Math.floor(Math.random() * 16).toString(16),
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 80,
|
||||||
|
serviceName: "activepieces",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`AP_HOST=${mainDomain}`,
|
||||||
|
`AP_API_KEY=${apiKey}`,
|
||||||
|
`AP_ENCRYPTION_KEY=${encryptionKey}`,
|
||||||
|
`AP_JWT_SECRET=${jwtSecret}`,
|
||||||
|
`AP_POSTGRES_PASSWORD=${postgresPassword}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
26
apps/dokploy/templates/blender/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
blender:
|
||||||
|
image: lscr.io/linuxserver/blender:latest
|
||||||
|
runtime: nvidia
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities:
|
||||||
|
- gpu
|
||||||
|
environment:
|
||||||
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
- NVIDIA_DRIVER_CAPABILITIES=all
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=Etc/UTC
|
||||||
|
- SUBFOLDER=/ #optional
|
||||||
|
ports:
|
||||||
|
- 3000
|
||||||
|
- 3001
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 1gb
|
||||||
34
apps/dokploy/templates/blender/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateHash,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainServiceHash = generateHash(schema.projectName);
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 3000,
|
||||||
|
serviceName: "blender",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
"PUID=1000",
|
||||||
|
"PGID=1000",
|
||||||
|
"TZ=Etc/UTC",
|
||||||
|
"SUBFOLDER=/",
|
||||||
|
"NVIDIA_VISIBLE_DEVICES=all",
|
||||||
|
"NVIDIA_DRIVER_CAPABILITIES=all",
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
54
apps/dokploy/templates/discord-tickets/docker-compose.yml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8
|
||||||
|
restart: unless-stopped
|
||||||
|
hostname: mysql
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- tickets-mysql:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
|
MYSQL_USER: ${MYSQL_USER}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
bot:
|
||||||
|
image: eartharoid/discord-tickets:4.0.21
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
hostname: bot
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- tickets-bot:/home/container/user
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
environment:
|
||||||
|
DB_CONNECTION_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql/${MYSQL_DATABASE}
|
||||||
|
DISCORD_SECRET: ${DISCORD_SECRET}
|
||||||
|
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
||||||
|
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||||
|
DB_PROVIDER: mysql
|
||||||
|
HTTP_EXTERNAL: https://${TICKETS_HOST}
|
||||||
|
HTTP_HOST: 0.0.0.0
|
||||||
|
HTTP_PORT: 8169
|
||||||
|
HTTP_TRUST_PROXY: "true"
|
||||||
|
PUBLIC_BOT: "false"
|
||||||
|
PUBLISH_COMMANDS: "true"
|
||||||
|
SUPER: ${SUPER_USERS}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tickets-mysql:
|
||||||
|
tickets-bot:
|
||||||
47
apps/dokploy/templates/discord-tickets/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const mysqlPassword = generatePassword();
|
||||||
|
const mysqlRootPassword = generatePassword();
|
||||||
|
const mysqlUser = "tickets";
|
||||||
|
const mysqlDatabase = "tickets";
|
||||||
|
|
||||||
|
// Generate encryption key in the format they use
|
||||||
|
const encryptionKey = Array.from({ length: 48 }, () =>
|
||||||
|
Math.floor(Math.random() * 16).toString(16),
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 8169,
|
||||||
|
serviceName: "bot",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`TICKETS_HOST=${mainDomain}`,
|
||||||
|
`MYSQL_DATABASE=${mysqlDatabase}`,
|
||||||
|
`MYSQL_PASSWORD=${mysqlPassword}`,
|
||||||
|
`MYSQL_ROOT_PASSWORD=${mysqlRootPassword}`,
|
||||||
|
`MYSQL_USER=${mysqlUser}`,
|
||||||
|
`ENCRYPTION_KEY=${encryptionKey}`,
|
||||||
|
// These need to be set by the user through the UI
|
||||||
|
"# Follow the guide at: https://discordtickets.app/self-hosting/installation/docker/#creating-the-discord-application",
|
||||||
|
"DISCORD_SECRET=",
|
||||||
|
"DISCORD_TOKEN=",
|
||||||
|
"SUPER_USERS=YOUR_DISCORD_USER_ID", // Default super user
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
55
apps/dokploy/templates/invoiceshelf/docker-compose.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
invoiceshelf_db:
|
||||||
|
image: postgres:15
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
- POSTGRES_USER=${DB_USERNAME}
|
||||||
|
- POSTGRES_DB=${DB_DATABASE}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
invoiceshelf:
|
||||||
|
image: invoiceshelf/invoiceshelf:latest
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- app_data:/data
|
||||||
|
- app_conf:/conf
|
||||||
|
environment:
|
||||||
|
- PHP_TZ=UTC
|
||||||
|
- TIMEZONE=UTC
|
||||||
|
- APP_NAME=InvoiceShelf
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=false
|
||||||
|
- APP_URL=http://${INVOICESHELF_HOST}
|
||||||
|
- DB_CONNECTION=pgsql
|
||||||
|
- DB_HOST=invoiceshelf_db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_DATABASE=${DB_DATABASE}
|
||||||
|
- DB_USERNAME=${DB_USERNAME}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
- CACHE_STORE=file
|
||||||
|
- SESSION_DRIVER=file
|
||||||
|
- SESSION_LIFETIME=120
|
||||||
|
- SESSION_ENCRYPT=true
|
||||||
|
- SESSION_PATH=/
|
||||||
|
- SESSION_DOMAIN=${INVOICESHELF_HOST}
|
||||||
|
- SANCTUM_STATEFUL_DOMAINS=${INVOICESHELF_HOST}
|
||||||
|
- STARTUP_DELAY=10
|
||||||
|
depends_on:
|
||||||
|
invoiceshelf_db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
app_data:
|
||||||
|
app_conf:
|
||||||
34
apps/dokploy/templates/invoiceshelf/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const dbPassword = generatePassword();
|
||||||
|
const dbUsername = "invoiceshelf";
|
||||||
|
const dbDatabase = "invoiceshelf";
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 80,
|
||||||
|
serviceName: "invoiceshelf",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`INVOICESHELF_HOST=${mainDomain}`,
|
||||||
|
`DB_PASSWORD=${dbPassword}`,
|
||||||
|
`DB_USERNAME=${dbUsername}`,
|
||||||
|
`DB_DATABASE=${dbDatabase}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
38
apps/dokploy/templates/nextcloud-aio/docker-compose.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
services:
|
||||||
|
nextcloud:
|
||||||
|
image: nextcloud:30.0.2
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
ports:
|
||||||
|
- 80
|
||||||
|
volumes:
|
||||||
|
- nextcloud_data:/var/www/html
|
||||||
|
environment:
|
||||||
|
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN}
|
||||||
|
- MYSQL_HOST=nextcloud_db
|
||||||
|
- MYSQL_DATABASE=nextcloud
|
||||||
|
- MYSQL_USER=nextcloud
|
||||||
|
- MYSQL_PASSWORD=${MYSQL_SECRET_PASSWORD}
|
||||||
|
- OVERWRITEPROTOCOL=https
|
||||||
|
|
||||||
|
nextcloud_db:
|
||||||
|
image: mariadb
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- nextcloud_db_data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=${MYSQL_SECRET_PASSWORD_ROOT}
|
||||||
|
- MYSQL_DATABASE=nextcloud
|
||||||
|
- MYSQL_USER=nextcloud
|
||||||
|
- MYSQL_PASSWORD=${MYSQL_SECRET_PASSWORD}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nextcloud_data:
|
||||||
|
nextcloud_db_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
28
apps/dokploy/templates/nextcloud-aio/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const randomDomain = generateRandomDomain(schema);
|
||||||
|
const databasePassword = generatePassword();
|
||||||
|
const databaseRootPassword = generatePassword();
|
||||||
|
const envs = [
|
||||||
|
`NEXTCLOUD_DOMAIN=${randomDomain}`,
|
||||||
|
`MYSQL_SECRET_PASSWORD=${databasePassword}`,
|
||||||
|
`MYSQL_SECRET_PASSWORD_ROOT=${databaseRootPassword}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: randomDomain,
|
||||||
|
port: 80,
|
||||||
|
serviceName: "nextcloud",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return { envs, domains };
|
||||||
|
}
|
||||||
36
apps/dokploy/templates/peppermint/docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
peppermint_postgres:
|
||||||
|
image: postgres:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: peppermint
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: peppermint
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U peppermint"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
peppermint:
|
||||||
|
image: pepperlabs/peppermint:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
depends_on:
|
||||||
|
peppermint_postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DB_USERNAME: "peppermint"
|
||||||
|
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
DB_HOST: "peppermint_postgres"
|
||||||
|
SECRET: ${SECRET}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
43
apps/dokploy/templates/peppermint/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateBase64,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
// Generate domains and secrets
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const apiDomain = generateRandomDomain(schema);
|
||||||
|
const postgresPassword = generatePassword();
|
||||||
|
const secret = generateBase64(32);
|
||||||
|
|
||||||
|
// Configure domain routing
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 3000,
|
||||||
|
serviceName: "peppermint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host: apiDomain,
|
||||||
|
port: 5003,
|
||||||
|
serviceName: "peppermint",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set environment variables
|
||||||
|
const envs = [
|
||||||
|
`MAIN_DOMAIN=${mainDomain}`,
|
||||||
|
`API_DOMAIN=${apiDomain}`,
|
||||||
|
`POSTGRES_PASSWORD=${postgresPassword}`,
|
||||||
|
`SECRET=${secret}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
64
apps/dokploy/templates/postiz/docker-compose.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postiz:
|
||||||
|
image: ghcr.io/gitroomhq/postiz-app:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
MAIN_URL: "https://${POSTIZ_HOST}"
|
||||||
|
FRONTEND_URL: "https://${POSTIZ_HOST}"
|
||||||
|
NEXT_PUBLIC_BACKEND_URL: "https://${POSTIZ_HOST}/api"
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
DATABASE_URL: "postgresql://${DB_USER}:${DB_PASSWORD}@postiz-postgres:5432/${DB_NAME}"
|
||||||
|
REDIS_URL: "redis://postiz-redis:6379"
|
||||||
|
BACKEND_INTERNAL_URL: "http://localhost:3000"
|
||||||
|
IS_GENERAL: "true"
|
||||||
|
STORAGE_PROVIDER: "local"
|
||||||
|
UPLOAD_DIRECTORY: "/uploads"
|
||||||
|
NEXT_PUBLIC_UPLOAD_DIRECTORY: "/uploads"
|
||||||
|
volumes:
|
||||||
|
- postiz-config:/config/
|
||||||
|
- postiz-uploads:/uploads/
|
||||||
|
depends_on:
|
||||||
|
postiz-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
postiz-redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
postiz-postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_DB: ${DB_NAME}
|
||||||
|
volumes:
|
||||||
|
- postgres-volume:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U ${DB_USER} -d ${DB_NAME}
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
postiz-redis:
|
||||||
|
image: redis:7.2
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
volumes:
|
||||||
|
- postiz-redis-data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-volume:
|
||||||
|
postiz-redis-data:
|
||||||
|
postiz-config:
|
||||||
|
postiz-uploads:
|
||||||
37
apps/dokploy/templates/postiz/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateBase64,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const dbPassword = generatePassword();
|
||||||
|
const dbUser = "postiz";
|
||||||
|
const dbName = "postiz";
|
||||||
|
const jwtSecret = generateBase64(32);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 5000,
|
||||||
|
serviceName: "postiz",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`POSTIZ_HOST=${mainDomain}`,
|
||||||
|
`DB_PASSWORD=${dbPassword}`,
|
||||||
|
`DB_USER=${dbUser}`,
|
||||||
|
`DB_NAME=${dbName}`,
|
||||||
|
`JWT_SECRET=${jwtSecret}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
37
apps/dokploy/templates/slash/docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
slash:
|
||||||
|
image: yourselfhosted/slash:latest
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- slash_data:/var/opt/slash
|
||||||
|
environment:
|
||||||
|
- SLASH_DRIVER=postgres
|
||||||
|
- SLASH_DSN=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}?sslmode=disable
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${DB_USER}
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
- POSTGRES_DB=${DB_NAME}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
slash_data:
|
||||||
|
postgres_data:
|
||||||
33
apps/dokploy/templates/slash/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const dbPassword = generatePassword();
|
||||||
|
const dbUser = "slash";
|
||||||
|
const dbName = "slash";
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 5231,
|
||||||
|
serviceName: "slash",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`DB_USER=${dbUser}`,
|
||||||
|
`DB_PASSWORD=${dbPassword}`,
|
||||||
|
`DB_NAME=${dbName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -702,4 +702,139 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["IA", "chat"],
|
tags: ["IA", "chat"],
|
||||||
load: () => import("./lobe-chat/index").then((m) => m.generate),
|
load: () => import("./lobe-chat/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "peppermint",
|
||||||
|
name: "Peppermint",
|
||||||
|
version: "latest",
|
||||||
|
description:
|
||||||
|
"Peppermint is a modern, open-source API development platform that helps you build, test and document your APIs.",
|
||||||
|
logo: "peppermint.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/Peppermint-Lab/peppermint",
|
||||||
|
website: "https://peppermint.sh/",
|
||||||
|
docs: "https://docs.peppermint.sh/",
|
||||||
|
},
|
||||||
|
tags: ["api", "development", "documentation"],
|
||||||
|
load: () => import("./peppermint/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "windmill",
|
||||||
|
name: "Windmill",
|
||||||
|
version: "latest",
|
||||||
|
description:
|
||||||
|
"A developer platform to build production-grade workflows and internal apps. Open-source alternative to Airplane, Retool, and GitHub Actions.",
|
||||||
|
logo: "windmill.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/windmill-labs/windmill",
|
||||||
|
website: "https://www.windmill.dev/",
|
||||||
|
docs: "https://docs.windmill.dev/",
|
||||||
|
},
|
||||||
|
tags: ["workflow", "automation", "development"],
|
||||||
|
load: () => import("./windmill/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "activepieces",
|
||||||
|
name: "Activepieces",
|
||||||
|
version: "0.35.0",
|
||||||
|
description:
|
||||||
|
"Open-source no-code business automation tool. An alternative to Zapier, Make.com, and Tray.",
|
||||||
|
logo: "activepieces.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/activepieces/activepieces",
|
||||||
|
website: "https://www.activepieces.com/",
|
||||||
|
docs: "https://www.activepieces.com/docs",
|
||||||
|
},
|
||||||
|
tags: ["automation", "workflow", "no-code"],
|
||||||
|
load: () => import("./activepieces/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "invoiceshelf",
|
||||||
|
name: "InvoiceShelf",
|
||||||
|
version: "latest",
|
||||||
|
description:
|
||||||
|
"InvoiceShelf is a self-hosted open source invoicing system for freelancers and small businesses.",
|
||||||
|
logo: "invoiceshelf.png",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/InvoiceShelf/invoiceshelf",
|
||||||
|
website: "https://invoiceshelf.com",
|
||||||
|
docs: "https://github.com/InvoiceShelf/invoiceshelf#readme",
|
||||||
|
},
|
||||||
|
tags: ["invoice", "business", "finance"],
|
||||||
|
load: () => import("./invoiceshelf/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "postiz",
|
||||||
|
name: "Postiz",
|
||||||
|
version: "latest",
|
||||||
|
description:
|
||||||
|
"Postiz is a modern, open-source platform for managing and publishing content across multiple channels.",
|
||||||
|
logo: "postiz.png",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/gitroomhq/postiz",
|
||||||
|
website: "https://postiz.io",
|
||||||
|
docs: "https://docs.postiz.io",
|
||||||
|
},
|
||||||
|
tags: ["cms", "content-management", "publishing"],
|
||||||
|
load: () => import("./postiz/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "slash",
|
||||||
|
name: "Slash",
|
||||||
|
version: "latest",
|
||||||
|
description:
|
||||||
|
"Slash is a modern, self-hosted bookmarking service and link shortener that helps you organize and share your favorite links.",
|
||||||
|
logo: "slash.png",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/yourselfhosted/slash",
|
||||||
|
website: "https://github.com/yourselfhosted/slash#readme",
|
||||||
|
docs: "https://github.com/yourselfhosted/slash/wiki",
|
||||||
|
},
|
||||||
|
tags: ["bookmarks", "link-shortener", "self-hosted"],
|
||||||
|
load: () => import("./slash/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "discord-tickets",
|
||||||
|
name: "Discord Tickets",
|
||||||
|
version: "4.0.21",
|
||||||
|
description:
|
||||||
|
"An open-source Discord bot for creating and managing support ticket channels.",
|
||||||
|
logo: "discord-tickets.png",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/discord-tickets/bot",
|
||||||
|
website: "https://discordtickets.app",
|
||||||
|
docs: "https://discordtickets.app/self-hosting/installation/docker/",
|
||||||
|
},
|
||||||
|
tags: ["discord", "tickets", "support"],
|
||||||
|
load: () => import("./discord-tickets/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nextcloud-aio",
|
||||||
|
name: "Nextcloud All in One",
|
||||||
|
version: "30.0.2",
|
||||||
|
description:
|
||||||
|
"Nextcloud (AIO) is a self-hosted file storage and sync platform with powerful collaboration capabilities. It integrates Files, Talk, Groupware, Office, Assistant and more into a single platform for remote work and data protection.",
|
||||||
|
logo: "nextcloud-aio.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/nextcloud/docker",
|
||||||
|
website: "https://nextcloud.com/",
|
||||||
|
docs: "https://docs.nextcloud.com/",
|
||||||
|
},
|
||||||
|
tags: ["file", "sync"],
|
||||||
|
load: () => import("./nextcloud-aio/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "blender",
|
||||||
|
name: "Blender",
|
||||||
|
version: "latest",
|
||||||
|
description:
|
||||||
|
"Blender is a free and open-source 3D creation suite. It supports the entire 3D pipeline—modeling, rigging, animation, simulation, rendering, compositing and motion tracking, video editing and 2D animation pipeline.",
|
||||||
|
logo: "blender.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/linuxserver/docker-blender",
|
||||||
|
website: "https://www.blender.org/",
|
||||||
|
docs: "https://docs.blender.org/",
|
||||||
|
},
|
||||||
|
tags: ["3d", "rendering", "animation"],
|
||||||
|
load: () => import("./blender/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
107
apps/dokploy/templates/windmill/docker-compose.yml
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
shm_size: 1g
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: windmill
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
windmill_server:
|
||||||
|
image: ghcr.io/windmill-labs/windmill:main
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
- MODE=server
|
||||||
|
- BASE_URL=http://${WINDMILL_HOST}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- worker_logs:/tmp/windmill/logs
|
||||||
|
|
||||||
|
windmill_worker:
|
||||||
|
image: ghcr.io/windmill-labs/windmill:main
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1"
|
||||||
|
memory: 2048M
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
- MODE=worker
|
||||||
|
- WORKER_GROUP=default
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- worker_dependency_cache:/tmp/windmill/cache
|
||||||
|
- worker_logs:/tmp/windmill/logs
|
||||||
|
|
||||||
|
windmill_worker_native:
|
||||||
|
image: ghcr.io/windmill-labs/windmill:main
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.1"
|
||||||
|
memory: 128M
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
- MODE=worker
|
||||||
|
- WORKER_GROUP=native
|
||||||
|
- NUM_WORKERS=8
|
||||||
|
- SLEEP_QUEUE=200
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- worker_logs:/tmp/windmill/logs
|
||||||
|
|
||||||
|
lsp:
|
||||||
|
image: ghcr.io/windmill-labs/windmill-lsp:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- lsp_cache:/root/.cache
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: ghcr.io/windmill-labs/caddy-l4:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
volumes:
|
||||||
|
- ../files/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
environment:
|
||||||
|
- BASE_URL=":80"
|
||||||
|
depends_on:
|
||||||
|
- windmill_server
|
||||||
|
- lsp
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
worker_dependency_cache:
|
||||||
|
worker_logs:
|
||||||
|
lsp_cache:
|
||||||
43
apps/dokploy/templates/windmill/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const postgresPassword = generatePassword();
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 80,
|
||||||
|
serviceName: "caddy",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`WINDMILL_HOST=${mainDomain}`,
|
||||||
|
`POSTGRES_PASSWORD=${postgresPassword}`,
|
||||||
|
`DATABASE_URL=postgres://postgres:${postgresPassword}@db/windmill?sslmode=disable`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const mounts: Template["mounts"] = [
|
||||||
|
{
|
||||||
|
filePath: "Caddyfile",
|
||||||
|
content: `:80 {
|
||||||
|
bind 0.0.0.0
|
||||||
|
reverse_proxy /ws/* http://lsp:3001
|
||||||
|
reverse_proxy /* http://windmill_server:8000
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
mounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
"**/*.js",
|
"**/*.js",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
"env.js",
|
"env.js",
|
||||||
"next.config.mjs"
|
"next.config.mjs",
|
||||||
|
"next-i18next.config.mjs"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
19
apps/dokploy/utils/hooks/use-locale.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
const SUPPORTED_LOCALES = ["en", "pl", "zh-Hans"] as const;
|
||||||
|
|
||||||
|
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||||
|
|
||||||
|
export default function useLocale() {
|
||||||
|
const currentLocale = (Cookies.get("DOKPLOY_LOCALE") ?? "en") as Locale;
|
||||||
|
|
||||||
|
const setLocale = (locale: Locale) => {
|
||||||
|
Cookies.set("DOKPLOY_LOCALE", locale, { expires: 365 });
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale: currentLocale,
|
||||||
|
setLocale,
|
||||||
|
};
|
||||||
|
}
|
||||||
15
apps/dokploy/utils/i18n.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { NextApiRequestCookies } from "next/dist/server/api-utils";
|
||||||
|
|
||||||
|
export function getLocale(cookies: NextApiRequestCookies) {
|
||||||
|
const locale = cookies.DOKPLOY_LOCALE ?? "en";
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// libs/i18n.js
|
||||||
|
import { serverSideTranslations as originalServerSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
import nextI18NextConfig from "../next-i18next.config.cjs";
|
||||||
|
|
||||||
|
export const serverSideTranslations = (
|
||||||
|
locale: string,
|
||||||
|
namespaces = ["common"],
|
||||||
|
) => originalServerSideTranslations(locale, namespaces, nextI18NextConfig);
|
||||||
@@ -53,6 +53,8 @@ const createSchema = createInsertSchema(admins, {
|
|||||||
letsEncryptEmail: z.string().optional(),
|
letsEncryptEmail: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiUpdateAdmin = createSchema.partial();
|
||||||
|
|
||||||
export const apiSaveSSHKey = createSchema
|
export const apiSaveSSHKey = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
sshPrivateKey: true,
|
sshPrivateKey: true,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const projects = pgTable("project", {
|
|||||||
adminId: text("adminId")
|
adminId: text("adminId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||||
|
env: text("env").notNull().default(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const projectRelations = relations(projects, ({ many, one }) => ({
|
export const projectRelations = relations(projects, ({ many, one }) => ({
|
||||||
@@ -65,10 +66,16 @@ export const apiRemoveProject = createSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
export const apiUpdateProject = createSchema
|
// export const apiUpdateProject = createSchema
|
||||||
.pick({
|
// .pick({
|
||||||
name: true,
|
// name: true,
|
||||||
description: true,
|
// description: true,
|
||||||
projectId: true,
|
// projectId: true,
|
||||||
})
|
// env: true,
|
||||||
.required();
|
// })
|
||||||
|
// .required();
|
||||||
|
|
||||||
|
export const apiUpdateProject = createSchema.partial().extend({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
});
|
||||||
|
// .omit({ serverId: true });
|
||||||
|
|||||||
@@ -116,3 +116,4 @@ export * from "./monitoring/utilts";
|
|||||||
|
|
||||||
export * from "./db/validations/domain";
|
export * from "./db/validations/domain";
|
||||||
export * from "./db/validations/index";
|
export * from "./db/validations/index";
|
||||||
|
export * from "./utils/gpu-setup";
|
||||||
|
|||||||
@@ -180,7 +180,10 @@ const createEnvFile = (compose: ComposeNested) => {
|
|||||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
const envFileContent = prepareEnvironmentVariables(
|
||||||
|
envContent,
|
||||||
|
compose.project.env,
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
if (!existsSync(dirname(envFilePath))) {
|
if (!existsSync(dirname(envFilePath))) {
|
||||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||||
@@ -206,7 +209,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
|||||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
const envFileContent = prepareEnvironmentVariables(
|
||||||
|
envContent,
|
||||||
|
compose.project.env,
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
const encodedContent = encodeBase64(envFileContent);
|
const encodedContent = encodeBase64(envFileContent);
|
||||||
return `
|
return `
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export const buildCustomDocker = async (
|
|||||||
|
|
||||||
const defaultContextPath =
|
const defaultContextPath =
|
||||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||||
const args = prepareEnvironmentVariables(buildArgs);
|
const args = prepareEnvironmentVariables(
|
||||||
|
buildArgs,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
|
|
||||||
const dockerContextPath = getDockerContextPath(application);
|
const dockerContextPath = getDockerContextPath(application);
|
||||||
|
|
||||||
@@ -38,7 +41,7 @@ export const buildCustomDocker = async (
|
|||||||
as it could be publicly exposed.
|
as it could be publicly exposed.
|
||||||
*/
|
*/
|
||||||
if (!publishDirectory) {
|
if (!publishDirectory) {
|
||||||
createEnvFile(dockerFilePath, env);
|
createEnvFile(dockerFilePath, env, application.project.env);
|
||||||
}
|
}
|
||||||
|
|
||||||
await spawnAsync(
|
await spawnAsync(
|
||||||
@@ -71,7 +74,10 @@ export const getDockerCommand = (
|
|||||||
|
|
||||||
const defaultContextPath =
|
const defaultContextPath =
|
||||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||||
const args = prepareEnvironmentVariables(buildArgs);
|
const args = prepareEnvironmentVariables(
|
||||||
|
buildArgs,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
|
|
||||||
const dockerContextPath =
|
const dockerContextPath =
|
||||||
getDockerContextPath(application) || defaultContextPath;
|
getDockerContextPath(application) || defaultContextPath;
|
||||||
@@ -92,7 +98,11 @@ export const getDockerCommand = (
|
|||||||
*/
|
*/
|
||||||
let command = "";
|
let command = "";
|
||||||
if (!publishDirectory) {
|
if (!publishDirectory) {
|
||||||
command += createEnvFileCommand(dockerFilePath, env);
|
command += createEnvFileCommand(
|
||||||
|
dockerFilePath,
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
command += `
|
command += `
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export const buildHeroku = async (
|
|||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const args = [
|
const args = [
|
||||||
"build",
|
"build",
|
||||||
@@ -44,7 +47,10 @@ export const getHerokuCommand = (
|
|||||||
const { env, appName } = application;
|
const { env, appName } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"build",
|
"build",
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ import { buildStatic, getStaticCommand } from "./static";
|
|||||||
// DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
// DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
||||||
export type ApplicationNested = InferResultType<
|
export type ApplicationNested = InferResultType<
|
||||||
"applications",
|
"applications",
|
||||||
{ mounts: true; security: true; redirects: true; ports: true; registry: true }
|
{
|
||||||
|
mounts: true;
|
||||||
|
security: true;
|
||||||
|
redirects: true;
|
||||||
|
ports: true;
|
||||||
|
registry: true;
|
||||||
|
project: true;
|
||||||
|
}
|
||||||
>;
|
>;
|
||||||
export const buildApplication = async (
|
export const buildApplication = async (
|
||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
@@ -133,7 +140,10 @@ export const mechanizeDockerContainer = async (
|
|||||||
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
const filesMount = generateFileMounts(appName, application);
|
const filesMount = generateFileMounts(appName, application);
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
|
|
||||||
const image = getImageName(application);
|
const image = getImageName(application);
|
||||||
const authConfig = getAuthConfig(application);
|
const authConfig = getAuthConfig(application);
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export const buildNixpacks = async (
|
|||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
|
|
||||||
const writeToStream = (data: string) => {
|
const writeToStream = (data: string) => {
|
||||||
if (writeStream.writable) {
|
if (writeStream.writable) {
|
||||||
@@ -92,7 +95,10 @@ export const getNixpacksCommand = (
|
|||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
|
|
||||||
const args = ["build", buildAppDirectory, "--name", appName];
|
const args = ["build", buildAppDirectory, "--name", appName];
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ export const buildPaketo = async (
|
|||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const args = [
|
const args = [
|
||||||
"build",
|
"build",
|
||||||
@@ -43,7 +46,10 @@ export const getPaketoCommand = (
|
|||||||
const { env, appName } = application;
|
const { env, appName } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.project.env,
|
||||||
|
);
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"build",
|
"build",
|
||||||
|
|||||||
@@ -2,17 +2,29 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||||
|
|
||||||
export const createEnvFile = (directory: string, env: string | null) => {
|
export const createEnvFile = (
|
||||||
|
directory: string,
|
||||||
|
env: string | null,
|
||||||
|
projectEnv?: string | null,
|
||||||
|
) => {
|
||||||
const envFilePath = join(dirname(directory), ".env");
|
const envFilePath = join(dirname(directory), ".env");
|
||||||
if (!existsSync(dirname(envFilePath))) {
|
if (!existsSync(dirname(envFilePath))) {
|
||||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||||
}
|
}
|
||||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
|
||||||
|
"\n",
|
||||||
|
);
|
||||||
writeFileSync(envFilePath, envFileContent);
|
writeFileSync(envFilePath, envFileContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createEnvFileCommand = (directory: string, env: string | null) => {
|
export const createEnvFileCommand = (
|
||||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
directory: string,
|
||||||
|
env: string | null,
|
||||||
|
projectEnv?: string | null,
|
||||||
|
) => {
|
||||||
|
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
|
||||||
|
"\n",
|
||||||
|
);
|
||||||
|
|
||||||
const encodedContent = encodeBase64(envFileContent || "");
|
const encodedContent = encodeBase64(envFileContent || "");
|
||||||
const envFilePath = join(dirname(directory), ".env");
|
const envFilePath = join(dirname(directory), ".env");
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
import { getRemoteDocker } from "../servers/remote-docker";
|
||||||
|
|
||||||
export type MariadbNested = InferResultType<"mariadb", { mounts: true }>;
|
export type MariadbNested = InferResultType<
|
||||||
|
"mariadb",
|
||||||
|
{ mounts: true; project: true }
|
||||||
|
>;
|
||||||
export const buildMariadb = async (mariadb: MariadbNested) => {
|
export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
@@ -37,7 +40,10 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
|||||||
cpuLimit,
|
cpuLimit,
|
||||||
cpuReservation,
|
cpuReservation,
|
||||||
});
|
});
|
||||||
const envVariables = prepareEnvironmentVariables(defaultMariadbEnv);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
defaultMariadbEnv,
|
||||||
|
mariadb.project.env,
|
||||||
|
);
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
const volumesMount = generateVolumeMounts(mounts);
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
const filesMount = generateFileMounts(appName, mariadb);
|
const filesMount = generateFileMounts(appName, mariadb);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
import { getRemoteDocker } from "../servers/remote-docker";
|
||||||
|
|
||||||
export type MongoNested = InferResultType<"mongo", { mounts: true }>;
|
export type MongoNested = InferResultType<
|
||||||
|
"mongo",
|
||||||
|
{ mounts: true; project: true }
|
||||||
|
>;
|
||||||
|
|
||||||
export const buildMongo = async (mongo: MongoNested) => {
|
export const buildMongo = async (mongo: MongoNested) => {
|
||||||
const {
|
const {
|
||||||
@@ -36,7 +39,10 @@ export const buildMongo = async (mongo: MongoNested) => {
|
|||||||
cpuLimit,
|
cpuLimit,
|
||||||
cpuReservation,
|
cpuReservation,
|
||||||
});
|
});
|
||||||
const envVariables = prepareEnvironmentVariables(defaultMongoEnv);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
defaultMongoEnv,
|
||||||
|
mongo.project.env,
|
||||||
|
);
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
const volumesMount = generateVolumeMounts(mounts);
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
const filesMount = generateFileMounts(appName, mongo);
|
const filesMount = generateFileMounts(appName, mongo);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
import { getRemoteDocker } from "../servers/remote-docker";
|
||||||
|
|
||||||
export type MysqlNested = InferResultType<"mysql", { mounts: true }>;
|
export type MysqlNested = InferResultType<
|
||||||
|
"mysql",
|
||||||
|
{ mounts: true; project: true }
|
||||||
|
>;
|
||||||
|
|
||||||
export const buildMysql = async (mysql: MysqlNested) => {
|
export const buildMysql = async (mysql: MysqlNested) => {
|
||||||
const {
|
const {
|
||||||
@@ -43,7 +46,10 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
|||||||
cpuLimit,
|
cpuLimit,
|
||||||
cpuReservation,
|
cpuReservation,
|
||||||
});
|
});
|
||||||
const envVariables = prepareEnvironmentVariables(defaultMysqlEnv);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
defaultMysqlEnv,
|
||||||
|
mysql.project.env,
|
||||||
|
);
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
const volumesMount = generateVolumeMounts(mounts);
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
const filesMount = generateFileMounts(appName, mysql);
|
const filesMount = generateFileMounts(appName, mysql);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
import { getRemoteDocker } from "../servers/remote-docker";
|
||||||
|
|
||||||
export type PostgresNested = InferResultType<"postgres", { mounts: true }>;
|
export type PostgresNested = InferResultType<
|
||||||
|
"postgres",
|
||||||
|
{ mounts: true; project: true }
|
||||||
|
>;
|
||||||
export const buildPostgres = async (postgres: PostgresNested) => {
|
export const buildPostgres = async (postgres: PostgresNested) => {
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
@@ -36,7 +39,10 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
|||||||
cpuLimit,
|
cpuLimit,
|
||||||
cpuReservation,
|
cpuReservation,
|
||||||
});
|
});
|
||||||
const envVariables = prepareEnvironmentVariables(defaultPostgresEnv);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
defaultPostgresEnv,
|
||||||
|
postgres.project.env,
|
||||||
|
);
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
const volumesMount = generateVolumeMounts(mounts);
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
const filesMount = generateFileMounts(appName, postgres);
|
const filesMount = generateFileMounts(appName, postgres);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
import { getRemoteDocker } from "../servers/remote-docker";
|
||||||
|
|
||||||
export type RedisNested = InferResultType<"redis", { mounts: true }>;
|
export type RedisNested = InferResultType<
|
||||||
|
"redis",
|
||||||
|
{ mounts: true; project: true }
|
||||||
|
>;
|
||||||
export const buildRedis = async (redis: RedisNested) => {
|
export const buildRedis = async (redis: RedisNested) => {
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
@@ -34,7 +37,10 @@ export const buildRedis = async (redis: RedisNested) => {
|
|||||||
cpuLimit,
|
cpuLimit,
|
||||||
cpuReservation,
|
cpuReservation,
|
||||||
});
|
});
|
||||||
const envVariables = prepareEnvironmentVariables(defaultRedisEnv);
|
const envVariables = prepareEnvironmentVariables(
|
||||||
|
defaultRedisEnv,
|
||||||
|
redis.project.env,
|
||||||
|
);
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
const volumesMount = generateVolumeMounts(mounts);
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
const filesMount = generateFileMounts(appName, redis);
|
const filesMount = generateFileMounts(appName, redis);
|
||||||
|
|||||||
@@ -258,8 +258,28 @@ export const removeService = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prepareEnvironmentVariables = (env: string | null) =>
|
export const prepareEnvironmentVariables = (
|
||||||
Object.entries(parse(env ?? "")).map(([key, value]) => `${key}=${value}`);
|
serviceEnv: string | null,
|
||||||
|
projectEnv?: string | null,
|
||||||
|
) => {
|
||||||
|
const projectVars = parse(projectEnv ?? "");
|
||||||
|
const serviceVars = parse(serviceEnv ?? "");
|
||||||
|
|
||||||
|
const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {
|
||||||
|
let resolvedValue = value;
|
||||||
|
if (projectVars) {
|
||||||
|
resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => {
|
||||||
|
if (projectVars[ref] !== undefined) {
|
||||||
|
return projectVars[ref];
|
||||||
|
}
|
||||||
|
throw new Error(`Invalid project environment variable: project.${ref}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return `${key}=${resolvedValue}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return resolvedVars;
|
||||||
|
};
|
||||||
|
|
||||||
export const prepareBuildArgs = (input: string | null) => {
|
export const prepareBuildArgs = (input: string | null) => {
|
||||||
const pairs = (input ?? "").split("\n");
|
const pairs = (input ?? "").split("\n");
|
||||||
|
|||||||
349
packages/server/src/utils/gpu-setup.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import { execAsync, sleep } from "../utils/process/execAsync";
|
||||||
|
import { execAsyncRemote } from "../utils/process/execAsync";
|
||||||
|
|
||||||
|
interface GPUInfo {
|
||||||
|
driverInstalled: boolean;
|
||||||
|
driverVersion?: string;
|
||||||
|
gpuModel?: string;
|
||||||
|
runtimeInstalled: boolean;
|
||||||
|
runtimeConfigured: boolean;
|
||||||
|
cudaSupport: boolean;
|
||||||
|
cudaVersion?: string;
|
||||||
|
memoryInfo?: string;
|
||||||
|
availableGPUs: number;
|
||||||
|
swarmEnabled: boolean;
|
||||||
|
gpuResources: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
|
||||||
|
try {
|
||||||
|
const [driverInfo, runtimeInfo, swarmInfo, gpuInfo, cudaInfo] =
|
||||||
|
await Promise.all([
|
||||||
|
checkGpuDriver(serverId),
|
||||||
|
checkRuntime(serverId),
|
||||||
|
checkSwarmResources(serverId),
|
||||||
|
checkGpuInfo(serverId),
|
||||||
|
checkCudaSupport(serverId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...driverInfo,
|
||||||
|
...runtimeInfo,
|
||||||
|
...swarmInfo,
|
||||||
|
...gpuInfo,
|
||||||
|
...cudaInfo,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in checkGPUStatus:", error);
|
||||||
|
return {
|
||||||
|
driverInstalled: false,
|
||||||
|
driverVersion: undefined,
|
||||||
|
runtimeInstalled: false,
|
||||||
|
runtimeConfigured: false,
|
||||||
|
cudaSupport: false,
|
||||||
|
cudaVersion: undefined,
|
||||||
|
gpuModel: undefined,
|
||||||
|
memoryInfo: undefined,
|
||||||
|
availableGPUs: 0,
|
||||||
|
swarmEnabled: false,
|
||||||
|
gpuResources: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkGpuDriver = async (serverId?: string) => {
|
||||||
|
let driverVersion: string | undefined;
|
||||||
|
let driverInstalled = false;
|
||||||
|
let availableGPUs = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const driverCommand =
|
||||||
|
"nvidia-smi --query-gpu=driver_version --format=csv,noheader";
|
||||||
|
const { stdout: nvidiaSmi } = serverId
|
||||||
|
? await execAsyncRemote(serverId, driverCommand)
|
||||||
|
: await execAsync(driverCommand);
|
||||||
|
|
||||||
|
driverVersion = nvidiaSmi.trim();
|
||||||
|
if (driverVersion) {
|
||||||
|
driverInstalled = true;
|
||||||
|
const countCommand =
|
||||||
|
"nvidia-smi --query-gpu=gpu_name --format=csv,noheader | wc -l";
|
||||||
|
const { stdout: gpuCount } = serverId
|
||||||
|
? await execAsyncRemote(serverId, countCommand)
|
||||||
|
: await execAsync(countCommand);
|
||||||
|
|
||||||
|
availableGPUs = Number.parseInt(gpuCount.trim(), 10);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("GPU driver check:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { driverVersion, driverInstalled, availableGPUs };
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkRuntime = async (serverId?: string) => {
|
||||||
|
let runtimeInstalled = false;
|
||||||
|
let runtimeConfigured = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First check: Is nvidia-container-runtime installed?
|
||||||
|
const checkBinaryCommand = "command -v nvidia-container-runtime";
|
||||||
|
try {
|
||||||
|
const { stdout } = serverId
|
||||||
|
? await execAsyncRemote(serverId, checkBinaryCommand)
|
||||||
|
: await execAsync(checkBinaryCommand);
|
||||||
|
runtimeInstalled = !!stdout.trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("Runtime binary check:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second check: Is it configured in Docker?
|
||||||
|
try {
|
||||||
|
const runtimeCommand = 'docker info --format "{{json .Runtimes}}"';
|
||||||
|
const { stdout: runtimeInfo } = serverId
|
||||||
|
? await execAsyncRemote(serverId, runtimeCommand)
|
||||||
|
: await execAsync(runtimeCommand);
|
||||||
|
|
||||||
|
const defaultCommand = 'docker info --format "{{.DefaultRuntime}}"';
|
||||||
|
const { stdout: defaultRuntime } = serverId
|
||||||
|
? await execAsyncRemote(serverId, defaultCommand)
|
||||||
|
: await execAsync(defaultCommand);
|
||||||
|
|
||||||
|
const runtimes = JSON.parse(runtimeInfo);
|
||||||
|
const hasNvidiaRuntime = "nvidia" in runtimes;
|
||||||
|
const isDefaultRuntime = defaultRuntime.trim() === "nvidia";
|
||||||
|
|
||||||
|
// Only set runtimeConfigured if both conditions are met
|
||||||
|
runtimeConfigured = hasNvidiaRuntime && isDefaultRuntime;
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("Runtime configuration check:", error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("Runtime check:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { runtimeInstalled, runtimeConfigured };
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkSwarmResources = async (serverId?: string) => {
|
||||||
|
let swarmEnabled = false;
|
||||||
|
let gpuResources = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodeCommand =
|
||||||
|
"docker node inspect self --format '{{json .Description.Resources.GenericResources}}'";
|
||||||
|
const { stdout: resources } = serverId
|
||||||
|
? await execAsyncRemote(serverId, nodeCommand)
|
||||||
|
: await execAsync(nodeCommand);
|
||||||
|
|
||||||
|
if (resources && resources !== "null") {
|
||||||
|
const genericResources = JSON.parse(resources);
|
||||||
|
for (const resource of genericResources) {
|
||||||
|
if (
|
||||||
|
resource.DiscreteResourceSpec &&
|
||||||
|
(resource.DiscreteResourceSpec.Kind === "GPU" ||
|
||||||
|
resource.DiscreteResourceSpec.Kind === "gpu")
|
||||||
|
) {
|
||||||
|
gpuResources = resource.DiscreteResourceSpec.Value;
|
||||||
|
swarmEnabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("Swarm resource check:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { swarmEnabled, gpuResources };
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkGpuInfo = async (serverId?: string) => {
|
||||||
|
let gpuModel: string | undefined;
|
||||||
|
let memoryInfo: string | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gpuInfoCommand =
|
||||||
|
"nvidia-smi --query-gpu=gpu_name,memory.total --format=csv,noheader";
|
||||||
|
const { stdout: gpuInfo } = serverId
|
||||||
|
? await execAsyncRemote(serverId, gpuInfoCommand)
|
||||||
|
: await execAsync(gpuInfoCommand);
|
||||||
|
|
||||||
|
[gpuModel, memoryInfo] = gpuInfo.split(",").map((s) => s.trim());
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("GPU info check:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { gpuModel, memoryInfo };
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkCudaSupport = async (serverId?: string) => {
|
||||||
|
let cudaVersion: string | undefined;
|
||||||
|
let cudaSupport = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cudaCommand = 'nvidia-smi -q | grep "CUDA Version"';
|
||||||
|
const { stdout: cudaInfo } = serverId
|
||||||
|
? await execAsyncRemote(serverId, cudaCommand)
|
||||||
|
: await execAsync(cudaCommand);
|
||||||
|
|
||||||
|
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d\.]+)/);
|
||||||
|
cudaVersion = cudaMatch ? cudaMatch[1] : undefined;
|
||||||
|
cudaSupport = !!cudaVersion;
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("CUDA support check:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cudaVersion, cudaSupport };
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function setupGPUSupport(serverId?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 1. Initial status check and validation
|
||||||
|
const initialStatus = await checkGPUStatus(serverId);
|
||||||
|
const shouldContinue = await validatePrerequisites(initialStatus);
|
||||||
|
if (!shouldContinue) return;
|
||||||
|
|
||||||
|
// 2. Get node ID
|
||||||
|
const nodeId = await getNodeId(serverId);
|
||||||
|
|
||||||
|
// 3. Create daemon configuration
|
||||||
|
const daemonConfig = createDaemonConfig(initialStatus.availableGPUs);
|
||||||
|
|
||||||
|
// 4. Setup server based on environment
|
||||||
|
if (serverId) {
|
||||||
|
await setupRemoteServer(serverId, daemonConfig);
|
||||||
|
} else {
|
||||||
|
await setupLocalServer(daemonConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Wait for Docker restart
|
||||||
|
await sleep(10000);
|
||||||
|
|
||||||
|
// 6. Add GPU label
|
||||||
|
await addGpuLabel(nodeId, serverId);
|
||||||
|
|
||||||
|
// 7. Final verification
|
||||||
|
await sleep(5000);
|
||||||
|
await verifySetup(nodeId, serverId);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("password is required")
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Sudo access required. Please run with appropriate permissions.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatePrerequisites = async (initialStatus: GPUInfo) => {
|
||||||
|
if (!initialStatus.driverInstalled) {
|
||||||
|
throw new Error(
|
||||||
|
"NVIDIA drivers not installed. Please install appropriate NVIDIA drivers first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialStatus.runtimeInstalled) {
|
||||||
|
throw new Error(
|
||||||
|
"NVIDIA Container Runtime not installed. Please install nvidia-container-runtime first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialStatus.swarmEnabled && initialStatus.runtimeConfigured) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeId = async (serverId?: string) => {
|
||||||
|
const nodeIdCommand = 'docker info --format "{{.Swarm.NodeID}}"';
|
||||||
|
const { stdout: nodeId } = serverId
|
||||||
|
? await execAsyncRemote(serverId, nodeIdCommand)
|
||||||
|
: await execAsync(nodeIdCommand);
|
||||||
|
|
||||||
|
const trimmedNodeId = nodeId.trim();
|
||||||
|
if (!trimmedNodeId) {
|
||||||
|
throw new Error("Setup Server before enabling GPU support");
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedNodeId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDaemonConfig = (availableGPUs: number) => ({
|
||||||
|
runtimes: {
|
||||||
|
nvidia: {
|
||||||
|
path: "nvidia-container-runtime",
|
||||||
|
runtimeArgs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"default-runtime": "nvidia",
|
||||||
|
"node-generic-resources": [`GPU=${availableGPUs}`],
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupRemoteServer = async (serverId: string, daemonConfig: any) => {
|
||||||
|
const setupCommands = [
|
||||||
|
"sudo -n true",
|
||||||
|
`echo '${JSON.stringify(daemonConfig, null, 2)}' | sudo tee /etc/docker/daemon.json`,
|
||||||
|
"sudo mkdir -p /etc/nvidia-container-runtime",
|
||||||
|
'sudo sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml',
|
||||||
|
'echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" | sudo tee -a /etc/nvidia-container-runtime/config.toml',
|
||||||
|
"sudo systemctl daemon-reload",
|
||||||
|
"sudo systemctl restart docker",
|
||||||
|
].join(" && ");
|
||||||
|
|
||||||
|
await execAsyncRemote(serverId, setupCommands);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupLocalServer = async (daemonConfig: any) => {
|
||||||
|
const configFile = `/tmp/docker-daemon-${Date.now()}.json`;
|
||||||
|
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
||||||
|
|
||||||
|
const setupCommands = [
|
||||||
|
`pkexec sh -c '
|
||||||
|
cp ${configFile} /etc/docker/daemon.json &&
|
||||||
|
mkdir -p /etc/nvidia-container-runtime &&
|
||||||
|
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
|
||||||
|
echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" >> /etc/nvidia-container-runtime/config.toml &&
|
||||||
|
systemctl daemon-reload &&
|
||||||
|
systemctl restart docker
|
||||||
|
'`,
|
||||||
|
`rm ${configFile}`,
|
||||||
|
].join(" && ");
|
||||||
|
|
||||||
|
await execAsync(setupCommands);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGpuLabel = async (nodeId: string, serverId?: string) => {
|
||||||
|
const labelCommand = `docker node update --label-add gpu=true ${nodeId}`;
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, labelCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(labelCommand);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifySetup = async (nodeId: string, serverId?: string) => {
|
||||||
|
const finalStatus = await checkGPUStatus(serverId);
|
||||||
|
|
||||||
|
if (!finalStatus.swarmEnabled) {
|
||||||
|
const diagnosticCommands = [
|
||||||
|
`docker node inspect ${nodeId}`,
|
||||||
|
'nvidia-smi -a | grep "GPU UUID"',
|
||||||
|
"cat /etc/docker/daemon.json",
|
||||||
|
"cat /etc/nvidia-container-runtime/config.toml",
|
||||||
|
].join(" && ");
|
||||||
|
|
||||||
|
const { stdout: diagnostics } = serverId
|
||||||
|
? await execAsyncRemote(serverId, diagnosticCommands)
|
||||||
|
: await execAsync(diagnosticCommands);
|
||||||
|
|
||||||
|
console.error("Diagnostic Information:", diagnostics);
|
||||||
|
throw new Error("GPU support not detected in swarm after setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalStatus;
|
||||||
|
};
|
||||||
108
pnpm-lock.yaml
generated
@@ -247,9 +247,15 @@ importers:
|
|||||||
drizzle-zod:
|
drizzle-zod:
|
||||||
specifier: 0.5.1
|
specifier: 0.5.1
|
||||||
version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8)
|
version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8)
|
||||||
|
i18next:
|
||||||
|
specifier: ^23.16.4
|
||||||
|
version: 23.16.5
|
||||||
input-otp:
|
input-otp:
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
js-cookie:
|
||||||
|
specifier: ^3.0.5
|
||||||
|
version: 3.0.5
|
||||||
js-yaml:
|
js-yaml:
|
||||||
specifier: 4.1.0
|
specifier: 4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -268,6 +274,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: ^15.0.1
|
specifier: ^15.0.1
|
||||||
version: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
next-i18next:
|
||||||
|
specifier: ^15.3.1
|
||||||
|
version: 15.3.1(i18next@23.16.5)(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.2.1
|
specifier: ^0.2.1
|
||||||
version: 0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@@ -295,6 +304,9 @@ importers:
|
|||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.49.3
|
specifier: ^7.49.3
|
||||||
version: 7.52.1(react@18.2.0)
|
version: 7.52.1(react@18.2.0)
|
||||||
|
react-i18next:
|
||||||
|
specifier: ^15.1.0
|
||||||
|
version: 15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.12.7
|
specifier: ^2.12.7
|
||||||
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@@ -347,6 +359,9 @@ importers:
|
|||||||
'@types/bcrypt':
|
'@types/bcrypt':
|
||||||
specifier: 5.0.2
|
specifier: 5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
|
'@types/js-cookie':
|
||||||
|
specifier: ^3.0.6
|
||||||
|
version: 3.0.6
|
||||||
'@types/js-yaml':
|
'@types/js-yaml':
|
||||||
specifier: 4.0.9
|
specifier: 4.0.9
|
||||||
version: 4.0.9
|
version: 4.0.9
|
||||||
@@ -3206,12 +3221,18 @@ packages:
|
|||||||
'@types/hast@2.3.10':
|
'@types/hast@2.3.10':
|
||||||
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
|
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
|
||||||
|
|
||||||
|
'@types/hoist-non-react-statics@3.3.5':
|
||||||
|
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4':
|
'@types/http-cache-semantics@4.0.4':
|
||||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||||
|
|
||||||
'@types/http-errors@2.0.4':
|
'@types/http-errors@2.0.4':
|
||||||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||||
|
|
||||||
|
'@types/js-cookie@3.0.6':
|
||||||
|
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||||
|
|
||||||
'@types/js-yaml@4.0.9':
|
'@types/js-yaml@4.0.9':
|
||||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||||
|
|
||||||
@@ -3893,6 +3914,9 @@ packages:
|
|||||||
core-js-pure@3.38.1:
|
core-js-pure@3.38.1:
|
||||||
resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==}
|
resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==}
|
||||||
|
|
||||||
|
core-js@3.39.0:
|
||||||
|
resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
|
||||||
|
|
||||||
cosmiconfig-typescript-loader@5.0.0:
|
cosmiconfig-typescript-loader@5.0.0:
|
||||||
resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==}
|
resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==}
|
||||||
engines: {node: '>=v16'}
|
engines: {node: '>=v16'}
|
||||||
@@ -4662,10 +4686,16 @@ packages:
|
|||||||
highlight.js@10.7.3:
|
highlight.js@10.7.3:
|
||||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||||
|
|
||||||
|
hoist-non-react-statics@3.3.2:
|
||||||
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
|
|
||||||
hono@4.5.8:
|
hono@4.5.8:
|
||||||
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==}
|
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
html-parse-stringify@3.0.1:
|
||||||
|
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||||
|
|
||||||
html-to-text@9.0.5:
|
html-to-text@9.0.5:
|
||||||
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -4701,6 +4731,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
|
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
|
||||||
engines: {node: '>=10.18'}
|
engines: {node: '>=10.18'}
|
||||||
|
|
||||||
|
i18next-fs-backend@2.3.2:
|
||||||
|
resolution: {integrity: sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q==}
|
||||||
|
|
||||||
|
i18next@23.16.5:
|
||||||
|
resolution: {integrity: sha512-KTlhE3EP9x6pPTAW7dy0WKIhoCpfOGhRQlO+jttQLgzVaoOjWwBWramu7Pp0i+8wDNduuzXfe3kkVbzrKyrbTA==}
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -5247,6 +5283,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
|
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
next-i18next@15.3.1:
|
||||||
|
resolution: {integrity: sha512-+pa2pZJb7B6k5PKW3TLVMmAodqkNaOBWVYlpWX56mgcEJz0UMW+MKSdKM9Z72CHp6Bp48g7OWwDnLqxXNp/84w==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
i18next: '>= 23.7.13'
|
||||||
|
next: '>= 12.0.0'
|
||||||
|
react: '>= 17.0.2'
|
||||||
|
react-i18next: '>= 13.5.0'
|
||||||
|
|
||||||
next-themes@0.2.1:
|
next-themes@0.2.1:
|
||||||
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
|
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5726,6 +5771,19 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
||||||
|
react-i18next@15.1.1:
|
||||||
|
resolution: {integrity: sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==}
|
||||||
|
peerDependencies:
|
||||||
|
i18next: '>= 23.2.3'
|
||||||
|
react: '>= 16.8.0'
|
||||||
|
react-dom: '*'
|
||||||
|
react-native: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-native:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-immutable-proptypes@2.2.0:
|
react-immutable-proptypes@2.2.0:
|
||||||
resolution: {integrity: sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==}
|
resolution: {integrity: sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6570,6 +6628,10 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
void-elements@3.1.0:
|
||||||
|
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
w3c-keyname@2.2.8:
|
w3c-keyname@2.2.8:
|
||||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
|
|
||||||
@@ -9282,10 +9344,17 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 2.0.10
|
'@types/unist': 2.0.10
|
||||||
|
|
||||||
|
'@types/hoist-non-react-statics@3.3.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.3.5
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4': {}
|
'@types/http-cache-semantics@4.0.4': {}
|
||||||
|
|
||||||
'@types/http-errors@2.0.4': {}
|
'@types/http-errors@2.0.4': {}
|
||||||
|
|
||||||
|
'@types/js-cookie@3.0.6': {}
|
||||||
|
|
||||||
'@types/js-yaml@4.0.9': {}
|
'@types/js-yaml@4.0.9': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
@@ -10056,6 +10125,8 @@ snapshots:
|
|||||||
|
|
||||||
core-js-pure@3.38.1: {}
|
core-js-pure@3.38.1: {}
|
||||||
|
|
||||||
|
core-js@3.39.0: {}
|
||||||
|
|
||||||
cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3):
|
cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.42
|
'@types/node': 18.19.42
|
||||||
@@ -10820,8 +10891,16 @@ snapshots:
|
|||||||
|
|
||||||
highlight.js@10.7.3: {}
|
highlight.js@10.7.3: {}
|
||||||
|
|
||||||
|
hoist-non-react-statics@3.3.2:
|
||||||
|
dependencies:
|
||||||
|
react-is: 16.13.1
|
||||||
|
|
||||||
hono@4.5.8: {}
|
hono@4.5.8: {}
|
||||||
|
|
||||||
|
html-parse-stringify@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
void-elements: 3.1.0
|
||||||
|
|
||||||
html-to-text@9.0.5:
|
html-to-text@9.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@selderee/plugin-htmlparser2': 0.11.0
|
'@selderee/plugin-htmlparser2': 0.11.0
|
||||||
@@ -10865,6 +10944,12 @@ snapshots:
|
|||||||
|
|
||||||
hyperdyperid@1.2.0: {}
|
hyperdyperid@1.2.0: {}
|
||||||
|
|
||||||
|
i18next-fs-backend@2.3.2: {}
|
||||||
|
|
||||||
|
i18next@23.16.5:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
@@ -11365,6 +11450,18 @@ snapshots:
|
|||||||
|
|
||||||
neotraverse@0.6.18: {}
|
neotraverse@0.6.18: {}
|
||||||
|
|
||||||
|
next-i18next@15.3.1(i18next@23.16.5)(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
'@types/hoist-non-react-statics': 3.3.5
|
||||||
|
core-js: 3.39.0
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
i18next: 23.16.5
|
||||||
|
i18next-fs-backend: 2.3.2
|
||||||
|
next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
react-i18next: 15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
|
||||||
next-themes@0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
next-themes@0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
@@ -11854,6 +11951,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
|
react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
html-parse-stringify: 3.0.1
|
||||||
|
i18next: 23.16.5
|
||||||
|
react: 18.2.0
|
||||||
|
optionalDependencies:
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
|
||||||
react-immutable-proptypes@2.2.0(immutable@3.8.2):
|
react-immutable-proptypes@2.2.0(immutable@3.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
immutable: 3.8.2
|
immutable: 3.8.2
|
||||||
@@ -12791,6 +12897,8 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
|
void-elements@3.1.0: {}
|
||||||
|
|
||||||
w3c-keyname@2.2.8: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
watchpack@2.4.1:
|
watchpack@2.4.1:
|
||||||
|
|||||||