feat: introduce Vite-based Dokploy application

- Added a new Vite application (`apps/vite`) to serve as a standalone SPA for the Dokploy dashboard, leveraging TanStack Router for routing.
- Implemented server-side handling with a custom Node.js server to manage API requests and serve static files.
- Integrated existing components and utilities from the Dokploy monorepo, ensuring no duplication of code.
- Updated `.dockerignore` and `.gitignore` to include new build artifacts and environment files.
- Enhanced `pnpm-lock.yaml` with new dependencies for the Vite application.
- Created configuration files for Vite, TypeScript, and ESBuild to support the new application structure.
This commit is contained in:
Mauricio Siu 2026-08-10 23:38:04 -06:00
parent 2d7caceeed
commit 2b225a1e4f
78 changed files with 10881 additions and 4 deletions

View File

@ -2,4 +2,8 @@ node_modules
.git
.gitignore
*.md
dist
dist
**/.next
**/dist
**/dist-server
**/.env

6
.gitignore vendored
View File

@ -46,4 +46,8 @@ yarn-error.log*
.db
.playwright-*
.credentials
.credentials
dist/
dist-server/
.tanstack
.env.*

54
Dockerfile.vite Normal file
View File

@ -0,0 +1,54 @@
# syntax=docker/dockerfile:1
FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --no-frozen-lockfile
ENV NODE_ENV=production
RUN pnpm --filter=dokploy-vite build
RUN pnpm --filter=dokploy-vite build:server
RUN pnpm --filter=dokploy-vite --prod deploy --legacy /prod/vite
# Drop auto-installed optional peers that are never used at runtime
# (better-auth peers next/prisma adapters; drizzle peers pglite/sqlite; prisma pulls effect/sharp)
RUN cd /prod/vite/node_modules && rm -rf \
.pnpm/next@* .pnpm/@next+swc* \
.pnpm/prisma@* .pnpm/@prisma+* \
.pnpm/effect@* .pnpm/@electric-sql+* \
.pnpm/better-sqlite3@* \
.pnpm/sharp@* .pnpm/@img+* \
.pnpm/typescript@* \
next prisma typescript better-sqlite3 sharp
FROM base AS runtime
WORKDIR /app
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# For a full production replacement of the official image, add the same
# tooling block as ./Dockerfile (docker cli, nixpacks, railpack, pack, rclone)
# — required for builds/deployments, not for serving the panel.
COPY --from=build /prod/vite/node_modules ./node_modules
COPY --from=build /prod/vite/package.json ./package.json
COPY --from=build /usr/src/app/apps/vite/dist ./dist
COPY --from=build /usr/src/app/apps/vite/dist-server ./dist-server
COPY --from=build /usr/src/app/apps/dokploy/drizzle ./drizzle
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
CMD curl -fs http://localhost:3000/api/health || exit 1
CMD ["sh", "-c", "node dist-server/wait-for-postgres.mjs && node dist-server/migration.mjs && exec node dist-server/server.mjs"]

50
apps/vite/README.md Normal file
View File

@ -0,0 +1,50 @@
# dokploy-vite
Vite + TanStack Router migration of the Dokploy dashboard UI. The Next.js app (`apps/dokploy`) stays intact and keeps serving the backend (tRPC, better-auth, websockets, webhooks). This app is a pure SPA that reuses the existing UI code directly from `apps/dokploy` — components are NOT duplicated.
## How it works
- **Aliases** (`vite.config.ts`): `@/*` resolves into `apps/dokploy/*`, so all 380+ components, hooks, lib and utils are consumed from their original location. `~/*` resolves to this app's `src/`.
- **Next.js shims** (`src/shims/`): `next/link`, `next/router`, `next/navigation`, `next/head`, `next/dynamic`, `next/script` and `nextjs-toploader` are aliased to thin adapters over TanStack Router, so shared components run unmodified.
- **tRPC**: `@/utils/api` is re-aliased to `src/utils/api.ts` (`createTRPCReact` instead of `createTRPCNext`), same links (ws split + FormData split + superjson).
- **Auth**: SSR `getServerSideProps` guards were replaced by `beforeLoad` guards using the existing better-auth client (`@/lib/auth-client`) with a 30s session cache (`src/utils/session.ts`).
- **Routes** (`src/routes/`): file-based TanStack routes mirroring `apps/dokploy/pages`. `/dashboard` is a shared layout route rendering `DashboardLayout` once (it persists across navigations, unlike the pages-router setup).
- **Dev proxy**: `/api` and every websocket endpoint (`/drawer-logs`, `/terminal`, `/docker-container-*`, `/listen-*`) proxy to the Next custom server on `localhost:3000`.
## Standalone server (no Next.js)
`server/server.ts` is a Next-free replacement for `apps/dokploy/server/server.ts`. It reuses the entire existing backend from the monorepo:
- **tRPC** (`/api/trpc/*`) and **OpenAPI REST** (`/api/*` catch-all) — the exact same handlers from `apps/dokploy/pages/api`, mounted through a thin Next-API compat adapter (`server/next-compat.ts`) that provides `req.query/body/cookies` and `res.status/json/send/redirect` on plain Node req/res.
- **better-auth** (`/api/auth/*`) — `toNodeHandler(auth.handler)`, already framework-agnostic.
- **Webhooks/callbacks** — deploy (github, refreshToken, compose), Stripe (raw body preserved for signature check), GitHub setup/webhook, GitLab/Gitea OAuth callbacks: all reused unchanged via the adapter.
- **Websockets** — same `setup*WebSocketServer` functions from `apps/dokploy/server/wss`.
- **Bootstrapping** — same production init (Traefik config, cron jobs, schedules, volume backups, deployment worker) as the original server.
- **Static SPA** — in production it serves `dist/` with SPA fallback to `index.html` (`server/static.ts`).
## Development
One command — the server embeds Vite in middleware mode, so API + websockets + UI (with HMR) all run same-origin on :3000:
```bash
pnpm --filter dokploy-vite dev
# → http://localhost:3000 (no Next involved)
```
Alternative split mode (Vite dev server on :5173 proxying to whichever backend runs on :3000 — the Next custom server or this one):
```bash
pnpm --filter dokploy dev # or: nothing, if dev above is running
pnpm --filter dokploy-vite dev:client # → :5173
```
The `/api` proxy rewrites the `origin` header to :3000 so better-auth's trusted-origin check passes in split mode.
Production: `pnpm --filter dokploy-vite build && pnpm --filter dokploy-vite start` — one process serving API + websockets + static SPA on :3000, no Next.js at runtime.
The server loads `.env` from `apps/vite` first, then falls back to `apps/dokploy/.env`.
## Dropped vs Next (known gaps)
- SSR prefetching (`createServerSideHelpers`) — the SPA fetches on mount instead.
- Per-page IS_CLOUD / role / permission SSR redirects beyond session checks — components and tRPC procedures still enforce them; route-level guards can be added incrementally in `beforeLoad`.

View File

@ -0,0 +1,24 @@
import esbuild from "esbuild";
esbuild
.build({
entryPoints: {
server: "server/server.ts",
migration: "../dokploy/migration.ts",
"wait-for-postgres": "../dokploy/wait-for-postgres.ts",
"reset-password": "../dokploy/reset-password.ts",
"reset-2fa": "../dokploy/reset-2fa.ts",
"migrate-auth-secret": "../dokploy/scripts/migrate-auth-secret.ts",
},
bundle: true,
platform: "node",
format: "esm",
target: "node18",
outExtension: { ".js": ".mjs" },
minify: true,
sourcemap: true,
outdir: "dist-server",
tsconfig: "tsconfig.json",
packages: "external",
})
.catch(() => process.exit(1));

19
apps/vite/index.html Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dokploy</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

142
apps/vite/package.json Normal file
View File

@ -0,0 +1,142 @@
{
"name": "dokploy-vite",
"version": "0.0.1",
"private": true,
"license": "Apache-2.0",
"type": "module",
"scripts": {
"dev": "tsx -r dotenv/config server/server.ts",
"dev:client": "vite",
"build": "vite build",
"build:server": "tsx esbuild.config.ts",
"start": "NODE_ENV=production tsx -r dotenv/config server/server.ts",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.44",
"@ai-sdk/azure": "^3.0.30",
"@ai-sdk/cohere": "^3.0.21",
"@ai-sdk/deepinfra": "^2.0.34",
"@ai-sdk/mistral": "^3.0.20",
"@ai-sdk/openai": "^3.0.29",
"@ai-sdk/openai-compatible": "^2.0.30",
"@aws-sdk/client-secrets-manager": "^3.1097.0",
"@better-auth/api-key": "1.6.23",
"@better-auth/passkey": "1.6.23",
"@better-auth/scim": "1.6.23",
"@better-auth/sso": "1.6.23",
"@dokploy/server": "workspace:*",
"@dokploy/trpc-openapi": "0.0.18",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.1.3",
"@octokit/webhooks": "^13.9.0",
"@react-email/components": "^1.0.12",
"@trpc/server": "^11.10.0",
"adm-zip": "^0.5.16",
"ai": "^6.0.86",
"ai-sdk-ollama": "^3.7.0",
"bcrypt": "5.1.1",
"better-auth": "1.6.23",
"bl": "6.0.11",
"boxen": "^7.1.1",
"date-fns": "3.6.0",
"dockerode": "4.0.2",
"dotenv": "16.4.5",
"drizzle-orm": "0.45.2",
"drizzle-zod": "0.8.3",
"lodash": "4.17.21",
"micromatch": "4.0.8",
"nanoid": "3.3.11",
"node-os-utils": "2.0.1",
"node-pty": "1.1.0",
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"postgres": "3.4.4",
"public-ip": "6.0.2",
"react": "19.2.7",
"react-dom": "19.2.7",
"resend": "^6.0.2",
"semver": "7.7.3",
"shell-quote": "^1.8.1",
"slugify": "^1.6.6",
"ssh2": "~1.16.0",
"stripe": "17.2.0",
"superjson": "^2.2.2",
"toml": "3.0.0",
"undici": "^6.21.3",
"ws": "8.16.0",
"yaml": "2.8.1",
"zod": "^4.3.6",
"zod-form-data": "^3.0.1"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.0",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/search": "^6.6.0",
"@codemirror/view": "^6.39.15",
"@hookform/resolvers": "^5.2.2",
"@stepperize/react": "4.0.1",
"@stripe/stripe-js": "4.8.0",
"@tailwindcss/typography": "0.5.16",
"@tailwindcss/vite": "^4.3.1",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-table": "^8.21.3",
"@tanstack/router-plugin": "^1.132.0",
"@trpc/client": "^11.10.0",
"@trpc/react-query": "^11.10.0",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "4.17.4",
"@types/node": "^24.4.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/semver": "7.7.1",
"@types/swagger-ui-react": "^4.19.0",
"@uiw/codemirror-theme-github": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"@vitejs/plugin-react": "^5.0.0",
"@xterm/addon-attach": "0.10.0",
"@xterm/addon-clipboard": "0.1.0",
"@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^0.2.1",
"copy-to-clipboard": "^3.3.3",
"dompurify": "^3.3.3",
"esbuild": "0.20.2",
"fancy-ansi": "^0.1.3",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.469.0",
"next-themes": "^0.2.1",
"qrcode": "^1.5.4",
"radix-ui": "^1.6.0",
"react-confetti-explosion": "3.0.3",
"react-day-picker": "10.0.1",
"react-hook-form": "^7.71.2",
"react-markdown": "^9.1.0",
"recharts": "^3.8.0",
"shadcn": "^4.11.0",
"sonner": "^1.7.4",
"swagger-ui-react": "^5.32.6",
"tailwind-merge": "^2.6.1",
"tailwindcss": "^4.3.1",
"tailwindcss-animate": "^1.0.7",
"tsx": "^4.22.4",
"tw-animate-css": "^1.4.0",
"typescript": "^5.8.3",
"use-resize-observer": "9.1.0",
"vite": "^7.1.0",
"xterm-addon-fit": "^0.8.0"
}
}

View File

