mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 11:06:15 +05:00
feat: introduce Vite-based Dokploy application
- Added a new Vite application (`apps/vite`) to serve as a standalone SPA for the Dokploy dashboard, leveraging TanStack Router for routing. - Implemented server-side handling with a custom Node.js server to manage API requests and serve static files. - Integrated existing components and utilities from the Dokploy monorepo, ensuring no duplication of code. - Updated `.dockerignore` and `.gitignore` to include new build artifacts and environment files. - Enhanced `pnpm-lock.yaml` with new dependencies for the Vite application. - Created configuration files for Vite, TypeScript, and ESBuild to support the new application structure.
This commit is contained in:
parent
2d7caceeed
commit
2b225a1e4f
@ -2,4 +2,8 @@ node_modules
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
dist
|
||||
dist
|
||||
**/.next
|
||||
**/dist
|
||||
**/dist-server
|
||||
**/.env
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -46,4 +46,8 @@ yarn-error.log*
|
||||
.db
|
||||
|
||||
.playwright-*
|
||||
.credentials
|
||||
.credentials
|
||||
dist/
|
||||
dist-server/
|
||||
.tanstack
|
||||
.env.*
|
||||
54
Dockerfile.vite
Normal file
54
Dockerfile.vite
Normal file
@ -0,0 +1,54 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:24.4.0-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@10.22.0 --activate
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --no-frozen-lockfile
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=dokploy-vite build
|
||||
RUN pnpm --filter=dokploy-vite build:server
|
||||
|
||||
RUN pnpm --filter=dokploy-vite --prod deploy --legacy /prod/vite
|
||||
|
||||
# Drop auto-installed optional peers that are never used at runtime
|
||||
# (better-auth peers next/prisma adapters; drizzle peers pglite/sqlite; prisma pulls effect/sharp)
|
||||
RUN cd /prod/vite/node_modules && rm -rf \
|
||||
.pnpm/next@* .pnpm/@next+swc* \
|
||||
.pnpm/prisma@* .pnpm/@prisma+* \
|
||||
.pnpm/effect@* .pnpm/@electric-sql+* \
|
||||
.pnpm/better-sqlite3@* \
|
||||
.pnpm/sharp@* .pnpm/@img+* \
|
||||
.pnpm/typescript@* \
|
||||
next prisma typescript better-sqlite3 sharp
|
||||
|
||||
FROM base AS runtime
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# For a full production replacement of the official image, add the same
|
||||
# tooling block as ./Dockerfile (docker cli, nixpacks, railpack, pack, rclone)
|
||||
# — required for builds/deployments, not for serving the panel.
|
||||
|
||||
COPY --from=build /prod/vite/node_modules ./node_modules
|
||||
COPY --from=build /prod/vite/package.json ./package.json
|
||||
COPY --from=build /usr/src/app/apps/vite/dist ./dist
|
||||
COPY --from=build /usr/src/app/apps/vite/dist-server ./dist-server
|
||||
COPY --from=build /usr/src/app/apps/dokploy/drizzle ./drizzle
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
|
||||
CMD curl -fs http://localhost:3000/api/health || exit 1
|
||||
|
||||
CMD ["sh", "-c", "node dist-server/wait-for-postgres.mjs && node dist-server/migration.mjs && exec node dist-server/server.mjs"]
|
||||
50
apps/vite/README.md
Normal file
50
apps/vite/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# dokploy-vite
|
||||
|
||||
Vite + TanStack Router migration of the Dokploy dashboard UI. The Next.js app (`apps/dokploy`) stays intact and keeps serving the backend (tRPC, better-auth, websockets, webhooks). This app is a pure SPA that reuses the existing UI code directly from `apps/dokploy` — components are NOT duplicated.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Aliases** (`vite.config.ts`): `@/*` resolves into `apps/dokploy/*`, so all 380+ components, hooks, lib and utils are consumed from their original location. `~/*` resolves to this app's `src/`.
|
||||
- **Next.js shims** (`src/shims/`): `next/link`, `next/router`, `next/navigation`, `next/head`, `next/dynamic`, `next/script` and `nextjs-toploader` are aliased to thin adapters over TanStack Router, so shared components run unmodified.
|
||||
- **tRPC**: `@/utils/api` is re-aliased to `src/utils/api.ts` (`createTRPCReact` instead of `createTRPCNext`), same links (ws split + FormData split + superjson).
|
||||
- **Auth**: SSR `getServerSideProps` guards were replaced by `beforeLoad` guards using the existing better-auth client (`@/lib/auth-client`) with a 30s session cache (`src/utils/session.ts`).
|
||||
- **Routes** (`src/routes/`): file-based TanStack routes mirroring `apps/dokploy/pages`. `/dashboard` is a shared layout route rendering `DashboardLayout` once (it persists across navigations, unlike the pages-router setup).
|
||||
- **Dev proxy**: `/api` and every websocket endpoint (`/drawer-logs`, `/terminal`, `/docker-container-*`, `/listen-*`) proxy to the Next custom server on `localhost:3000`.
|
||||
|
||||
## Standalone server (no Next.js)
|
||||
|
||||
`server/server.ts` is a Next-free replacement for `apps/dokploy/server/server.ts`. It reuses the entire existing backend from the monorepo:
|
||||
|
||||
- **tRPC** (`/api/trpc/*`) and **OpenAPI REST** (`/api/*` catch-all) — the exact same handlers from `apps/dokploy/pages/api`, mounted through a thin Next-API compat adapter (`server/next-compat.ts`) that provides `req.query/body/cookies` and `res.status/json/send/redirect` on plain Node req/res.
|
||||
- **better-auth** (`/api/auth/*`) — `toNodeHandler(auth.handler)`, already framework-agnostic.
|
||||
- **Webhooks/callbacks** — deploy (github, refreshToken, compose), Stripe (raw body preserved for signature check), GitHub setup/webhook, GitLab/Gitea OAuth callbacks: all reused unchanged via the adapter.
|
||||
- **Websockets** — same `setup*WebSocketServer` functions from `apps/dokploy/server/wss`.
|
||||
- **Bootstrapping** — same production init (Traefik config, cron jobs, schedules, volume backups, deployment worker) as the original server.
|
||||
- **Static SPA** — in production it serves `dist/` with SPA fallback to `index.html` (`server/static.ts`).
|
||||
|
||||
## Development
|
||||
|
||||
One command — the server embeds Vite in middleware mode, so API + websockets + UI (with HMR) all run same-origin on :3000:
|
||||
|
||||
```bash
|
||||
pnpm --filter dokploy-vite dev
|
||||
# → http://localhost:3000 (no Next involved)
|
||||
```
|
||||
|
||||
Alternative split mode (Vite dev server on :5173 proxying to whichever backend runs on :3000 — the Next custom server or this one):
|
||||
|
||||
```bash
|
||||
pnpm --filter dokploy dev # or: nothing, if dev above is running
|
||||
pnpm --filter dokploy-vite dev:client # → :5173
|
||||
```
|
||||
|
||||
The `/api` proxy rewrites the `origin` header to :3000 so better-auth's trusted-origin check passes in split mode.
|
||||
|
||||
Production: `pnpm --filter dokploy-vite build && pnpm --filter dokploy-vite start` — one process serving API + websockets + static SPA on :3000, no Next.js at runtime.
|
||||
|
||||
The server loads `.env` from `apps/vite` first, then falls back to `apps/dokploy/.env`.
|
||||
|
||||
## Dropped vs Next (known gaps)
|
||||
|
||||
- SSR prefetching (`createServerSideHelpers`) — the SPA fetches on mount instead.
|
||||
- Per-page IS_CLOUD / role / permission SSR redirects beyond session checks — components and tRPC procedures still enforce them; route-level guards can be added incrementally in `beforeLoad`.
|
||||
24
apps/vite/esbuild.config.ts
Normal file
24
apps/vite/esbuild.config.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import esbuild from "esbuild";
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: {
|
||||
server: "server/server.ts",
|
||||
migration: "../dokploy/migration.ts",
|
||||
"wait-for-postgres": "../dokploy/wait-for-postgres.ts",
|
||||
"reset-password": "../dokploy/reset-password.ts",
|
||||
"reset-2fa": "../dokploy/reset-2fa.ts",
|
||||
"migrate-auth-secret": "../dokploy/scripts/migrate-auth-secret.ts",
|
||||
},
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
format: "esm",
|
||||
target: "node18",
|
||||
outExtension: { ".js": ".mjs" },
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
outdir: "dist-server",
|
||||
tsconfig: "tsconfig.json",
|
||||
packages: "external",
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
19
apps/vite/index.html
Normal file
19
apps/vite/index.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Dokploy</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
142
apps/vite/package.json
Normal file
142
apps/vite/package.json
Normal file
@ -0,0 +1,142 @@
|
||||
{
|
||||
"name": "dokploy-vite",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx -r dotenv/config server/server.ts",
|
||||
"dev:client": "vite",
|
||||
"build": "vite build",
|
||||
"build:server": "tsx esbuild.config.ts",
|
||||
"start": "NODE_ENV=production tsx -r dotenv/config server/server.ts",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.44",
|
||||
"@ai-sdk/azure": "^3.0.30",
|
||||
"@ai-sdk/cohere": "^3.0.21",
|
||||
"@ai-sdk/deepinfra": "^2.0.34",
|
||||
"@ai-sdk/mistral": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@aws-sdk/client-secrets-manager": "^3.1097.0",
|
||||
"@better-auth/api-key": "1.6.23",
|
||||
"@better-auth/passkey": "1.6.23",
|
||||
"@better-auth/scim": "1.6.23",
|
||||
"@better-auth/sso": "1.6.23",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.18",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
"@octokit/webhooks": "^13.9.0",
|
||||
"@react-email/components": "^1.0.12",
|
||||
"@trpc/server": "^11.10.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"ai": "^6.0.86",
|
||||
"ai-sdk-ollama": "^3.7.0",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "1.6.23",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-zod": "0.8.3",
|
||||
"lodash": "4.17.21",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
"node-os-utils": "2.0.1",
|
||||
"node-pty": "1.1.0",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"postgres": "3.4.4",
|
||||
"public-ip": "6.0.2",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
"resend": "^6.0.2",
|
||||
"semver": "7.7.3",
|
||||
"shell-quote": "^1.8.1",
|
||||
"slugify": "^1.6.6",
|
||||
"ssh2": "~1.16.0",
|
||||
"stripe": "17.2.0",
|
||||
"superjson": "^2.2.2",
|
||||
"toml": "3.0.0",
|
||||
"undici": "^6.21.3",
|
||||
"ws": "8.16.0",
|
||||
"yaml": "2.8.1",
|
||||
"zod": "^4.3.6",
|
||||
"zod-form-data": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@stepperize/react": "4.0.1",
|
||||
"@stripe/stripe-js": "4.8.0",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router": "^1.132.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"@trpc/client": "^11.10.0",
|
||||
"@trpc/react-query": "^11.10.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/node": "^24.4.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/swagger-ui-react": "^4.19.0",
|
||||
"@uiw/codemirror-theme-github": "^4.23.12",
|
||||
"@uiw/react-codemirror": "^4.23.12",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@xterm/addon-attach": "0.10.0",
|
||||
"@xterm/addon-clipboard": "0.1.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^0.2.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dompurify": "^3.3.3",
|
||||
"esbuild": "0.20.2",
|
||||
"fancy-ansi": "^0.1.3",
|
||||
"input-otp": "^1.4.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.469.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"radix-ui": "^1.6.0",
|
||||
"react-confetti-explosion": "3.0.3",
|
||||
"react-day-picker": "10.0.1",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-markdown": "^9.1.0",
|
||||
"recharts": "^3.8.0",
|
||||
"shadcn": "^4.11.0",
|
||||
"sonner": "^1.7.4",
|
||||
"swagger-ui-react": "^5.32.6",
|
||||
"tailwind-merge": "^2.6.1",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsx": "^4.22.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"use-resize-observer": "9.1.0",
|
||||
"vite": "^7.1.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
}
|
||||
}
|
||||
107
apps/vite/server/next-compat.ts
Normal file
107
apps/vite/server/next-compat.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { buffer } from "node:stream/consumers";
|
||||
|
||||
type NextStyleHandler = (req: any, res: any) => unknown | Promise<unknown>;
|
||||
|
||||
interface RunOptions {
|
||||
params?: Record<string, string | string[]>;
|
||||
parseBody?: boolean;
|
||||
}
|
||||
|
||||
const parseCookies = (header: string | undefined) => {
|
||||
const cookies: Record<string, string> = {};
|
||||
if (!header) return cookies;
|
||||
for (const part of header.split(";")) {
|
||||
const index = part.indexOf("=");
|
||||
if (index === -1) continue;
|
||||
const key = part.slice(0, index).trim();
|
||||
const value = part.slice(index + 1).trim();
|
||||
if (key) cookies[key] = decodeURIComponent(value);
|
||||
}
|
||||
return cookies;
|
||||
};
|
||||
|
||||
const parseQuery = (url: URL, params: Record<string, string | string[]>) => {
|
||||
const query: Record<string, string | string[]> = {};
|
||||
for (const [key, value] of url.searchParams) {
|
||||
const existing = query[key];
|
||||
if (existing === undefined) {
|
||||
query[key] = value;
|
||||
} else if (Array.isArray(existing)) {
|
||||
existing.push(value);
|
||||
} else {
|
||||
query[key] = [existing, value];
|
||||
}
|
||||
}
|
||||
Object.assign(query, params);
|
||||
return query;
|
||||
};
|
||||
|
||||
const parseBody = async (req: IncomingMessage) => {
|
||||
if (req.method === "GET" || req.method === "HEAD") return undefined;
|
||||
const raw = await buffer(req);
|
||||
if (raw.length === 0) return undefined;
|
||||
const contentType = req.headers["content-type"] ?? "";
|
||||
const text = raw.toString("utf8");
|
||||
if (contentType.includes("application/json")) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
if (contentType.includes("application/x-www-form-urlencoded")) {
|
||||
return Object.fromEntries(new URLSearchParams(text));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const enhanceResponse = (res: ServerResponse) => {
|
||||
const anyRes = res as any;
|
||||
anyRes.status = (code: number) => {
|
||||
res.statusCode = code;
|
||||
return anyRes;
|
||||
};
|
||||
anyRes.json = (data: unknown) => {
|
||||
if (!res.headersSent && !res.getHeader("content-type")) {
|
||||
res.setHeader("content-type", "application/json; charset=utf-8");
|
||||
}
|
||||
res.end(JSON.stringify(data));
|
||||
return anyRes;
|
||||
};
|
||||
anyRes.send = (data: unknown) => {
|
||||
if (typeof data === "string" || Buffer.isBuffer(data)) {
|
||||
res.end(data);
|
||||
} else if (data === undefined || data === null) {
|
||||
res.end();
|
||||
} else {
|
||||
anyRes.json(data);
|
||||
}
|
||||
return anyRes;
|
||||
};
|
||||
anyRes.redirect = (statusOrUrl: number | string, maybeUrl?: string) => {
|
||||
const statusCode = typeof statusOrUrl === "number" ? statusOrUrl : 307;
|
||||
const location =
|
||||
typeof statusOrUrl === "string" ? statusOrUrl : (maybeUrl ?? "/");
|
||||
res.writeHead(statusCode, { location });
|
||||
res.end();
|
||||
return anyRes;
|
||||
};
|
||||
return anyRes;
|
||||
};
|
||||
|
||||
export const runNextApiHandler = async (
|
||||
handler: NextStyleHandler,
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
options: RunOptions = {},
|
||||
) => {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
const anyReq = req as any;
|
||||
anyReq.query = parseQuery(url, options.params ?? {});
|
||||
anyReq.cookies = parseCookies(req.headers.cookie);
|
||||
if (options.parseBody !== false) {
|
||||
anyReq.body = await parseBody(req);
|
||||
}
|
||||
await handler(anyReq, enhanceResponse(res));
|
||||
};
|
||||
203
apps/vite/server/server.ts
Normal file
203
apps/vite/server/server.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import {
|
||||
auth,
|
||||
createDefaultMiddlewares,
|
||||
createDefaultServerTraefikConfig,
|
||||
createDefaultTraefikConfig,
|
||||
IS_CLOUD,
|
||||
initCancelDeployments,
|
||||
initCronJobs,
|
||||
initEnterpriseBackupCronJobs,
|
||||
initializeNetwork,
|
||||
initSchedules,
|
||||
initVolumeBackupsCronJobs,
|
||||
sendDokployRestartNotifications,
|
||||
setupDirectories,
|
||||
} from "@dokploy/server";
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
import { config } from "dotenv";
|
||||
import openApiHandler from "@/pages/api/[...trpc]";
|
||||
import deployRefreshTokenHandler from "@/pages/api/deploy/[refreshToken]";
|
||||
import deployComposeHandler from "@/pages/api/deploy/compose/[refreshToken]";
|
||||
import deployGithubHandler from "@/pages/api/deploy/github";
|
||||
import healthHandler from "@/pages/api/health";
|
||||
import giteaAuthorizeHandler from "@/pages/api/providers/gitea/authorize";
|
||||
import giteaCallbackHandler from "@/pages/api/providers/gitea/callback";
|
||||
import githubSetupHandler from "@/pages/api/providers/github/setup";
|
||||
import githubWebhookHandler from "@/pages/api/providers/github/webhook";
|
||||
import gitlabCallbackHandler from "@/pages/api/providers/gitlab/callback";
|
||||
import stripeWebhookHandler from "@/pages/api/stripe/webhook";
|
||||
import trpcHandler from "@/pages/api/trpc/[trpc]";
|
||||
import { setupDockerContainerLogsWebSocketServer } from "@/server/wss/docker-container-logs";
|
||||
import { setupDockerContainerTerminalWebSocketServer } from "@/server/wss/docker-container-terminal";
|
||||
import { setupDockerStatsMonitoringSocketServer } from "@/server/wss/docker-stats";
|
||||
import { setupDrawerLogsWebSocketServer } from "@/server/wss/drawer-logs";
|
||||
import { setupDeploymentLogsWebSocketServer } from "@/server/wss/listen-deployment";
|
||||
import { setupTerminalWebSocketServer } from "@/server/wss/terminal";
|
||||
import packageInfo from "../package.json";
|
||||
import { runNextApiHandler } from "./next-compat";
|
||||
import { createStaticHandler } from "./static";
|
||||
|
||||
config({ path: ".env" });
|
||||
config({ path: "../dokploy/.env" });
|
||||
|
||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
if (isProd && !IS_CLOUD) {
|
||||
setupDirectories();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
console.log("✅ initialization complete");
|
||||
}
|
||||
|
||||
const authHandler = toNodeHandler(auth.handler);
|
||||
|
||||
const handleApi = async (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
pathname: string,
|
||||
) => {
|
||||
if (pathname === "/api/health") {
|
||||
return runNextApiHandler(healthHandler, req, res);
|
||||
}
|
||||
if (pathname.startsWith("/api/auth/")) {
|
||||
return authHandler(req, res);
|
||||
}
|
||||
if (pathname.startsWith("/api/trpc/")) {
|
||||
const trpc = decodeURIComponent(pathname.slice("/api/trpc/".length));
|
||||
return runNextApiHandler(trpcHandler, req, res, {
|
||||
params: { trpc },
|
||||
parseBody: false,
|
||||
});
|
||||
}
|
||||
if (pathname === "/api/deploy/github") {
|
||||
return runNextApiHandler(deployGithubHandler, req, res);
|
||||
}
|
||||
if (pathname === "/api/stripe/webhook") {
|
||||
return runNextApiHandler(stripeWebhookHandler, req, res, {
|
||||
parseBody: false,
|
||||
});
|
||||
}
|
||||
if (pathname === "/api/providers/github/setup") {
|
||||
return runNextApiHandler(githubSetupHandler, req, res);
|
||||
}
|
||||
if (pathname === "/api/providers/github/webhook") {
|
||||
return runNextApiHandler(githubWebhookHandler, req, res);
|
||||
}
|
||||
if (pathname === "/api/providers/gitlab/callback") {
|
||||
return runNextApiHandler(gitlabCallbackHandler, req, res);
|
||||
}
|
||||
if (pathname === "/api/providers/gitea/authorize") {
|
||||
return runNextApiHandler(giteaAuthorizeHandler, req, res);
|
||||
}
|
||||
if (pathname === "/api/providers/gitea/callback") {
|
||||
return runNextApiHandler(giteaCallbackHandler, req, res);
|
||||
}
|
||||
const composeMatch = pathname.match(/^\/api\/deploy\/compose\/([^/]+)$/);
|
||||
if (composeMatch?.[1]) {
|
||||
return runNextApiHandler(deployComposeHandler, req, res, {
|
||||
params: { refreshToken: composeMatch[1] },
|
||||
});
|
||||
}
|
||||
const deployMatch = pathname.match(/^\/api\/deploy\/([^/]+)$/);
|
||||
if (deployMatch?.[1]) {
|
||||
return runNextApiHandler(deployRefreshTokenHandler, req, res, {
|
||||
params: { refreshToken: deployMatch[1] },
|
||||
});
|
||||
}
|
||||
const segments = pathname
|
||||
.slice("/api/".length)
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map(decodeURIComponent);
|
||||
return runNextApiHandler(openApiHandler, req, res, {
|
||||
params: { trpc: segments },
|
||||
});
|
||||
};
|
||||
|
||||
const startServer = async () => {
|
||||
console.log("Running DokployVersion: ", packageInfo.version);
|
||||
|
||||
let handleUi: (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
url: URL,
|
||||
) => void = (_req, res) => {
|
||||
res.writeHead(503, { "content-type": "text/plain" });
|
||||
res.end("UI not ready");
|
||||
};
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
if (url.pathname.startsWith("/api")) {
|
||||
await handleApi(req, res, url.pathname);
|
||||
return;
|
||||
}
|
||||
handleUi(req, res, url);
|
||||
} catch (error) {
|
||||
console.error("Request error", error);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { "content-type": "text/plain" });
|
||||
}
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
if (isProd) {
|
||||
handleUi = createStaticHandler(path.resolve(import.meta.dirname, "../dist"));
|
||||
} else {
|
||||
const { createServer: createViteServer } = await import("vite");
|
||||
const vite = await createViteServer({
|
||||
root: path.resolve(import.meta.dirname, ".."),
|
||||
configFile: path.resolve(import.meta.dirname, "../vite.config.ts"),
|
||||
appType: "spa",
|
||||
server: { middlewareMode: true, hmr: { server } },
|
||||
});
|
||||
handleUi = (req, res) => {
|
||||
vite.middlewares(req, res, () => {
|
||||
res.statusCode = 404;
|
||||
res.end();
|
||||
});
|
||||
};
|
||||
console.log("Vite dev middleware enabled (UI + HMR on this port)");
|
||||
}
|
||||
|
||||
setupDrawerLogsWebSocketServer(server);
|
||||
setupDeploymentLogsWebSocketServer(server);
|
||||
setupDockerContainerLogsWebSocketServer(server);
|
||||
setupDockerContainerTerminalWebSocketServer(server);
|
||||
setupTerminalWebSocketServer(server);
|
||||
if (!IS_CLOUD) {
|
||||
setupDockerStatsMonitoringSocketServer(server);
|
||||
}
|
||||
|
||||
server.listen(PORT, HOST);
|
||||
console.log(`Standalone Server Started on: http://${HOST}:${PORT}`);
|
||||
|
||||
if (isProd && !IS_CLOUD) {
|
||||
createDefaultMiddlewares();
|
||||
await initializeNetwork();
|
||||
await initCronJobs();
|
||||
await initSchedules();
|
||||
await initCancelDeployments();
|
||||
await initVolumeBackupsCronJobs();
|
||||
await sendDokployRestartNotifications();
|
||||
}
|
||||
await initEnterpriseBackupCronJobs();
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
console.log("Starting Deployment Worker");
|
||||
const { startDeploymentWorker } = await import(
|
||||
"@/server/queues/queueSetup"
|
||||
);
|
||||
await startDeploymentWorker();
|
||||
}
|
||||
};
|
||||
|
||||
startServer().catch((error) => {
|
||||
console.error("Main Server Error", error);
|
||||
});
|
||||
79
apps/vite/server/static.ts
Normal file
79
apps/vite/server/static.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import fs from "node:fs";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import path from "node:path";
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".avif": "image/avif",
|
||||
".ico": "image/x-icon",
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
".map": "application/json; charset=utf-8",
|
||||
".wasm": "application/wasm",
|
||||
".webmanifest": "application/manifest+json",
|
||||
};
|
||||
|
||||
const sendFile = (
|
||||
res: ServerResponse,
|
||||
filePath: string,
|
||||
cacheControl: string,
|
||||
) => {
|
||||
res.writeHead(200, {
|
||||
"content-type":
|
||||
MIME_TYPES[path.extname(filePath).toLowerCase()] ??
|
||||
"application/octet-stream",
|
||||
"cache-control": cacheControl,
|
||||
});
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
};
|
||||
|
||||
export const createStaticHandler = (distDir: string) => {
|
||||
const hasDist = fs.existsSync(path.join(distDir, "index.html"));
|
||||
|
||||
return (req: IncomingMessage, res: ServerResponse, url: URL) => {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
res.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
if (!hasDist) {
|
||||
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
||||
res.end(
|
||||
"UI build not found. In development run the Vite dev server (pnpm --filter dokploy-vite dev); for production run vite build first.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const pathname = decodeURIComponent(url.pathname);
|
||||
const filePath = path.join(distDir, pathname);
|
||||
if (!filePath.startsWith(distDir)) {
|
||||
res.writeHead(403).end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||
const cacheControl = pathname.startsWith("/assets/")
|
||||
? "public, max-age=31536000, immutable"
|
||||
: "public, max-age=0, must-revalidate";
|
||||
sendFile(res, filePath, cacheControl);
|
||||
return;
|
||||
}
|
||||
|
||||
sendFile(
|
||||
res,
|
||||
path.join(distDir, "index.html"),
|
||||
"no-cache, no-store, must-revalidate",
|
||||
);
|
||||
};
|
||||
};
|
||||
27
apps/vite/src/main.tsx
Normal file
27
apps/vite/src/main.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import "./styles/globals.css";
|
||||
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import { setRouterInstance } from "./shims/next-router";
|
||||
import { TRPCProvider } from "./utils/trpc-provider";
|
||||
|
||||
const router = createRouter({ routeTree });
|
||||
|
||||
setRouterInstance(router);
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
if (rootElement && !rootElement.innerHTML) {
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<TRPCProvider>
|
||||
<RouterProvider router={router} />
|
||||
</TRPCProvider>,
|
||||
);
|
||||
}
|
||||
1111
apps/vite/src/routeTree.gen.ts
Normal file
1111
apps/vite/src/routeTree.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
30
apps/vite/src/routes/__root.tsx
Normal file
30
apps/vite/src/routes/__root.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { SearchCommand } from "@/components/dashboard/search-command";
|
||||
import { WhitelabelingProvider } from "@/components/proprietary/whitelabeling/whitelabeling-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import TopLoader from "~/shims/toploader";
|
||||
|
||||
const RootComponent = () => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TopLoader />
|
||||
<WhitelabelingProvider />
|
||||
<Toaster richColors />
|
||||
<SearchCommand />
|
||||
<Outlet />
|
||||
</ThemeProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
});
|
||||
30
apps/vite/src/routes/accept-invitation/$invitationId.tsx
Normal file
30
apps/vite/src/routes/accept-invitation/$invitationId.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
export const AcceptInvitation = () => {
|
||||
const { invitationId } = Route.useParams();
|
||||
|
||||
// const { data: organization } = api.organization.getById.useQuery({
|
||||
// id: id as string
|
||||
// })
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const result = await authClient.organization.acceptInvitation({
|
||||
invitationId: invitationId,
|
||||
});
|
||||
console.log(result);
|
||||
}}
|
||||
>
|
||||
Accept Invitation
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/accept-invitation/$invitationId")({
|
||||
component: AcceptInvitation,
|
||||
});
|
||||
74
apps/vite/src/routes/dashboard/deployments.tsx
Normal file
74
apps/vite/src/routes/dashboard/deployments.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Rocket } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ShowDeploymentsTable } from "@/components/dashboard/deployments/show-deployments-table";
|
||||
import { ShowQueueTable } from "@/components/dashboard/deployments/show-queue-table";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
const TAB_VALUES = ["deployments", "queue"] as const;
|
||||
type TabValue = (typeof TAB_VALUES)[number];
|
||||
|
||||
function isValidTab(t: string): t is TabValue {
|
||||
return TAB_VALUES.includes(t as TabValue);
|
||||
}
|
||||
|
||||
function DeploymentsPage() {
|
||||
const router = useRouter();
|
||||
const tab =
|
||||
router.query.tab && isValidTab(router.query.tab as string)
|
||||
? (router.query.tab as TabValue)
|
||||
: "deployments";
|
||||
|
||||
const setTab = (value: string) => {
|
||||
if (!isValidTab(value)) return;
|
||||
router.replace(
|
||||
{ pathname: "/dashboard/deployments", query: { tab: value } },
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
|
||||
<div className="rounded-xl bg-background shadow-md h-full">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||
<Rocket className="size-5" />
|
||||
Deployments
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
All application and compose deployments in one place.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs value={tab} onValueChange={setTab} className="w-full min-w-0">
|
||||
<TabsList className="mt-2">
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="queue">Queue</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="deployments" className="mt-0 min-w-0 pt-4">
|
||||
<ShowDeploymentsTable />
|
||||
</TabsContent>
|
||||
<TabsContent value="queue" className="mt-0 pt-4">
|
||||
<ShowQueueTable />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardHeader>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/dashboard/deployments")({
|
||||
component: DeploymentsPage,
|
||||
});
|
||||
82
apps/vite/src/routes/dashboard/docker.tsx
Normal file
82
apps/vite/src/routes/dashboard/docker.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useRouter } from "next/router";
|
||||
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
||||
import { ShowVolumes } from "@/components/dashboard/docker/volumes/show-volumes";
|
||||
import { ShowNetworks } from "@/components/dashboard/networks/show-networks";
|
||||
import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers";
|
||||
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
||||
import { ServerFilter } from "@/components/shared/server-filter";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DEFAULT_TAB = "containers";
|
||||
|
||||
const Dashboard = () => {
|
||||
const router = useRouter();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const queryTab =
|
||||
typeof router.query.tab === "string" ? router.query.tab : DEFAULT_TAB;
|
||||
const activeTab = isCloud && queryTab === "networks" ? DEFAULT_TAB : queryTab;
|
||||
|
||||
const setTab = (value: string) => {
|
||||
const { tab: _current, ...query } = router.query;
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: value === DEFAULT_TAB ? query : { ...query, tab: value },
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ServerFilter>
|
||||
{(serverId) => (
|
||||
<Tabs value={activeTab} onValueChange={setTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||
<TabsTrigger value="swarm">Swarm</TabsTrigger>
|
||||
<TabsTrigger value="volumes">Volumes</TabsTrigger>
|
||||
{!isCloud && <TabsTrigger value="networks">Networks</TabsTrigger>}
|
||||
</TabsList>
|
||||
<TabsContent value="containers">
|
||||
<ShowContainers serverId={serverId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="swarm">
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<SwarmMonitorCard serverId={serverId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="containers">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md p-6">
|
||||
<ShowSwarmContainers serverId={serverId} />
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</TabsContent>
|
||||
<TabsContent value="volumes">
|
||||
<ShowVolumes serverId={serverId} />
|
||||
</TabsContent>
|
||||
{!isCloud && (
|
||||
<TabsContent value="networks">
|
||||
<ShowNetworks serverId={serverId} />
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</ServerFilter>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/docker")({
|
||||
component: Dashboard,
|
||||
});
|
||||
10
apps/vite/src/routes/dashboard/home.tsx
Normal file
10
apps/vite/src/routes/dashboard/home.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowHome } from "@/components/dashboard/home/show-home";
|
||||
|
||||
const Home = () => {
|
||||
return <ShowHome />;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/home")({
|
||||
component: Home,
|
||||
});
|
||||
83
apps/vite/src/routes/dashboard/monitoring.tsx
Normal file
83
apps/vite/src/routes/dashboard/monitoring.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||
import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const BASE_URL = "http://localhost:3001/metrics";
|
||||
|
||||
const DEFAULT_TOKEN = "metrics";
|
||||
|
||||
const Dashboard = () => {
|
||||
const [toggleMonitoring, _setToggleMonitoring] = useLocalStorage(
|
||||
"monitoring-enabled",
|
||||
false,
|
||||
);
|
||||
|
||||
const { data: monitoring, isPending } = api.user.getMetricsToken.useQuery();
|
||||
return (
|
||||
<div className="space-y-4 pb-10">
|
||||
{/* <AlertBlock>
|
||||
You are watching the <strong>Free</strong> plan.{" "}
|
||||
<a
|
||||
href="https://dokploy.com#pricing"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Upgrade
|
||||
</a>{" "}
|
||||
to get more features.
|
||||
</AlertBlock> */}
|
||||
{isPending ? (
|
||||
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto items-center">
|
||||
<div className="rounded-xl bg-background flex shadow-md px-4 w-full min-h-[50vh] justify-center items-center text-muted-foreground">
|
||||
Loading... <Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||
<Label className="text-muted-foreground">Change Monitoring</Label>
|
||||
<Switch
|
||||
checked={toggleMonitoring}
|
||||
onCheckedChange={setToggleMonitoring}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
{toggleMonitoring ? (
|
||||
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<ShowPaidMonitoring
|
||||
BASE_URL={
|
||||
process.env.NODE_ENV === "production"
|
||||
? `http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}/metrics`
|
||||
: BASE_URL
|
||||
}
|
||||
token={
|
||||
process.env.NODE_ENV === "production"
|
||||
? monitoring?.metricsConfig?.server?.token
|
||||
: DEFAULT_TOKEN
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||
<div className="rounded-xl bg-background shadow-md p-6">
|
||||
<ContainerFreeMonitoring appName="dokploy" />
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/monitoring")({
|
||||
component: Dashboard,
|
||||
});
|
||||
19
apps/vite/src/routes/dashboard/networks.tsx
Normal file
19
apps/vite/src/routes/dashboard/networks.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
const Networks = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/networks")({
|
||||
beforeLoad: ({ location }) => {
|
||||
const serverId = (location.search as Record<string, unknown>).serverId;
|
||||
throw redirect({
|
||||
href: `/dashboard/docker?tab=networks${
|
||||
typeof serverId === "string"
|
||||
? `&serverId=${encodeURIComponent(serverId)}`
|
||||
: ""
|
||||
}`,
|
||||
});
|
||||
},
|
||||
component: Networks,
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@ -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 });
|
||||
@ -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 });
|
||||
@ -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 });
|
||||
@ -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 });
|
||||
@ -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 });
|
||||
@ -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 });
|
||||
@ -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 });
|
||||
@ -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 });
|
||||
27
apps/vite/src/routes/dashboard/projects.tsx
Normal file
27
apps/vite/src/routes/dashboard/projects.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const ShowWelcomeDokploy = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/settings/billing/show-welcome-dokploy").then(
|
||||
(mod) => mod.ShowWelcomeDokploy,
|
||||
),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const Dashboard = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
return (
|
||||
<>
|
||||
{isCloud && <ShowWelcomeDokploy />}
|
||||
|
||||
<ShowProjects />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/projects")({
|
||||
component: Dashboard,
|
||||
});
|
||||
10
apps/vite/src/routes/dashboard/requests.tsx
Normal file
10
apps/vite/src/routes/dashboard/requests.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
|
||||
|
||||
function Requests() {
|
||||
return <ShowRequests />;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/dashboard/requests")({
|
||||
component: Requests,
|
||||
});
|
||||
21
apps/vite/src/routes/dashboard/route.tsx
Normal file
21
apps/vite/src/routes/dashboard/route.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { getCachedSession } from "~/utils/session";
|
||||
|
||||
const DashboardRouteComponent = () => {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Outlet />
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard")({
|
||||
beforeLoad: async () => {
|
||||
const session = await getCachedSession();
|
||||
if (!session?.session) {
|
||||
throw redirect({ to: "/" });
|
||||
}
|
||||
},
|
||||
component: DashboardRouteComponent,
|
||||
});
|
||||
27
apps/vite/src/routes/dashboard/schedules.tsx
Normal file
27
apps/vite/src/routes/dashboard/schedules.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||
import { ServerFilter } from "@/components/shared/server-filter";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
function SchedulesPage() {
|
||||
return (
|
||||
<ServerFilter>
|
||||
{(serverId) => (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full min-h-[45vh]">
|
||||
<div className="rounded-xl bg-background shadow-md h-full">
|
||||
<ShowSchedules
|
||||
scheduleType={serverId ? "server" : "dokploy-server"}
|
||||
id={serverId ?? "dokploy-server"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</ServerFilter>
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/dashboard/schedules")({
|
||||
component: SchedulesPage,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/ai.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/ai.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AiForm } from "@/components/dashboard/settings/ai-form";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<AiForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/ai")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/audit-logs.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/audit-logs.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowAuditLogs } from "@/components/proprietary/audit-logs/show-audit-logs";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowAuditLogs />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/audit-logs")({
|
||||
component: Page,
|
||||
});
|
||||
10
apps/vite/src/routes/dashboard/settings/billing.tsx
Normal file
10
apps/vite/src/routes/dashboard/settings/billing.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowBilling } from "@/components/dashboard/settings/billing/show-billing";
|
||||
|
||||
const Page = () => {
|
||||
return <ShowBilling />;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/billing")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/certificates.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/certificates.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowCertificates />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/certificates")({
|
||||
component: Page,
|
||||
});
|
||||
19
apps/vite/src/routes/dashboard/settings/cluster.tsx
Normal file
19
apps/vite/src/routes/dashboard/settings/cluster.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
||||
import { ServerFilter } from "@/components/shared/server-filter";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<ServerFilter>
|
||||
{(serverId) => (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowNodes serverId={serverId} />
|
||||
</div>
|
||||
)}
|
||||
</ServerFilter>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/cluster")({
|
||||
component: Page,
|
||||
});
|
||||
73
apps/vite/src/routes/dashboard/settings/deployments.tsx
Normal file
73
apps/vite/src/routes/dashboard/settings/deployments.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { BuildsConcurrency } from "@/components/dashboard/settings/servers/actions/builds-concurrency";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Page = () => {
|
||||
const { data: servers } = api.server.all.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Concurrent Builds</CardTitle>
|
||||
<CardDescription>
|
||||
Configure how many deployments can build at the same time on
|
||||
each server. Builds of the same service are always serialized.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<AlertBlock type="warning">
|
||||
Running multiple builds at once increases CPU, memory and disk
|
||||
usage on each server. Each concurrent build runs its own builder
|
||||
and image build, so set this based on the resources the machine
|
||||
can handle — too high a value can exhaust memory and make
|
||||
deployments fail.
|
||||
</AlertBlock>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Dokploy Server
|
||||
</p>
|
||||
<BuildsConcurrency />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Remote Servers
|
||||
</p>
|
||||
{servers && servers.length > 0 ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{servers.map((server) => (
|
||||
<BuildsConcurrency
|
||||
key={server.serverId}
|
||||
serverId={server.serverId}
|
||||
label={server.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground rounded-lg border border-dashed p-4 text-center">
|
||||
No remote servers added yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/deployments")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/destinations.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/destinations.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowDestinations />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/destinations")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/git-providers.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/git-providers.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowGitProviders } from "@/components/dashboard/settings/git/show-git-providers";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowGitProviders />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/git-providers")({
|
||||
component: Page,
|
||||
});
|
||||
10
apps/vite/src/routes/dashboard/settings/invoices.tsx
Normal file
10
apps/vite/src/routes/dashboard/settings/invoices.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices";
|
||||
|
||||
const Page = () => {
|
||||
return <ShowBillingInvoices />;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/invoices")({
|
||||
component: Page,
|
||||
});
|
||||
23
apps/vite/src/routes/dashboard/settings/license.tsx
Normal file
23
apps/vite/src/routes/dashboard/settings/license.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { LicenseKeySettings } from "@/components/proprietary/license-keys/license-key";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<div className="p-6">
|
||||
<LicenseKeySettings />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/license")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/notifications.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/notifications.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowNotifications />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/notifications")({
|
||||
component: Page,
|
||||
});
|
||||
24
apps/vite/src/routes/dashboard/settings/profile.tsx
Normal file
24
apps/vite/src/routes/dashboard/settings/profile.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
|
||||
import { LinkingAccount } from "@/components/dashboard/settings/linking-account/linking-account";
|
||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Page = () => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
{isCloud && <LinkingAccount />}
|
||||
{permissions?.api.read && <ShowApiKeys />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/profile")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/registry.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/registry.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowRegistry />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/registry")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/secrets.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/secrets.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowVaultProviders } from "@/components/dashboard/settings/vault/show-vault-providers";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowVaultProviders />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/secrets")({
|
||||
component: Page,
|
||||
});
|
||||
31
apps/vite/src/routes/dashboard/settings/server.tsx
Normal file
31
apps/vite/src/routes/dashboard/settings/server.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Page = () => {
|
||||
const { data: user } = api.user.get.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<WebDomain />
|
||||
<WebServer />
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<ShowBackups
|
||||
id={user?.userId ?? ""}
|
||||
databaseType="web-server"
|
||||
backupType="database"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/server")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/servers.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/servers.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowServers />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/servers")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/ssh-keys.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/ssh-keys.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowDestinations />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/ssh-keys")({
|
||||
component: Page,
|
||||
});
|
||||
83
apps/vite/src/routes/dashboard/settings/sso.tsx
Normal file
83
apps/vite/src/routes/dashboard/settings/sso.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ToggleEnforceSSO } from "@/components/dashboard/settings/servers/actions/toggle-enforce-sso";
|
||||
import { ToggleRemoteServersOnly } from "@/components/dashboard/settings/servers/actions/toggle-remote-servers-only";
|
||||
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { ForwardAuthServers } from "@/components/proprietary/sso/forward-auth-servers";
|
||||
import { SSOSettings } from "@/components/proprietary/sso/sso-settings";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Page = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<EnterpriseFeatureGate
|
||||
lockedProps={{
|
||||
title: "Enterprise SSO",
|
||||
description:
|
||||
"Single sign-on (SSO) with OIDC and SAML is part of Dokploy Enterprise. Add a valid license to configure it.",
|
||||
ctaLabel: "Go to License",
|
||||
}}
|
||||
>
|
||||
<SSOSettings />
|
||||
</EnterpriseFeatureGate>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<EnterpriseFeatureGate
|
||||
lockedProps={{
|
||||
title: "Application Authentication",
|
||||
description:
|
||||
"Protect deployed applications behind an OIDC SSO gate (oauth2-proxy). Part of Dokploy Enterprise.",
|
||||
ctaLabel: "Go to License",
|
||||
}}
|
||||
>
|
||||
<ForwardAuthServers />
|
||||
</EnterpriseFeatureGate>
|
||||
</div>
|
||||
</Card>
|
||||
{!isCloud && (
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<EnterpriseFeatureGate
|
||||
lockedProps={{
|
||||
title: "Self-hosted Restrictions",
|
||||
description:
|
||||
"Deployment and authentication restrictions are part of Dokploy Enterprise. Add a valid license to configure them.",
|
||||
ctaLabel: "Go to License",
|
||||
}}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">
|
||||
Self-hosted Restrictions
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Control deployment targets and authentication behavior.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ToggleRemoteServersOnly />
|
||||
<ToggleEnforceSSO />
|
||||
</CardContent>
|
||||
</EnterpriseFeatureGate>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/sso")({
|
||||
component: Page,
|
||||
});
|
||||
14
apps/vite/src/routes/dashboard/settings/tags.tsx
Normal file
14
apps/vite/src/routes/dashboard/settings/tags.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { TagManager } from "@/components/dashboard/settings/tags/tag-manager";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<TagManager />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/tags")({
|
||||
component: Page,
|
||||
});
|
||||
24
apps/vite/src/routes/dashboard/settings/users.tsx
Normal file
24
apps/vite/src/routes/dashboard/settings/users.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Page = () => {
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const isOwnerOrAdmin = auth?.role === "owner" || auth?.role === "admin";
|
||||
const canCreateMembers = permissions?.member.create ?? false;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowUsers />
|
||||
{canCreateMembers && <ShowInvitations />}
|
||||
{isOwnerOrAdmin && <ManageCustomRoles />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/users")({
|
||||
component: Page,
|
||||
});
|
||||
33
apps/vite/src/routes/dashboard/settings/whitelabeling.tsx
Normal file
33
apps/vite/src/routes/dashboard/settings/whitelabeling.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { WhitelabelingSettings } from "@/components/proprietary/whitelabeling/whitelabeling-settings";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<div className="p-6">
|
||||
<EnterpriseFeatureGate
|
||||
lockedProps={{
|
||||
title: "Enterprise Whitelabeling",
|
||||
description:
|
||||
"Whitelabeling allows you to fully customize logos, colors, CSS, error pages, and more. Add a valid license to configure it.",
|
||||
ctaLabel: "Go to License",
|
||||
}}
|
||||
>
|
||||
<WhitelabelingSettings />
|
||||
</EnterpriseFeatureGate>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/settings/whitelabeling")({
|
||||
component: Page,
|
||||
});
|
||||
19
apps/vite/src/routes/dashboard/swarm.tsx
Normal file
19
apps/vite/src/routes/dashboard/swarm.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
const Swarm = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/swarm")({
|
||||
beforeLoad: ({ location }) => {
|
||||
const serverId = (location.search as Record<string, unknown>).serverId;
|
||||
throw redirect({
|
||||
href: `/dashboard/docker?tab=swarm${
|
||||
typeof serverId === "string"
|
||||
? `&serverId=${encodeURIComponent(serverId)}`
|
||||
: ""
|
||||
}`,
|
||||
});
|
||||
},
|
||||
component: Swarm,
|
||||
});
|
||||
15
apps/vite/src/routes/dashboard/traefik.tsx
Normal file
15
apps/vite/src/routes/dashboard/traefik.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
||||
import { ServerFilter } from "@/components/shared/server-filter";
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<ServerFilter>
|
||||
{(serverId) => <ShowTraefikSystem serverId={serverId} />}
|
||||
</ServerFilter>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/dashboard/traefik")({
|
||||
component: Dashboard,
|
||||
});
|
||||
478
apps/vite/src/routes/index.tsx
Normal file
478
apps/vite/src/routes/index.tsx
Normal file
@ -0,0 +1,478 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
import { Fingerprint } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { SignInWithGithub } from "@/components/proprietary/auth/sign-in-with-github";
|
||||
import { SignInWithGoogle } from "@/components/proprietary/auth/sign-in-with-google";
|
||||
import { SignInWithSSO } from "@/components/proprietary/sso/sign-in-with-sso";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent, CardDescription } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
import { getCachedSession } from "~/utils/session";
|
||||
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
const _TwoFactorSchema = z.object({
|
||||
code: z.string().min(6),
|
||||
});
|
||||
|
||||
type LoginForm = z.infer<typeof LoginSchema>;
|
||||
|
||||
function Home() {
|
||||
const router = useRouter();
|
||||
const { data: IS_CLOUD } = api.settings.isCloud.useQuery();
|
||||
const { data: enforceSSO } = api.sso.enforceSSO.useQuery();
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
|
||||
const [isTwoFactor, setIsTwoFactor] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [twoFactorCode, setTwoFactorCode] = useState("");
|
||||
const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false);
|
||||
const [backupCode, setBackupCode] = useState("");
|
||||
const loginForm = useForm<LoginForm>({
|
||||
resolver: zodResolver(LoginSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: LoginForm) => {
|
||||
setIsLoginLoading(true);
|
||||
try {
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const isEmailNotVerified =
|
||||
error.code === "EMAIL_NOT_VERIFIED" ||
|
||||
error.message?.toLowerCase().includes("email not verified");
|
||||
if (isEmailNotVerified) {
|
||||
const msg =
|
||||
"Your email is not verified. We've sent a new verification link to your email.";
|
||||
toast.info(msg);
|
||||
setError(msg);
|
||||
return;
|
||||
}
|
||||
toast.error(error.message);
|
||||
setError(error.message || "An error occurred while logging in");
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
if (data?.twoFactorRedirect as boolean) {
|
||||
setTwoFactorCode("");
|
||||
setIsTwoFactor(true);
|
||||
toast.info("Please enter your 2FA code");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while logging in");
|
||||
} finally {
|
||||
setIsLoginLoading(false);
|
||||
}
|
||||
};
|
||||
const onPasskeySignIn = async () => {
|
||||
setIsPasskeyLoading(true);
|
||||
try {
|
||||
const { data, error } = await authClient.signIn.passkey();
|
||||
|
||||
if (error) {
|
||||
const errorCode = "code" in error ? error.code : undefined;
|
||||
if (
|
||||
errorCode !== "AUTH_CANCELLED" &&
|
||||
errorCode !== "ERROR_CEREMONY_ABORTED"
|
||||
) {
|
||||
toast.error(error.message || "Failed to sign in with passkey");
|
||||
setError(error.message || "Failed to sign in with passkey");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/home");
|
||||
}
|
||||
} catch {
|
||||
toast.error("An error occurred while signing in with passkey");
|
||||
} finally {
|
||||
setIsPasskeyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onTwoFactorSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (twoFactorCode.length !== 6) {
|
||||
toast.error("Please enter a valid 6-digit code");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTwoFactorLoading(true);
|
||||
try {
|
||||
const { error } = await authClient.twoFactor.verifyTotp({
|
||||
code: twoFactorCode.replace(/\s/g, ""),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
setError(error.message || "An error occurred while verifying 2FA code");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while verifying 2FA code");
|
||||
} finally {
|
||||
setIsTwoFactorLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onBackupCodeSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (backupCode.length < 8) {
|
||||
toast.error("Please enter a valid backup code");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBackupCodeLoading(true);
|
||||
try {
|
||||
const { error } = await authClient.twoFactor.verifyBackupCode({
|
||||
code: backupCode.trim(),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
setError(
|
||||
error.message || "An error occurred while verifying backup code",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Logged in successfully");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while verifying backup code");
|
||||
} finally {
|
||||
setIsBackupCodeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loginContent = (
|
||||
<>
|
||||
{IS_CLOUD && <SignInWithGithub />}
|
||||
{IS_CLOUD && <SignInWithGoogle />}
|
||||
<Form {...loginForm}>
|
||||
<form
|
||||
method="post"
|
||||
onSubmit={loginForm.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="login-form"
|
||||
>
|
||||
<FormField
|
||||
control={loginForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={loginForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button className="w-full" type="submit" isLoading={isLoginLoading}>
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full mt-4"
|
||||
type="button"
|
||||
onClick={onPasskeySignIn}
|
||||
isLoading={isPasskeyLoading}
|
||||
>
|
||||
<Fingerprint className="size-4" />
|
||||
Sign in with Passkey
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
<div className="flex flex-row items-center justify-center gap-2">
|
||||
<Logo
|
||||
className="size-12"
|
||||
logoUrl={
|
||||
whitelabeling?.loginLogoUrl ||
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
Sign in
|
||||
</div>
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your email and password to sign in
|
||||
</p>
|
||||
</div>
|
||||
{error && (
|
||||
<AlertBlock type="error" className="my-2">
|
||||
<span>{error}</span>
|
||||
</AlertBlock>
|
||||
)}
|
||||
<CardContent className="p-0">
|
||||
{!isTwoFactor ? (
|
||||
<>
|
||||
{enforceSSO ? (
|
||||
<SignInWithSSO enforce />
|
||||
) : showSignInWithSSO ? (
|
||||
<SignInWithSSO>{loginContent}</SignInWithSSO>
|
||||
) : (
|
||||
loginContent
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<form
|
||||
method="post"
|
||||
onSubmit={onTwoFactorSubmit}
|
||||
className="space-y-4"
|
||||
id="two-factor-form"
|
||||
autoComplete="on"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="totp-code">2FA Code</Label>
|
||||
<InputOTP
|
||||
id="totp-code"
|
||||
name="totp"
|
||||
value={twoFactorCode}
|
||||
onChange={setTwoFactorCode}
|
||||
maxLength={6}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
autoFocus
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
<CardDescription>
|
||||
Enter the 6-digit code from your authenticator app
|
||||
</CardDescription>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsBackupCodeModalOpen(true)}
|
||||
className="text-sm text-muted-foreground hover:underline self-start mt-2"
|
||||
>
|
||||
Lost access to your authenticator app?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsTwoFactor(false);
|
||||
setTwoFactorCode("");
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
type="submit"
|
||||
isLoading={isTwoFactorLoading}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Dialog
|
||||
open={isBackupCodeModalOpen}
|
||||
onOpenChange={setIsBackupCodeModalOpen}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enter Backup Code</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter one of your backup codes to access your account
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
onSubmit={onBackupCodeSubmit}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Backup Code</Label>
|
||||
<Input
|
||||
value={backupCode}
|
||||
onChange={(e) => setBackupCode(e.target.value)}
|
||||
placeholder="Enter your backup code"
|
||||
className="font-mono"
|
||||
/>
|
||||
<CardDescription>
|
||||
Enter one of the backup codes you received when setting up
|
||||
2FA
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsBackupCodeModalOpen(false);
|
||||
setBackupCode("");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
type="submit"
|
||||
isLoading={isBackupCodeLoading}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row justify-between flex-wrap">
|
||||
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||
{IS_CLOUD && (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/register"
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
||||
{IS_CLOUD ? (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="https://docs.dokploy.com/docs/core/reset-password"
|
||||
target="_blank"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2" />
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<Home />
|
||||
</OnboardingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomePage,
|
||||
beforeLoad: async () => {
|
||||
const session = await getCachedSession();
|
||||
if (session?.session) {
|
||||
throw redirect({ to: "/dashboard/projects" });
|
||||
}
|
||||
},
|
||||
});
|
||||
323
apps/vite/src/routes/invitation.tsx
Normal file
323
apps/vite/src/routes/invitation.tsx
Normal file
@ -0,0 +1,323 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "First name is required",
|
||||
}),
|
||||
lastName: z.string().min(1, {
|
||||
message: "Last name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Email is required",
|
||||
})
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.refine((password) => password === "" || password.length >= 8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.refine(
|
||||
(confirmPassword) =>
|
||||
confirmPassword === "" || confirmPassword.length >= 8,
|
||||
{
|
||||
message: "Password must be at least 8 characters",
|
||||
},
|
||||
),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
type Register = z.infer<typeof registerSchema>;
|
||||
|
||||
const Invitation = () => {
|
||||
const router = useRouter();
|
||||
const token =
|
||||
typeof router.query.token === "string" ? router.query.token : "";
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data } = api.user.getUserByToken.useQuery(
|
||||
{
|
||||
token,
|
||||
},
|
||||
{
|
||||
enabled: !!token,
|
||||
},
|
||||
);
|
||||
const userAlreadyExists = data?.userAlreadyExists;
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
resolver: zodResolver(registerSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.email) {
|
||||
form.reset({
|
||||
email: data?.email || "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (values: Register) => {
|
||||
try {
|
||||
const { error } = await authClient.signUp.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
lastName: values.lastName,
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
"x-dokploy-token": token,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const _result = await authClient.organization.acceptInvitation({
|
||||
invitationId: token,
|
||||
});
|
||||
|
||||
toast.success("Account created successfully");
|
||||
router.push("/dashboard/home");
|
||||
} catch {
|
||||
toast.error("An error occurred while creating your account");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex h-screen w-full items-center justify-center ">
|
||||
<div className="flex flex-col items-center gap-4 w-full">
|
||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||
<Link href="/" className="flex flex-row items-center gap-2">
|
||||
<Logo
|
||||
className="size-12"
|
||||
logoUrl={
|
||||
whitelabeling?.loginLogoUrl ||
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
Invitation
|
||||
</CardTitle>
|
||||
{userAlreadyExists ? (
|
||||
<div className="flex flex-col gap-4 justify-center items-center">
|
||||
<AlertBlock type="success">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-medium">Valid Invitation!</span>
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
We detected that you already have an account with this
|
||||
email. Please sign in to accept the invitation.
|
||||
</span>
|
||||
</div>
|
||||
</AlertBlock>
|
||||
|
||||
<Button asChild variant="default" className="w-full">
|
||||
<Link href="/">Sign In</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CardDescription>
|
||||
Fill the form below to create your account
|
||||
</CardDescription>
|
||||
<div className="w-full">
|
||||
<div className="p-3" />
|
||||
|
||||
{/* {isError && (
|
||||
<div className="mx-5 my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<CardContent className="p-0">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled
|
||||
placeholder="Email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
className="w-full"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
||||
{isCloud && (
|
||||
<>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InvitationPage = () => {
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<Invitation />
|
||||
</OnboardingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/invitation")({
|
||||
component: InvitationPage,
|
||||
});
|
||||
307
apps/vite/src/routes/register.tsx
Normal file
307
apps/vite/src/routes/register.tsx
Normal file
@ -0,0 +1,307 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { SignInWithGithub } from "@/components/proprietary/auth/sign-in-with-github";
|
||||
import { SignInWithGoogle } from "@/components/proprietary/auth/sign-in-with-google";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
import { getCachedSession } from "~/utils/session";
|
||||
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "First name is required",
|
||||
}),
|
||||
lastName: z.string().min(1, {
|
||||
message: "Last name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Email is required",
|
||||
})
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.refine((password) => password === "" || password.length >= 8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.refine(
|
||||
(confirmPassword) =>
|
||||
confirmPassword === "" || confirmPassword.length >= 8,
|
||||
{
|
||||
message: "Password must be at least 8 characters",
|
||||
},
|
||||
),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
type Register = z.infer<typeof registerSchema>;
|
||||
|
||||
const Register = () => {
|
||||
const router = useRouter();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState<any>(null);
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
resolver: zodResolver(registerSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Register) => {
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
lastName: values.lastName,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setIsError(true);
|
||||
setError(error.message || "An error occurred");
|
||||
} else {
|
||||
toast.success("User registered successfully", {
|
||||
duration: 2000,
|
||||
});
|
||||
if (!isCloud) {
|
||||
router.push("/");
|
||||
} else {
|
||||
setData(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex w-full items-center justify-center ">
|
||||
<div className="flex flex-col items-center gap-4 w-full">
|
||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||
<Link href="/" className="flex flex-row items-center gap-2">
|
||||
<Logo
|
||||
className="size-12"
|
||||
logoUrl={
|
||||
whitelabeling?.loginLogoUrl ||
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
{isCloud ? "Sign Up" : "Setup the server"}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email and password to{" "}
|
||||
{isCloud ? "create an account" : "setup the server"}
|
||||
</CardDescription>
|
||||
<div className="mx-auto w-full max-w-lg bg-transparent">
|
||||
{isError && (
|
||||
<div className="my-2 flex flex-row items-center gap-2 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isCloud && data && (
|
||||
<AlertBlock type="success" className="my-2">
|
||||
<span>
|
||||
Registered successfully, please check your inbox or spam
|
||||
folder to confirm your account.
|
||||
</span>
|
||||
</AlertBlock>
|
||||
)}
|
||||
<CardContent className="p-0">
|
||||
{isCloud && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<SignInWithGithub />
|
||||
<SignInWithGoogle />
|
||||
</div>
|
||||
)}
|
||||
{isCloud && (
|
||||
<p className="mb-4 text-center text-xs text-muted-foreground">
|
||||
Or register with email
|
||||
</p>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
method="post"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="email@dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
className="w-full"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="flex flex-row justify-between flex-wrap">
|
||||
{isCloud && (
|
||||
<div className="mt-4 text-center text-sm flex gap-2 text-muted-foreground">
|
||||
Already have account?
|
||||
<Link className="underline" href="/">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2 text-muted-foreground">
|
||||
Need help?
|
||||
<Link
|
||||
className="underline"
|
||||
href="https://dokploy.com"
|
||||
target="_blank"
|
||||
>
|
||||
Contact us
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterPage = () => {
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<Register />
|
||||
</OnboardingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/register")({
|
||||
component: RegisterPage,
|
||||
beforeLoad: async () => {
|
||||
const session = await getCachedSession();
|
||||
if (session?.session) {
|
||||
throw redirect({ to: "/dashboard/projects" });
|
||||
}
|
||||
},
|
||||
});
|
||||
193
apps/vite/src/routes/reset-password.tsx
Normal file
193
apps/vite/src/routes/reset-password.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
const loginSchema = z
|
||||
.object({
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.min(8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.min(8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
type Login = z.infer<typeof loginSchema>;
|
||||
|
||||
function Home() {
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const form = useForm<Login>({
|
||||
defaultValues: {
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const token = new URLSearchParams(window.location.search).get("token");
|
||||
|
||||
if (token) {
|
||||
setToken(token);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Login) => {
|
||||
setIsLoading(true);
|
||||
const { error } = await authClient.resetPassword({
|
||||
newPassword: values.password,
|
||||
token: token || "",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setError(error.message || "An error occurred");
|
||||
} else {
|
||||
toast.success("Password reset successfully");
|
||||
router.push("/");
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center ">
|
||||
<div className="flex flex-col items-center gap-4 w-full">
|
||||
<CardTitle className="text-2xl font-bold flex flex-row gap-2 items-center">
|
||||
<Link href="/" className="flex flex-row items-center gap-2">
|
||||
<Logo
|
||||
className="size-12"
|
||||
logoUrl={
|
||||
whitelabeling?.loginLogoUrl ||
|
||||
whitelabeling?.logoUrl ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
Reset Password
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email to reset your password
|
||||
</CardDescription>
|
||||
|
||||
<div className="w-full">
|
||||
<CardContent className="p-0">
|
||||
{error && (
|
||||
<AlertBlock type="error" className="my-2">
|
||||
{error}
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
method="post"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm flex gap-2 text-muted-foreground">
|
||||
<Link href="/">Sign in</Link>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ResetPasswordPage = () => {
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<Home />
|
||||
</OnboardingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/reset-password")({
|
||||
component: ResetPasswordPage,
|
||||
});
|
||||
174
apps/vite/src/routes/send-reset-password.tsx
Normal file
174
apps/vite/src/routes/send-reset-password.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Email is required",
|
||||
})
|
||||
.max(255, {
|
||||
message: "Email must be at most 255 characters",
|
||||
})
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
});
|
||||
|
||||
type Login = z.infer<typeof loginSchema>;
|
||||
|
||||
type AuthResponse = {
|
||||
is2FAEnabled: boolean;
|
||||
authId: string;
|
||||
};
|
||||
|
||||
function Home() {
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const [temp, _setTemp] = useState<AuthResponse>({
|
||||
is2FAEnabled: false,
|
||||
authId: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const _router = useRouter();
|
||||
const form = useForm<Login>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Login) => {
|
||||
setIsLoading(true);
|
||||
const { error } = await authClient.requestPasswordReset({
|
||||
email: values.email,
|
||||
redirectTo: "/reset-password",
|
||||
});
|
||||
if (error) {
|
||||
setError(error.message || "An error occurred");
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
toast.success("Email sent", {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center ">
|
||||
<div className="flex flex-col items-center gap-4 w-full">
|
||||
<Link href="/" className="flex flex-row items-center gap-2">
|
||||
<Logo
|
||||
logoUrl={
|
||||
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined
|
||||
}
|
||||
/>
|
||||
<span className="font-medium text-sm">
|
||||
{whitelabeling?.appName || "Dokploy"}
|
||||
</span>
|
||||
</Link>
|
||||
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email to reset your password
|
||||
</CardDescription>
|
||||
|
||||
<div className="mx-auto w-full max-w-lg bg-transparent ">
|
||||
<CardContent className="p-0">
|
||||
{error && (
|
||||
<AlertBlock type="error" className="my-2">
|
||||
{error}
|
||||
</AlertBlock>
|
||||
)}
|
||||
{!temp.is2FAEnabled ? (
|
||||
<Form {...form}>
|
||||
<form
|
||||
method="post"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Email"
|
||||
maxLength={255}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
Send Reset Link
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-row justify-between flex-wrap">
|
||||
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SendResetPasswordPage = () => {
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<Home />
|
||||
</OnboardingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/send-reset-password")({
|
||||
component: SendResetPasswordPage,
|
||||
});
|
||||
83
apps/vite/src/routes/swagger.tsx
Normal file
83
apps/vite/src/routes/swagger.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import dynamic from "next/dynamic";
|
||||
import { api } from "@/utils/api";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getCachedSession } from "~/utils/session";
|
||||
|
||||
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||
|
||||
const Home = () => {
|
||||
const { data } = api.settings.getOpenApiDocument.useQuery();
|
||||
const [spec, setSpec] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
|
||||
// Force OpenAPI 3.0 so Swagger UI uses the 3.0 parser (avoids ApiDOM 3.1 refract bug)
|
||||
const newSpec = {
|
||||
...data,
|
||||
openapi: "3.0.3",
|
||||
servers: [{ url: protocolAndHost }],
|
||||
externalDocs: {
|
||||
url: `${protocolAndHost}/trpc/settings.getOpenApiDocument`,
|
||||
},
|
||||
};
|
||||
// Remove 3.1-only fields that could confuse the 3.0 parser
|
||||
if ("jsonSchemaDialect" in newSpec) {
|
||||
delete (newSpec as Record<string, unknown>).jsonSchemaDialect;
|
||||
}
|
||||
setSpec(newSpec);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-white">
|
||||
<SwaggerUI
|
||||
spec={spec}
|
||||
persistAuthorization={true}
|
||||
plugins={[
|
||||
{
|
||||
statePlugins: {
|
||||
auth: {
|
||||
wrapActions: {
|
||||
authorize: (ori: any) => (args: any) => {
|
||||
const result = ori(args);
|
||||
const apiKey = args?.apiKey?.value;
|
||||
if (apiKey) {
|
||||
localStorage.setItem("swagger_api_key", apiKey);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
logout: (ori: any) => (args: any) => {
|
||||
const result = ori(args);
|
||||
localStorage.removeItem("swagger_api_key");
|
||||
return result;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
requestInterceptor={(request: any) => {
|
||||
const apiKey = localStorage.getItem("swagger_api_key");
|
||||
if (apiKey) {
|
||||
request.headers = request.headers || {};
|
||||
request.headers["x-api-key"] = apiKey;
|
||||
}
|
||||
return request;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/swagger")({
|
||||
component: Home,
|
||||
beforeLoad: async () => {
|
||||
const session = await getCachedSession();
|
||||
if (!session?.session) {
|
||||
throw redirect({ to: "/" });
|
||||
}
|
||||
},
|
||||
});
|
||||
35
apps/vite/src/shims/next-dynamic.tsx
Normal file
35
apps/vite/src/shims/next-dynamic.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import * as React from "react";
|
||||
|
||||
type ComponentModule<P> =
|
||||
| { default: React.ComponentType<P> }
|
||||
| React.ComponentType<P>;
|
||||
|
||||
interface DynamicOptions {
|
||||
ssr?: boolean;
|
||||
loading?: React.ComponentType;
|
||||
}
|
||||
|
||||
const dynamic = <P extends object>(
|
||||
loader: () => Promise<ComponentModule<P>>,
|
||||
options?: DynamicOptions,
|
||||
) => {
|
||||
const Lazy = React.lazy(async () => {
|
||||
const mod = await loader();
|
||||
if (mod && typeof mod === "object" && "default" in mod) {
|
||||
return mod as { default: React.ComponentType<P> };
|
||||
}
|
||||
return { default: mod as React.ComponentType<P> };
|
||||
}) as unknown as React.ComponentType<P>;
|
||||
|
||||
const Loading = options?.loading;
|
||||
|
||||
const DynamicComponent = (props: P) => (
|
||||
<React.Suspense fallback={Loading ? <Loading /> : null}>
|
||||
<Lazy {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
return DynamicComponent;
|
||||
};
|
||||
|
||||
export default dynamic;
|
||||
24
apps/vite/src/shims/next-head.tsx
Normal file
24
apps/vite/src/shims/next-head.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const Head = ({ children }: { children?: React.ReactNode }) => {
|
||||
useEffect(() => {
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (!React.isValidElement(child)) return;
|
||||
if (child.type === "title") {
|
||||
const titleChildren = (child.props as { children?: React.ReactNode })
|
||||
.children;
|
||||
const text = Array.isArray(titleChildren)
|
||||
? titleChildren.join("")
|
||||
: titleChildren;
|
||||
if (typeof text === "string") {
|
||||
document.title = text;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [children]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Head;
|
||||
65
apps/vite/src/shims/next-link.tsx
Normal file
65
apps/vite/src/shims/next-link.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Link as RouterLink, useLocation } from "@tanstack/react-router";
|
||||
import * as React from "react";
|
||||
import type { NextUrl } from "./next-router";
|
||||
|
||||
type NextLinkProps = Omit<React.ComponentPropsWithoutRef<"a">, "href"> & {
|
||||
href: NextUrl;
|
||||
replace?: boolean;
|
||||
prefetch?: boolean;
|
||||
shallow?: boolean;
|
||||
scroll?: boolean;
|
||||
passHref?: boolean;
|
||||
legacyBehavior?: boolean;
|
||||
locale?: string | false;
|
||||
};
|
||||
|
||||
const isExternal = (href: string) => /^(https?:|mailto:|tel:|\/\/)/.test(href);
|
||||
|
||||
const Link = React.forwardRef<HTMLAnchorElement, NextLinkProps>(
|
||||
(
|
||||
{
|
||||
href,
|
||||
replace,
|
||||
prefetch: _prefetch,
|
||||
shallow: _shallow,
|
||||
scroll: _scroll,
|
||||
passHref: _passHref,
|
||||
legacyBehavior: _legacyBehavior,
|
||||
locale: _locale,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const location = useLocation();
|
||||
|
||||
let hrefString: string;
|
||||
if (typeof href === "string") {
|
||||
hrefString = href;
|
||||
} else {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(href.query ?? {})) {
|
||||
if (value === undefined || value === null) continue;
|
||||
params.set(key, String(value));
|
||||
}
|
||||
const search = params.toString();
|
||||
hrefString = `${href.pathname ?? location.pathname}${search ? `?${search}` : ""}${href.hash ? `#${href.hash.replace(/^#/, "")}` : ""}`;
|
||||
}
|
||||
|
||||
if (isExternal(hrefString)) {
|
||||
return <a ref={ref} href={hrefString} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RouterLink
|
||||
ref={ref}
|
||||
to={hrefString}
|
||||
replace={replace}
|
||||
{...(props as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Link.displayName = "NextLinkShim";
|
||||
|
||||
export default Link;
|
||||
29
apps/vite/src/shims/next-navigation.ts
Normal file
29
apps/vite/src/shims/next-navigation.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {
|
||||
useLocation,
|
||||
useRouter as useTanstackRouter,
|
||||
} from "@tanstack/react-router";
|
||||
|
||||
export const usePathname = () => {
|
||||
return useLocation().pathname;
|
||||
};
|
||||
|
||||
export const useSearchParams = () => {
|
||||
const location = useLocation();
|
||||
return new URLSearchParams(location.searchStr ?? "");
|
||||
};
|
||||
|
||||
export const useParams = () => {
|
||||
return useLocation().pathname;
|
||||
};
|
||||
|
||||
export const useRouter = () => {
|
||||
const router = useTanstackRouter();
|
||||
return {
|
||||
push: (href: string) => router.history.push(href),
|
||||
replace: (href: string) => router.history.replace(href),
|
||||
back: () => router.history.back(),
|
||||
forward: () => router.history.forward(),
|
||||
refresh: () => router.invalidate(),
|
||||
prefetch: (_href: string) => {},
|
||||
};
|
||||
};
|
||||
110
apps/vite/src/shims/next-router.ts
Normal file
110
apps/vite/src/shims/next-router.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import {
|
||||
type AnyRouter,
|
||||
useLocation,
|
||||
useParams,
|
||||
useSearch,
|
||||
useRouter as useTanstackRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type NextUrl =
|
||||
| string
|
||||
| {
|
||||
pathname?: string | null;
|
||||
query?: Record<string, unknown> | null;
|
||||
hash?: string | null;
|
||||
};
|
||||
|
||||
let routerInstance: AnyRouter | null = null;
|
||||
|
||||
export const setRouterInstance = (router: AnyRouter) => {
|
||||
routerInstance = router;
|
||||
};
|
||||
|
||||
const buildHref = (url: NextUrl, currentPathname: string): string => {
|
||||
if (typeof url === "string") {
|
||||
if (url.startsWith("?")) return `${currentPathname}${url}`;
|
||||
return url;
|
||||
}
|
||||
const pathname = url.pathname ?? currentPathname;
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(url.query ?? {})) {
|
||||
if (value === undefined || value === null) continue;
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) params.append(key, String(v));
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
const search = params.toString();
|
||||
const hash = url.hash ? `#${url.hash.replace(/^#/, "")}` : "";
|
||||
return `${pathname}${search ? `?${search}` : ""}${hash}`;
|
||||
};
|
||||
|
||||
const routerEvents = {
|
||||
on: (_event: string, _handler: (...args: unknown[]) => void) => {},
|
||||
off: (_event: string, _handler: (...args: unknown[]) => void) => {},
|
||||
emit: (_event: string, ..._args: unknown[]) => {},
|
||||
};
|
||||
|
||||
export const useRouter = () => {
|
||||
const tanstackRouter = useTanstackRouter();
|
||||
const location = useLocation();
|
||||
const params = useParams({ strict: false }) as Record<string, string>;
|
||||
const search = useSearch({ strict: false }) as Record<string, unknown>;
|
||||
|
||||
return useMemo(() => {
|
||||
const pathname = location.pathname;
|
||||
return {
|
||||
pathname,
|
||||
route: pathname,
|
||||
asPath: `${pathname}${location.searchStr ?? ""}${location.hash ? `#${location.hash}` : ""}`,
|
||||
query: { ...search, ...params } as Record<string, string | string[]>,
|
||||
isReady: true,
|
||||
events: routerEvents,
|
||||
push: (url: NextUrl, _as?: unknown, _options?: unknown) => {
|
||||
tanstackRouter.history.push(buildHref(url, pathname));
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
replace: (url: NextUrl, _as?: unknown, _options?: unknown) => {
|
||||
tanstackRouter.history.replace(buildHref(url, pathname));
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
back: () => tanstackRouter.history.back(),
|
||||
forward: () => tanstackRouter.history.forward(),
|
||||
reload: () => window.location.reload(),
|
||||
prefetch: (_url: string) => Promise.resolve(),
|
||||
};
|
||||
}, [
|
||||
location.pathname,
|
||||
location.searchStr,
|
||||
location.hash,
|
||||
params,
|
||||
search,
|
||||
tanstackRouter,
|
||||
]);
|
||||
};
|
||||
|
||||
const singletonRouter = {
|
||||
push: (url: NextUrl) => {
|
||||
if (routerInstance) {
|
||||
routerInstance.history.push(buildHref(url, window.location.pathname));
|
||||
} else {
|
||||
window.location.href = buildHref(url, window.location.pathname);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
replace: (url: NextUrl) => {
|
||||
if (routerInstance) {
|
||||
routerInstance.history.replace(buildHref(url, window.location.pathname));
|
||||
} else {
|
||||
window.location.replace(buildHref(url, window.location.pathname));
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
back: () => window.history.back(),
|
||||
reload: () => window.location.reload(),
|
||||
events: routerEvents,
|
||||
};
|
||||
|
||||
export default singletonRouter;
|
||||
40
apps/vite/src/shims/next-script.tsx
Normal file
40
apps/vite/src/shims/next-script.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface ScriptProps {
|
||||
src?: string;
|
||||
id?: string;
|
||||
strategy?: string;
|
||||
onLoad?: () => void;
|
||||
onError?: () => void;
|
||||
children?: string;
|
||||
dangerouslySetInnerHTML?: { __html: string };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const Script = ({
|
||||
src,
|
||||
id,
|
||||
strategy: _strategy,
|
||||
onLoad,
|
||||
onError,
|
||||
children,
|
||||
dangerouslySetInnerHTML,
|
||||
}: ScriptProps) => {
|
||||
useEffect(() => {
|
||||
if (src && document.querySelector(`script[src="${src}"]`)) return;
|
||||
if (id && document.getElementById(id)) return;
|
||||
|
||||
const script = document.createElement("script");
|
||||
if (src) script.src = src;
|
||||
if (id) script.id = id;
|
||||
const inline = dangerouslySetInnerHTML?.__html ?? children;
|
||||
if (!src && inline) script.textContent = inline;
|
||||
if (onLoad) script.onload = onLoad;
|
||||
if (onError) script.onerror = onError;
|
||||
document.body.appendChild(script);
|
||||
}, [src, id]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Script;
|
||||
37
apps/vite/src/shims/toploader.tsx
Normal file
37
apps/vite/src/shims/toploader.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface TopLoaderProps {
|
||||
color?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const TopLoader = ({ color, height = 3 }: TopLoaderProps) => {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubStart = router.subscribe("onBeforeNavigate", () =>
|
||||
setLoading(true),
|
||||
);
|
||||
const unsubEnd = router.subscribe("onResolved", () => setLoading(false));
|
||||
return () => {
|
||||
unsubStart();
|
||||
unsubEnd();
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
if (!loading) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-x-0 top-0 z-[9999] animate-pulse"
|
||||
style={{
|
||||
height,
|
||||
background: color ?? "hsl(var(--sidebar-ring))",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopLoader;
|
||||
539
apps/vite/src/styles/globals.css
Normal file
539
apps/vite/src/styles/globals.css
Normal file
@ -0,0 +1,539 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@plugin "tailwindcss-animate";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "fancy-ansi/plugin";
|
||||
|
||||
@source "../../../dokploy/components";
|
||||
@source "../../../dokploy/lib";
|
||||
@source "../../../dokploy/utils";
|
||||
@source "../../../dokploy/hooks";
|
||||
@source "../";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--font-inter: "Inter";
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
@media (width >= --theme(--breakpoint-3xl)) {
|
||||
max-width: none;
|
||||
}
|
||||
@media (width >= 87.5rem) {
|
||||
max-width: 87.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans:
|
||||
var(--font-inter), ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
|
||||
--breakpoint-3xl: 1920px;
|
||||
|
||||
--container-2xl: 40rem;
|
||||
--container-8xl: 85rem;
|
||||
--container-9xl: 95rem;
|
||||
--container-10xl: 105rem;
|
||||
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--color-sidebar: hsl(var(--sidebar-background));
|
||||
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
||||
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
||||
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
||||
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
||||
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
||||
--color-sidebar-border: hsl(var(--sidebar-border));
|
||||
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--animate-caret-blink: caret-blink 1.25s ease-out infinite;
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes caret-blink {
|
||||
0%,
|
||||
70%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
20%,
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
/* Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
@utility custom-logs-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--muted-foreground)) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--muted-foreground) / 0.3);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--terminal-paste: rgba(0, 0, 0, 0.2);
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 50.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
--overlay: rgba(0, 0, 0, 0.2);
|
||||
|
||||
--chart-1: 173 58% 39%;
|
||||
--chart-2: 12 76% 61%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--terminal-paste: rgba(255, 255, 255, 0.2);
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 4% 10%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 4% 10%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 84.2% 50.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 4% 10%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-5: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-2: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* Cursor pointer on buttons */
|
||||
button:not([disabled]),
|
||||
[role="button"]:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.3125rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 0.3125rem;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.xterm-viewport {
|
||||
border-radius: 0.75rem /* 12px */ !important;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Codemirror */
|
||||
.cm-editor {
|
||||
@apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-hidden;
|
||||
}
|
||||
|
||||
.cm-editor .cm-scroller {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused {
|
||||
@apply outline-hidden;
|
||||
}
|
||||
|
||||
/* fix: placeholder bg */
|
||||
.cm-editor .cm-activeLine:has(.cm-placeholder) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.compose-file-editor .cm-editor {
|
||||
@apply min-h-100;
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
75% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-heartbeat {
|
||||
animation: heartbeat 2.5s infinite;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.swagger-ui {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.swagger-ui .info {
|
||||
margin: 0px !important;
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-logs-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--muted-foreground)) transparent;
|
||||
}
|
||||
|
||||
.custom-logs-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.custom-logs-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-logs-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--muted-foreground) / 0.3);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.custom-logs-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
/* Docker Logs Scrollbar */
|
||||
}
|
||||
|
||||
.xterm-bg-257.xterm-fg-257 {
|
||||
background-color: var(--terminal-paste) !important;
|
||||
color: currentColor !important;
|
||||
}
|
||||
|
||||
.cm-content,
|
||||
.cm-lineWrapping {
|
||||
@apply font-mono;
|
||||
}
|
||||
|
||||
/* HubSpot Widget - Force light color-scheme to prevent white background */
|
||||
#hubspot-messages-iframe-container,
|
||||
#hubspot-messages-iframe-container * {
|
||||
background-color: transparent !important;
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
#hubspot-messages-iframe-container .hs-shadow-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#hubspot-conversations-iframe {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
8
apps/vite/src/utils/api.ts
Normal file
8
apps/vite/src/utils/api.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||
import type { AppRouter } from "@/server/api/root";
|
||||
|
||||
export const api = createTRPCReact<AppRouter>();
|
||||
|
||||
export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||
20
apps/vite/src/utils/session.ts
Normal file
20
apps/vite/src/utils/session.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
type SessionData = Awaited<ReturnType<typeof authClient.getSession>>["data"];
|
||||
|
||||
let cached: { at: number; session: SessionData } | null = null;
|
||||
|
||||
const TTL_MS = 30_000;
|
||||
|
||||
export const getCachedSession = async (): Promise<SessionData> => {
|
||||
if (cached && Date.now() - cached.at < TTL_MS) {
|
||||
return cached.session;
|
||||
}
|
||||
const { data } = await authClient.getSession();
|
||||
cached = { at: Date.now(), session: data };
|
||||
return data;
|
||||
};
|
||||
|
||||
export const clearSessionCache = () => {
|
||||
cached = null;
|
||||
};
|
||||
63
apps/vite/src/utils/trpc-provider.tsx
Normal file
63
apps/vite/src/utils/trpc-provider.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import {
|
||||
createWSClient,
|
||||
httpBatchLink,
|
||||
httpLink,
|
||||
splitLink,
|
||||
wsLink,
|
||||
} from "@trpc/client";
|
||||
import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { api } from "./api";
|
||||
|
||||
const getWsUrl = () => {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${window.location.host}/drawer-logs`;
|
||||
};
|
||||
|
||||
let wsClientSingleton: ReturnType<typeof createWSClient> | null = null;
|
||||
|
||||
const getOrCreateWSClient = () => {
|
||||
if (!wsClientSingleton) {
|
||||
wsClientSingleton = createWSClient({
|
||||
url: getWsUrl(),
|
||||
lazy: { enabled: true, closeMs: 3000 },
|
||||
retryDelayMs: () => 3000,
|
||||
});
|
||||
}
|
||||
return wsClientSingleton;
|
||||
};
|
||||
|
||||
const createLinks = () => [
|
||||
splitLink({
|
||||
condition: (op) => op.type === "subscription",
|
||||
true: wsLink({
|
||||
client: getOrCreateWSClient(),
|
||||
transformer: superjson,
|
||||
}),
|
||||
false: splitLink({
|
||||
condition: (op) => op.input instanceof FormData,
|
||||
true: httpLink({
|
||||
url: "/api/trpc",
|
||||
transformer: superjson,
|
||||
}),
|
||||
false: httpBatchLink({
|
||||
url: "/api/trpc",
|
||||
transformer: superjson,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
export const TRPCProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
const [trpcClient] = useState(() =>
|
||||
api.createClient({ links: createLinks() }),
|
||||
);
|
||||
|
||||
return (
|
||||
<api.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</api.Provider>
|
||||
);
|
||||
};
|
||||
36
apps/vite/tsconfig.json
Normal file
36
apps/vite/tsconfig.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "es2022",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vite/client", "node"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"@/utils/api": ["./src/utils/api.ts"],
|
||||
"next/link": ["./src/shims/next-link.tsx"],
|
||||
"next/router": ["./src/shims/next-router.ts"],
|
||||
"next/navigation": ["./src/shims/next-navigation.ts"],
|
||||
"next/head": ["./src/shims/next-head.tsx"],
|
||||
"next/dynamic": ["./src/shims/next-dynamic.tsx"],
|
||||
"next/script": ["./src/shims/next-script.tsx"],
|
||||
"nextjs-toploader": ["./src/shims/toploader.tsx"],
|
||||
"@/*": ["../dokploy/*"],
|
||||
"@dokploy/server": ["../../packages/server/src/index.ts"],
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "server", "vite.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
81
apps/vite/vite.config.ts
Normal file
81
apps/vite/vite.config.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import path from "node:path";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const dokployApp = path.resolve(__dirname, "../dokploy");
|
||||
const serverPkg = path.resolve(__dirname, "../../packages/server/src");
|
||||
|
||||
const wsPaths = [
|
||||
"/drawer-logs",
|
||||
"/docker-container-logs",
|
||||
"/docker-container-terminal",
|
||||
"/listen-deployment",
|
||||
"/listen-docker-stats-monitoring",
|
||||
"/terminal",
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tanstackRouter({ target: "react", autoCodeSplitting: true }),
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
publicDir: path.resolve(dokployApp, "public"),
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@\/utils\/api$/,
|
||||
replacement: path.resolve(__dirname, "src/utils/api.ts"),
|
||||
},
|
||||
{
|
||||
find: /^next\/link$/,
|
||||
replacement: path.resolve(__dirname, "src/shims/next-link.tsx"),
|
||||
},
|
||||
{
|
||||
find: /^next\/router$/,
|
||||
replacement: path.resolve(__dirname, "src/shims/next-router.ts"),
|
||||
},
|
||||
{
|
||||
find: /^next\/navigation$/,
|
||||
replacement: path.resolve(__dirname, "src/shims/next-navigation.ts"),
|
||||
},
|
||||
{
|
||||
find: /^next\/head$/,
|
||||
replacement: path.resolve(__dirname, "src/shims/next-head.tsx"),
|
||||
},
|
||||
{
|
||||
find: /^next\/dynamic$/,
|
||||
replacement: path.resolve(__dirname, "src/shims/next-dynamic.tsx"),
|
||||
},
|
||||
{
|
||||
find: /^next\/script$/,
|
||||
replacement: path.resolve(__dirname, "src/shims/next-script.tsx"),
|
||||
},
|
||||
{
|
||||
find: /^nextjs-toploader$/,
|
||||
replacement: path.resolve(__dirname, "src/shims/toploader.tsx"),
|
||||
},
|
||||
{ find: /^~\//, replacement: `${path.resolve(__dirname, "src")}/` },
|
||||
{ find: /^@\//, replacement: `${dokployApp}/` },
|
||||
{ find: /^@dokploy\/server\//, replacement: `${serverPkg}/` },
|
||||
{
|
||||
find: /^@dokploy\/server$/,
|
||||
replacement: path.resolve(serverPkg, "index.ts"),
|
||||
},
|
||||
],
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3000",
|
||||
headers: { origin: "http://localhost:3000" },
|
||||
},
|
||||
...Object.fromEntries(
|
||||
wsPaths.map((p) => [p, { target: "ws://localhost:3000", ws: true }]),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -13,6 +13,7 @@
|
||||
"!**/.next/**",
|
||||
"!**/dist",
|
||||
"!**/drizzle/**",
|
||||
"!**/routeTree.gen.ts",
|
||||
"!node_modules/**",
|
||||
"!packages/server/package.json"
|
||||
],
|
||||
|
||||
@ -48,6 +48,7 @@ const resolveTrustedOrigins = async () => {
|
||||
? [
|
||||
"http://localhost:3000",
|
||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
||||
"http://localhost:5173"
|
||||
]
|
||||
: [];
|
||||
return [
|
||||
|
||||
881
pnpm-lock.yaml
881
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ packages:
|
||||
- "apps/api"
|
||||
- "apps/dokploy"
|
||||
- "apps/schedules"
|
||||
- "apps/vite"
|
||||
- "packages/server"
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user