Merge pull request #553 from Dokploy/canary

v0.10.0
This commit is contained in:
Mauricio Siu
2024-10-13 16:30:00 -06:00
committed by GitHub
369 changed files with 14466 additions and 2413 deletions

View File

@@ -11,6 +11,7 @@ jobs:
command: | command: |
cp apps/dokploy/.env.production.example .env.production cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- run: - run:
name: Build and push AMD64 image name: Build and push AMD64 image
command: | command: |
@@ -61,7 +62,7 @@ jobs:
VERSION=$(node -p "require('./apps/dokploy/package.json').version") VERSION=$(node -p "require('./apps/dokploy/package.json').version")
echo $VERSION echo $VERSION
TAG="latest" TAG="latest"
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -1,4 +1,4 @@
name: Build Docs & Website Docker images name: Build Docker images
on: on:
push: push:
@@ -48,3 +48,74 @@ jobs:
push: true push: true
tags: dokploy/website:latest tags: dokploy/website:latest
platforms: linux/amd64 platforms: linux/amd64
build-and-push-cloud-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.cloud
push: true
tags: |
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64
build-and-push-schedule-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.schedule
push: true
tags: |
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64
build-and-push-server-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.server
push: true
tags: |
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64

View File

@@ -18,6 +18,7 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm biome ci - run: pnpm biome ci
- run: pnpm typecheck - run: pnpm typecheck
@@ -32,6 +33,7 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm build - run: pnpm build
parallel-tests: parallel-tests:
@@ -44,4 +46,5 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm test - run: pnpm test

View File

@@ -15,7 +15,9 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Deploy only the dokploy app # Deploy only the dokploy app
ENV NODE_ENV=production ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next

52
Dockerfile.cloud Normal file
View File

@@ -0,0 +1,52 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle
COPY --from=build /prod/dokploy/components.json ./components.json
COPY --from=build /prod/dokploy/node_modules ./node_modules
# Install RCLONE
RUN curl https://rclone.org/install.sh | bash
# tsx
RUN pnpm install -g tsx
EXPOSE 3000
CMD [ "pnpm", "start" ]

36
Dockerfile.schedule Normal file
View File

@@ -0,0 +1,36 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/schedules run build
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
# Copy only the necessary files
COPY --from=build /prod/schedules/dist ./dist
COPY --from=build /prod/schedules/package.json ./package.json
COPY --from=build /prod/schedules/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start

36
Dockerfile.server Normal file
View File

@@ -0,0 +1,36 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/api run build
RUN pnpm --filter=./apps/api --prod deploy /prod/api
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
# Copy only the necessary files
COPY --from=build /prod/api/dist ./dist
COPY --from=build /prod/api/package.json ./package.json
COPY --from=build /prod/api/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start

View File

@@ -32,6 +32,7 @@ Dokploy include multiples features to make your life easier.
- **Docker Management**: Easily deploy and manage Docker containers. - **Docker Management**: Easily deploy and manage Docker containers.
- **CLI/API**: Manage your applications and databases using the command line or trought the API. - **CLI/API**: Manage your applications and databases using the command line or trought the API.
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.) - **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
- **Multi Server**: Deploy and manager your applications remotely to external servers.
- **Self-Hosted**: Self-host Dokploy on your VPS. - **Self-Hosted**: Self-host Dokploy on your VPS.
## 🚀 Getting Started ## 🚀 Getting Started
@@ -58,7 +59,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Hero Sponsors 🎖 ### Hero Sponsors 🎖
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" ><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/></a> <div style="display: flex; align-items: center; gap: 20px;">
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
</a>
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
</a>
</div>
### Premium Supporters 🥇 ### Premium Supporters 🥇
@@ -81,6 +89,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<div style="display: flex; gap: 30px; flex-wrap: wrap;"> <div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a> <a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
</div> </div>
#### Organizations: #### Organizations:

View File

@@ -1,15 +1,33 @@
{ {
"name": "my-app", "name": "@dokploy/api",
"version": "0.0.1",
"type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts" "dev": "PORT=4000 tsx watch src/index.ts",
"build": "tsc --project tsconfig.json",
"start": "node --experimental-specifier-resolution=node dist/index.js",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@hono/zod-validator": "0.3.0",
"zod": "^3.23.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1", "@hono/node-server": "^1.12.1",
"hono": "^4.5.8", "hono": "^4.5.8",
"dotenv": "^16.3.1" "dotenv": "^16.3.1",
"redis": "4.7.0",
"@nerimity/mimiqueue": "1.2.3"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.4.2",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"tsx": "^4.7.1" "tsx": "^4.7.1"
} },
"packageManager": "pnpm@9.5.0"
} }

View File

@@ -1,66 +1,61 @@
import { serve } from "@hono/node-server"; import { serve } from "@hono/node-server";
import { config } from "dotenv";
import { Hono } from "hono"; import { Hono } from "hono";
import { cors } from "hono/cors"; import "dotenv/config";
import { validateLemonSqueezyLicense } from "./utils"; import { zValidator } from "@hono/zod-validator";
import { Queue } from "@nerimity/mimiqueue";
config(); import { createClient } from "redis";
import { logger } from "./logger";
import { type DeployJob, deployJobSchema } from "./schema";
import { deploy } from "./utils";
const app = new Hono(); const app = new Hono();
const redisClient = createClient({
app.use( url: process.env.REDIS_URL,
"/*",
cors({
origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
maxAge: 600,
credentials: true,
}),
);
export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
app.get("/v1/health", (c) => {
return c.text("Hello Hono!");
}); });
app.post("/v1/validate-license", async (c) => { app.use(async (c, next) => {
const { licenseKey } = await c.req.json(); if (c.req.path === "/health") {
return next();
}
const authHeader = c.req.header("X-API-Key");
if (!licenseKey) { if (process.env.API_KEY !== authHeader) {
return c.json({ error: "License key is required" }, 400); return c.json({ message: "Invalid API Key" }, 403);
} }
try { return next();
const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
if (licenseValidation.valid) {
return c.json({
valid: true,
message: "License is valid",
metadata: licenseValidation.meta,
});
}
return c.json(
{
valid: false,
message: licenseValidation.error || "Invalid license",
},
400,
);
} catch (error) {
console.error("Error during license validation:", error);
return c.json({ error: "Internal server error" }, 500);
}
}); });
const port = 4000; app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
console.log(`Server is running on port ${port}`); const data = c.req.valid("json");
const res = queue.add(data, { groupName: data.serverId });
serve({ return c.json(
fetch: app.fetch, {
port, message: "Deployment Added",
},
200,
);
}); });
app.get("/health", async (c) => {
return c.json({ status: "ok" });
});
const queue = new Queue({
name: "deployments",
process: async (job: DeployJob) => {
logger.info("Deploying job", job);
return await deploy(job);
},
redisClient,
});
(async () => {
await redisClient.connect();
await redisClient.flushAll();
logger.info("Redis Cleaned");
})();
const port = Number.parseInt(process.env.PORT || "3000");
logger.info("Starting Deployments Server ✅", port);
serve({ fetch: app.fetch, port });