@ -0,0 +1,107 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { buffer } from "node:stream/consumers";
type NextStyleHandler = (req: any, res: any) => unknown | Promise<unknown>;
interface RunOptions {
params?: Record<string, string | string[]>;
parseBody?: boolean;
}
const parseCookies = (header: string | undefined) => {
const cookies: Record<string, string> = {};
if (!header) return cookies;
for (const part of header.split(";")) {
const index = part.indexOf("=");
if (index === -1) continue;
const key = part.slice(0, index).trim();
const value = part.slice(index + 1).trim();
if (key) cookies[key] = decodeURIComponent(value);
}
return cookies;
};
const parseQuery = (url: URL, params: Record<string, string | string[]>) => {
const query: Record<string, string | string[]> = {};
for (const [key, value] of url.searchParams) {
const existing = query[key];
if (existing === undefined) {
query[key] = value;
} else if (Array.isArray(existing)) {
existing.push(value);
} else {
query[key] = [existing, value];
}
}
Object.assign(query, params);
return query;
};
const parseBody = async (req: IncomingMessage) => {
if (req.method === "GET" || req.method === "HEAD") return undefined;
const raw = await buffer(req);
if (raw.length === 0) return undefined;
const contentType = req.headers["content-type"] ?? "";
const text = raw.toString("utf8");
if (contentType.includes("application/json")) {
try {
return JSON.parse(text);
} catch {
return text;
}
}
if (contentType.includes("application/x-www-form-urlencoded")) {
return Object.fromEntries(new URLSearchParams(text));
}
return text;
};
const enhanceResponse = (res: ServerResponse) => {
const anyRes = res as any;
anyRes.status = (code: number) => {
res.statusCode = code;
return anyRes;
};
anyRes.json = (data: unknown) => {
if (!res.headersSent && !res.getHeader("content-type")) {
res.setHeader("content-type", "application/json; charset=utf-8");
}
res.end(JSON.stringify(data));
return anyRes;
};
anyRes.send = (data: unknown) => {
if (typeof data === "string" || Buffer.isBuffer(data)) {
res.end(data);
} else if (data === undefined || data === null) {
res.end();
} else {
anyRes.json(data);
}
return anyRes;
};
anyRes.redirect = (statusOrUrl: number | string, maybeUrl?: string) => {
const statusCode = typeof statusOrUrl === "number" ? statusOrUrl : 307;
const location =
typeof statusOrUrl === "string" ? statusOrUrl : (maybeUrl ?? "/");
res.writeHead(statusCode, { location });
res.end();
return anyRes;
};
return anyRes;
};
export const runNextApiHandler = async (
handler: NextStyleHandler,
req: IncomingMessage,
res: ServerResponse,
options: RunOptions = {},
) => {
const url = new URL(req.url ?? "/", "http://localhost");
const anyReq = req as any;
anyReq.query = parseQuery(url, options.params ?? {});
anyReq.cookies = parseCookies(req.headers.cookie);
if (options.parseBody !== false) {
anyReq.body = await parseBody(req);
}
await handler(anyReq, enhanceResponse(res));
};

203
apps/vite/server/server.ts Normal file
View File

@ -0,0 +1,203 @@
import http from "node:http";
import path from "node:path";
import {
auth,
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
IS_CLOUD,
initCancelDeployments,
initCronJobs,
initEnterpriseBackupCronJobs,
initializeNetwork,
initSchedules,
initVolumeBackupsCronJobs,
sendDokployRestartNotifications,
setupDirectories,
} from "@dokploy/server";
import { toNodeHandler } from "better-auth/node";
import { config } from "dotenv";
import openApiHandler from "@/pages/api/[...trpc]";
import deployRefreshTokenHandler from "@/pages/api/deploy/[refreshToken]";
import deployComposeHandler from "@/pages/api/deploy/compose/[refreshToken]";
import deployGithubHandler from "@/pages/api/deploy/github";
import healthHandler from "@/pages/api/health";
import giteaAuthorizeHandler from "@/pages/api/providers/gitea/authorize";
import giteaCallbackHandler from "@/pages/api/providers/gitea/callback";
import githubSetupHandler from "@/pages/api/providers/github/setup";
import githubWebhookHandler from "@/pages/api/providers/github/webhook";
import gitlabCallbackHandler from "@/pages/api/providers/gitlab/callback";
import stripeWebhookHandler from "@/pages/api/stripe/webhook";
import trpcHandler from "@/pages/api/trpc/[trpc]";
import { setupDockerContainerLogsWebSocketServer } from "@/server/wss/docker-container-logs";
import { setupDockerContainerTerminalWebSocketServer } from "@/server/wss/docker-container-terminal";
import { setupDockerStatsMonitoringSocketServer } from "@/server/wss/docker-stats";
import { setupDrawerLogsWebSocketServer } from "@/server/wss/drawer-logs";
import { setupDeploymentLogsWebSocketServer } from "@/server/wss/listen-deployment";
import { setupTerminalWebSocketServer } from "@/server/wss/terminal";
import packageInfo from "../package.json";
import { runNextApiHandler } from "./next-compat";
import { createStaticHandler } from "./static";
config({ path: ".env" });
config({ path: "../dokploy/.env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const HOST = process.env.HOST || "0.0.0.0";
const isProd = process.env.NODE_ENV === "production";
if (isProd && !IS_CLOUD) {
setupDirectories();
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
console.log("✅ initialization complete");
}
const authHandler = toNodeHandler(auth.handler);
const handleApi = async (
req: http.IncomingMessage,
res: http.ServerResponse,
pathname: string,
) => {
if (pathname === "/api/health") {
return runNextApiHandler(healthHandler, req, res);
}
if (pathname.startsWith("/api/auth/")) {
return authHandler(req, res);
}
if (pathname.startsWith("/api/trpc/")) {
const trpc = decodeURIComponent(pathname.slice("/api/trpc/".length));
return runNextApiHandler(trpcHandler, req, res, {
params: { trpc },
parseBody: false,
});
}
if (pathname === "/api/deploy/github") {
return runNextApiHandler(deployGithubHandler, req, res);
}
if (pathname === "/api/stripe/webhook") {
return runNextApiHandler(stripeWebhookHandler, req, res, {
parseBody: false,
});
}
if (pathname === "/api/providers/github/setup") {
return runNextApiHandler(githubSetupHandler, req, res);
}
if (pathname === "/api/providers/github/webhook") {
return runNextApiHandler(githubWebhookHandler, req, res);
}
if (pathname === "/api/providers/gitlab/callback") {
return runNextApiHandler(gitlabCallbackHandler, req, res);
}
if (pathname === "/api/providers/gitea/authorize") {
return runNextApiHandler(giteaAuthorizeHandler, req, res);
}
if (pathname === "/api/providers/gitea/callback") {
return runNextApiHandler(giteaCallbackHandler, req, res);
}
const composeMatch = pathname.match(/^\/api\/deploy\/compose\/([^/]+)$/);
if (composeMatch?.[1]) {
return runNextApiHandler(deployComposeHandler, req, res, {
params: { refreshToken: composeMatch[1] },
});
}
const deployMatch = pathname.match(/^\/api\/deploy\/([^/]+)$/);
if (deployMatch?.[1]) {
return runNextApiHandler(deployRefreshTokenHandler, req, res, {
params: { refreshToken: deployMatch[1] },
});
}
const segments = pathname
.slice("/api/".length)
.split("/")
.filter(Boolean)
.map(decodeURIComponent);
return runNextApiHandler(openApiHandler, req, res, {
params: { trpc: segments },
});
};
const startServer = async () => {
console.log("Running DokployVersion: ", packageInfo.version);
let handleUi: (
req: http.IncomingMessage,
res: http.ServerResponse,
url: URL,
) => void = (_req, res) => {
res.writeHead(503, { "content-type": "text/plain" });
res.end("UI not ready");
};
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url ?? "/", "http://localhost");
if (url.pathname.startsWith("/api")) {
await handleApi(req, res, url.pathname);
return;
}
handleUi(req, res, url);
} catch (error) {
console.error("Request error", error);
if (!res.headersSent) {
res.writeHead(500, { "content-type": "text/plain" });
}
res.end("Internal Server Error");
}
});
if (isProd) {
handleUi = createStaticHandler(path.resolve(import.meta.dirname, "../dist"));
} else {
const { createServer: createViteServer } = await import("vite");
const vite = await createViteServer({
root: path.resolve(import.meta.dirname, ".."),
configFile: path.resolve(import.meta.dirname, "../vite.config.ts"),
appType: "spa",
server: { middlewareMode: true, hmr: { server } },
});
handleUi = (req, res) => {
vite.middlewares(req, res, () => {
res.statusCode = 404;
res.end();
});
};
console.log("Vite dev middleware enabled (UI + HMR on this port)");
}
setupDrawerLogsWebSocketServer(server);
setupDeploymentLogsWebSocketServer(server);
setupDockerContainerLogsWebSocketServer(server);
setupDockerContainerTerminalWebSocketServer(server);
setupTerminalWebSocketServer(server);
if (!IS_CLOUD) {
setupDockerStatsMonitoringSocketServer(server);
}
server.listen(PORT, HOST);
console.log(`Standalone Server Started on: http://${HOST}:${PORT}`);
if (isProd && !IS_CLOUD) {
createDefaultMiddlewares();
await initializeNetwork();
await initCronJobs();
await initSchedules();
await initCancelDeployments();
await initVolumeBackupsCronJobs();
await sendDokployRestartNotifications();
}
await initEnterpriseBackupCronJobs();
if (!IS_CLOUD) {
console.log("Starting Deployment Worker");
const { startDeploymentWorker } = await import(
"@/server/queues/queueSetup"
);
await startDeploymentWorker();
}
};
startServer().catch((error) => {
console.error("Main Server Error", error);
});

View File

@ -0,0 +1,79 @@
import fs from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
const MIME_TYPES: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
".ico": "image/x-icon",
".txt": "text/plain; charset=utf-8",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".map": "application/json; charset=utf-8",
".wasm": "application/wasm",
".webmanifest": "application/manifest+json",
};
const sendFile = (
res: ServerResponse,
filePath: string,
cacheControl: string,
) => {
res.writeHead(200, {
"content-type":
MIME_TYPES[path.extname(filePath).toLowerCase()] ??
"application/octet-stream",
"cache-control": cacheControl,
});
fs.createReadStream(filePath).pipe(res);
};
export const createStaticHandler = (distDir: string) => {
const hasDist = fs.existsSync(path.join(distDir, "index.html"));
return (req: IncomingMessage, res: ServerResponse, url: URL) => {
if (req.method !== "GET" && req.method !== "HEAD") {
res.writeHead(404).end();
return;
}
if (!hasDist) {
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
res.end(
"UI build not found. In development run the Vite dev server (pnpm --filter dokploy-vite dev); for production run vite build first.",
);
return;
}
const pathname = decodeURIComponent(url.pathname);
const filePath = path.join(distDir, pathname);
if (!filePath.startsWith(distDir)) {
res.writeHead(403).end();
return;
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const cacheControl = pathname.startsWith("/assets/")
? "public, max-age=31536000, immutable"
: "public, max-age=0, must-revalidate";
sendFile(res, filePath, cacheControl);
return;
}
sendFile(
res,
path.join(distDir, "index.html"),
"no-cache, no-store, must-revalidate",
);
};
};

27
apps/vite/src/main.tsx Normal file
View File

