mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-20 07:31:37 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7adc3c36b | |||
| f838c07348 | |||
| 2b225a1e4f |
+5
-1
@@ -2,4 +2,8 @@ node_modules
|
|||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
*.md
|
*.md
|
||||||
dist
|
dist
|
||||||
|
**/.next
|
||||||
|
**/dist
|
||||||
|
**/dist-server
|
||||||
|
**/.env
|
||||||
+5
-1
@@ -46,4 +46,8 @@ yarn-error.log*
|
|||||||
.db
|
.db
|
||||||
|
|
||||||
.playwright-*
|
.playwright-*
|
||||||
.credentials
|
.credentials
|
||||||
|
dist/
|
||||||
|
dist-server/
|
||||||
|
.tanstack
|
||||||
|
.env.*
|
||||||
@@ -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"]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# dokploy-vite
|
||||||
|
|
||||||
|
TanStack Start migration of the Dokploy dashboard. The Next.js app (`apps/dokploy`) stays intact; this app reuses all its UI code and its backend logic without duplicating either.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Two tiers, one process in production:
|
||||||
|
|
||||||
|
- **UI tier (TanStack Start)**: file-based routes in `src/routes/` with **selective SSR** — public pages (login, register, invitation, reset-password) are fully server-rendered (real first paint with content and whitelabel branding), while everything under `/dashboard` is `ssr: false` (pure client, no SSR cost). The document shell (`src/routes/__root.tsx`) SSRs per request with whitelabel title/meta/favicon/customCss loaded from the DB via a server function.
|
||||||
|
- **API tier (`server/server.ts`)**: Next-free node server owning tRPC, better-auth, OpenAPI REST, deploy/Stripe/git webhooks (reused from `apps/dokploy/pages/api` through `server/next-compat.ts`), websockets, cron jobs and the deployment worker. Start's server functions never import backend code — they call this tier over loopback HTTP (`/api/branding`, `/api/auth/get-session`), which keeps the UI dev process free of native/server deps.
|
||||||
|
|
||||||
|
Code reuse (unchanged from the SPA phase): `@/*` aliases into `apps/dokploy/*` (383 components consumed in place), `next/*` shims over TanStack Router in `src/shims/`, `@/utils/api` re-aliased to a `createTRPCReact` client with identical hooks/links.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
One command (runs both tiers via concurrently):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter dokploy-vite dev
|
||||||
|
# [api] API + websockets → :3000
|
||||||
|
# [ui] Start dev (UI + SSR + HMR) → :5173 ← open this one
|
||||||
|
```
|
||||||
|
|
||||||
|
Or individually: `dev:api` / `dev:ui`. The Vite dev server proxies `/api` and all websocket paths to `:3000` (rewriting the `origin` header so better-auth accepts it). Note: in dev the two tiers are separate processes because Start's dev server owns the Vite process (it's a full SSR runtime, not an embeddable middleware); in production it's a single process (`server.ts` bridges Start's built handler).
|
||||||
|
|
||||||
|
Env loading: the API tier loads `apps/vite/.env` and falls back to `apps/dokploy/.env`. The UI tier intentionally reads no env files — its server functions only talk to the API over loopback, and leaking `NODE_ENV` from a dotfile into `vite build` produces broken dev-mode SSR bundles (`jsxDEV is not a function`).
|
||||||
|
|
||||||
|
## Production — single process, no Next
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter dokploy-vite build # Start build → dist/client + dist/server
|
||||||
|
pnpm --filter dokploy-vite build:server # esbuild → dist-server/*.mjs (server + migration + scripts)
|
||||||
|
pnpm --filter dokploy-vite start # node server on :3000
|
||||||
|
```
|
||||||
|
|
||||||
|
`server/server.ts` serves `/api/*` + websockets directly, static assets from `dist/client`, and bridges every other request to Start's built `fetch` handler (`server/start-bridge.ts`) for SSR. Docker: `Dockerfile.vite` at the repo root (image ~1GB vs 4.33GB for the official Next-based image; ~2.6x less memory).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `routeTree.gen.ts` is generated by the Start plugin (ignored by biome).
|
||||||
|
- The theme anti-flash script is SSR'd automatically by next-themes — no manual hack.
|
||||||
|
- Route-level data preload can be added per route with `loader` + `queryClient.ensureQueryData` (framework-native now).
|
||||||
|
- Endgame cleanup (when `apps/dokploy` is retired): move components in, codemod the 35 `useRouter` call sites to native TanStack APIs, delete `src/shims/` and `server/next-compat.ts` (ideally porting API handlers to Hono routes in the same pass).
|
||||||
@@ -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));
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"name": "dokploy-vite",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently -n api,ui -c blue,green \"pnpm dev:api\" \"pnpm dev:ui\"",
|
||||||
|
"dev:api": "tsx -r dotenv/config server/server.ts",
|
||||||
|
"dev:ui": "vite dev",
|
||||||
|
"build": "NODE_ENV=production vite build",
|
||||||
|
"build:server": "tsx esbuild.config.ts",
|
||||||
|
"start": "NODE_ENV=production tsx -r dotenv/config server/server.ts",
|
||||||
|
"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",
|
||||||
|
"@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",
|
||||||
|
"@dokploy/server": "workspace:*",
|
||||||
|
"@dokploy/trpc-openapi": "0.0.18",
|
||||||
|
"@faker-js/faker": "^8.4.1",
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@octokit/auth-app": "^6.1.3",
|
||||||
|
"@octokit/webhooks": "^13.9.0",
|
||||||
|
"@react-email/components": "^1.0.12",
|
||||||
|
"@stepperize/react": "4.0.1",
|
||||||
|
"@stripe/stripe-js": "4.8.0",
|
||||||
|
"@tanstack/react-query": "^5.90.21",
|
||||||
|
"@tanstack/react-router": "^1.170.25",
|
||||||
|
"@tanstack/react-start": "^1.168.42",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@trpc/client": "^11.10.0",
|
||||||
|
"@trpc/react-query": "^11.10.0",
|
||||||
|
"@trpc/server": "^11.10.0",
|
||||||
|
"@uiw/codemirror-theme-github": "^4.23.12",
|
||||||
|
"@uiw/react-codemirror": "^4.23.12",
|
||||||
|
"@xterm/addon-attach": "0.10.0",
|
||||||
|
"@xterm/addon-clipboard": "0.1.0",
|
||||||
|
"@xterm/xterm": "^5.5.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",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^0.2.1",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"date-fns": "3.6.0",
|
||||||
|
"dockerode": "4.0.2",
|
||||||
|
"dompurify": "^3.3.3",
|
||||||
|
"dotenv": "16.4.5",
|
||||||
|
"drizzle-orm": "0.45.2",
|
||||||
|
"drizzle-zod": "0.8.3",
|
||||||
|
"fancy-ansi": "^0.1.3",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"lucide-react": "^0.469.0",
|
||||||
|
"micromatch": "4.0.8",
|
||||||
|
"nanoid": "3.3.11",
|
||||||
|
"next-themes": "^0.2.1",
|
||||||
|
"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",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"radix-ui": "^1.6.0",
|
||||||
|
"react": "19.2.7",
|
||||||
|
"react-confetti-explosion": "3.0.3",
|
||||||
|
"react-day-picker": "10.0.1",
|
||||||
|
"react-dom": "19.2.7",
|
||||||
|
"react-hook-form": "^7.71.2",
|
||||||
|
"react-markdown": "^9.1.0",
|
||||||
|
"recharts": "^3.8.0",
|
||||||
|
"resend": "^6.0.2",
|
||||||
|
"semver": "7.7.3",
|
||||||
|
"shell-quote": "^1.8.1",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
|
"sonner": "^1.7.4",
|
||||||
|
"ssh2": "~1.16.0",
|
||||||
|
"stripe": "17.2.0",
|
||||||
|
"superjson": "^2.2.2",
|
||||||
|
"swagger-ui-react": "^5.32.6",
|
||||||
|
"tailwind-merge": "^2.6.1",
|
||||||
|
"toml": "3.0.0",
|
||||||
|
"undici": "^6.21.3",
|
||||||
|
"use-resize-observer": "9.1.0",
|
||||||
|
"ws": "8.16.0",
|
||||||
|
"xterm-addon-fit": "^0.8.0",
|
||||||
|
"yaml": "2.8.1",
|
||||||
|
"zod": "^4.3.6",
|
||||||
|
"zod-form-data": "^3.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "0.5.16",
|
||||||
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
"@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",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"concurrently": "^10.0.4",
|
||||||
|
"esbuild": "0.20.2",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"shadcn": "^4.11.0",
|
||||||
|
"tailwindcss": "^4.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tsx": "^4.22.4",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite": "^7.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
};
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import http from "node:http";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import {
|
||||||
|
auth,
|
||||||
|
createDefaultMiddlewares,
|
||||||
|
createDefaultServerTraefikConfig,
|
||||||
|
createDefaultTraefikConfig,
|
||||||
|
getPublicWhitelabelingConfig,
|
||||||
|
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 { createStartBridge } from "./start-bridge";
|
||||||
|
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 === "/api/branding") {
|
||||||
|
let config = null;
|
||||||
|
try {
|
||||||
|
config = await getPublicWhitelabelingConfig();
|
||||||
|
} catch {
|
||||||
|
config = null;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
||||||
|
res.end(JSON.stringify(config));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 | Promise<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;
|
||||||
|
}
|
||||||
|
await 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) {
|
||||||
|
const clientDir = path.resolve(import.meta.dirname, "../dist/client");
|
||||||
|
const serverEntry = path.resolve(
|
||||||
|
import.meta.dirname,
|
||||||
|
"../dist/server/server.js",
|
||||||
|
);
|
||||||
|
const staticHandler = createStaticHandler(clientDir);
|
||||||
|
const entry = await import(pathToFileURL(serverEntry).href);
|
||||||
|
const bridge = createStartBridge(entry.default);
|
||||||
|
handleUi = async (req, res, url) => {
|
||||||
|
const pathname = decodeURIComponent(url.pathname);
|
||||||
|
const filePath = path.join(clientDir, pathname);
|
||||||
|
if (
|
||||||
|
pathname !== "/" &&
|
||||||
|
filePath.startsWith(clientDir) &&
|
||||||
|
fs.existsSync(filePath) &&
|
||||||
|
fs.statSync(filePath).isFile()
|
||||||
|
) {
|
||||||
|
staticHandler(req, res, url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await bridge(req, res);
|
||||||
|
};
|
||||||
|
console.log("TanStack Start SSR bridge enabled");
|
||||||
|
} else {
|
||||||
|
handleUi = (_req, res) => {
|
||||||
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
||||||
|
res.end(
|
||||||
|
"API-only mode: the UI dev server runs separately (pnpm --filter dokploy-vite dev → http://localhost:5173).",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
|
||||||
|
interface FetchHandler {
|
||||||
|
fetch: (request: Request) => Response | Promise<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createStartBridge = (handler: FetchHandler) => {
|
||||||
|
return async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
||||||
|
const headers = new Headers();
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (value === undefined) continue;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) headers.append(key, item);
|
||||||
|
} else {
|
||||||
|
headers.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const method = req.method ?? "GET";
|
||||||
|
const hasBody = method !== "GET" && method !== "HEAD";
|
||||||
|
const request = new Request(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
...(hasBody
|
||||||
|
? {
|
||||||
|
body: Readable.toWeb(req) as unknown as ReadableStream,
|
||||||
|
duplex: "half",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} as RequestInit);
|
||||||
|
|
||||||
|
const response = await handler.fetch(request);
|
||||||
|
|
||||||
|
const outHeaders: Record<string, string | string[]> = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
if (key === "set-cookie") return;
|
||||||
|
outHeaders[key] = value;
|
||||||
|
});
|
||||||
|
const setCookies = response.headers.getSetCookie();
|
||||||
|
if (setCookies.length > 0) {
|
||||||
|
outHeaders["set-cookie"] = setCookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(response.status, outHeaders);
|
||||||
|
if (response.body) {
|
||||||
|
Readable.fromWeb(response.body as never).pipe(res);
|
||||||
|
} else {
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createStaticHandler = (distDir: string) => {
|
||||||
|
return (req: IncomingMessage, res: ServerResponse, url: URL) => {
|
||||||
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||||
|
res.writeHead(404).end();
|
||||||
|
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()) {
|
||||||
|
res.writeHead(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheControl = pathname.startsWith("/assets/")
|
||||||
|
? "public, max-age=31536000, immutable"
|
||||||
|
: "public, max-age=0, must-revalidate";
|
||||||
|
res.writeHead(200, {
|
||||||
|
"content-type":
|
||||||
|
MIME_TYPES[path.extname(filePath).toLowerCase()] ??
|
||||||
|
"application/octet-stream",
|
||||||
|
"cache-control": cacheControl,
|
||||||
|
});
|
||||||
|
fs.createReadStream(filePath).pipe(res);
|
||||||
|
};
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
import { createRouter } from "@tanstack/react-router";
|
||||||
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
import { setRouterInstance } from "./shims/next-router";
|
||||||
|
import { TRPCProvider } from "./utils/trpc-provider";
|
||||||
|
|
||||||
|
export function getRouter() {
|
||||||
|
const router = createRouter({
|
||||||
|
routeTree,
|
||||||
|
defaultPreload: "intent",
|
||||||
|
Wrap: ({ children }) => <TRPCProvider>{children}</TRPCProvider>,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
setRouterInstance(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: ReturnType<typeof getRouter>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
createRootRoute,
|
||||||
|
HeadContent,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
} 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 { getBrandingFn } from "~/server-fns/branding";
|
||||||
|
import TopLoader from "~/shims/toploader";
|
||||||
|
import appCss from "../styles/globals.css?url";
|
||||||
|
|
||||||
|
const RootComponent = () => {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<TopLoader />
|
||||||
|
<WhitelabelingProvider />
|
||||||
|
<Toaster richColors />
|
||||||
|
<SearchCommand />
|
||||||
|
<Outlet />
|
||||||
|
</ThemeProvider>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootDocument = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<HeadContent />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
loader: () => getBrandingFn(),
|
||||||
|
head: ({ loaderData }) => {
|
||||||
|
const branding = loaderData ?? null;
|
||||||
|
const title = branding?.metaTitle || branding?.appName || "Dokploy";
|
||||||
|
return {
|
||||||
|
meta: [
|
||||||
|
{ charSet: "utf-8" },
|
||||||
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||||
|
{ title },
|
||||||
|
{ property: "og:title", content: title },
|
||||||
|
...(branding?.appDescription
|
||||||
|
? [
|
||||||
|
{ name: "description", content: branding.appDescription },
|
||||||
|
{ property: "og:description", content: branding.appDescription },
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(branding?.logoUrl
|
||||||
|
? [{ property: "og:image", content: branding.logoUrl }]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{ rel: "stylesheet", href: appCss },
|
||||||
|
{ rel: "icon", href: branding?.faviconUrl || "/icon.svg" },
|
||||||
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
|
{
|
||||||
|
rel: "preconnect",
|
||||||
|
href: "https://fonts.gstatic.com",
|
||||||
|
crossOrigin: "anonymous",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
styles: branding?.customCss
|
||||||
|
? [{ children: branding.customCss }]
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
shellComponent: RootDocument,
|
||||||
|
component: RootComponent,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
+1823
File diff suppressed because it is too large
Load Diff
+435
@@ -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 });
|
||||||
+444
@@ -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 });
|
||||||
+297
@@ -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 });
|
||||||
+326
@@ -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 });
|
||||||
+330
@@ -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 });
|
||||||
+308
@@ -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 });
|
||||||
+315
@@ -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 });
|
||||||
+319
@@ -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 });
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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")({
|
||||||
|
ssr: false,
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const session = await getCachedSession();
|
||||||
|
if (!session?.session) {
|
||||||
|
throw redirect({ to: "/" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: DashboardRouteComponent,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -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: "/" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
|
||||||
|
export interface PublicBranding {
|
||||||
|
appName: string | null;
|
||||||
|
appDescription: string | null;
|
||||||
|
logoUrl: string | null;
|
||||||
|
loginLogoUrl: string | null;
|
||||||
|
faviconUrl: string | null;
|
||||||
|
customCss: string | null;
|
||||||
|
metaTitle: string | null;
|
||||||
|
errorPageTitle: string | null;
|
||||||
|
errorPageDescription: string | null;
|
||||||
|
footerText: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cached: { at: number; value: PublicBranding | null } | null = null;
|
||||||
|
|
||||||
|
export const getBrandingFn = createServerFn({ method: "GET" }).handler(
|
||||||
|
async (): Promise<PublicBranding | null> => {
|
||||||
|
if (cached && Date.now() - cached.at < 60_000) {
|
||||||
|
return cached.value;
|
||||||
|
}
|
||||||
|
let value: PublicBranding | null = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`http://localhost:${process.env.PORT ?? 3000}/api/branding`,
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
value = (await response.json()) as PublicBranding | null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
cached = { at: Date.now(), value };
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
|
||||||
|
interface SessionPayload {
|
||||||
|
session: { id: string; userId: string } | null;
|
||||||
|
user: { id: string; email: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSession = async (): Promise<SessionPayload | null> => {
|
||||||
|
const { getRequest } = await import("@tanstack/react-start/server");
|
||||||
|
try {
|
||||||
|
const request = getRequest();
|
||||||
|
const cookie = request.headers.get("cookie") ?? "";
|
||||||
|
if (!cookie) return null;
|
||||||
|
const response = await fetch(
|
||||||
|
`http://localhost:${process.env.PORT ?? 3000}/api/auth/get-session`,
|
||||||
|
{ headers: { cookie } },
|
||||||
|
);
|
||||||
|
if (!response.ok) return null;
|
||||||
|
return (await response.json()) as SessionPayload | null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSessionFn = createServerFn({ method: "GET" }).handler(() =>
|
||||||
|
fetchSession(),
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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) => {},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { getSessionFn } from "~/server-fns/session";
|
||||||
|
|
||||||
|
type SessionData = Awaited<ReturnType<typeof getSessionFn>>;
|
||||||
|
|
||||||
|
let cached: { at: number; session: SessionData } | null = null;
|
||||||
|
|
||||||
|
const TTL_MS = 30_000;
|
||||||
|
|
||||||
|
export const getCachedSession = async (): Promise<SessionData> => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return await getSessionFn();
|
||||||
|
}
|
||||||
|
if (cached && Date.now() - cached.at < TTL_MS) {
|
||||||
|
return cached.session;
|
||||||
|
}
|
||||||
|
const session = await getSessionFn();
|
||||||
|
cached = { at: Date.now(), session };
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearSessionCache = () => {
|
||||||
|
cached = null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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 = () => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return [
|
||||||
|
httpBatchLink({
|
||||||
|
url: "/api/trpc",
|
||||||
|
transformer: superjson,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { tanstackStart } from "@tanstack/react-start/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: [tanstackStart(), 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 }]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"!**/.next/**",
|
"!**/.next/**",
|
||||||
"!**/dist",
|
"!**/dist",
|
||||||
"!**/drizzle/**",
|
"!**/drizzle/**",
|
||||||
|
"!**/routeTree.gen.ts",
|
||||||
"!node_modules/**",
|
"!node_modules/**",
|
||||||
"!packages/server/package.json"
|
"!packages/server/package.json"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const resolveTrustedOrigins = async () => {
|
|||||||
? [
|
? [
|
||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
"https://absolutely-handy-falcon.ngrok-free.app",
|
||||||
|
"http://localhost:5173"
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
return [
|
return [
|
||||||
|
|||||||
Generated
+1906
-101
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ packages:
|
|||||||
- "apps/api"
|
- "apps/api"
|
||||||
- "apps/dokploy"
|
- "apps/dokploy"
|
||||||
- "apps/schedules"
|
- "apps/schedules"
|
||||||
|
- "apps/vite"
|
||||||
- "packages/server"
|
- "packages/server"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user