10
apps/api/src/logger.ts Normal file
View File

@@ -0,0 +1,10 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
});

24
apps/api/src/schema.ts Normal file
View File

@@ -0,0 +1,24 @@
import { z } from "zod";
export const deployJobSchema = z.discriminatedUnion("applicationType", [
z.object({
applicationId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application"),
serverId: z.string(),
}),
z.object({
composeId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("compose"),
serverId: z.string(),
}),
]);
export type DeployJob = z.infer<typeof deployJobSchema>;

View File

@@ -1,28 +1,96 @@
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from "."; import {
deployApplication,
deployCompose,
deployRemoteApplication,
deployRemoteCompose,
rebuildApplication,
rebuildCompose,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
} from "@dokploy/server";
import type { DeployJob } from "./schema";
import type { LemonSqueezyLicenseResponse } from "./types"; import type { LemonSqueezyLicenseResponse } from "./types";
export const validateLemonSqueezyLicense = async ( // const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
licenseKey: string, // const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
): Promise<LemonSqueezyLicenseResponse> => { // export const validateLemonSqueezyLicense = async (
try { // licenseKey: string,
const response = await fetch( // ): Promise<LemonSqueezyLicenseResponse> => {
"https://api.lemonsqueezy.com/v1/licenses/validate", // try {
{ // const response = await fetch(
method: "POST", // "https://api.lemonsqueezy.com/v1/licenses/validate",
headers: { // {
"Content-Type": "application/json", // method: "POST",
"x-api-key": LEMON_SQUEEZY_API_KEY as string, // headers: {
}, // "Content-Type": "application/json",
body: JSON.stringify({ // "x-api-key": LEMON_SQUEEZY_API_KEY as string,
license_key: licenseKey, // },
store_id: LEMON_SQUEEZY_STORE_ID as string, // body: JSON.stringify({
}), // license_key: licenseKey,
}, // store_id: LEMON_SQUEEZY_STORE_ID as string,
); // }),
// },
// );
return response.json(); // return response.json();
// } catch (error) {
// console.error("Error validating license:", error);
// return { valid: false, error: "Error validating license" };
// }
// };
export const deploy = async (job: DeployJob) => {
try {
if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "running");
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
}
}
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "running",
});
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
}
}
}
} catch (error) { } catch (error) {
console.error("Error validating license:", error); console.log(error);
return { valid: false, error: "Error validating license" }; if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "error");
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "error",
});
}
} }
return true;
}; };

View File

@@ -2,11 +2,12 @@
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Node",
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["node"], "outDir": "dist",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "hono/jsx" "jsxImportSource": "hono/jsx"
} },
"exclude": ["node_modules", "dist"]
} }

View File

@@ -31,8 +31,7 @@ The following templates are available:
- **Wordpress**: Open Source Content Management System - **Wordpress**: Open Source Content Management System
- **Open WebUI**: Free and Open Source ChatGPT Alternative - **Open WebUI**: Free and Open Source ChatGPT Alternative
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres - **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
- **Roundcube**: Free and open source webmail software for the masses, written in PHP, uses SMTP[^1].
## Create your own template ## Create your own template
@@ -41,3 +40,5 @@ We accept contributions to upload new templates to the dokploy repository.
Make sure to follow the guidelines for creating a template: Make sure to follow the guidelines for creating a template:
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates) [Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
[^1]: Please note that if you're self-hosting a mail server you need port 25 to be open for SMTP (Mail Transmission Protocol that allows you to send and receive) to work properly. Some VPS providers like [Hetzner](https://docs.hetzner.com/cloud/servers/faq/#why-can-i-not-send-any-mails-from-my-server) block this port by default for new clients.

View File

@@ -10,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
p: ({ children }) => ( p: ({ children }) => (
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p> <p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
), ),
li: ({ children }) => ( li: ({ children, id }) => (
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li> <li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
{children}
</li>
), ),
}; };
} }

View File