@ -0,0 +1,27 @@
import "./styles/globals.css";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import ReactDOM from "react-dom/client";
import { routeTree } from "./routeTree.gen";
import { setRouterInstance } from "./shims/next-router";
import { TRPCProvider } from "./utils/trpc-provider";
const router = createRouter({ routeTree });
setRouterInstance(router);
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
const rootElement = document.getElementById("root");
if (rootElement && !rootElement.innerHTML) {
ReactDOM.createRoot(rootElement).render(
<TRPCProvider>
<RouterProvider router={router} />
</TRPCProvider>,
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { ThemeProvider } from "next-themes";
import { SearchCommand } from "@/components/dashboard/search-command";
import { WhitelabelingProvider } from "@/components/proprietary/whitelabeling/whitelabeling-provider";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import TopLoader from "~/shims/toploader";
const RootComponent = () => {
return (
<TooltipProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<TopLoader />
<WhitelabelingProvider />
<Toaster richColors />
<SearchCommand />
<Outlet />
</ThemeProvider>
</TooltipProvider>
);
};
export const Route = createRootRoute({
component: RootComponent,
});

View File

@ -0,0 +1,30 @@
import { createFileRoute } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
export const AcceptInvitation = () => {
const { invitationId } = Route.useParams();
// const { data: organization } = api.organization.getById.useQuery({
// id: id as string
// })
return (
<div>
<Button
onClick={async () => {
const result = await authClient.organization.acceptInvitation({
invitationId: invitationId,
});
console.log(result);
}}
>
Accept Invitation
</Button>
</div>
);
};
export const Route = createFileRoute("/accept-invitation/$invitationId")({
component: AcceptInvitation,
});

View File

@ -0,0 +1,74 @@
import { createFileRoute } from "@tanstack/react-router";
import { Rocket } from "lucide-react";
import { useRouter } from "next/router";
import { ShowDeploymentsTable } from "@/components/dashboard/deployments/show-deployments-table";
import { ShowQueueTable } from "@/components/dashboard/deployments/show-queue-table";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const TAB_VALUES = ["deployments", "queue"] as const;
type TabValue = (typeof TAB_VALUES)[number];
function isValidTab(t: string): t is TabValue {
return TAB_VALUES.includes(t as TabValue);
}
function DeploymentsPage() {
const router = useRouter();
const tab =
router.query.tab && isValidTab(router.query.tab as string)
? (router.query.tab as TabValue)
: "deployments";
const setTab = (value: string) => {
if (!isValidTab(value)) return;
router.replace(
{ pathname: "/dashboard/deployments", query: { tab: value } },
undefined,
{ shallow: true },
);
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full">
<CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2">
<Rocket className="size-5" />
Deployments
</CardTitle>
<CardDescription>
All application and compose deployments in one place.
</CardDescription>
</div>
</div>
<Tabs value={tab} onValueChange={setTab} className="w-full min-w-0">
<TabsList className="mt-2">
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="queue">Queue</TabsTrigger>
</TabsList>
<TabsContent value="deployments" className="mt-0 min-w-0 pt-4">
<ShowDeploymentsTable />
</TabsContent>
<TabsContent value="queue" className="mt-0 pt-4">
<ShowQueueTable />
</TabsContent>
</Tabs>
</CardHeader>
</div>
</Card>
</div>
);
}
export const Route = createFileRoute("/dashboard/deployments")({
component: DeploymentsPage,
});

View File

@ -0,0 +1,82 @@
import { createFileRoute } from "@tanstack/react-router";
import { useRouter } from "next/router";
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
import { ShowVolumes } from "@/components/dashboard/docker/volumes/show-volumes";
import { ShowNetworks } from "@/components/dashboard/networks/show-networks";
import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers";
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
import { ServerFilter } from "@/components/shared/server-filter";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
const DEFAULT_TAB = "containers";
const Dashboard = () => {
const router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery();
const queryTab =
typeof router.query.tab === "string" ? router.query.tab : DEFAULT_TAB;
const activeTab = isCloud && queryTab === "networks" ? DEFAULT_TAB : queryTab;
const setTab = (value: string) => {
const { tab: _current, ...query } = router.query;
router.replace(
{
pathname: router.pathname,
query: value === DEFAULT_TAB ? query : { ...query, tab: value },
},
undefined,
{ shallow: true },
);
};
return (
<ServerFilter>
{(serverId) => (
<Tabs value={activeTab} onValueChange={setTab}>
<TabsList>
<TabsTrigger value="containers">Containers</TabsTrigger>
<TabsTrigger value="swarm">Swarm</TabsTrigger>
<TabsTrigger value="volumes">Volumes</TabsTrigger>
{!isCloud && <TabsTrigger value="networks">Networks</TabsTrigger>}
</TabsList>
<TabsContent value="containers">
<ShowContainers serverId={serverId} />
</TabsContent>
<TabsContent value="swarm">
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="containers">Containers</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<SwarmMonitorCard serverId={serverId} />
</TabsContent>
<TabsContent value="containers">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md p-6">
<ShowSwarmContainers serverId={serverId} />
</div>
</Card>
</TabsContent>
</Tabs>
</TabsContent>
<TabsContent value="volumes">
<ShowVolumes serverId={serverId} />
</TabsContent>
{!isCloud && (
<TabsContent value="networks">
<ShowNetworks serverId={serverId} />
</TabsContent>
)}
</Tabs>
)}
</ServerFilter>
);
};
export const Route = createFileRoute("/dashboard/docker")({
component: Dashboard,
});

View File

@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowHome } from "@/components/dashboard/home/show-home";
const Home = () => {
return <ShowHome />;
};
export const Route = createFileRoute("/dashboard/home")({
component: Home,
});

View File

@ -0,0 +1,83 @@
import { createFileRoute } from "@tanstack/react-router";
import { Loader2 } from "lucide-react";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring";
import { Card } from "@/components/ui/card";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { api } from "@/utils/api";
const BASE_URL = "http://localhost:3001/metrics";
const DEFAULT_TOKEN = "metrics";
const Dashboard = () => {
const [toggleMonitoring, _setToggleMonitoring] = useLocalStorage(
"monitoring-enabled",
false,
);
const { data: monitoring, isPending } = api.user.getMetricsToken.useQuery();
return (
<div className="space-y-4 pb-10">
{/* <AlertBlock>
You are watching the <strong>Free</strong> plan.{" "}
<a
href="https://dokploy.com#pricing"
target="_blank"
className="underline"
rel="noreferrer"
>
Upgrade
</a>{" "}
to get more features.
</AlertBlock> */}
{isPending ? (
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto items-center">
<div className="rounded-xl bg-background flex shadow-md px-4 w-full min-h-[50vh] justify-center items-center text-muted-foreground">
Loading... <Loader2 className="h-4 w-4 animate-spin" />
</div>
</Card>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">Change Monitoring</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)} */}
{toggleMonitoring ? (
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<ShowPaidMonitoring
BASE_URL={
process.env.NODE_ENV === "production"
? `http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}/metrics`
: BASE_URL
}
token={
process.env.NODE_ENV === "production"
? monitoring?.metricsConfig?.server?.token
: DEFAULT_TOKEN
}
/>
</div>
</Card>
) : (
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
<div className="rounded-xl bg-background shadow-md p-6">
<ContainerFreeMonitoring appName="dokploy" />
</div>
</Card>
)}
</>
)}
</div>
);
};
export const Route = createFileRoute("/dashboard/monitoring")({
component: Dashboard,
});

View File

@ -0,0 +1,19 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
const Networks = () => {
return null;
};
export const Route = createFileRoute("/dashboard/networks")({
beforeLoad: ({ location }) => {
const serverId = (location.search as Record<string, unknown>).serverId;
throw redirect({
href: `/dashboard/docker?tab=networks${
typeof serverId === "string"
? `&serverId=${encodeURIComponent(serverId)}`
: ""
}`,
});
},
component: Networks,
});

View File

@ -0,0 +1,435 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowClusterSettings } from "@/components/dashboard/application/advanced/cluster/show-cluster-settings";
import { AddCommand } from "@/components/dashboard/application/advanced/general/add-command";
import { ShowPorts } from "@/components/dashboard/application/advanced/ports/show-port";
import { ShowRedirects } from "@/components/dashboard/application/advanced/redirects/show-redirects";
import { ShowSecurity } from "@/components/dashboard/application/advanced/security/show-security";
import { ShowBuildServer } from "@/components/dashboard/application/advanced/show-build-server";
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
import { ShowTraefikConfig } from "@/components/dashboard/application/advanced/traefik/show-traefik-config";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show";
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
import { ShowIconSettings } from "@/components/dashboard/application/icon/show-icon-settings";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { ShowPatches } from "@/components/dashboard/application/patches/show-patches";
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { AssignNetworks } from "@/components/dashboard/networks/assign-networks";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { api } from "@/utils/api";
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
type TabState =
| "projects"
| "settings"
| "advanced"
| "deployments"
| "domains"
| "monitoring"
| "preview-deployments"
| "volume-backups"
| "icon";
const Service = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { applicationId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setTab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
useEffect(() => {
if (router.query.tab) {
setTab(router.query.tab as TabState);
}
}, [router.query.tab]);
const { data } = api.application.one.useQuery(
{ applicationId },
{
refetchInterval: 5000,
},
);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.project?.projectId || "",
});
const { config: whitelabeling } = useWhitelabeling();
const appName = whitelabeling?.appName || "Dokploy";
const environmentDropdownItems =
environments?.map((env) => ({
name: env.name,
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
})) || [];
return (
<div className="pb-10">
<UseKeyboardNav forPage="application" />
<AdvanceBreadcrumb />
<Head>
<title>
Application: {data?.name} - {data?.environment.project.name} |{" "}
{appName}
</title>
</Head>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2 items-center">
<div className="relative flex flex-row gap-4 items-center">
<ShowIconSettings
serviceId={applicationId}
serviceType="application"
icon={data?.icon}
/>
<div className="absolute -right-1 -top-2 z-10">
<StatusTooltip status={data?.applicationStatus} />
</div>
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to add
more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
{permissions?.service.create && (
<UpdateApplication applicationId={applicationId} />
)}
{permissions?.service.delete && (
<DeleteService id={applicationId} type="application" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setTab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/application/${applicationId}?tab=${e}`;
router.push(newPath);
}}
>
<div className="flex flex-row items-center justify-between w-full overflow-auto">
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
<TabsTrigger value="general">General</TabsTrigger>
{permissions?.envVars.read && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{permissions?.domain.read && (
<TabsTrigger value="domains">Domains</TabsTrigger>
)}
{permissions?.deployment.read && (
<TabsTrigger value="deployments">
Deployments
</TabsTrigger>
)}
{permissions?.deployment.read && (
<TabsTrigger value="preview-deployments">
Preview Deployments
</TabsTrigger>
)}
{permissions?.schedule.read && (
<TabsTrigger value="schedules">Schedules</TabsTrigger>
)}
{permissions?.volumeBackup.read && (
<TabsTrigger value="volume-backups">
Volume Backups
</TabsTrigger>
)}
{permissions?.logs.read && (
<TabsTrigger value="logs">Logs</TabsTrigger>
)}
{data?.sourceType !== "docker" && (
<TabsTrigger value="patches">Patches</TabsTrigger>
)}
{permissions?.monitoring.read &&
((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
)}
{permissions?.service.create && (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralApplication applicationId={applicationId} />
</div>
</TabsContent>
{permissions?.envVars.read && (
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment applicationId={applicationId} />
</div>
</TabsContent>
)}
{permissions?.monitoring.read && (
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures &&
isCloud &&
data?.serverId && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)} */}
{/* {toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : ( */}
<div>
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
</div>
{/* )} */}
</>
)}
</div>
</div>
</TabsContent>
)}
{permissions?.logs.read && (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
serviceId={data?.applicationId}
/>
</div>
</TabsContent>
)}
{permissions?.schedule.read && (
<TabsContent value="schedules">
<div className="flex flex-col gap-4 pt-2.5">
<ShowSchedules
id={applicationId}
scheduleType="application"
/>
</div>
</TabsContent>
)}
{permissions?.deployment.read && (
<TabsContent value="deployments" className="w-full pt-2.5">
<div className="flex flex-col gap-4 ">
<ShowDeployments
id={applicationId}
type="application"
serverId={data?.serverId || ""}
refreshToken={data?.refreshToken || ""}
/>
</div>
</TabsContent>
)}
{permissions?.volumeBackup.read && (
<TabsContent
value="volume-backups"
className="w-full pt-2.5"
>
<div className="flex flex-col gap-4 ">
<ShowVolumeBackups
id={applicationId}
type="application"
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
)}
{permissions?.deployment.read && (
<TabsContent value="preview-deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPreviewDeployments applicationId={applicationId} />
</div>
</TabsContent>
)}
{permissions?.domain.read && (
<TabsContent value="domains" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomains id={applicationId} type="application" />
</div>
</TabsContent>
)}
<TabsContent value="patches" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPatches id={applicationId} type="application" />
</div>
</TabsContent>
{permissions?.service.create && (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} />
<ShowClusterSettings
id={applicationId}
type="application"
/>
<ShowBuildServer applicationId={applicationId} />
<ShowResources id={applicationId} type="application" />
<ShowVolumes id={applicationId} type="application" />
<AssignNetworks id={applicationId} type="application" />
<ShowRedirects applicationId={applicationId} />
<ShowSecurity applicationId={applicationId} />
<ShowPorts applicationId={applicationId} />
<ShowTraefikConfig applicationId={applicationId} />
</div>
</TabsContent>
)}
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId",
)({ component: Service });

View File

@ -0,0 +1,444 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowIconSettings } from "@/components/dashboard/application/icon/show-icon-settings";
import { ShowPatches } from "@/components/dashboard/application/patches/show-patches";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { IsolatedDeploymentTab } from "@/components/dashboard/compose/advanced/add-isolation";
import { ShowComposeContainers } from "@/components/dashboard/compose/containers/show-compose-containers";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
import { AssignComposeNetworks } from "@/components/dashboard/networks/assign-compose-networks";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { api } from "@/utils/api";
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
type TabState =
| "projects"
| "settings"
| "advanced"
| "deployments"
| "domains"
| "containers"
| "monitoring"
| "volumeBackups";
const Service = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { composeId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setTab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
useEffect(() => {
if (router.query.tab) {
setTab(router.query.tab as TabState);
}
}, [router.query.tab]);
const { data } = api.compose.one.useQuery({ composeId });
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "",
});
const { config: whitelabeling } = useWhitelabeling();
const appName = whitelabeling?.appName || "Dokploy";
const environmentDropdownItems =
environments?.map((env) => ({
name: env.name,
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
})) || [];
return (
<div className="pb-10">
<UseKeyboardNav forPage="compose" />
<AdvanceBreadcrumb />
<Head>
<title>
Compose: {data?.name} - {data?.environment?.project?.name} | {appName}
</title>
</Head>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<div className="flex flex-col gap-4">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2 items-center">
<div className="relative flex flex-row gap-4 items-center">
<ShowIconSettings
serviceId={composeId}
serviceType="compose"
icon={data?.icon}
/>
<div className="absolute -right-1 -top-2 z-10">
<StatusTooltip status={data?.composeStatus} />
</div>
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to
add more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
{permissions?.service.create && (
<UpdateCompose composeId={composeId} />
)}
{permissions?.service.delete && (
<DeleteService id={composeId} type="compose" />
)}
</div>
</div>
</CardHeader>
</div>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setTab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/compose/${composeId}?tab=${e}`;
router.push(newPath);
}}
>
<div className="flex flex-row items-center w-full overflow-auto">
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
<TabsTrigger value="general">General</TabsTrigger>
{permissions?.envVars.read && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{permissions?.domain.read && (
<TabsTrigger value="domains">Domains</TabsTrigger>
)}
{permissions?.deployment.read && (
<TabsTrigger value="deployments">
Deployments
</TabsTrigger>
)}
{permissions?.service.read && (
<TabsTrigger value="containers">Containers</TabsTrigger>
)}
{permissions?.service.create && (
<TabsTrigger value="backups">Backups</TabsTrigger>
)}
{permissions?.schedule.read && (
<TabsTrigger value="schedules">Schedules</TabsTrigger>
)}
{permissions?.volumeBackup.read && (
<TabsTrigger value="volumeBackups">
Volume Backups
</TabsTrigger>
)}
{permissions?.logs.read && (
<TabsTrigger value="logs">Logs</TabsTrigger>
)}
{data?.sourceType !== "raw" && (
<TabsTrigger value="patches">Patches</TabsTrigger>
)}
{permissions?.monitoring.read &&
((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
)}
{permissions?.service.create && (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralCompose composeId={composeId} />
</div>
</TabsContent>
{permissions?.envVars.read && (
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={composeId} type="compose" />
</div>
</TabsContent>
)}
{permissions?.service.create && (
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackups id={composeId} backupType="compose" />
</div>
</TabsContent>
)}
{permissions?.schedule.read && (
<TabsContent value="schedules">
<div className="flex flex-col gap-4 pt-2.5">
<ShowSchedules id={composeId} scheduleType="compose" />
</div>
</TabsContent>
)}
{permissions?.volumeBackup.read && (
<TabsContent value="volumeBackups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowVolumeBackups
id={composeId}
type="compose"
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
)}
{permissions?.service.read && (
<TabsContent value="containers">
<div className="flex flex-col gap-4 pt-2.5">
<ShowComposeContainers
serverId={data?.serverId || undefined}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
serviceId={data?.composeId}
/>
</div>
</TabsContent>
)}
{permissions?.monitoring.read && (
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col border rounded-lg ">
{data?.serverId && isCloud ? (
<ComposePaidMonitoring
serverId={data?.serverId || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
appName={data?.appName || ""}
token={
data?.server?.metricsConfig?.server?.token || ""
}
appType={data?.composeType || "docker-compose"}
/>
) : (
<>
{/* {monitoring?.enabledFeatures &&
isCloud &&
data?.serverId && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2 m-4">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ComposePaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
appType={data?.composeType || "docker-compose"}
/>
) : ( */}
{/* <div> */}
<ComposeFreeMonitoring
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</div>
</TabsContent>
)}
{permissions?.logs.read && (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
{data?.composeType === "docker-compose" ? (
<ShowDockerLogsCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
serviceId={data?.composeId}
/>
) : (
<ShowDockerLogsStack
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.composeId}
/>
)}
</div>
</TabsContent>
)}
{permissions?.deployment.read && (
<TabsContent value="deployments" className="w-full pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg">
<ShowDeployments
id={composeId}
type="compose"
serverId={data?.serverId || ""}
refreshToken={data?.refreshToken || ""}
/>
</div>
</TabsContent>
)}
{permissions?.domain.read && (
<TabsContent value="domains">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomains id={composeId} type="compose" />
</div>
</TabsContent>
)}
<TabsContent value="patches" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPatches id={composeId} type="compose" />
</div>
</TabsContent>
{permissions?.service.create && (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
<ShowVolumes id={composeId} type="compose" />
<ShowImport composeId={composeId} />
<AssignComposeNetworks composeId={composeId} />
<IsolatedDeploymentTab composeId={composeId} />
</div>
</TabsContent>
)}
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId",
)({ component: Service });

View File

@ -0,0 +1,297 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-external-libsql-credentials";
import { ShowGeneralLibsql } from "@/components/dashboard/libsql/general/show-general-libsql";
import { ShowInternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-internal-libsql-credentials";
import { UpdateLibsql } from "@/components/dashboard/libsql/update-libsql";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { LibsqlIcon } from "@/components/icons/data-tools-icons";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Libsql = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { libsqlId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
const { data } = api.libsql.one.useQuery({ libsqlId });
const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
return (
<div className="pb-10">
<UseKeyboardNav forPage="libsql" />
<AdvanceBreadcrumb />
<div className="flex flex-col gap-4">
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |
Dokploy
</title>
</Head>
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2">
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.applicationStatus} />
</div>
<LibsqlIcon className="h-6 w-6 text-muted-foreground" />
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to add
more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
<UpdateLibsql libsqlId={libsqlId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && (
<DeleteService id={libsqlId} type="libsql" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/libsql/${libsqlId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-auto">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralLibsql libsqlId={libsqlId} />
<ShowInternalLibsqlCredentials libsqlId={libsqlId} />
<ShowExternalLibsqlCredentials libsqlId={libsqlId} />
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={libsqlId} type="libsql" />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.libsqlId}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackups
id={libsqlId}
databaseType="libsql"
backupType="database"
/>
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDatabaseAdvancedSettings
id={libsqlId}
type="libsql"
/>
</div>
</TabsContent>
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId",
)({ component: Libsql });

View File

@ -0,0 +1,326 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMariadbCredentials } from "@/components/dashboard/mariadb/general/show-external-mariadb-credentials";
import { ShowGeneralMariadb } from "@/components/dashboard/mariadb/general/show-general-mariadb";
import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/general/show-internal-mariadb-credentials";
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Mariadb = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mariadbId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
const { data } = api.mariadb.one.useQuery({ mariadbId });
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "",
});
const { config: whitelabeling } = useWhitelabeling();
const appName = whitelabeling?.appName || "Dokploy";
const environmentDropdownItems =
environments?.map((env) => ({
name: env.name,
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
})) || [];
return (
<div className="pb-10">
<UseKeyboardNav forPage="mariadb" />
<AdvanceBreadcrumb />
<div className="flex flex-col gap-4">
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |
{appName}
</title>
</Head>
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2">
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.applicationStatus} />
</div>
<MariadbIcon className="h-6 w-6 text-muted-foreground" />
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to add
more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
{permissions?.service.create && (
<UpdateMariadb mariadbId={mariadbId} />
)}
{permissions?.service.delete && (
<DeleteService id={mariadbId} type="mariadb" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mariadb/${mariadbId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-auto">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{permissions?.envVars.read && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{permissions?.logs.read && (
<TabsTrigger value="logs">Logs</TabsTrigger>
)}
{permissions?.monitoring.read &&
((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
{permissions?.service.create && (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMariadb mariadbId={mariadbId} />
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
</div>
</TabsContent>
{permissions?.envVars.read && (
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={mariadbId} type="mariadb" />
</div>
</TabsContent>
)}
{permissions?.monitoring.read && (
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</div>
</TabsContent>
)}
{permissions?.logs.read && (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.mariadbId}
/>
</div>
</TabsContent>
)}
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackups id={mariadbId} databaseType="mariadb" />
</div>
</TabsContent>
{permissions?.service.create && (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDatabaseAdvancedSettings
id={mariadbId}
type="mariadb"
/>
</div>
</TabsContent>
)}
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId",
)({ component: Mariadb });

View File

@ -0,0 +1,330 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMongoCredentials } from "@/components/dashboard/mongo/general/show-external-mongo-credentials";
import { ShowGeneralMongo } from "@/components/dashboard/mongo/general/show-general-mongo";
import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/general/show-internal-mongo-credentials";
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Mongo = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mongoId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
const { data } = api.mongo.one.useQuery({ mongoId });
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "",
});
const { config: whitelabeling } = useWhitelabeling();
const appName = whitelabeling?.appName || "Dokploy";
const environmentDropdownItems =
environments?.map((env) => ({
name: env.name,
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
})) || [];
return (
<div className="pb-10">
<UseKeyboardNav forPage="mongodb" />
<AdvanceBreadcrumb />
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
{appName}
</title>
</Head>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2">
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.applicationStatus} />
</div>
<MongodbIcon className="h-6 w-6 text-muted-foreground" />
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to add
more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
{permissions?.service.create && (
<UpdateMongo mongoId={mongoId} />
)}
{permissions?.service.delete && (
<DeleteService id={mongoId} type="mongo" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mongo/${mongoId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-auto">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{permissions?.envVars.read && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{permissions?.logs.read && (
<TabsTrigger value="logs">Logs</TabsTrigger>
)}
{permissions?.monitoring.read &&
((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
{permissions?.service.create && (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMongo mongoId={mongoId} />
<ShowInternalMongoCredentials mongoId={mongoId} />
<ShowExternalMongoCredentials mongoId={mongoId} />
</div>
</TabsContent>
{permissions?.envVars.read && (
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={mongoId} type="mongo" />
</div>
</TabsContent>
)}
{permissions?.monitoring.read && (
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</div>
</TabsContent>
)}
{permissions?.logs.read && (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.mongoId}
/>
</div>
</TabsContent>
)}
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackups
id={mongoId}
databaseType="mongo"
backupType="database"
/>
</div>
</TabsContent>
{permissions?.service.create && (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDatabaseAdvancedSettings
id={mongoId}
type="mongo"
/>
</div>
</TabsContent>
)}
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId",
)({ component: Mongo });

View File

@ -0,0 +1,308 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowExternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-external-mysql-credentials";
import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-general-mysql";
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const MySql = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { mysqlId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
const { data } = api.mysql.one.useQuery({ mysqlId });
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "",
});
const { config: whitelabeling } = useWhitelabeling();
const appName = whitelabeling?.appName || "Dokploy";
const environmentDropdownItems =
environments?.map((env) => ({
name: env.name,
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
})) || [];
return (
<div className="pb-10">
<UseKeyboardNav forPage="mysql" />
<AdvanceBreadcrumb />
<div className="flex flex-col gap-4">
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |
{appName}
</title>
</Head>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2">
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.applicationStatus} />
</div>
<MysqlIcon className="h-6 w-6 text-muted-foreground" />
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to
add more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
{permissions?.service.create && (
<UpdateMysql mysqlId={mysqlId} />
)}
{permissions?.service.delete && (
<DeleteService id={mysqlId} type="mysql" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase
more servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mysql/${mysqlId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-auto">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{permissions?.envVars.read && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{permissions?.logs.read && (
<TabsTrigger value="logs">Logs</TabsTrigger>
)}
{permissions?.monitoring.read &&
((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
{permissions?.service.create && (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMysql mysqlId={mysqlId} />
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
</div>
</TabsContent>
{permissions?.envVars.read && (
<TabsContent value="environment" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={mysqlId} type="mysql" />
</div>
</TabsContent>
)}
{permissions?.monitoring.read && (
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token ||
""
}
/>
) : (
<>
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
</>
)}
</div>
</div>
</TabsContent>
)}
{permissions?.logs.read && (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.mysqlId}
/>
</div>
</TabsContent>
)}
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackups
id={mysqlId}
databaseType="mysql"
backupType="database"
/>
</div>
</TabsContent>
{permissions?.service.create && (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDatabaseAdvancedSettings
id={mysqlId}
type="mysql"
/>
</div>
</TabsContent>
)}
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId",
)({ component: MySql });

View File

@ -0,0 +1,315 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-external-postgres-credentials";
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
const Postgresql = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { postgresId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
const { data } = api.postgres.one.useQuery({ postgresId });
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "",
});
const { config: whitelabeling } = useWhitelabeling();
const appName = whitelabeling?.appName || "Dokploy";
const environmentDropdownItems =
environments?.map((env) => ({
name: env.name,
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
})) || [];
return (
<div className="pb-10">
<UseKeyboardNav forPage="postgres" />
<AdvanceBreadcrumb />
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
{appName}
</title>
</Head>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2">
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.applicationStatus} />
</div>
<PostgresqlIcon className="h-6 w-6 text-muted-foreground" />
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to add
more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
{permissions?.service.create && (
<UpdatePostgres postgresId={postgresId} />
)}
{permissions?.service.delete && (
<DeleteService id={postgresId} type="postgres" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/postgres/${postgresId}?tab=${e}`;
router.push(newPath, undefined, {
shallow: true,
});
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-auto">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "md:grid-cols-6"
: data?.serverId
? "md:grid-cols-5"
: "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{permissions?.envVars.read && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{permissions?.logs.read && (
<TabsTrigger value="logs">Logs</TabsTrigger>
)}
{permissions?.monitoring.read &&
((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
{permissions?.service.create && (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralPostgres postgresId={postgresId} />
<ShowInternalPostgresCredentials
postgresId={postgresId}
/>
<ShowExternalPostgresCredentials
postgresId={postgresId}
/>
</div>
</TabsContent>
{permissions?.envVars.read && (
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={postgresId} type="postgres" />
</div>
</TabsContent>
)}
{permissions?.monitoring.read && (
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${
data?.serverId
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
: "http://localhost:4500"
}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
</>
)}
</div>
</div>
</TabsContent>
)}
{permissions?.logs.read && (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.postgresId}
/>
</div>
</TabsContent>
)}
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackups
id={postgresId}
databaseType="postgres"
backupType="database"
/>
</div>
</TabsContent>
{permissions?.service.create && (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDatabaseAdvancedSettings
id={postgresId}
type="postgres"
/>
</div>
</TabsContent>
)}
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId",
)({ component: Postgresql });

View File

@ -0,0 +1,319 @@
import { createFileRoute } from "@tanstack/react-router";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowExternalRedisCredentials } from "@/components/dashboard/redis/general/show-external-redis-credentials";
import { ShowGeneralRedis } from "@/components/dashboard/redis/general/show-general-redis";
import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { RedisIcon } from "@/components/icons/data-tools-icons";
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
type TabState = "projects" | "monitoring" | "settings" | "advanced";
const Redis = () => {
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
const { redisId } = Route.useParams();
const router = useRouter();
const { projectId, environmentId } = router.query;
const [tab, setSab] = useState<TabState>(
(router.query.tab as TabState) || "general",
);
const { data } = api.redis.one.useQuery({ redisId });
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "",
});
const { config: whitelabeling } = useWhitelabeling();
const appName = whitelabeling?.appName || "Dokploy";
const environmentDropdownItems =
environments?.map((env) => ({
name: env.name,
href: `/dashboard/project/${projectId}/environment/${env.environmentId}`,
})) || [];
return (
<div className="pb-10">
<UseKeyboardNav forPage="redis" />
<AdvanceBreadcrumb />
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
{appName}
</title>
</Head>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row justify-between items-center">
<div className="flex flex-col">
<CardTitle className="text-xl flex flex-row gap-2">
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.applicationStatus} />
</div>
<RedisIcon className="h-6 w-6 text-muted-foreground" />
</div>
{data?.name}
</CardTitle>
{data?.description && (
<CardDescription>{data?.description}</CardDescription>
)}
<span className="text-sm text-muted-foreground">
{data?.appName}
</span>
</div>
<div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={
!data?.serverId
? "default"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-999 w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the
server is inactive, please upgrade your plan to add
more servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2 justify-end">
{permissions?.service.create && (
<UpdateRedis redisId={redisId} />
)}
{permissions?.service.delete && (
<DeleteService id={redisId} type="redis" />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name},
but this server has been disabled because your current
plan doesn't include enough servers. Please purchase more
servers to regain access to this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/redis/${redisId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-auto">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "md:grid-cols-5"
: data?.serverId
? "md:grid-cols-4"
: "md:grid-cols-5",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{permissions?.envVars.read && (
<TabsTrigger value="environment">
Environment
</TabsTrigger>
)}
{permissions?.logs.read && (
<TabsTrigger value="logs">Logs</TabsTrigger>
)}
{permissions?.monitoring.read &&
((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">
Monitoring
</TabsTrigger>
)}
{permissions?.service.create && (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralRedis redisId={redisId} />
<ShowInternalRedisCredentials redisId={redisId} />
<ShowExternalRedisCredentials redisId={redisId} />
</div>
</TabsContent>
{permissions?.envVars.read && (
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment id={redisId} type="redis" />
</div>
</TabsContent>
)}
{permissions?.monitoring.read && (
<TabsContent value="monitoring">
<div className="pt-2.5">
<div className="flex flex-col gap-4 border rounded-lg p-6">
{data?.serverId && isCloud ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
token={
data?.server?.metricsConfig?.server?.token || ""
}
/>
) : (
<>
{/* {monitoring?.enabledFeatures && (
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
<Label className="text-muted-foreground">
Change Monitoring
</Label>
<Switch
checked={toggleMonitoring}
onCheckedChange={setToggleMonitoring}
/>
</div>
)}
{toggleMonitoring ? (
<ContainerPaidMonitoring
appName={data?.appName || ""}
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
token={
monitoring?.metricsConfig?.server?.token || ""
}
/>
) : (
<div> */}
<ContainerFreeMonitoring
appName={data?.appName || ""}
/>
{/* </div> */}
{/* )} */}
</>
)}
</div>
</div>
</TabsContent>
)}
{permissions?.logs.read && (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.redisId}
/>
</div>
</TabsContent>
)}
{permissions?.service.create && (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDatabaseAdvancedSettings
id={redisId}
type="redis"
/>
</div>
</TabsContent>
)}
</Tabs>
)}
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute(
"/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId",
)({ component: Redis });

View File

@ -0,0 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import dynamic from "next/dynamic";
import { ShowProjects } from "@/components/dashboard/projects/show";
import { api } from "@/utils/api";
const ShowWelcomeDokploy = dynamic(
() =>
import("@/components/dashboard/settings/billing/show-welcome-dokploy").then(
(mod) => mod.ShowWelcomeDokploy,
),
{ ssr: false },
);
const Dashboard = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<>
{isCloud && <ShowWelcomeDokploy />}
<ShowProjects />
</>
);
};
export const Route = createFileRoute("/dashboard/projects")({
component: Dashboard,
});

View File

@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
function Requests() {
return <ShowRequests />;
}
export const Route = createFileRoute("/dashboard/requests")({
component: Requests,
});

View File

@ -0,0 +1,21 @@
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { getCachedSession } from "~/utils/session";
const DashboardRouteComponent = () => {
return (
<DashboardLayout>
<Outlet />
</DashboardLayout>
);
};
export const Route = createFileRoute("/dashboard")({
beforeLoad: async () => {
const session = await getCachedSession();
if (!session?.session) {
throw redirect({ to: "/" });
}
},
component: DashboardRouteComponent,
});

View File

@ -0,0 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { ServerFilter } from "@/components/shared/server-filter";
import { Card } from "@/components/ui/card";
function SchedulesPage() {
return (
<ServerFilter>
{(serverId) => (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full">
<ShowSchedules
scheduleType={serverId ? "server" : "dokploy-server"}
id={serverId ?? "dokploy-server"}
/>
</div>
</Card>
</div>
)}
</ServerFilter>
);
}
export const Route = createFileRoute("/dashboard/schedules")({
component: SchedulesPage,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { AiForm } from "@/components/dashboard/settings/ai-form";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<AiForm />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/ai")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowAuditLogs } from "@/components/proprietary/audit-logs/show-audit-logs";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowAuditLogs />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/audit-logs")({
component: Page,
});

View File

@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowBilling } from "@/components/dashboard/settings/billing/show-billing";
const Page = () => {
return <ShowBilling />;
};
export const Route = createFileRoute("/dashboard/settings/billing")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowCertificates />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/certificates")({
component: Page,
});

View File

@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
import { ServerFilter } from "@/components/shared/server-filter";
const Page = () => {
return (
<ServerFilter>
{(serverId) => (
<div className="flex flex-col gap-4 w-full">
<ShowNodes serverId={serverId} />
</div>
)}
</ServerFilter>
);
};
export const Route = createFileRoute("/dashboard/settings/cluster")({
component: Page,
});

View File

@ -0,0 +1,73 @@
import { createFileRoute } from "@tanstack/react-router";
import { BuildsConcurrency } from "@/components/dashboard/settings/servers/actions/builds-concurrency";
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
const Page = () => {
const { data: servers } = api.server.all.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<CardHeader>
<CardTitle className="text-xl">Concurrent Builds</CardTitle>
<CardDescription>
Configure how many deployments can build at the same time on
each server. Builds of the same service are always serialized.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<AlertBlock type="warning">
Running multiple builds at once increases CPU, memory and disk
usage on each server. Each concurrent build runs its own builder
and image build, so set this based on the resources the machine
can handle too high a value can exhaust memory and make
deployments fail.
</AlertBlock>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-muted-foreground">
Dokploy Server
</p>
<BuildsConcurrency />
</div>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-muted-foreground">
Remote Servers
</p>
{servers && servers.length > 0 ? (
<div className="flex flex-col gap-3">
{servers.map((server) => (
<BuildsConcurrency
key={server.serverId}
serverId={server.serverId}
label={server.name}
/>
))}
</div>
) : (
<p className="text-sm text-muted-foreground rounded-lg border border-dashed p-4 text-center">
No remote servers added yet.
</p>
)}
</div>
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/deployments")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowDestinations />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/destinations")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowGitProviders } from "@/components/dashboard/settings/git/show-git-providers";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowGitProviders />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/git-providers")({
component: Page,
});

View File

@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices";
const Page = () => {
return <ShowBillingInvoices />;
};
export const Route = createFileRoute("/dashboard/settings/invoices")({
component: Page,
});

View File

@ -0,0 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { LicenseKeySettings } from "@/components/proprietary/license-keys/license-key";
import { Card } from "@/components/ui/card";
const Page = () => {
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<div className="p-6">
<LicenseKeySettings />
</div>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/license")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowNotifications />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/notifications")({
component: Page,
});

View File

@ -0,0 +1,24 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
import { LinkingAccount } from "@/components/dashboard/settings/linking-account/linking-account";
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { api } from "@/utils/api";
const Page = () => {
const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<ProfileForm />
{isCloud && <LinkingAccount />}
{permissions?.api.read && <ShowApiKeys />}
</div>
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/profile")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowRegistry />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/registry")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowVaultProviders } from "@/components/dashboard/settings/vault/show-vault-providers";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowVaultProviders />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/secrets")({
component: Page,
});

View File

@ -0,0 +1,31 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
const Page = () => {
const { data: user } = api.user.get.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<WebDomain />
<WebServer />
<div className="w-full flex flex-col gap-4">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<ShowBackups
id={user?.userId ?? ""}
databaseType="web-server"
backupType="database"
/>
</Card>
</div>
</div>
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/server")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowServers />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/servers")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowDestinations />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/ssh-keys")({
component: Page,
});

View File

@ -0,0 +1,83 @@
import { createFileRoute } from "@tanstack/react-router";
import { ToggleEnforceSSO } from "@/components/dashboard/settings/servers/actions/toggle-enforce-sso";
import { ToggleRemoteServersOnly } from "@/components/dashboard/settings/servers/actions/toggle-remote-servers-only";
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
import { ForwardAuthServers } from "@/components/proprietary/sso/forward-auth-servers";
import { SSOSettings } from "@/components/proprietary/sso/sso-settings";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
const Page = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<EnterpriseFeatureGate
lockedProps={{
title: "Enterprise SSO",
description:
"Single sign-on (SSO) with OIDC and SAML is part of Dokploy Enterprise. Add a valid license to configure it.",
ctaLabel: "Go to License",
}}
>
<SSOSettings />
</EnterpriseFeatureGate>
</div>
</Card>
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<EnterpriseFeatureGate
lockedProps={{
title: "Application Authentication",
description:
"Protect deployed applications behind an OIDC SSO gate (oauth2-proxy). Part of Dokploy Enterprise.",
ctaLabel: "Go to License",
}}
>
<ForwardAuthServers />
</EnterpriseFeatureGate>
</div>
</Card>
{!isCloud && (
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<EnterpriseFeatureGate
lockedProps={{
title: "Self-hosted Restrictions",
description:
"Deployment and authentication restrictions are part of Dokploy Enterprise. Add a valid license to configure them.",
ctaLabel: "Go to License",
}}
>
<CardHeader>
<CardTitle className="text-xl">
Self-hosted Restrictions
</CardTitle>
<CardDescription>
Control deployment targets and authentication behavior.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<ToggleRemoteServersOnly />
<ToggleEnforceSSO />
</CardContent>
</EnterpriseFeatureGate>
</div>
</Card>
)}
</div>
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/sso")({
component: Page,
});

View File

@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { TagManager } from "@/components/dashboard/settings/tags/tag-manager";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<TagManager />
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/tags")({
component: Page,
});

View File

@ -0,0 +1,24 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
import { api } from "@/utils/api";
const Page = () => {
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const isOwnerOrAdmin = auth?.role === "owner" || auth?.role === "admin";
const canCreateMembers = permissions?.member.create ?? false;
return (
<div className="flex flex-col gap-4 w-full">
<ShowUsers />
{canCreateMembers && <ShowInvitations />}
{isOwnerOrAdmin && <ManageCustomRoles />}
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/users")({
component: Page,
});

View File

@ -0,0 +1,33 @@
import { createFileRoute } from "@tanstack/react-router";
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
import { WhitelabelingSettings } from "@/components/proprietary/whitelabeling/whitelabeling-settings";
import { Card } from "@/components/ui/card";
const Page = () => {
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<div className="p-6">
<EnterpriseFeatureGate
lockedProps={{
title: "Enterprise Whitelabeling",
description:
"Whitelabeling allows you to fully customize logos, colors, CSS, error pages, and more. Add a valid license to configure it.",
ctaLabel: "Go to License",
}}
>
<WhitelabelingSettings />
</EnterpriseFeatureGate>
</div>
</div>
</Card>
</div>
</div>
);
};
export const Route = createFileRoute("/dashboard/settings/whitelabeling")({
component: Page,
});

View File

@ -0,0 +1,19 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
const Swarm = () => {
return null;
};
export const Route = createFileRoute("/dashboard/swarm")({
beforeLoad: ({ location }) => {
const serverId = (location.search as Record<string, unknown>).serverId;
throw redirect({
href: `/dashboard/docker?tab=swarm${
typeof serverId === "string"
? `&serverId=${encodeURIComponent(serverId)}`
: ""
}`,
});
},
component: Swarm,
});

View File

@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
import { ServerFilter } from "@/components/shared/server-filter";
const Dashboard = () => {
return (
<ServerFilter>
{(serverId) => <ShowTraefikSystem serverId={serverId} />}
</ServerFilter>
);
};
export const Route = createFileRoute("/dashboard/traefik")({
component: Dashboard,
});

View File

@ -0,0 +1,478 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { Fingerprint } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { SignInWithGithub } from "@/components/proprietary/auth/sign-in-with-github";
import { SignInWithGoogle } from "@/components/proprietary/auth/sign-in-with-google";
import { SignInWithSSO } from "@/components/proprietary/sso/sign-in-with-sso";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import { CardContent, CardDescription } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
import { getCachedSession } from "~/utils/session";
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const _TwoFactorSchema = z.object({
code: z.string().min(6),
});
type LoginForm = z.infer<typeof LoginSchema>;
function Home() {
const router = useRouter();
const { data: IS_CLOUD } = api.settings.isCloud.useQuery();
const { data: enforceSSO } = api.sso.enforceSSO.useQuery();
const { config: whitelabeling } = useWhitelabelingPublic();
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
const [isTwoFactor, setIsTwoFactor] = useState(false);
const [error, setError] = useState<string | null>(null);
const [twoFactorCode, setTwoFactorCode] = useState("");
const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false);
const [backupCode, setBackupCode] = useState("");
const loginForm = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: "",
password: "",
},
});
const onSubmit = async (values: LoginForm) => {
setIsLoginLoading(true);
try {
const { data, error } = await authClient.signIn.email({
email: values.email,
password: values.password,
});
if (error) {
const isEmailNotVerified =
error.code === "EMAIL_NOT_VERIFIED" ||
error.message?.toLowerCase().includes("email not verified");
if (isEmailNotVerified) {
const msg =
"Your email is not verified. We've sent a new verification link to your email.";
toast.info(msg);
setError(msg);
return;
}
toast.error(error.message);
setError(error.message || "An error occurred while logging in");
return;
}
// @ts-expect-error
if (data?.twoFactorRedirect as boolean) {
setTwoFactorCode("");
setIsTwoFactor(true);
toast.info("Please enter your 2FA code");
return;
}
toast.success("Logged in successfully");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while logging in");
} finally {
setIsLoginLoading(false);
}
};
const onPasskeySignIn = async () => {
setIsPasskeyLoading(true);
try {
const { data, error } = await authClient.signIn.passkey();
if (error) {
const errorCode = "code" in error ? error.code : undefined;
if (
errorCode !== "AUTH_CANCELLED" &&
errorCode !== "ERROR_CEREMONY_ABORTED"
) {
toast.error(error.message || "Failed to sign in with passkey");
setError(error.message || "Failed to sign in with passkey");
}
return;
}
if (data) {
toast.success("Logged in successfully");
router.push("/dashboard/home");
}
} catch {
toast.error("An error occurred while signing in with passkey");
} finally {
setIsPasskeyLoading(false);
}
};
const onTwoFactorSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (twoFactorCode.length !== 6) {
toast.error("Please enter a valid 6-digit code");
return;
}
setIsTwoFactorLoading(true);
try {
const { error } = await authClient.twoFactor.verifyTotp({
code: twoFactorCode.replace(/\s/g, ""),
});
if (error) {
toast.error(error.message);
setError(error.message || "An error occurred while verifying 2FA code");
return;
}
toast.success("Logged in successfully");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while verifying 2FA code");
} finally {
setIsTwoFactorLoading(false);
}
};
const onBackupCodeSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (backupCode.length < 8) {
toast.error("Please enter a valid backup code");
return;
}
setIsBackupCodeLoading(true);
try {
const { error } = await authClient.twoFactor.verifyBackupCode({
code: backupCode.trim(),
});
if (error) {
toast.error(error.message);
setError(
error.message || "An error occurred while verifying backup code",
);
return;
}
toast.success("Logged in successfully");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while verifying backup code");
} finally {
setIsBackupCodeLoading(false);
}
};
const loginContent = (
<>
{IS_CLOUD && <SignInWithGithub />}
{IS_CLOUD && <SignInWithGoogle />}
<Form {...loginForm}>
<form
method="post"
onSubmit={loginForm.handleSubmit(onSubmit)}
className="space-y-4"
id="login-form"
>
<FormField
control={loginForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className="w-full" type="submit" isLoading={isLoginLoading}>
Login
</Button>
</form>
</Form>
<Button
variant="outline"
className="w-full mt-4"
type="button"
onClick={onPasskeySignIn}
isLoading={isPasskeyLoading}
>
<Fingerprint className="size-4" />
Sign in with Passkey
</Button>
</>
);
return (
<>
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
<div className="flex flex-row items-center justify-center gap-2">
<Logo
className="size-12"
logoUrl={
whitelabeling?.loginLogoUrl ||
whitelabeling?.logoUrl ||
undefined
}
/>
Sign in
</div>
</h1>
<p className="text-sm text-muted-foreground">
Enter your email and password to sign in
</p>
</div>
{error && (
<AlertBlock type="error" className="my-2">
<span>{error}</span>
</AlertBlock>
)}
<CardContent className="p-0">
{!isTwoFactor ? (
<>
{enforceSSO ? (
<SignInWithSSO enforce />
) : showSignInWithSSO ? (
<SignInWithSSO>{loginContent}</SignInWithSSO>
) : (
loginContent
)}
</>
) : (
<>
<form
method="post"
onSubmit={onTwoFactorSubmit}
className="space-y-4"
id="two-factor-form"
autoComplete="on"
>
<div className="flex flex-col gap-2">
<Label htmlFor="totp-code">2FA Code</Label>
<InputOTP
id="totp-code"
name="totp"
value={twoFactorCode}
onChange={setTwoFactorCode}
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
autoFocus
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<CardDescription>
Enter the 6-digit code from your authenticator app
</CardDescription>
<button
type="button"
onClick={() => setIsBackupCodeModalOpen(true)}
className="text-sm text-muted-foreground hover:underline self-start mt-2"
>
Lost access to your authenticator app?
</button>
</div>
<div className="grid grid-cols-2 gap-4">
<Button
variant="outline"
className="w-full"
type="button"
onClick={() => {
setIsTwoFactor(false);
setTwoFactorCode("");
}}
>
Back
</Button>
<Button
className="w-full"
type="submit"
isLoading={isTwoFactorLoading}
>
Verify
</Button>
</div>
</form>
<Dialog
open={isBackupCodeModalOpen}
onOpenChange={setIsBackupCodeModalOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Enter Backup Code</DialogTitle>
<DialogDescription>
Enter one of your backup codes to access your account
</DialogDescription>
</DialogHeader>
<form
method="post"
onSubmit={onBackupCodeSubmit}
className="space-y-4"
>
<div className="flex flex-col gap-2">
<Label>Backup Code</Label>
<Input
value={backupCode}
onChange={(e) => setBackupCode(e.target.value)}
placeholder="Enter your backup code"
className="font-mono"
/>
<CardDescription>
Enter one of the backup codes you received when setting up
2FA
</CardDescription>
</div>
<div className="flex gap-4">
<Button
variant="outline"
className="w-full"
type="button"
onClick={() => {
setIsBackupCodeModalOpen(false);
setBackupCode("");
}}
>
Cancel
</Button>
<Button
className="w-full"
type="submit"
isLoading={isBackupCodeLoading}
>
Verify
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
)}
<div className="flex flex-row justify-between flex-wrap">
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
{IS_CLOUD && (
<Link
className="hover:underline text-muted-foreground"
href="/register"
>
Create an account
</Link>
)}
</div>
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
{IS_CLOUD ? (
<Link
className="hover:underline text-muted-foreground"
href="/send-reset-password"
>
Lost your password?
</Link>
) : (
<Link
className="hover:underline text-muted-foreground"
href="https://docs.dokploy.com/docs/core/reset-password"
target="_blank"
>
Lost your password?
</Link>
)}
</div>
</div>
<div className="p-2" />
</CardContent>
</>
);
}
const HomePage = () => {
return (
<OnboardingLayout>
<Home />
</OnboardingLayout>
);
};
export const Route = createFileRoute("/")({
component: HomePage,
beforeLoad: async () => {
const session = await getCachedSession();
if (session?.session) {
throw redirect({ to: "/dashboard/projects" });
}
},
});

View File

@ -0,0 +1,323 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { createFileRoute } from "@tanstack/react-router";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
const registerSchema = z
.object({
name: z.string().min(1, {
message: "First name is required",
}),
lastName: z.string().min(1, {
message: "Last name is required",
}),
email: z
.string()
.min(1, {
message: "Email is required",
})
.email({
message: "Email must be a valid email",
}),
password: z
.string()
.min(1, {
message: "Password is required",
})
.refine((password) => password === "" || password.length >= 8, {
message: "Password must be at least 8 characters",
}),
confirmPassword: z
.string()
.min(1, {
message: "Password is required",
})
.refine(
(confirmPassword) =>
confirmPassword === "" || confirmPassword.length >= 8,
{
message: "Password must be at least 8 characters",
},
),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
type Register = z.infer<typeof registerSchema>;
const Invitation = () => {
const router = useRouter();
const token =
typeof router.query.token === "string" ? router.query.token : "";
const { config: whitelabeling } = useWhitelabelingPublic();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data } = api.user.getUserByToken.useQuery(
{
token,
},
{
enabled: !!token,
},
);
const userAlreadyExists = data?.userAlreadyExists;
const form = useForm<Register>({
defaultValues: {
name: "",
lastName: "",
email: "",
password: "",
confirmPassword: "",
},
resolver: zodResolver(registerSchema),
});
useEffect(() => {
if (data?.email) {
form.reset({
email: data?.email || "",
password: "",
confirmPassword: "",
});
}
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (values: Register) => {
try {
const { error } = await authClient.signUp.email({
email: values.email,
password: values.password,
name: values.name,
lastName: values.lastName,
fetchOptions: {
headers: {
"x-dokploy-token": token,
},
},
});
if (error) {
toast.error(error.message);
return;
}
const _result = await authClient.organization.acceptInvitation({
invitationId: token,
});
toast.success("Account created successfully");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while creating your account");
}
};
return (
<div>
<div className="flex h-screen w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<CardTitle className="text-2xl font-bold flex items-center gap-2">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo
className="size-12"
logoUrl={
whitelabeling?.loginLogoUrl ||
whitelabeling?.logoUrl ||
undefined
}
/>
</Link>
Invitation
</CardTitle>
{userAlreadyExists ? (
<div className="flex flex-col gap-4 justify-center items-center">
<AlertBlock type="success">
<div className="flex flex-col gap-2">
<span className="font-medium">Valid Invitation!</span>
<span className="text-sm text-green-600 dark:text-green-400">
We detected that you already have an account with this
email. Please sign in to accept the invitation.
</span>
</div>
</AlertBlock>
<Button asChild variant="default" className="w-full">
<Link href="/">Sign In</Link>
</Button>
</div>
) : (
<>
<CardDescription>
Fill the form below to create your account
</CardDescription>
<div className="w-full">
<div className="p-3" />
{/* {isError && (
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)} */}
<CardContent className="p-0">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
disabled
placeholder="Email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Confirm Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
className="w-full"
>
Register
</Button>
</div>
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
{isCloud && (
<>
<Link
className="hover:underline text-muted-foreground"
href="/"
>
Login
</Link>
<Link
className="hover:underline text-muted-foreground"
href="/send-reset-password"
>
Lost your password?
</Link>
</>
)}
</div>
</form>
</Form>
</CardContent>
</div>
</>
)}
</div>
</div>
</div>
);
};
const InvitationPage = () => {
return (
<OnboardingLayout>
<Invitation />
</OnboardingLayout>
);
};
export const Route = createFileRoute("/invitation")({
component: InvitationPage,
});

View File

@ -0,0 +1,307 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { AlertTriangle } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { SignInWithGithub } from "@/components/proprietary/auth/sign-in-with-github";
import { SignInWithGoogle } from "@/components/proprietary/auth/sign-in-with-google";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
import { getCachedSession } from "~/utils/session";
const registerSchema = z
.object({
name: z.string().min(1, {
message: "First name is required",
}),
lastName: z.string().min(1, {
message: "Last name is required",
}),
email: z
.string()
.min(1, {
message: "Email is required",
})
.email({
message: "Email must be a valid email",
}),
password: z
.string()
.min(1, {
message: "Password is required",
})
.refine((password) => password === "" || password.length >= 8, {
message: "Password must be at least 8 characters",
}),
confirmPassword: z
.string()
.min(1, {
message: "Password is required",
})
.refine(
(confirmPassword) =>
confirmPassword === "" || confirmPassword.length >= 8,
{
message: "Password must be at least 8 characters",
},
),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
type Register = z.infer<typeof registerSchema>;
const Register = () => {
const router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { config: whitelabeling } = useWhitelabelingPublic();
const [isError, setIsError] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<any>(null);
const form = useForm<Register>({
defaultValues: {
name: "",
lastName: "",
email: "",
password: "",
confirmPassword: "",
},
resolver: zodResolver(registerSchema),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Register) => {
const { data, error } = await authClient.signUp.email({
email: values.email,
password: values.password,
name: values.name,
lastName: values.lastName,
});
if (error) {
setIsError(true);
setError(error.message || "An error occurred");
} else {
toast.success("User registered successfully", {
duration: 2000,
});
if (!isCloud) {
router.push("/");
} else {
setData(data);
}
}
};
return (
<div className="">
<div className="flex w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<CardTitle className="text-2xl font-bold flex items-center gap-2">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo
className="size-12"
logoUrl={
whitelabeling?.loginLogoUrl ||
whitelabeling?.logoUrl ||
undefined
}
/>
</Link>
{isCloud ? "Sign Up" : "Setup the server"}
</CardTitle>
<CardDescription>
Enter your email and password to{" "}
{isCloud ? "create an account" : "setup the server"}
</CardDescription>
<div className="mx-auto w-full max-w-lg bg-transparent">
{isError && (
<div className="my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error}
</span>
</div>
)}
{isCloud && data && (
<AlertBlock type="success" className="my-2">
<span>
Registered successfully, please check your inbox or spam
folder to confirm your account.
</span>
</AlertBlock>
)}
<CardContent className="p-0">
{isCloud && (
<div className="flex flex-col gap-2">
<SignInWithGithub />
<SignInWithGoogle />
</div>
)}
{isCloud && (
<p className="mb-4 text-center text-xs text-muted-foreground">
Or register with email
</p>
)}
<Form {...form}>
<form
method="post"
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="email@dokploy.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
className="w-full"
>
Register
</Button>
</div>
</form>
</Form>
<div className="flex flex-row justify-between flex-wrap">
{isCloud && (
<div className="mt-4 text-center text-sm flex gap-2 text-muted-foreground">
Already have account?
<Link className="underline" href="/">
Sign in
</Link>
</div>
)}
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2 text-muted-foreground">
Need help?
<Link
className="underline"
href="https://dokploy.com"
target="_blank"
>
Contact us
</Link>
</div>
</div>
</CardContent>
</div>
</div>
</div>
</div>
);
};
const RegisterPage = () => {
return (
<OnboardingLayout>
<Register />
</OnboardingLayout>
);
};
export const Route = createFileRoute("/register")({
component: RegisterPage,
beforeLoad: async () => {
const session = await getCachedSession();
if (session?.session) {
throw redirect({ to: "/dashboard/projects" });
}
},
});

View File

@ -0,0 +1,193 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { createFileRoute } from "@tanstack/react-router";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
const loginSchema = z
.object({
password: z
.string()
.min(1, {
message: "Password is required",
})
.min(8, {
message: "Password must be at least 8 characters",
}),
confirmPassword: z
.string()
.min(1, {
message: "Password is required",
})
.min(8, {
message: "Password must be at least 8 characters",
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
type Login = z.infer<typeof loginSchema>;
function Home() {
const { config: whitelabeling } = useWhitelabelingPublic();
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const form = useForm<Login>({
defaultValues: {
password: "",
confirmPassword: "",
},
resolver: zodResolver(loginSchema),
});
useEffect(() => {
const token = new URLSearchParams(window.location.search).get("token");
if (token) {
setToken(token);
}
}, [token]);
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => {
setIsLoading(true);
const { error } = await authClient.resetPassword({
newPassword: values.password,
token: token || "",
});
if (error) {
setError(error.message || "An error occurred");
} else {
toast.success("Password reset successfully");
router.push("/");
}
setIsLoading(false);
};
return (
<div className="flex h-screen w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<CardTitle className="text-2xl font-bold flex flex-row gap-2 items-center">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo
className="size-12"
logoUrl={
whitelabeling?.loginLogoUrl ||
whitelabeling?.logoUrl ||
undefined
}
/>
</Link>
Reset Password
</CardTitle>
<CardDescription>
Enter your email to reset your password
</CardDescription>
<div className="w-full">
<CardContent className="p-0">
{error && (
<AlertBlock type="error" className="my-2">
{error}
</AlertBlock>
)}
<Form {...form}>
<form
method="post"
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={isLoading}
className="w-full"
>
Confirm
</Button>
</div>
<div className="text-center text-sm flex gap-2 text-muted-foreground">
<Link href="/">Sign in</Link>
</div>
</form>
</Form>
</CardContent>
</div>
</div>
</div>
);
}
const ResetPasswordPage = () => {
return (
<OnboardingLayout>
<Home />
</OnboardingLayout>
);
};
export const Route = createFileRoute("/reset-password")({
component: ResetPasswordPage,
});

View File

@ -0,0 +1,174 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { createFileRoute } from "@tanstack/react-router";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
const loginSchema = z.object({
email: z
.string()
.min(1, {
message: "Email is required",
})
.max(255, {
message: "Email must be at most 255 characters",
})
.email({
message: "Email must be a valid email",
}),
});
type Login = z.infer<typeof loginSchema>;
type AuthResponse = {
is2FAEnabled: boolean;
authId: string;
};
function Home() {
const { config: whitelabeling } = useWhitelabelingPublic();
const [temp, _setTemp] = useState<AuthResponse>({
is2FAEnabled: false,
authId: "",
});
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const _router = useRouter();
const form = useForm<Login>({
defaultValues: {
email: "",
},
resolver: zodResolver(loginSchema),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => {
setIsLoading(true);
const { error } = await authClient.requestPasswordReset({
email: values.email,
redirectTo: "/reset-password",
});
if (error) {
setError(error.message || "An error occurred");
setIsLoading(false);
} else {
toast.success("Email sent", {
duration: 2000,
});
}
setIsLoading(false);
};
return (
<div className="flex w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo
logoUrl={
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined
}
/>
<span className="font-medium text-sm">
{whitelabeling?.appName || "Dokploy"}
</span>
</Link>
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
<CardDescription>
Enter your email to reset your password
</CardDescription>
<div className="mx-auto w-full max-w-lg bg-transparent ">
<CardContent className="p-0">
{error && (
<AlertBlock type="error" className="my-2">
{error}
</AlertBlock>
)}
{!temp.is2FAEnabled ? (
<Form {...form}>
<form
method="post"
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="Email"
maxLength={255}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={isLoading}
className="w-full"
>
Send Reset Link
</Button>
</div>
</form>
</Form>
) : null}
<div className="flex flex-row justify-between flex-wrap">
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
<Link
className="hover:underline text-muted-foreground"
href="/"
>
Login
</Link>
</div>
</div>
</CardContent>
</div>
</div>
</div>
);
}
const SendResetPasswordPage = () => {
return (
<OnboardingLayout>
<Home />
</OnboardingLayout>
);
};
export const Route = createFileRoute("/send-reset-password")({
component: SendResetPasswordPage,
});

View File

@ -0,0 +1,83 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import dynamic from "next/dynamic";
import { api } from "@/utils/api";
import "swagger-ui-react/swagger-ui.css";
import { useEffect, useState } from "react";
import { getCachedSession } from "~/utils/session";
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
const Home = () => {
const { data } = api.settings.getOpenApiDocument.useQuery();
const [spec, setSpec] = useState({});
useEffect(() => {
if (data) {
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
// Force OpenAPI 3.0 so Swagger UI uses the 3.0 parser (avoids ApiDOM 3.1 refract bug)
const newSpec = {
...data,
openapi: "3.0.3",
servers: [{ url: protocolAndHost }],
externalDocs: {
url: `${protocolAndHost}/trpc/settings.getOpenApiDocument`,
},
};
// Remove 3.1-only fields that could confuse the 3.0 parser
if ("jsonSchemaDialect" in newSpec) {
delete (newSpec as Record<string, unknown>).jsonSchemaDialect;
}
setSpec(newSpec);
}
}, [data]);
return (
<div className="h-screen bg-white">
<SwaggerUI
spec={spec}
persistAuthorization={true}
plugins={[
{
statePlugins: {
auth: {
wrapActions: {
authorize: (ori: any) => (args: any) => {
const result = ori(args);
const apiKey = args?.apiKey?.value;
if (apiKey) {
localStorage.setItem("swagger_api_key", apiKey);
}
return result;
},
logout: (ori: any) => (args: any) => {
const result = ori(args);
localStorage.removeItem("swagger_api_key");
return result;
},
},
},
},
},
]}
requestInterceptor={(request: any) => {
const apiKey = localStorage.getItem("swagger_api_key");
if (apiKey) {
request.headers = request.headers || {};
request.headers["x-api-key"] = apiKey;
}
return request;
}}
/>
</div>
);
};
export const Route = createFileRoute("/swagger")({
component: Home,
beforeLoad: async () => {
const session = await getCachedSession();
if (!session?.session) {
throw redirect({ to: "/" });
}
},
});

View File

@ -0,0 +1,35 @@
import * as React from "react";
type ComponentModule<P> =
| { default: React.ComponentType<P> }
| React.ComponentType<P>;
interface DynamicOptions {
ssr?: boolean;
loading?: React.ComponentType;
}
const dynamic = <P extends object>(
loader: () => Promise<ComponentModule<P>>,
options?: DynamicOptions,
) => {
const Lazy = React.lazy(async () => {
const mod = await loader();
if (mod && typeof mod === "object" && "default" in mod) {
return mod as { default: React.ComponentType<P> };
}
return { default: mod as React.ComponentType<P> };
}) as unknown as React.ComponentType<P>;
const Loading = options?.loading;
const DynamicComponent = (props: P) => (
<React.Suspense fallback={Loading ? <Loading /> : null}>
<Lazy {...props} />
</React.Suspense>
);
return DynamicComponent;
};
export default dynamic;

View File

@ -0,0 +1,24 @@
import * as React from "react";
import { useEffect } from "react";
const Head = ({ children }: { children?: React.ReactNode }) => {
useEffect(() => {
React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) return;
if (child.type === "title") {
const titleChildren = (child.props as { children?: React.ReactNode })
.children;
const text = Array.isArray(titleChildren)
? titleChildren.join("")
: titleChildren;
if (typeof text === "string") {
document.title = text;
}
}
});
}, [children]);
return null;
};
export default Head;

View File

@ -0,0 +1,65 @@
import { Link as RouterLink, useLocation } from "@tanstack/react-router";
import * as React from "react";
import type { NextUrl } from "./next-router";
type NextLinkProps = Omit<React.ComponentPropsWithoutRef<"a">, "href"> & {
href: NextUrl;
replace?: boolean;
prefetch?: boolean;
shallow?: boolean;
scroll?: boolean;
passHref?: boolean;
legacyBehavior?: boolean;
locale?: string | false;
};
const isExternal = (href: string) => /^(https?:|mailto:|tel:|\/\/)/.test(href);
const Link = React.forwardRef<HTMLAnchorElement, NextLinkProps>(
(
{
href,
replace,
prefetch: _prefetch,
shallow: _shallow,
scroll: _scroll,
passHref: _passHref,
legacyBehavior: _legacyBehavior,
locale: _locale,
...props
},
ref,
) => {
const location = useLocation();
let hrefString: string;
if (typeof href === "string") {
hrefString = href;
} else {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(href.query ?? {})) {
if (value === undefined || value === null) continue;
params.set(key, String(value));
}
const search = params.toString();
hrefString = `${href.pathname ?? location.pathname}${search ? `?${search}` : ""}${href.hash ? `#${href.hash.replace(/^#/, "")}` : ""}`;
}
if (isExternal(hrefString)) {
return <a ref={ref} href={hrefString} {...props} />;
}
return (
<RouterLink
ref={ref}
to={hrefString}
replace={replace}
{...(props as Record<string, unknown>)}
/>
);
},
);
Link.displayName = "NextLinkShim";
export default Link;

View File

@ -0,0 +1,29 @@
import {
useLocation,
useRouter as useTanstackRouter,
} from "@tanstack/react-router";
export const usePathname = () => {
return useLocation().pathname;
};
export const useSearchParams = () => {
const location = useLocation();
return new URLSearchParams(location.searchStr ?? "");
};
export const useParams = () => {
return useLocation().pathname;
};
export const useRouter = () => {
const router = useTanstackRouter();
return {
push: (href: string) => router.history.push(href),
replace: (href: string) => router.history.replace(href),
back: () => router.history.back(),
forward: () => router.history.forward(),
refresh: () => router.invalidate(),
prefetch: (_href: string) => {},
};
};

View File

@ -0,0 +1,110 @@
import {
type AnyRouter,
useLocation,
useParams,
useSearch,
useRouter as useTanstackRouter,
} from "@tanstack/react-router";
import { useMemo } from "react";
export type NextUrl =
| string
| {
pathname?: string | null;
query?: Record<string, unknown> | null;
hash?: string | null;
};
let routerInstance: AnyRouter | null = null;
export const setRouterInstance = (router: AnyRouter) => {
routerInstance = router;
};
const buildHref = (url: NextUrl, currentPathname: string): string => {
if (typeof url === "string") {
if (url.startsWith("?")) return `${currentPathname}${url}`;
return url;
}
const pathname = url.pathname ?? currentPathname;
const params = new URLSearchParams();
for (const [key, value] of Object.entries(url.query ?? {})) {
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
for (const v of value) params.append(key, String(v));
} else {
params.set(key, String(value));
}
}
const search = params.toString();
const hash = url.hash ? `#${url.hash.replace(/^#/, "")}` : "";
return `${pathname}${search ? `?${search}` : ""}${hash}`;
};
const routerEvents = {
on: (_event: string, _handler: (...args: unknown[]) => void) => {},
off: (_event: string, _handler: (...args: unknown[]) => void) => {},
emit: (_event: string, ..._args: unknown[]) => {},
};
export const useRouter = () => {
const tanstackRouter = useTanstackRouter();
const location = useLocation();
const params = useParams({ strict: false }) as Record<string, string>;
const search = useSearch({ strict: false }) as Record<string, unknown>;
return useMemo(() => {
const pathname = location.pathname;
return {
pathname,
route: pathname,
asPath: `${pathname}${location.searchStr ?? ""}${location.hash ? `#${location.hash}` : ""}`,
query: { ...search, ...params } as Record<string, string | string[]>,
isReady: true,
events: routerEvents,
push: (url: NextUrl, _as?: unknown, _options?: unknown) => {
tanstackRouter.history.push(buildHref(url, pathname));
return Promise.resolve(true);
},
replace: (url: NextUrl, _as?: unknown, _options?: unknown) => {
tanstackRouter.history.replace(buildHref(url, pathname));
return Promise.resolve(true);
},
back: () => tanstackRouter.history.back(),
forward: () => tanstackRouter.history.forward(),
reload: () => window.location.reload(),
prefetch: (_url: string) => Promise.resolve(),
};
}, [
location.pathname,
location.searchStr,
location.hash,
params,
search,
tanstackRouter,
]);
};
const singletonRouter = {
push: (url: NextUrl) => {
if (routerInstance) {
routerInstance.history.push(buildHref(url, window.location.pathname));
} else {
window.location.href = buildHref(url, window.location.pathname);
}
return Promise.resolve(true);
},
replace: (url: NextUrl) => {
if (routerInstance) {
routerInstance.history.replace(buildHref(url, window.location.pathname));
} else {
window.location.replace(buildHref(url, window.location.pathname));
}
return Promise.resolve(true);
},
back: () => window.history.back(),
reload: () => window.location.reload(),
events: routerEvents,
};
export default singletonRouter;

View File

@ -0,0 +1,40 @@
import { useEffect } from "react";
interface ScriptProps {
src?: string;
id?: string;
strategy?: string;
onLoad?: () => void;
onError?: () => void;
children?: string;
dangerouslySetInnerHTML?: { __html: string };
[key: string]: unknown;
}
const Script = ({
src,
id,
strategy: _strategy,
onLoad,
onError,
children,
dangerouslySetInnerHTML,
}: ScriptProps) => {
useEffect(() => {
if (src && document.querySelector(`script[src="${src}"]`)) return;
if (id && document.getElementById(id)) return;
const script = document.createElement("script");
if (src) script.src = src;
if (id) script.id = id;
const inline = dangerouslySetInnerHTML?.__html ?? children;
if (!src && inline) script.textContent = inline;
if (onLoad) script.onload = onLoad;
if (onError) script.onerror = onError;
document.body.appendChild(script);
}, [src, id]);
return null;
};
export default Script;

View File

@ -0,0 +1,37 @@
import { useRouter } from "@tanstack/react-router";
import { useEffect, useState } from "react";
interface TopLoaderProps {
color?: string;
height?: number;
}
const TopLoader = ({ color, height = 3 }: TopLoaderProps) => {
const router = useRouter();
const [loading, setLoading] = useState(false);
useEffect(() => {
const unsubStart = router.subscribe("onBeforeNavigate", () =>
setLoading(true),
);
const unsubEnd = router.subscribe("onResolved", () => setLoading(false));
return () => {
unsubStart();
unsubEnd();
};
}, [router]);
if (!loading) return null;
return (
<div
className="fixed inset-x-0 top-0 z-[9999] animate-pulse"
style={{
height,
background: color ?? "hsl(var(--sidebar-ring))",
}}
/>
);
};
export default TopLoader;

View File

@ -0,0 +1,539 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@plugin "tailwindcss-animate";
@plugin "@tailwindcss/typography";
@plugin "fancy-ansi/plugin";
@source "../../../dokploy/components";
@source "../../../dokploy/lib";
@source "../../../dokploy/utils";
@source "../../../dokploy/hooks";
@source "../";
@custom-variant dark (&:is(.dark *));
:root {
--font-inter: "Inter";
}
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= --theme(--breakpoint-3xl)) {
max-width: none;
}
@media (width >= 87.5rem) {
max-width: 87.5rem;
}
}
@theme {
--font-sans:
var(--font-inter), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--breakpoint-3xl: 1920px;
--container-2xl: 40rem;
--container-8xl: 85rem;
--container-9xl: 95rem;
--container-10xl: 105rem;
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
html {
scrollbar-gutter: stable;
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@utility no-scrollbar {
/* Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@utility custom-logs-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground)) transparent;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: hsl(var(--muted-foreground) / 0.3);
border-radius: 20px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.5);
}
}
@layer base {
:root {
--terminal-paste: rgba(0, 0, 0, 0.2);
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
--overlay: rgba(0, 0, 0, 0.2);
--chart-1: 173 58% 39%;
--chart-2: 12 76% 61%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--terminal-paste: rgba(255, 255, 255, 0.2);
--background: 0 0% 0%;
--foreground: 0 0% 98%;
--card: 240 4% 10%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 4% 10%;
--ring: 240 4.9% 83.9%;
--overlay: rgba(0, 0, 0, 0.5);
--chart-1: 220 70% 50%;
--chart-5: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-2: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
/* Cursor pointer on buttons */
button:not([disabled]),
[role="button"]:not([disabled]) {
cursor: pointer;
}
/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 0.3125rem;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 0.3125rem;
}
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
}
@layer utilities {
.xterm-viewport {
border-radius: 0.75rem /* 12px */ !important;
}
.xterm .xterm-viewport {
overflow-y: auto !important;
}
.xterm .xterm-screen {
overflow: hidden;
}
}
@layer utilities {
/* Codemirror */
.cm-editor {
@apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-hidden;
}
.cm-editor .cm-scroller {
font-family: inherit;
line-height: inherit;
}
.cm-editor.cm-focused {
@apply outline-hidden;
}
/* fix: placeholder bg */
.cm-editor .cm-activeLine:has(.cm-placeholder) {
background-color: transparent;
}
.compose-file-editor .cm-editor {
@apply min-h-100;
}
@keyframes heartbeat {
0% {
transform: scale(1);
opacity: 0.7;
}
25% {
transform: scale(1.1);
opacity: 1;
}
50% {
transform: scale(1);
opacity: 0.7;
}
75% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0.7;
}
}
.animate-heartbeat {
animation: heartbeat 2.5s infinite;
}
@media (prefers-color-scheme: dark) {
.swagger-ui {
background-color: white;
}
.swagger-ui .info {
margin: 0px !important;
padding-top: 1rem !important;
}
}
.custom-logs-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground)) transparent;
}
.custom-logs-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-logs-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-logs-scrollbar::-webkit-scrollbar-thumb {
background-color: hsl(var(--muted-foreground) / 0.3);
border-radius: 20px;
}
.custom-logs-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.5);
}
/* Docker Logs Scrollbar */
}
.xterm-bg-257.xterm-fg-257 {
background-color: var(--terminal-paste) !important;
color: currentColor !important;
}
.cm-content,
.cm-lineWrapping {
@apply font-mono;
}
/* HubSpot Widget - Force light color-scheme to prevent white background */
#hubspot-messages-iframe-container,
#hubspot-messages-iframe-container * {
background-color: transparent !important;
color-scheme: light !important;
}
#hubspot-messages-iframe-container .hs-shadow-container {
display: none !important;
}
#hubspot-conversations-iframe {
color-scheme: light !important;
}
/*
---break---
*/
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
/*
---break---
*/
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
/*
---break---
*/
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

View File

@ -0,0 +1,8 @@
import { createTRPCReact } from "@trpc/react-query";
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import type { AppRouter } from "@/server/api/root";
export const api = createTRPCReact<AppRouter>();
export type RouterInputs = inferRouterInputs<AppRouter>;
export type RouterOutputs = inferRouterOutputs<AppRouter>;

View File

@ -0,0 +1,20 @@
import { authClient } from "@/lib/auth-client";
type SessionData = Awaited<ReturnType<typeof authClient.getSession>>["data"];
let cached: { at: number; session: SessionData } | null = null;
const TTL_MS = 30_000;
export const getCachedSession = async (): Promise<SessionData> => {
if (cached && Date.now() - cached.at < TTL_MS) {
return cached.session;
}
const { data } = await authClient.getSession();
cached = { at: Date.now(), session: data };
return data;
};
export const clearSessionCache = () => {
cached = null;
};

View File

@ -0,0 +1,63 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
createWSClient,
httpBatchLink,
httpLink,
splitLink,
wsLink,
} from "@trpc/client";
import { useState } from "react";
import superjson from "superjson";
import { api } from "./api";
const getWsUrl = () => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${window.location.host}/drawer-logs`;
};
let wsClientSingleton: ReturnType<typeof createWSClient> | null = null;
const getOrCreateWSClient = () => {
if (!wsClientSingleton) {
wsClientSingleton = createWSClient({
url: getWsUrl(),
lazy: { enabled: true, closeMs: 3000 },
retryDelayMs: () => 3000,
});
}
return wsClientSingleton;
};
const createLinks = () => [
splitLink({
condition: (op) => op.type === "subscription",
true: wsLink({
client: getOrCreateWSClient(),
transformer: superjson,
}),
false: splitLink({
condition: (op) => op.input instanceof FormData,
true: httpLink({
url: "/api/trpc",
transformer: superjson,
}),
false: httpBatchLink({
url: "/api/trpc",
transformer: superjson,
}),
}),
}),
];
export const TRPCProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
api.createClient({ links: createLinks() }),
);
return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</api.Provider>
);
};

36
apps/vite/tsconfig.json Normal file
View File

@ -0,0 +1,36 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"lib": ["dom", "dom.iterable", "ES2022"],
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"types": ["vite/client", "node"],
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"],
"@/utils/api": ["./src/utils/api.ts"],
"next/link": ["./src/shims/next-link.tsx"],
"next/router": ["./src/shims/next-router.ts"],
"next/navigation": ["./src/shims/next-navigation.ts"],
"next/head": ["./src/shims/next-head.tsx"],
"next/dynamic": ["./src/shims/next-dynamic.tsx"],
"next/script": ["./src/shims/next-script.tsx"],
"nextjs-toploader": ["./src/shims/toploader.tsx"],
"@/*": ["../dokploy/*"],
"@dokploy/server": ["../../packages/server/src/index.ts"],
"@dokploy/server/*": ["../../packages/server/src/*"]
}
},
"include": ["src", "server", "vite.config.ts"],
"exclude": ["node_modules", "dist"]
}

81
apps/vite/vite.config.ts Normal file
View File

@ -0,0 +1,81 @@
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
const dokployApp = path.resolve(__dirname, "../dokploy");
const serverPkg = path.resolve(__dirname, "../../packages/server/src");
const wsPaths = [
"/drawer-logs",
"/docker-container-logs",
"/docker-container-terminal",
"/listen-deployment",
"/listen-docker-stats-monitoring",
"/terminal",
];
export default defineConfig({
plugins: [
tanstackRouter({ target: "react", autoCodeSplitting: true }),
react(),
tailwindcss(),
],
publicDir: path.resolve(dokployApp, "public"),
resolve: {
alias: [
{
find: /^@\/utils\/api$/,
replacement: path.resolve(__dirname, "src/utils/api.ts"),
},
{
find: /^next\/link$/,
replacement: path.resolve(__dirname, "src/shims/next-link.tsx"),
},
{
find: /^next\/router$/,
replacement: path.resolve(__dirname, "src/shims/next-router.ts"),
},
{
find: /^next\/navigation$/,
replacement: path.resolve(__dirname, "src/shims/next-navigation.ts"),
},
{
find: /^next\/head$/,
replacement: path.resolve(__dirname, "src/shims/next-head.tsx"),
},
{
find: /^next\/dynamic$/,
replacement: path.resolve(__dirname, "src/shims/next-dynamic.tsx"),
},
{
find: /^next\/script$/,
replacement: path.resolve(__dirname, "src/shims/next-script.tsx"),
},
{
find: /^nextjs-toploader$/,
replacement: path.resolve(__dirname, "src/shims/toploader.tsx"),
},
{ find: /^~\//, replacement: `${path.resolve(__dirname, "src")}/` },
{ find: /^@\//, replacement: `${dokployApp}/` },
{ find: /^@dokploy\/server\//, replacement: `${serverPkg}/` },
{
find: /^@dokploy\/server$/,
replacement: path.resolve(serverPkg, "index.ts"),
},
],
},
server: {
port: 5173,
proxy: {
"/api": {
target: "http://localhost:3000",
headers: { origin: "http://localhost:3000" },
},
...Object.fromEntries(
wsPaths.map((p) => [p, { target: "ws://localhost:3000", ws: true }]),
),
},
},
});

View File

@ -13,6 +13,7 @@
"!**/.next/**",
"!**/dist",
"!**/drizzle/**",
"!**/routeTree.gen.ts",
"!node_modules/**",
"!packages/server/package.json"
],

View File

@ -48,6 +48,7 @@ const resolveTrustedOrigins = async () => {
? [
"http://localhost:3000",
"https://absolutely-handy-falcon.ngrok-free.app",
"http://localhost:5173"
]
: [];
return [

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ packages:
- "apps/api"
- "apps/dokploy"
- "apps/schedules"
- "apps/vite"
- "packages/server"