@@ -21,15 +21,11 @@
"react-ga4": "^2.1.0" "react-ga4": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"tsx": "4.15.7", "autoprefixer": "10.4.12",
"@biomejs/biome": "1.8.1",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^20.14.2",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.1",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllProperties } from "@/server/utils/docker/compose"; import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs"; import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs"; import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
addSuffixToAllConfigs, import type { ComposeSpecification } from "@dokploy/server";
addSuffixToConfigsRoot,
} from "@/server/utils/docker/compose/configs";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import type { Domain } from "@/server/api/services/domain"; import type { Domain } from "@dokploy/server";
import { createDomainLabels } from "@/server/utils/docker/domain"; import { createDomainLabels } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("createDomainLabels", () => { describe("createDomainLabels", () => {

View File

@@ -1,4 +1,4 @@
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain"; import { addDokployNetworkToRoot } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("addDokployNetworkToRoot", () => { describe("addDokployNetworkToRoot", () => {

View File

@@ -1,4 +1,4 @@
import { addDokployNetworkToService } from "@/server/utils/docker/domain"; import { addDokployNetworkToService } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("addDokployNetworkToService", () => { describe("addDokployNetworkToService", () => {

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network"; import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network"; import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,10 +1,10 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import {
addSuffixToAllNetworks, addSuffixToAllNetworks,
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
} from "@/server/utils/docker/compose/network"; } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network"; import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets"; import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets"; import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets"; import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,8 @@
import { import {
addSuffixToAllServiceNames, addSuffixToAllServiceNames,
addSuffixToServiceNames, addSuffixToServiceNames,
} from "@/server/utils/docker/compose/service"; } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
addSuffixToAllVolumes, import type { ComposeSpecification } from "@dokploy/server";
addSuffixToVolumesRoot,
} from "@/server/utils/docker/compose/volume";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume"; import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume"; import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,9 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import {
addSuffixToAllVolumes, addSuffixToAllVolumes,
addSuffixToVolumesInServices, addSuffixToVolumesInServices,
} from "@/server/utils/docker/compose/volume"; } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,9 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { paths } from "@/server/constants"; import { paths } from "@dokploy/server/dist/constants";
const { APPLICATIONS_PATH } = paths(); const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@/server/utils/builders"; import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@/server/utils/builders/drop"; import { unzipDrop } from "@dokploy/server";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
@@ -81,14 +81,17 @@ const baseApp: ApplicationNested = {
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
}; };
//
vi.mock("@/server/constants", () => ({
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
}));
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
// @ts-ignore
...actual,
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
};
});
describe("unzipDrop using real zip files", () => { describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths(); // const { APPLICATIONS_PATH } = paths();
beforeAll(async () => { beforeAll(async () => {
@@ -102,15 +105,19 @@ describe("unzipDrop using real zip files", () => {
it("should correctly extract a zip with a single root folder", async () => { it("should correctly extract a zip with a single root folder", async () => {
baseApp.appName = "single-file"; baseApp.appName = "single-file";
// const appName = "single-file"; // const appName = "single-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); try {
const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
const zipBuffer = zip.toBuffer(); console.log(`Output Path: ${outputPath}`);
const file = new File([zipBuffer], "single.zip"); const zipBuffer = zip.toBuffer();
await unzipDrop(file, baseApp); const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true }); const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true); expect(files.some((f) => f.name === "test.txt")).toBe(true);
} catch (err) {
console.log(err);
} finally {
}
}); });
it("should correctly extract a zip with a single root folder and a subfolder", async () => { it("should correctly extract a zip with a single root folder and a subfolder", async () => {

View File

@@ -1,4 +1,4 @@
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils"; import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;

View File

@@ -5,11 +5,12 @@ vi.mock("node:fs", () => ({
default: fs, default: fs,
})); }));
import type { Admin } from "@/server/api/services/admin"; import type { Admin, FileConfig } from "@dokploy/server";
import { createDefaultServerTraefikConfig } from "@/server/setup/traefik-setup"; import {
import { loadOrCreateConfig } from "@/server/utils/traefik/application"; createDefaultServerTraefikConfig,
import type { FileConfig } from "@/server/utils/traefik/file-types"; loadOrCreateConfig,
import { updateServerTraefik } from "@/server/utils/traefik/web-server"; updateServerTraefik,
} from "@dokploy/server";
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = { const baseAdmin: Admin = {

View File

@@ -1,7 +1,7 @@
import type { Domain } from "@/server/api/services/domain"; import type { Domain } from "@dokploy/server";
import type { Redirect } from "@/server/api/services/redirect"; import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@/server/utils/builders"; import type { ApplicationNested } from "@dokploy/server";
import { createRouterConfig } from "@/server/utils/traefik/domain"; import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {

View File

@@ -13,4 +13,9 @@ export default defineConfig({
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"], exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
pool: "forks", pool: "forks",
}, },
define: {
"process.env": {
NODE: "test",
},
},
}); });

View File

@@ -80,7 +80,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -16,20 +16,37 @@ interface Props {
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
const [data, setData] = useState(""); const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null); const endOfLogsRef = useRef<HTMLDivElement>(null);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
setData("");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`; const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws; // Store WebSocket instance in ref
ws.onmessage = (e) => { ws.onmessage = (e) => {
setData((currentData) => currentData + e.data); setData((currentData) => currentData + e.data);
}; };
return () => ws.close(); ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
wsRef.current = null; // Clear reference on close
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
};
}, [logPath, open]); }, [logPath, open]);
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -45,7 +62,15 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
open={open} open={open}
onOpenChange={(e) => { onOpenChange={(e) => {
onClose(); onClose();
if (!e) setData(""); if (!e) {
setData("");
}
if (wsRef.current) {
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
}
}} }}
> >
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}> <DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>

View File

@@ -52,7 +52,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
<div className="flex w-full flex-col items-center justify-center gap-3"> <div className="flex w-full flex-col items-center justify-center gap-3">
<GlobeIcon className="size-8 text-muted-foreground" /> <GlobeIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To access to the application is required to set at least 1 To access the application it is required to set at least 1
domain domain
</span> </span>
<div className="flex flex-row gap-4 flex-wrap"> <div className="flex flex-row gap-4 flex-wrap">

View File

@@ -21,20 +21,38 @@ export const ShowDeploymentCompose = ({
}: Props) => { }: Props) => {
const [data, setData] = useState(""); const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null); const endOfLogsRef = useRef<HTMLDivElement>(null);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
setData("");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`; const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws; // Store WebSocket instance in ref
ws.onmessage = (e) => { ws.onmessage = (e) => {
setData((currentData) => currentData + e.data); setData((currentData) => currentData + e.data);
}; };
return () => ws.close(); ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
wsRef.current = null;
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
};
}, [logPath, open]); }, [logPath, open]);
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -50,7 +68,15 @@ export const ShowDeploymentCompose = ({
open={open} open={open}
onOpenChange={(e) => { onOpenChange={(e) => {
onClose(); onClose();
if (!e) setData(""); if (!e) {
setData("");
}
if (wsRef.current) {
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
}
}} }}
> >
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}> <DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>

View File

@@ -53,7 +53,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
<div className="flex w-full flex-col items-center justify-center gap-3"> <div className="flex w-full flex-col items-center justify-center gap-3">
<GlobeIcon className="size-8 text-muted-foreground" /> <GlobeIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To access to the application is required to set at least 1 To access to the application it is required to set at least 1
domain domain
</span> </span>
<div className="flex flex-row gap-4 flex-wrap"> <div className="flex flex-row gap-4 flex-wrap">

View File

@@ -77,7 +77,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
}); });
}) })
.catch((e) => { .catch((e) => {
console.log(e);
toast.error("Error to update the compose config"); toast.error("Error to update the compose config");
}); });
}; };

View File

@@ -11,7 +11,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react"; import { Puzzle, RefreshCw } from "lucide-react";
import { useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
interface Props { interface Props {
@@ -34,6 +34,16 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation(); const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
useEffect(() => {
if (isOpen) {
mutateAsync({ composeId })
.then(() => {
refetch();
})
.catch((err) => {});
}
}, [isOpen]);
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>

View File

@@ -1,7 +1,7 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import React, { useEffect } from "react"; import React, { useEffect, useRef } from "react";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css";
@@ -18,12 +18,24 @@ export const DockerLogsId: React.FC<Props> = ({
}) => { }) => {
const [term, setTerm] = React.useState<Terminal>(); const [term, setTerm] = React.useState<Terminal>();
const [lines, setLines] = React.useState<number>(40); const [lines, setLines] = React.useState<number>(40);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
const createTerminal = (): Terminal => { useEffect(() => {
// if (containerId === "select-a-container") {
// return;
// }
const container = document.getElementById(id); const container = document.getElementById(id);
if (container) { if (container) {
container.innerHTML = ""; container.innerHTML = "";
} }
if (wsRef.current) {
console.log(wsRef.current);
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
wsRef.current = null;
}
const termi = new Terminal({ const termi = new Terminal({
cursorBlink: true, cursorBlink: true,
cols: 80, cols: 80,
@@ -45,7 +57,7 @@ export const DockerLogsId: React.FC<Props> = ({
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`; const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws;
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
termi.loadAddon(fitAddon); termi.loadAddon(fitAddon);
// @ts-ignore // @ts-ignore
@@ -54,6 +66,10 @@ export const DockerLogsId: React.FC<Props> = ({
termi.focus(); termi.focus();
setTerm(termi); setTerm(termi);
ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onmessage = (e) => { ws.onmessage = (e) => {
termi.write(e.data); termi.write(e.data);
}; };
@@ -62,12 +78,14 @@ export const DockerLogsId: React.FC<Props> = ({
console.log(e.reason); console.log(e.reason);
termi.write(`Connection closed!\nReason: ${e.reason}\n`); termi.write(`Connection closed!\nReason: ${e.reason}\n`);
wsRef.current = null;
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
}; };
return termi;
};
useEffect(() => {
createTerminal();
}, [lines, containerId]); }, [lines, containerId]);
useEffect(() => { useEffect(() => {

View File

@@ -79,7 +79,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -44,7 +44,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. providers.
</CardDescription> </CardDescription>
</div> </div>
@@ -62,8 +62,8 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -79,7 +79,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -44,8 +44,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. provider.
</CardDescription> </CardDescription>
</div> </div>
@@ -62,8 +62,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -30,7 +30,7 @@ export const ShowVolumes = ({ mongoId }: Props) => {
<div> <div>
<CardTitle className="text-xl">Volumes</CardTitle> <CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription> <CardDescription>
If you want to persist data in this mongo use the following config If you want to persist data in this mongo use the following config.
to setup the volumes to setup the volumes
</CardDescription> </CardDescription>
</div> </div>

View File

@@ -191,7 +191,7 @@ export const DockerMonitoring = ({
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Monitoring</CardTitle> <CardTitle className="text-xl">Monitoring</CardTitle>
<CardDescription> <CardDescription>
Watch the usage of your server in the current app Watch the usage of your server in the current app.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">

View File

@@ -79,7 +79,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -44,8 +44,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. provider.
</CardDescription> </CardDescription>
</div> </div>
@@ -62,8 +62,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -79,7 +79,7 @@ export const ShowPostgresResources = ({ postgresId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -45,8 +45,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. provider.
</CardDescription> </CardDescription>
</div> </div>
@@ -63,8 +63,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -15,20 +15,25 @@ import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
AlertTriangle, AlertTriangle,
BookIcon, BookIcon,
CircuitBoard,
ExternalLink,
ExternalLinkIcon, ExternalLinkIcon,
FolderInput, FolderInput,
MoreHorizontalIcon, MoreHorizontalIcon,
TrashIcon, TrashIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Fragment } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { UpdateProject } from "./update"; import { UpdateProject } from "./update";
@@ -45,6 +50,7 @@ export const ShowProjects = () => {
}, },
); );
const { mutateAsync } = api.project.remove.useMutation(); const { mutateAsync } = api.project.remove.useMutation();
return ( return (
<> <>
{data?.length === 0 && ( {data?.length === 0 && (
@@ -74,17 +80,87 @@ export const ShowProjects = () => {
project?.redis.length + project?.redis.length +
project?.applications.length + project?.applications.length +
project?.compose.length; project?.compose.length;
const flattedDomains = [
...project.applications.flatMap((a) => a.domains),
...project.compose.flatMap((a) => a.domains),
];
const renderDomainsDropdown = (
item: typeof project.compose | typeof project.applications,
) =>
item[0] ? (
<DropdownMenuGroup>
<DropdownMenuLabel>
{"applicationId" in item[0] ? "Applications" : "Compose"}
</DropdownMenuLabel>
{item.map((a) => (
<Fragment
key={"applicationId" in a ? a.applicationId : a.composeId}
>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs ">
{a.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{a.domains.map((domain) => (
<DropdownMenuItem key={domain.domainId} asChild>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLink className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</Fragment>
))}
</DropdownMenuGroup>
) : null;
return ( return (
<div key={project.projectId} className="w-full lg:max-w-md"> <div key={project.projectId} className="w-full lg:max-w-md">
<Link href={`/dashboard/project/${project.projectId}`}> <Link href={`/dashboard/project/${project.projectId}`}>
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card"> <Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
<Button {flattedDomains.length > 1 ? (
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100" <DropdownMenu>
size="sm" <DropdownMenuTrigger asChild>
variant="default" <Button
> className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
<ExternalLinkIcon className="size-3.5" /> size="sm"
</Button> variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2"
onClick={(e) => e.stopPropagation()}
>
{renderDomainsDropdown(project.applications)}
{renderDomainsDropdown(project.compose)}
</DropdownMenuContent>
</DropdownMenu>
) : flattedDomains[0] ? (
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
onClick={(e) => e.stopPropagation()}
>
<Link
href={`${flattedDomains[0].https ? "https" : "http"}://${flattedDomains[0].host}${flattedDomains[0].path}`}
target="_blank"
>
<ExternalLinkIcon className="size-3.5" />
</Link>
</Button>
) : null}
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between gap-2"> <CardTitle className="flex items-center justify-between gap-2">
<span className="flex flex-col gap-1.5"> <span className="flex flex-col gap-1.5">

View File

@@ -79,7 +79,7 @@ export const ShowRedisResources = ({ redisId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -18,10 +18,25 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle, HelpCircle } from "lucide-react";
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";
@@ -35,6 +50,7 @@ const addCertificate = z.object({
certificateData: z.string().min(1, "Certificate data is required"), certificateData: z.string().min(1, "Certificate data is required"),
privateKey: z.string().min(1, "Private key is required"), privateKey: z.string().min(1, "Private key is required"),
autoRenew: z.boolean().optional(), autoRenew: z.boolean().optional(),
serverId: z.string().optional(),
}); });
type AddCertificate = z.infer<typeof addCertificate>; type AddCertificate = z.infer<typeof addCertificate>;
@@ -44,6 +60,7 @@ export const AddCertificate = () => {
const { mutateAsync, isError, error, isLoading } = const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation(); api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const form = useForm<AddCertificate>({ const form = useForm<AddCertificate>({
defaultValues: { defaultValues: {
@@ -64,6 +81,7 @@ export const AddCertificate = () => {
certificateData: data.certificateData, certificateData: data.certificateData,
privateKey: data.privateKey, privateKey: data.privateKey,
autoRenew: data.autoRenew, autoRenew: data.autoRenew,
serverId: data.serverId,
}) })
.then(async () => { .then(async () => {
toast.success("Certificate Created"); toast.success("Certificate Created");
@@ -144,6 +162,47 @@ export const AddCertificate = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form> </form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3"> <DialogFooter className="flex w-full flex-row !justify-between pt-3">

View File

@@ -27,7 +27,8 @@ export const ShowCertificates = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<ShieldCheck className="size-8 self-center text-muted-foreground" /> <ShieldCheck className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a certificate is required to upload your certificate To create a certificate it is required to upload an existing
certificate
</span> </span>
<AddCertificate /> <AddCertificate />
</div> </div>

View File

@@ -43,7 +43,7 @@ export const ShowRegistry = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<Server className="size-8 self-center text-muted-foreground" /> <Server className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground text-center"> <span className="text-base text-muted-foreground text-center">
To create a cluster is required to set a registry. To create a cluster it is required to set a registry.
</span> </span>
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center"> <div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">

View File

@@ -29,7 +29,7 @@ export const ShowDestinations = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<FolderUp className="size-8 self-center text-muted-foreground" /> <FolderUp className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. To create a backup it is required to set at least 1 provider.
</span> </span>
<AddDestination /> <AddDestination />
</div> </div>

View File

@@ -11,13 +11,11 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { format } from "date-fns"; import { format } from "date-fns";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const AddGithubProvider = () => { export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const [manifest, setManifest] = useState(""); const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false); const [isOrganization, setIsOrganization] = useState(false);

View File

@@ -34,7 +34,7 @@ export const ShowNotifications = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<BellRing className="size-8 self-center text-muted-foreground" /> <BellRing className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To send notifications is required to set at least 1 provider. To send notifications it is required to set at least 1 provider.
</span> </span>
<AddNotification /> <AddNotification />
</div> </div>

View File

@@ -95,7 +95,7 @@ export const ProfileForm = () => {
<div> <div>
<CardTitle className="text-xl">Account</CardTitle> <CardTitle className="text-xl">Account</CardTitle>
<CardDescription> <CardDescription>
Change your details of your profile here. Change the details of your profile here.
</CardDescription> </CardDescription>
</div> </div>
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />} {!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
@@ -145,7 +145,6 @@ export const ProfileForm = () => {
<FormControl> <FormControl>
<RadioGroup <RadioGroup
onValueChange={(e) => { onValueChange={(e) => {
console.log(e);
field.onChange(e); field.onChange(e);
}} }}
defaultValue={field.value} defaultValue={field.value}

View File

@@ -19,7 +19,6 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";

View File

@@ -29,11 +29,22 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const addServerDomain = z.object({ const addServerDomain = z
domain: z.string().min(1, { message: "URL is required" }), .object({
letsEncryptEmail: z.string().min(1, "Email is required").email(), domain: z.string().min(1, { message: "URL is required" }),
certificateType: z.enum(["letsencrypt", "none"]), letsEncryptEmail: z.string(),
}); certificateType: z.enum(["letsencrypt", "none"]),
})
.superRefine((data, ctx) => {
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"LetsEncrypt email is required when certificate type is letsencrypt",
path: ["letsEncryptEmail"],
});
}
});
type AddServerDomain = z.infer<typeof addServerDomain>; type AddServerDomain = z.infer<typeof addServerDomain>;
@@ -80,7 +91,7 @@ export const WebDomain = () => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Server Domain</CardTitle> <CardTitle className="text-xl">Server Domain</CardTitle>
<CardDescription> <CardDescription>
Add your server domain to your application Add a domain to your server application.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">

View File

@@ -1,7 +1,7 @@
import { AddProject } from "@/components/dashboard/projects/add"; import { AddProject } from "@/components/dashboard/projects/add";
import type { Auth } from "@/server/api/services/auth";
import type { User } from "@/server/api/services/user";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { Auth, IS_CLOUD, User } from "@dokploy/server";
import { is } from "drizzle-orm";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
@@ -11,6 +11,7 @@ interface TabInfo {
tabLabel?: string; tabLabel?: string;
description: string; description: string;
index: string; index: string;
type: TabState;
isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean;
} }
@@ -22,47 +23,65 @@ export type TabState =
| "requests" | "requests"
| "docker"; | "docker";
const tabMap: Record<TabState, TabInfo> = { const getTabMaps = (isCloud: boolean) => {
projects: { const elements: TabInfo[] = [
label: "Projects", {
description: "Manage your projects", label: "Projects",
index: "/dashboard/projects", description: "Manage your projects",
}, index: "/dashboard/projects",
monitoring: { type: "projects",
label: "Monitoring",
description: "Monitor your projects",
index: "/dashboard/monitoring",
},
traefik: {
label: "Traefik",
tabLabel: "Traefik File System",
description: "Manage your traefik",
index: "/dashboard/traefik",
isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToTraefikFiles);
}, },
}, ];
docker: {
label: "Docker", if (!isCloud) {
description: "Manage your docker", elements.push(
index: "/dashboard/docker", {
isShow: ({ rol, user }) => { label: "Monitoring",
return Boolean(rol === "admin" || user?.canAccessToDocker); description: "Monitor your projects",
}, index: "/dashboard/monitoring",
}, type: "monitoring",
requests: { },
label: "Requests", {
description: "Manage your requests", label: "Traefik",
index: "/dashboard/requests", tabLabel: "Traefik File System",
isShow: ({ rol, user }) => { description: "Manage your traefik",
return Boolean(rol === "admin" || user?.canAccessToDocker); index: "/dashboard/traefik",
}, isShow: ({ rol, user }) => {
}, return Boolean(rol === "admin" || user?.canAccessToTraefikFiles);
settings: { },
type: "traefik",
},
{
label: "Docker",
description: "Manage your docker",
index: "/dashboard/docker",
isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToDocker);
},
type: "docker",
},
{
label: "Requests",
description: "Manage your requests",
index: "/dashboard/requests",
isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToDocker);
},
type: "requests",
},
);
}
elements.push({
label: "Settings", label: "Settings",
description: "Manage your settings", description: "Manage your settings",
index: "/dashboard/settings/server", type: "settings",
}, index: isCloud
? "/dashboard/settings/profile"
: "/dashboard/settings/server",
});
return elements;
}; };
interface Props { interface Props {
@@ -72,9 +91,10 @@ interface Props {
export const NavigationTabs = ({ tab, children }: Props) => { export const NavigationTabs = ({ tab, children }: Props) => {
const router = useRouter(); const router = useRouter();
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const [activeTab, setActiveTab] = useState<TabState>(tab); const [activeTab, setActiveTab] = useState<TabState>(tab);
const { data: isCloud } = api.settings.isCloud.useQuery();
const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]);
const { data: user } = api.user.byAuthId.useQuery( const { data: user } = api.user.byAuthId.useQuery(
{ {
authId: data?.id || "", authId: data?.id || "",
@@ -89,7 +109,7 @@ export const NavigationTabs = ({ tab, children }: Props) => {
}, [tab]); }, [tab]);
const activeTabInfo = useMemo(() => { const activeTabInfo = useMemo(() => {
return tabMap[activeTab]; return tabMap.find((tab) => tab.type === activeTab);
}, [activeTab]); }, [activeTab]);
return ( return (
@@ -97,10 +117,10 @@ export const NavigationTabs = ({ tab, children }: Props) => {
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap"> <header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-xl font-bold lg:text-3xl"> <h1 className="text-xl font-bold lg:text-3xl">
{activeTabInfo.label} {activeTabInfo?.label}
</h1> </h1>
<p className="lg:text-medium text-muted-foreground"> <p className="lg:text-medium text-muted-foreground">
{activeTabInfo.description} {activeTabInfo?.description}
</p> </p>
</div> </div>
{tab === "projects" && {tab === "projects" &&
@@ -112,27 +132,26 @@ export const NavigationTabs = ({ tab, children }: Props) => {
className="w-full" className="w-full"
onValueChange={async (e) => { onValueChange={async (e) => {
setActiveTab(e as TabState); setActiveTab(e as TabState);
router.push(tabMap[e as TabState].index); const tab = tabMap.find((tab) => tab.type === e);
router.push(tab?.index || "");
}} }}
> >
{/* className="grid w-fit grid-cols-4 bg-transparent" */}
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto border-b border-b-divider pb-1"> <div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto border-b border-b-divider pb-1">
<TabsList className="bg-transparent relative px-0"> <TabsList className="bg-transparent relative px-0">
{Object.keys(tabMap).map((key) => { {tabMap.map((tab, index) => {
const tab = tabMap[key as TabState]; if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) {
if (tab.isShow && !tab.isShow?.({ rol: data?.rol, user })) {
return null; return null;
} }
return ( return (
<TabsTrigger <TabsTrigger
key={key} key={tab.type}
value={key} value={tab.type}
className="relative py-2.5 md:px-5 data-[state=active]:shadow-none data-[state=active]:bg-transparent rounded-md hover:bg-zinc-100 hover:dark:bg-zinc-800 data-[state=active]:hover:bg-zinc-100 data-[state=active]:hover:dark:bg-zinc-800" className="relative py-2.5 md:px-5 data-[state=active]:shadow-none data-[state=active]:bg-transparent rounded-md hover:bg-zinc-100 hover:dark:bg-zinc-800 data-[state=active]:hover:bg-zinc-100 data-[state=active]:hover:dark:bg-zinc-800"
> >
<span className="relative z-[1] w-full"> <span className="relative z-[1] w-full">
{tab.tabLabel || tab.label} {tab?.tabLabel || tab?.label}
</span> </span>
{key === activeTab && ( {tab.type === activeTab && (
<div className="absolute -bottom-[5.5px] w-full"> <div className="absolute -bottom-[5.5px] w-full">
<div className="h-0.5 bg-foreground rounded-t-md" /> <div className="h-0.5 bg-foreground rounded-t-md" />
</div> </div>

View File

@@ -4,6 +4,7 @@ interface Props {
export const SettingsLayout = ({ children }: Props) => { export const SettingsLayout = ({ children }: Props) => {
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.byAuthId.useQuery( const { data: user } = api.user.byAuthId.useQuery(
{ {
authId: data?.id || "", authId: data?.id || "",
@@ -17,7 +18,7 @@ export const SettingsLayout = ({ children }: Props) => {
<div className="md:max-w-[18rem] w-full"> <div className="md:max-w-[18rem] w-full">
<Nav <Nav
links={[ links={[
...(data?.rol === "admin" ...(data?.rol === "admin" && !isCloud
? [ ? [
{ {
title: "Server", title: "Server",
@@ -47,6 +48,7 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Database, icon: Database,
href: "/dashboard/settings/destinations", href: "/dashboard/settings/destinations",
}, },
{ {
title: "Certificates", title: "Certificates",
label: "", label: "",
@@ -60,7 +62,7 @@ export const SettingsLayout = ({ children }: Props) => {
href: "/dashboard/settings/ssh-keys", href: "/dashboard/settings/ssh-keys",
}, },
{ {
title: "Git ", title: "Git",
label: "", label: "",
icon: GitBranch, icon: GitBranch,
href: "/dashboard/settings/git-providers", href: "/dashboard/settings/git-providers",
@@ -71,12 +73,23 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Users, icon: Users,
href: "/dashboard/settings/users", href: "/dashboard/settings/users",
}, },
{ {
title: "Cluster", title: "Registry",
label: "", label: "",
icon: BoxesIcon, icon: ListMusic,
href: "/dashboard/settings/cluster", href: "/dashboard/settings/registry",
}, },
...(!isCloud
? [
{
title: "Cluster",
label: "",
icon: BoxesIcon,
href: "/dashboard/settings/cluster",
},
]
: []),
{ {
title: "Notifications", title: "Notifications",
label: "", label: "",
@@ -128,6 +141,7 @@ import {
GitBranch, GitBranch,
KeyIcon, KeyIcon,
KeyRound, KeyRound,
ListMusic,
type LucideIcon, type LucideIcon,
Route, Route,
Server, Server,

View File

@@ -0,0 +1,25 @@
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_authId_auth_id_fk";
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "adminId" text;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD COLUMN "privateKey" text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD COLUMN "adminId" text;--> statement-breakpoint
ALTER TABLE "git_provider" ADD COLUMN "adminId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "git_provider" DROP COLUMN IF EXISTS "authId";

View File

@@ -0,0 +1,13 @@
ALTER TABLE "certificate" ADD COLUMN "adminId" text;--> statement-breakpoint
ALTER TABLE "certificate" ADD COLUMN "serverId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -274,6 +274,20 @@
"when": 1727942090102, "when": 1727942090102,
"tag": "0038_rapid_landau", "tag": "0038_rapid_landau",
"breakpoints": true "breakpoints": true
},
{
"idx": 39,
"version": "6",
"when": 1728021127765,
"tag": "0039_many_tiger_shark",
"breakpoints": true
},
{
"idx": 40,
"version": "6",
"when": 1728780577084,
"tag": "0040_graceful_wolfsbane",
"breakpoints": true
} }
] ]
} }

View File

@@ -18,6 +18,7 @@ const nextConfig = {
typescript: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
transpilePackages: ["@dokploy/server"],
webpack: (config) => { webpack: (config) => {
config.plugins.push( config.plugins.push(
new CopyWebpackPlugin({ new CopyWebpackPlugin({

View File

@@ -1,17 +1,17 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.9.4", "version": "v0.10.0",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "npm run build-server && npm run build-next", "build": "npm run build-server && npm run build-next",
"start": "node dist/server.mjs", "start": "node -r dotenv/config dist/server.mjs",
"build-server": "tsx esbuild.config.ts", "build-server": "tsx esbuild.config.ts",
"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 dist/reset-password.mjs", "reset-password": "node dist/reset-password.mjs",
"dev": "tsx watch -r dotenv/config ./server/server.ts --project tsconfig.server.json ", "dev": "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",
@@ -34,17 +34,14 @@
"test": "vitest --config __test__/vitest.config.ts" "test": "vitest --config __test__/vitest.config.ts"
}, },
"dependencies": { "dependencies": {
"rotating-file-stream": "3.2.3", "@dokploy/server": "workspace:*",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1", "@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1", "@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "6.4.0", "@codemirror/legacy-modes": "6.4.0",
"@codemirror/view": "6.29.0", "@codemirror/view": "6.29.0",
"@dokploy/trpc-openapi": "0.0.4", "@dokploy/trpc-openapi": "0.0.4",
"@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4",
"@octokit/webhooks": "^13.2.7", "@octokit/webhooks": "^13.2.7",
"@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-accordion": "1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
@@ -64,7 +61,6 @@
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.21",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0", "@tanstack/react-table": "^8.16.0",
"@trpc/client": "^10.43.6", "@trpc/client": "^10.43.6",
@@ -77,8 +73,6 @@
"@xterm/xterm": "^5.4.0", "@xterm/xterm": "^5.4.0",
"adm-zip": "^0.5.14", "adm-zip": "^0.5.14",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"bl": "6.0.11",
"boxen": "^7.1.1",
"bullmq": "5.4.2", "bullmq": "5.4.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
@@ -86,31 +80,22 @@
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"date-fns": "3.6.0", "date-fns": "3.6.0",
"dockerode": "4.0.2",
"dockerode-compose": "^1.4.0",
"dockerstats": "2.4.2",
"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",
"hi-base32": "^0.5.1",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"k6": "^0.0.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": "^14.1.3", "next": "^14.1.3",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"node-os-utils": "1.3.7",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",
"otpauth": "^9.2.3",
"postgres": "3.4.4", "postgres": "3.4.4",
"public-ip": "6.0.2", "public-ip": "6.0.2",
"qrcode": "^1.5.3",
"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",
@@ -121,53 +106,35 @@
"swagger-ui-react": "^5.17.14", "swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tar-fs": "3.0.5",
"undici": "^6.19.2", "undici": "^6.19.2",
"use-resize-observer": "9.1.0", "use-resize-observer": "9.1.0",
"ws": "8.16.0", "ws": "8.16.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4", "zod": "^3.23.4",
"zod-form-data": "^2.0.2", "zod-form-data": "^2.0.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"ssh2": "1.15.0" "ssh2": "1.15.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3", "autoprefixer": "10.4.12",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@types/adm-zip": "^0.5.5", "@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
"@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",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6", "@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.15",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37", "@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^18.2.15",
"@types/swagger-ui-react": "^4.18.3", "@types/swagger-ui-react": "^4.18.3",
"@types/tar-fs": "2.0.4",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"autoprefixer": "^10.4.14",
"drizzle-kit": "^0.21.1", "drizzle-kit": "^0.21.1",
"esbuild": "0.20.2", "esbuild": "0.20.2",
"husky": "^9.0.11",
"lint-staged": "^15.2.7", "lint-staged": "^15.2.7",
"localtunnel": "2.0.2",
"memfs": "^4.11.0", "memfs": "^4.11.0",
"postcss": "^8.4.31",
"prettier": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tsconfig-paths": "4.2.0",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"vite-tsconfig-paths": "4.3.2", "vite-tsconfig-paths": "4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"xterm-readline": "1.1.1",
"@types/ssh2": "1.15.1" "@types/ssh2": "1.15.1"
}, },
"ct3aMetadata": { "ct3aMetadata": {

View File

@@ -10,7 +10,6 @@ interface Props {
export default function Custom404({ statusCode, error }: Props) { export default function Custom404({ statusCode, error }: Props) {
const displayStatusCode = statusCode || 400; const displayStatusCode = statusCode || 400;
console.log(error, statusCode);
return ( return (
<div className="h-screen"> <div className="h-screen">
<div className="max-w-[50rem] flex flex-col mx-auto size-full"> <div className="max-w-[50rem] flex flex-col mx-auto size-full">
@@ -92,7 +91,6 @@ export default function Custom404({ statusCode, error }: Props) {
// @ts-ignore // @ts-ignore
Error.getInitialProps = ({ res, err, ...rest }: NextPageContext) => { Error.getInitialProps = ({ res, err, ...rest }: NextPageContext) => {
console.log(err, rest);
const statusCode = res ? res.statusCode : err ? err.statusCode : 404; const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode, error: err }; return { statusCode, error: err };
}; };

View File

@@ -1,7 +1,6 @@
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc"; import { createTRPCContext } from "@/server/api/trpc";
import { validateRequest } from "@/server/auth/auth"; import { validateBearerToken, validateRequest } from "@dokploy/server";
import { validateBearerToken } from "@/server/auth/token";
import { createOpenApiNextHandler } from "@dokploy/trpc-openapi"; import { createOpenApiNextHandler } from "@dokploy/trpc-openapi";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -18,6 +17,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.status(401).json({ message: "Unauthorized" }); res.status(401).json({ message: "Unauthorized" });
return; return;
} }
// @ts-ignore // @ts-ignore
return createOpenApiNextHandler({ return createOpenApiNextHandler({
router: appRouter, router: appRouter,

View File

@@ -2,6 +2,8 @@ import { db } from "@/server/db";
import { applications } from "@/server/db/schema"; import { applications } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -89,6 +91,12 @@ export default async function handler(
applicationType: "application", applicationType: "application",
server: !!application.serverId, server: !!application.serverId,
}; };
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },

View File

@@ -2,6 +2,8 @@ import { db } from "@/server/db";
import { compose } from "@/server/db/schema"; import { compose } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { import {
@@ -65,6 +67,12 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`, descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeResult.serverId, server: !!composeResult.serverId,
}; };
if (IS_CLOUD && composeResult.serverId) {
jobData.serverId = composeResult.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },

View File

@@ -1,8 +1,9 @@
import { findAdmin } from "@/server/api/services/admin";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { applications, compose, github } from "@/server/db/schema"; import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue"; import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { Webhooks } from "@octokit/webhooks"; import { Webhooks } from "@octokit/webhooks";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@@ -12,17 +13,10 @@ export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
) { ) {
const admin = await findAdmin();
if (!admin) {
res.status(200).json({ message: "Could not find admin" });
return;
}
const signature = req.headers["x-hub-signature-256"]; const signature = req.headers["x-hub-signature-256"];
const githubBody = req.body; const githubBody = req.body;
if (!githubBody?.installation.id) { if (!githubBody?.installation?.id) {
res.status(400).json({ message: "Github Installation not found" }); res.status(400).json({ message: "Github Installation not found" });
return; return;
} }
@@ -88,6 +82,12 @@ export default async function handler(
applicationType: "application", applicationType: "application",
server: !!app.serverId, server: !!app.serverId,
}; };
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },
@@ -116,6 +116,12 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`, descriptionLog: `Hash: ${deploymentHash}`,
}; };
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
}
await myQueue.add( await myQueue.add(
"deployments", "deployments",
{ ...jobData }, { ...jobData },

View File

@@ -1,6 +1,11 @@
import { createGithub } from "@/server/api/services/github";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { github } from "@/server/db/schema"; import { github } from "@/server/db/schema";
import {
createGithub,
findAdminByAuthId,
findAuthById,
findUserByAuthId,
} from "@dokploy/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { Octokit } from "octokit"; import { Octokit } from "octokit";
@@ -34,16 +39,29 @@ export default async function handler(
}, },
); );
await createGithub({ const auth = await findAuthById(value as string);
name: data.name,
githubAppName: data.html_url, let adminId = "";
githubAppId: data.id, if (auth.rol === "admin") {
githubClientId: data.client_id, const admin = await findAdminByAuthId(auth.id);
githubClientSecret: data.client_secret, adminId = admin.adminId;
githubWebhookSecret: data.webhook_secret, } else {
githubPrivateKey: data.pem, const user = await findUserByAuthId(auth.id);
authId: value as string, adminId = user.adminId;
}); }
await createGithub(
{
name: data.name,
githubAppName: data.html_url,
githubAppId: data.id,
githubClientId: data.client_id,
githubClientSecret: data.client_secret,
githubWebhookSecret: data.webhook_secret,
githubPrivateKey: data.pem,
},
adminId,
);
} else if (action === "gh_setup") { } else if (action === "gh_setup") {
await db await db
.update(github) .update(github)

View File

@@ -1,4 +1,4 @@
import { findGitlabById, updateGitlab } from "@/server/api/services/gitlab"; import { findGitlabById, updateGitlab } from "@dokploy/server";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler( export default async function handler(

View File

@@ -1,18 +0,0 @@
import type { NextRequest } from "next/server";
import { renderToString } from "react-dom/server";
import Page418 from "../hola"; // Importa la página 418
export const GET = async (req: NextRequest) => {
// Renderiza el componente de la página 418 como HTML
const htmlContent = renderToString(Page418());
// Devuelve la respuesta con el código de estado HTTP 418
return new Response(htmlContent, {
headers: {
"Content-Type": "text/html",
},
status: 418,
});
};
export default GET;

View File

@@ -1,7 +1,7 @@
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers"; import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; 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";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
@@ -19,6 +19,14 @@ Dashboard.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user, session } = await validateRequest(ctx.req, ctx.res); const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) { if (!user) {
return { return {
@@ -28,7 +36,7 @@ export async function getServerSideProps(
}, },
}; };
} }
const { req, res, resolvedUrl } = ctx; const { req, res } = ctx;
const helpers = createServerSideHelpers({ const helpers = createServerSideHelpers({
router: appRouter, router: appRouter,

View File

@@ -1,6 +1,6 @@
import { ShowMonitoring } from "@/components/dashboard/monitoring/web-server/show"; import { ShowMonitoring } from "@/components/dashboard/monitoring/web-server/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { validateRequest } from "@/server/auth/auth"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
@@ -16,6 +16,14 @@ Dashboard.getLayout = (page: ReactElement) => {
export async function getServerSideProps( export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res); const { user } = await validateRequest(ctx.req, ctx.res);
if (!user) { if (!user) {
return { return {

View File

@@ -29,9 +29,9 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import type { findProjectById } from "@/server/api/services/project";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { findProjectById } from "@dokploy/server";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard, FolderInput, GlobeIcon, PlusIcon } from "lucide-react"; import { CircuitBoard, FolderInput, GlobeIcon, PlusIcon } from "lucide-react";
import type { import type {

View File

@@ -25,8 +25,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { GlobeIcon } from "lucide-react"; import { GlobeIcon } from "lucide-react";
import type { import type {

View File

@@ -19,8 +19,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard } from "lucide-react"; import { CircuitBoard } from "lucide-react";
import type { import type {

View File

@@ -20,8 +20,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,

View File

@@ -20,8 +20,8 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,

Some files were not shown because too many files have changed in this diff Show More