diff --git a/.dockerignore b/.dockerignore index 958b26c9d..29bf51675 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,8 @@ node_modules .git .gitignore *.md -dist \ No newline at end of file +dist +**/.next +**/dist +**/dist-server +**/.env \ No newline at end of file diff --git a/.gitignore b/.gitignore index 602556df8..70f6b8d11 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,8 @@ yarn-error.log* .db .playwright-* -.credentials \ No newline at end of file +.credentials +dist/ +dist-server/ +.tanstack +.env.* \ No newline at end of file diff --git a/Dockerfile.vite b/Dockerfile.vite new file mode 100644 index 000000000..bf0974d7e --- /dev/null +++ b/Dockerfile.vite @@ -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"] diff --git a/apps/vite/README.md b/apps/vite/README.md new file mode 100644 index 000000000..60cdae526 --- /dev/null +++ b/apps/vite/README.md @@ -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`. diff --git a/apps/vite/esbuild.config.ts b/apps/vite/esbuild.config.ts new file mode 100644 index 000000000..3d1af4da9 --- /dev/null +++ b/apps/vite/esbuild.config.ts @@ -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)); diff --git a/apps/vite/index.html b/apps/vite/index.html new file mode 100644 index 000000000..faa85f245 --- /dev/null +++ b/apps/vite/index.html @@ -0,0 +1,19 @@ + + + + + + + Dokploy + + + + + +
+ + + diff --git a/apps/vite/package.json b/apps/vite/package.json new file mode 100644 index 000000000..ab3542b8d --- /dev/null +++ b/apps/vite/package.json @@ -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" + } +} diff --git a/apps/vite/server/next-compat.ts b/apps/vite/server/next-compat.ts new file mode 100644 index 000000000..9d950a5ee --- /dev/null +++ b/apps/vite/server/next-compat.ts @@ -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; + +interface RunOptions { + params?: Record; + parseBody?: boolean; +} + +const parseCookies = (header: string | undefined) => { + const cookies: Record = {}; + 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) => { + const query: Record = {}; + 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)); +}; diff --git a/apps/vite/server/server.ts b/apps/vite/server/server.ts new file mode 100644 index 000000000..0dbd970ff --- /dev/null +++ b/apps/vite/server/server.ts @@ -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); +}); diff --git a/apps/vite/server/static.ts b/apps/vite/server/static.ts new file mode 100644 index 000000000..86179e824 --- /dev/null +++ b/apps/vite/server/static.ts @@ -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 = { + ".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", + ); + }; +}; diff --git a/apps/vite/src/main.tsx b/apps/vite/src/main.tsx new file mode 100644 index 000000000..3222bfacc --- /dev/null +++ b/apps/vite/src/main.tsx @@ -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( + + + , + ); +} diff --git a/apps/vite/src/routeTree.gen.ts b/apps/vite/src/routeTree.gen.ts new file mode 100644 index 000000000..6eae0fb25 --- /dev/null +++ b/apps/vite/src/routeTree.gen.ts @@ -0,0 +1,1111 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DashboardRouteRouteImport } from './routes/dashboard/route' +import { Route as InvitationRouteImport } from './routes/invitation' +import { Route as RegisterRouteImport } from './routes/register' +import { Route as ResetPasswordRouteImport } from './routes/reset-password' +import { Route as SendResetPasswordRouteImport } from './routes/send-reset-password' +import { Route as SwaggerRouteImport } from './routes/swagger' +import { Route as AcceptInvitationInvitationIdRouteImport } from './routes/accept-invitation/$invitationId' +import { Route as DashboardDeploymentsRouteImport } from './routes/dashboard/deployments' +import { Route as DashboardDockerRouteImport } from './routes/dashboard/docker' +import { Route as DashboardHomeRouteImport } from './routes/dashboard/home' +import { Route as DashboardMonitoringRouteImport } from './routes/dashboard/monitoring' +import { Route as DashboardNetworksRouteImport } from './routes/dashboard/networks' +import { Route as DashboardProjectsRouteImport } from './routes/dashboard/projects' +import { Route as DashboardRequestsRouteImport } from './routes/dashboard/requests' +import { Route as DashboardSchedulesRouteImport } from './routes/dashboard/schedules' +import { Route as DashboardSwarmRouteImport } from './routes/dashboard/swarm' +import { Route as DashboardTraefikRouteImport } from './routes/dashboard/traefik' +import { Route as DashboardSettingsAiRouteImport } from './routes/dashboard/settings/ai' +import { Route as DashboardSettingsAuditLogsRouteImport } from './routes/dashboard/settings/audit-logs' +import { Route as DashboardSettingsBillingRouteImport } from './routes/dashboard/settings/billing' +import { Route as DashboardSettingsCertificatesRouteImport } from './routes/dashboard/settings/certificates' +import { Route as DashboardSettingsClusterRouteImport } from './routes/dashboard/settings/cluster' +import { Route as DashboardSettingsDeploymentsRouteImport } from './routes/dashboard/settings/deployments' +import { Route as DashboardSettingsDestinationsRouteImport } from './routes/dashboard/settings/destinations' +import { Route as DashboardSettingsGitProvidersRouteImport } from './routes/dashboard/settings/git-providers' +import { Route as DashboardSettingsInvoicesRouteImport } from './routes/dashboard/settings/invoices' +import { Route as DashboardSettingsLicenseRouteImport } from './routes/dashboard/settings/license' +import { Route as DashboardSettingsNotificationsRouteImport } from './routes/dashboard/settings/notifications' +import { Route as DashboardSettingsProfileRouteImport } from './routes/dashboard/settings/profile' +import { Route as DashboardSettingsRegistryRouteImport } from './routes/dashboard/settings/registry' +import { Route as DashboardSettingsSecretsRouteImport } from './routes/dashboard/settings/secrets' +import { Route as DashboardSettingsServerRouteImport } from './routes/dashboard/settings/server' +import { Route as DashboardSettingsServersRouteImport } from './routes/dashboard/settings/servers' +import { Route as DashboardSettingsSshKeysRouteImport } from './routes/dashboard/settings/ssh-keys' +import { Route as DashboardSettingsSsoRouteImport } from './routes/dashboard/settings/sso' +import { Route as DashboardSettingsTagsRouteImport } from './routes/dashboard/settings/tags' +import { Route as DashboardSettingsUsersRouteImport } from './routes/dashboard/settings/users' +import { Route as DashboardSettingsWhitelabelingRouteImport } from './routes/dashboard/settings/whitelabeling' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/index' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId' +import { Route as DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRouteImport } from './routes/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardRouteRoute = DashboardRouteRouteImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => rootRouteImport, +} as any) +const InvitationRoute = InvitationRouteImport.update({ + id: '/invitation', + path: '/invitation', + getParentRoute: () => rootRouteImport, +} as any) +const RegisterRoute = RegisterRouteImport.update({ + id: '/register', + path: '/register', + getParentRoute: () => rootRouteImport, +} as any) +const ResetPasswordRoute = ResetPasswordRouteImport.update({ + id: '/reset-password', + path: '/reset-password', + getParentRoute: () => rootRouteImport, +} as any) +const SendResetPasswordRoute = SendResetPasswordRouteImport.update({ + id: '/send-reset-password', + path: '/send-reset-password', + getParentRoute: () => rootRouteImport, +} as any) +const SwaggerRoute = SwaggerRouteImport.update({ + id: '/swagger', + path: '/swagger', + getParentRoute: () => rootRouteImport, +} as any) +const AcceptInvitationInvitationIdRoute = + AcceptInvitationInvitationIdRouteImport.update({ + id: '/accept-invitation/$invitationId', + path: '/accept-invitation/$invitationId', + getParentRoute: () => rootRouteImport, + } as any) +const DashboardDeploymentsRoute = DashboardDeploymentsRouteImport.update({ + id: '/deployments', + path: '/deployments', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardDockerRoute = DashboardDockerRouteImport.update({ + id: '/docker', + path: '/docker', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardHomeRoute = DashboardHomeRouteImport.update({ + id: '/home', + path: '/home', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardMonitoringRoute = DashboardMonitoringRouteImport.update({ + id: '/monitoring', + path: '/monitoring', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardNetworksRoute = DashboardNetworksRouteImport.update({ + id: '/networks', + path: '/networks', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardProjectsRoute = DashboardProjectsRouteImport.update({ + id: '/projects', + path: '/projects', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardRequestsRoute = DashboardRequestsRouteImport.update({ + id: '/requests', + path: '/requests', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSchedulesRoute = DashboardSchedulesRouteImport.update({ + id: '/schedules', + path: '/schedules', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSwarmRoute = DashboardSwarmRouteImport.update({ + id: '/swarm', + path: '/swarm', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardTraefikRoute = DashboardTraefikRouteImport.update({ + id: '/traefik', + path: '/traefik', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSettingsAiRoute = DashboardSettingsAiRouteImport.update({ + id: '/settings/ai', + path: '/settings/ai', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSettingsAuditLogsRoute = + DashboardSettingsAuditLogsRouteImport.update({ + id: '/settings/audit-logs', + path: '/settings/audit-logs', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsBillingRoute = + DashboardSettingsBillingRouteImport.update({ + id: '/settings/billing', + path: '/settings/billing', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsCertificatesRoute = + DashboardSettingsCertificatesRouteImport.update({ + id: '/settings/certificates', + path: '/settings/certificates', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsClusterRoute = + DashboardSettingsClusterRouteImport.update({ + id: '/settings/cluster', + path: '/settings/cluster', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsDeploymentsRoute = + DashboardSettingsDeploymentsRouteImport.update({ + id: '/settings/deployments', + path: '/settings/deployments', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsDestinationsRoute = + DashboardSettingsDestinationsRouteImport.update({ + id: '/settings/destinations', + path: '/settings/destinations', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsGitProvidersRoute = + DashboardSettingsGitProvidersRouteImport.update({ + id: '/settings/git-providers', + path: '/settings/git-providers', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsInvoicesRoute = + DashboardSettingsInvoicesRouteImport.update({ + id: '/settings/invoices', + path: '/settings/invoices', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsLicenseRoute = + DashboardSettingsLicenseRouteImport.update({ + id: '/settings/license', + path: '/settings/license', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsNotificationsRoute = + DashboardSettingsNotificationsRouteImport.update({ + id: '/settings/notifications', + path: '/settings/notifications', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsProfileRoute = + DashboardSettingsProfileRouteImport.update({ + id: '/settings/profile', + path: '/settings/profile', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsRegistryRoute = + DashboardSettingsRegistryRouteImport.update({ + id: '/settings/registry', + path: '/settings/registry', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsSecretsRoute = + DashboardSettingsSecretsRouteImport.update({ + id: '/settings/secrets', + path: '/settings/secrets', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsServerRoute = DashboardSettingsServerRouteImport.update({ + id: '/settings/server', + path: '/settings/server', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSettingsServersRoute = + DashboardSettingsServersRouteImport.update({ + id: '/settings/servers', + path: '/settings/servers', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsSshKeysRoute = + DashboardSettingsSshKeysRouteImport.update({ + id: '/settings/ssh-keys', + path: '/settings/ssh-keys', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardSettingsSsoRoute = DashboardSettingsSsoRouteImport.update({ + id: '/settings/sso', + path: '/settings/sso', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSettingsTagsRoute = DashboardSettingsTagsRouteImport.update({ + id: '/settings/tags', + path: '/settings/tags', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSettingsUsersRoute = DashboardSettingsUsersRouteImport.update({ + id: '/settings/users', + path: '/settings/users', + getParentRoute: () => DashboardRouteRoute, +} as any) +const DashboardSettingsWhitelabelingRoute = + DashboardSettingsWhitelabelingRouteImport.update({ + id: '/settings/whitelabeling', + path: '/settings/whitelabeling', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRouteImport.update({ + id: '/project/$projectId/environment/$environmentId/', + path: '/project/$projectId/environment/$environmentId/', + getParentRoute: () => DashboardRouteRoute, + } as any) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/application/$applicationId', + path: '/project/$projectId/environment/$environmentId/services/application/$applicationId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/compose/$composeId', + path: '/project/$projectId/environment/$environmentId/services/compose/$composeId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/libsql/$libsqlId', + path: '/project/$projectId/environment/$environmentId/services/libsql/$libsqlId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId', + path: '/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/mongo/$mongoId', + path: '/project/$projectId/environment/$environmentId/services/mongo/$mongoId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/mysql/$mysqlId', + path: '/project/$projectId/environment/$environmentId/services/mysql/$mysqlId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/postgres/$postgresId', + path: '/project/$projectId/environment/$environmentId/services/postgres/$postgresId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) +const DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute = + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/services/redis/$redisId', + path: '/project/$projectId/environment/$environmentId/services/redis/$redisId', + getParentRoute: () => DashboardRouteRoute, + } as any, + ) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteRouteWithChildren + '/invitation': typeof InvitationRoute + '/register': typeof RegisterRoute + '/reset-password': typeof ResetPasswordRoute + '/send-reset-password': typeof SendResetPasswordRoute + '/swagger': typeof SwaggerRoute + '/accept-invitation/$invitationId': typeof AcceptInvitationInvitationIdRoute + '/dashboard/deployments': typeof DashboardDeploymentsRoute + '/dashboard/docker': typeof DashboardDockerRoute + '/dashboard/home': typeof DashboardHomeRoute + '/dashboard/monitoring': typeof DashboardMonitoringRoute + '/dashboard/networks': typeof DashboardNetworksRoute + '/dashboard/projects': typeof DashboardProjectsRoute + '/dashboard/requests': typeof DashboardRequestsRoute + '/dashboard/schedules': typeof DashboardSchedulesRoute + '/dashboard/swarm': typeof DashboardSwarmRoute + '/dashboard/traefik': typeof DashboardTraefikRoute + '/dashboard/settings/ai': typeof DashboardSettingsAiRoute + '/dashboard/settings/audit-logs': typeof DashboardSettingsAuditLogsRoute + '/dashboard/settings/billing': typeof DashboardSettingsBillingRoute + '/dashboard/settings/certificates': typeof DashboardSettingsCertificatesRoute + '/dashboard/settings/cluster': typeof DashboardSettingsClusterRoute + '/dashboard/settings/deployments': typeof DashboardSettingsDeploymentsRoute + '/dashboard/settings/destinations': typeof DashboardSettingsDestinationsRoute + '/dashboard/settings/git-providers': typeof DashboardSettingsGitProvidersRoute + '/dashboard/settings/invoices': typeof DashboardSettingsInvoicesRoute + '/dashboard/settings/license': typeof DashboardSettingsLicenseRoute + '/dashboard/settings/notifications': typeof DashboardSettingsNotificationsRoute + '/dashboard/settings/profile': typeof DashboardSettingsProfileRoute + '/dashboard/settings/registry': typeof DashboardSettingsRegistryRoute + '/dashboard/settings/secrets': typeof DashboardSettingsSecretsRoute + '/dashboard/settings/server': typeof DashboardSettingsServerRoute + '/dashboard/settings/servers': typeof DashboardSettingsServersRoute + '/dashboard/settings/ssh-keys': typeof DashboardSettingsSshKeysRoute + '/dashboard/settings/sso': typeof DashboardSettingsSsoRoute + '/dashboard/settings/tags': typeof DashboardSettingsTagsRoute + '/dashboard/settings/users': typeof DashboardSettingsUsersRoute + '/dashboard/settings/whitelabeling': typeof DashboardSettingsWhitelabelingRoute + '/dashboard/project/$projectId/environment/$environmentId/': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute + '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteRouteWithChildren + '/invitation': typeof InvitationRoute + '/register': typeof RegisterRoute + '/reset-password': typeof ResetPasswordRoute + '/send-reset-password': typeof SendResetPasswordRoute + '/swagger': typeof SwaggerRoute + '/accept-invitation/$invitationId': typeof AcceptInvitationInvitationIdRoute + '/dashboard/deployments': typeof DashboardDeploymentsRoute + '/dashboard/docker': typeof DashboardDockerRoute + '/dashboard/home': typeof DashboardHomeRoute + '/dashboard/monitoring': typeof DashboardMonitoringRoute + '/dashboard/networks': typeof DashboardNetworksRoute + '/dashboard/projects': typeof DashboardProjectsRoute + '/dashboard/requests': typeof DashboardRequestsRoute + '/dashboard/schedules': typeof DashboardSchedulesRoute + '/dashboard/swarm': typeof DashboardSwarmRoute + '/dashboard/traefik': typeof DashboardTraefikRoute + '/dashboard/settings/ai': typeof DashboardSettingsAiRoute + '/dashboard/settings/audit-logs': typeof DashboardSettingsAuditLogsRoute + '/dashboard/settings/billing': typeof DashboardSettingsBillingRoute + '/dashboard/settings/certificates': typeof DashboardSettingsCertificatesRoute + '/dashboard/settings/cluster': typeof DashboardSettingsClusterRoute + '/dashboard/settings/deployments': typeof DashboardSettingsDeploymentsRoute + '/dashboard/settings/destinations': typeof DashboardSettingsDestinationsRoute + '/dashboard/settings/git-providers': typeof DashboardSettingsGitProvidersRoute + '/dashboard/settings/invoices': typeof DashboardSettingsInvoicesRoute + '/dashboard/settings/license': typeof DashboardSettingsLicenseRoute + '/dashboard/settings/notifications': typeof DashboardSettingsNotificationsRoute + '/dashboard/settings/profile': typeof DashboardSettingsProfileRoute + '/dashboard/settings/registry': typeof DashboardSettingsRegistryRoute + '/dashboard/settings/secrets': typeof DashboardSettingsSecretsRoute + '/dashboard/settings/server': typeof DashboardSettingsServerRoute + '/dashboard/settings/servers': typeof DashboardSettingsServersRoute + '/dashboard/settings/ssh-keys': typeof DashboardSettingsSshKeysRoute + '/dashboard/settings/sso': typeof DashboardSettingsSsoRoute + '/dashboard/settings/tags': typeof DashboardSettingsTagsRoute + '/dashboard/settings/users': typeof DashboardSettingsUsersRoute + '/dashboard/settings/whitelabeling': typeof DashboardSettingsWhitelabelingRoute + '/dashboard/project/$projectId/environment/$environmentId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute + '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteRouteWithChildren + '/invitation': typeof InvitationRoute + '/register': typeof RegisterRoute + '/reset-password': typeof ResetPasswordRoute + '/send-reset-password': typeof SendResetPasswordRoute + '/swagger': typeof SwaggerRoute + '/accept-invitation/$invitationId': typeof AcceptInvitationInvitationIdRoute + '/dashboard/deployments': typeof DashboardDeploymentsRoute + '/dashboard/docker': typeof DashboardDockerRoute + '/dashboard/home': typeof DashboardHomeRoute + '/dashboard/monitoring': typeof DashboardMonitoringRoute + '/dashboard/networks': typeof DashboardNetworksRoute + '/dashboard/projects': typeof DashboardProjectsRoute + '/dashboard/requests': typeof DashboardRequestsRoute + '/dashboard/schedules': typeof DashboardSchedulesRoute + '/dashboard/swarm': typeof DashboardSwarmRoute + '/dashboard/traefik': typeof DashboardTraefikRoute + '/dashboard/settings/ai': typeof DashboardSettingsAiRoute + '/dashboard/settings/audit-logs': typeof DashboardSettingsAuditLogsRoute + '/dashboard/settings/billing': typeof DashboardSettingsBillingRoute + '/dashboard/settings/certificates': typeof DashboardSettingsCertificatesRoute + '/dashboard/settings/cluster': typeof DashboardSettingsClusterRoute + '/dashboard/settings/deployments': typeof DashboardSettingsDeploymentsRoute + '/dashboard/settings/destinations': typeof DashboardSettingsDestinationsRoute + '/dashboard/settings/git-providers': typeof DashboardSettingsGitProvidersRoute + '/dashboard/settings/invoices': typeof DashboardSettingsInvoicesRoute + '/dashboard/settings/license': typeof DashboardSettingsLicenseRoute + '/dashboard/settings/notifications': typeof DashboardSettingsNotificationsRoute + '/dashboard/settings/profile': typeof DashboardSettingsProfileRoute + '/dashboard/settings/registry': typeof DashboardSettingsRegistryRoute + '/dashboard/settings/secrets': typeof DashboardSettingsSecretsRoute + '/dashboard/settings/server': typeof DashboardSettingsServerRoute + '/dashboard/settings/servers': typeof DashboardSettingsServersRoute + '/dashboard/settings/ssh-keys': typeof DashboardSettingsSshKeysRoute + '/dashboard/settings/sso': typeof DashboardSettingsSsoRoute + '/dashboard/settings/tags': typeof DashboardSettingsTagsRoute + '/dashboard/settings/users': typeof DashboardSettingsUsersRoute + '/dashboard/settings/whitelabeling': typeof DashboardSettingsWhitelabelingRoute + '/dashboard/project/$projectId/environment/$environmentId/': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute + '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute + '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId': typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/dashboard' + | '/invitation' + | '/register' + | '/reset-password' + | '/send-reset-password' + | '/swagger' + | '/accept-invitation/$invitationId' + | '/dashboard/deployments' + | '/dashboard/docker' + | '/dashboard/home' + | '/dashboard/monitoring' + | '/dashboard/networks' + | '/dashboard/projects' + | '/dashboard/requests' + | '/dashboard/schedules' + | '/dashboard/swarm' + | '/dashboard/traefik' + | '/dashboard/settings/ai' + | '/dashboard/settings/audit-logs' + | '/dashboard/settings/billing' + | '/dashboard/settings/certificates' + | '/dashboard/settings/cluster' + | '/dashboard/settings/deployments' + | '/dashboard/settings/destinations' + | '/dashboard/settings/git-providers' + | '/dashboard/settings/invoices' + | '/dashboard/settings/license' + | '/dashboard/settings/notifications' + | '/dashboard/settings/profile' + | '/dashboard/settings/registry' + | '/dashboard/settings/secrets' + | '/dashboard/settings/server' + | '/dashboard/settings/servers' + | '/dashboard/settings/ssh-keys' + | '/dashboard/settings/sso' + | '/dashboard/settings/tags' + | '/dashboard/settings/users' + | '/dashboard/settings/whitelabeling' + | '/dashboard/project/$projectId/environment/$environmentId/' + | '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId' + | '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId' + | '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId' + | '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId' + | '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/dashboard' + | '/invitation' + | '/register' + | '/reset-password' + | '/send-reset-password' + | '/swagger' + | '/accept-invitation/$invitationId' + | '/dashboard/deployments' + | '/dashboard/docker' + | '/dashboard/home' + | '/dashboard/monitoring' + | '/dashboard/networks' + | '/dashboard/projects' + | '/dashboard/requests' + | '/dashboard/schedules' + | '/dashboard/swarm' + | '/dashboard/traefik' + | '/dashboard/settings/ai' + | '/dashboard/settings/audit-logs' + | '/dashboard/settings/billing' + | '/dashboard/settings/certificates' + | '/dashboard/settings/cluster' + | '/dashboard/settings/deployments' + | '/dashboard/settings/destinations' + | '/dashboard/settings/git-providers' + | '/dashboard/settings/invoices' + | '/dashboard/settings/license' + | '/dashboard/settings/notifications' + | '/dashboard/settings/profile' + | '/dashboard/settings/registry' + | '/dashboard/settings/secrets' + | '/dashboard/settings/server' + | '/dashboard/settings/servers' + | '/dashboard/settings/ssh-keys' + | '/dashboard/settings/sso' + | '/dashboard/settings/tags' + | '/dashboard/settings/users' + | '/dashboard/settings/whitelabeling' + | '/dashboard/project/$projectId/environment/$environmentId' + | '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId' + | '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId' + | '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId' + | '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId' + | '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId' + id: + | '__root__' + | '/' + | '/dashboard' + | '/invitation' + | '/register' + | '/reset-password' + | '/send-reset-password' + | '/swagger' + | '/accept-invitation/$invitationId' + | '/dashboard/deployments' + | '/dashboard/docker' + | '/dashboard/home' + | '/dashboard/monitoring' + | '/dashboard/networks' + | '/dashboard/projects' + | '/dashboard/requests' + | '/dashboard/schedules' + | '/dashboard/swarm' + | '/dashboard/traefik' + | '/dashboard/settings/ai' + | '/dashboard/settings/audit-logs' + | '/dashboard/settings/billing' + | '/dashboard/settings/certificates' + | '/dashboard/settings/cluster' + | '/dashboard/settings/deployments' + | '/dashboard/settings/destinations' + | '/dashboard/settings/git-providers' + | '/dashboard/settings/invoices' + | '/dashboard/settings/license' + | '/dashboard/settings/notifications' + | '/dashboard/settings/profile' + | '/dashboard/settings/registry' + | '/dashboard/settings/secrets' + | '/dashboard/settings/server' + | '/dashboard/settings/servers' + | '/dashboard/settings/ssh-keys' + | '/dashboard/settings/sso' + | '/dashboard/settings/tags' + | '/dashboard/settings/users' + | '/dashboard/settings/whitelabeling' + | '/dashboard/project/$projectId/environment/$environmentId/' + | '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId' + | '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId' + | '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId' + | '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId' + | '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId' + | '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DashboardRouteRoute: typeof DashboardRouteRouteWithChildren + InvitationRoute: typeof InvitationRoute + RegisterRoute: typeof RegisterRoute + ResetPasswordRoute: typeof ResetPasswordRoute + SendResetPasswordRoute: typeof SendResetPasswordRoute + SwaggerRoute: typeof SwaggerRoute + AcceptInvitationInvitationIdRoute: typeof AcceptInvitationInvitationIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof DashboardRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/invitation': { + id: '/invitation' + path: '/invitation' + fullPath: '/invitation' + preLoaderRoute: typeof InvitationRouteImport + parentRoute: typeof rootRouteImport + } + '/register': { + id: '/register' + path: '/register' + fullPath: '/register' + preLoaderRoute: typeof RegisterRouteImport + parentRoute: typeof rootRouteImport + } + '/reset-password': { + id: '/reset-password' + path: '/reset-password' + fullPath: '/reset-password' + preLoaderRoute: typeof ResetPasswordRouteImport + parentRoute: typeof rootRouteImport + } + '/send-reset-password': { + id: '/send-reset-password' + path: '/send-reset-password' + fullPath: '/send-reset-password' + preLoaderRoute: typeof SendResetPasswordRouteImport + parentRoute: typeof rootRouteImport + } + '/swagger': { + id: '/swagger' + path: '/swagger' + fullPath: '/swagger' + preLoaderRoute: typeof SwaggerRouteImport + parentRoute: typeof rootRouteImport + } + '/accept-invitation/$invitationId': { + id: '/accept-invitation/$invitationId' + path: '/accept-invitation/$invitationId' + fullPath: '/accept-invitation/$invitationId' + preLoaderRoute: typeof AcceptInvitationInvitationIdRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard/deployments': { + id: '/dashboard/deployments' + path: '/deployments' + fullPath: '/dashboard/deployments' + preLoaderRoute: typeof DashboardDeploymentsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/docker': { + id: '/dashboard/docker' + path: '/docker' + fullPath: '/dashboard/docker' + preLoaderRoute: typeof DashboardDockerRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/home': { + id: '/dashboard/home' + path: '/home' + fullPath: '/dashboard/home' + preLoaderRoute: typeof DashboardHomeRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/monitoring': { + id: '/dashboard/monitoring' + path: '/monitoring' + fullPath: '/dashboard/monitoring' + preLoaderRoute: typeof DashboardMonitoringRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/networks': { + id: '/dashboard/networks' + path: '/networks' + fullPath: '/dashboard/networks' + preLoaderRoute: typeof DashboardNetworksRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/projects': { + id: '/dashboard/projects' + path: '/projects' + fullPath: '/dashboard/projects' + preLoaderRoute: typeof DashboardProjectsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/requests': { + id: '/dashboard/requests' + path: '/requests' + fullPath: '/dashboard/requests' + preLoaderRoute: typeof DashboardRequestsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/schedules': { + id: '/dashboard/schedules' + path: '/schedules' + fullPath: '/dashboard/schedules' + preLoaderRoute: typeof DashboardSchedulesRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/swarm': { + id: '/dashboard/swarm' + path: '/swarm' + fullPath: '/dashboard/swarm' + preLoaderRoute: typeof DashboardSwarmRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/traefik': { + id: '/dashboard/traefik' + path: '/traefik' + fullPath: '/dashboard/traefik' + preLoaderRoute: typeof DashboardTraefikRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/ai': { + id: '/dashboard/settings/ai' + path: '/settings/ai' + fullPath: '/dashboard/settings/ai' + preLoaderRoute: typeof DashboardSettingsAiRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/audit-logs': { + id: '/dashboard/settings/audit-logs' + path: '/settings/audit-logs' + fullPath: '/dashboard/settings/audit-logs' + preLoaderRoute: typeof DashboardSettingsAuditLogsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/billing': { + id: '/dashboard/settings/billing' + path: '/settings/billing' + fullPath: '/dashboard/settings/billing' + preLoaderRoute: typeof DashboardSettingsBillingRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/certificates': { + id: '/dashboard/settings/certificates' + path: '/settings/certificates' + fullPath: '/dashboard/settings/certificates' + preLoaderRoute: typeof DashboardSettingsCertificatesRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/cluster': { + id: '/dashboard/settings/cluster' + path: '/settings/cluster' + fullPath: '/dashboard/settings/cluster' + preLoaderRoute: typeof DashboardSettingsClusterRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/deployments': { + id: '/dashboard/settings/deployments' + path: '/settings/deployments' + fullPath: '/dashboard/settings/deployments' + preLoaderRoute: typeof DashboardSettingsDeploymentsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/destinations': { + id: '/dashboard/settings/destinations' + path: '/settings/destinations' + fullPath: '/dashboard/settings/destinations' + preLoaderRoute: typeof DashboardSettingsDestinationsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/git-providers': { + id: '/dashboard/settings/git-providers' + path: '/settings/git-providers' + fullPath: '/dashboard/settings/git-providers' + preLoaderRoute: typeof DashboardSettingsGitProvidersRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/invoices': { + id: '/dashboard/settings/invoices' + path: '/settings/invoices' + fullPath: '/dashboard/settings/invoices' + preLoaderRoute: typeof DashboardSettingsInvoicesRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/license': { + id: '/dashboard/settings/license' + path: '/settings/license' + fullPath: '/dashboard/settings/license' + preLoaderRoute: typeof DashboardSettingsLicenseRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/notifications': { + id: '/dashboard/settings/notifications' + path: '/settings/notifications' + fullPath: '/dashboard/settings/notifications' + preLoaderRoute: typeof DashboardSettingsNotificationsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/profile': { + id: '/dashboard/settings/profile' + path: '/settings/profile' + fullPath: '/dashboard/settings/profile' + preLoaderRoute: typeof DashboardSettingsProfileRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/registry': { + id: '/dashboard/settings/registry' + path: '/settings/registry' + fullPath: '/dashboard/settings/registry' + preLoaderRoute: typeof DashboardSettingsRegistryRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/secrets': { + id: '/dashboard/settings/secrets' + path: '/settings/secrets' + fullPath: '/dashboard/settings/secrets' + preLoaderRoute: typeof DashboardSettingsSecretsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/server': { + id: '/dashboard/settings/server' + path: '/settings/server' + fullPath: '/dashboard/settings/server' + preLoaderRoute: typeof DashboardSettingsServerRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/servers': { + id: '/dashboard/settings/servers' + path: '/settings/servers' + fullPath: '/dashboard/settings/servers' + preLoaderRoute: typeof DashboardSettingsServersRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/ssh-keys': { + id: '/dashboard/settings/ssh-keys' + path: '/settings/ssh-keys' + fullPath: '/dashboard/settings/ssh-keys' + preLoaderRoute: typeof DashboardSettingsSshKeysRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/sso': { + id: '/dashboard/settings/sso' + path: '/settings/sso' + fullPath: '/dashboard/settings/sso' + preLoaderRoute: typeof DashboardSettingsSsoRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/tags': { + id: '/dashboard/settings/tags' + path: '/settings/tags' + fullPath: '/dashboard/settings/tags' + preLoaderRoute: typeof DashboardSettingsTagsRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/users': { + id: '/dashboard/settings/users' + path: '/settings/users' + fullPath: '/dashboard/settings/users' + preLoaderRoute: typeof DashboardSettingsUsersRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/settings/whitelabeling': { + id: '/dashboard/settings/whitelabeling' + path: '/settings/whitelabeling' + fullPath: '/dashboard/settings/whitelabeling' + preLoaderRoute: typeof DashboardSettingsWhitelabelingRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/': { + id: '/dashboard/project/$projectId/environment/$environmentId/' + path: '/project/$projectId/environment/$environmentId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId' + path: '/project/$projectId/environment/$environmentId/services/application/$applicationId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId' + path: '/project/$projectId/environment/$environmentId/services/compose/$composeId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId' + path: '/project/$projectId/environment/$environmentId/services/libsql/$libsqlId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId' + path: '/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId' + path: '/project/$projectId/environment/$environmentId/services/mongo/$mongoId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId' + path: '/project/$projectId/environment/$environmentId/services/mysql/$mysqlId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId' + path: '/project/$projectId/environment/$environmentId/services/postgres/$postgresId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId': { + id: '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId' + path: '/project/$projectId/environment/$environmentId/services/redis/$redisId' + fullPath: '/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId' + preLoaderRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRouteImport + parentRoute: typeof DashboardRouteRoute + } + } +} + +interface DashboardRouteRouteChildren { + DashboardDeploymentsRoute: typeof DashboardDeploymentsRoute + DashboardDockerRoute: typeof DashboardDockerRoute + DashboardHomeRoute: typeof DashboardHomeRoute + DashboardMonitoringRoute: typeof DashboardMonitoringRoute + DashboardNetworksRoute: typeof DashboardNetworksRoute + DashboardProjectsRoute: typeof DashboardProjectsRoute + DashboardRequestsRoute: typeof DashboardRequestsRoute + DashboardSchedulesRoute: typeof DashboardSchedulesRoute + DashboardSwarmRoute: typeof DashboardSwarmRoute + DashboardTraefikRoute: typeof DashboardTraefikRoute + DashboardSettingsAiRoute: typeof DashboardSettingsAiRoute + DashboardSettingsAuditLogsRoute: typeof DashboardSettingsAuditLogsRoute + DashboardSettingsBillingRoute: typeof DashboardSettingsBillingRoute + DashboardSettingsCertificatesRoute: typeof DashboardSettingsCertificatesRoute + DashboardSettingsClusterRoute: typeof DashboardSettingsClusterRoute + DashboardSettingsDeploymentsRoute: typeof DashboardSettingsDeploymentsRoute + DashboardSettingsDestinationsRoute: typeof DashboardSettingsDestinationsRoute + DashboardSettingsGitProvidersRoute: typeof DashboardSettingsGitProvidersRoute + DashboardSettingsInvoicesRoute: typeof DashboardSettingsInvoicesRoute + DashboardSettingsLicenseRoute: typeof DashboardSettingsLicenseRoute + DashboardSettingsNotificationsRoute: typeof DashboardSettingsNotificationsRoute + DashboardSettingsProfileRoute: typeof DashboardSettingsProfileRoute + DashboardSettingsRegistryRoute: typeof DashboardSettingsRegistryRoute + DashboardSettingsSecretsRoute: typeof DashboardSettingsSecretsRoute + DashboardSettingsServerRoute: typeof DashboardSettingsServerRoute + DashboardSettingsServersRoute: typeof DashboardSettingsServersRoute + DashboardSettingsSshKeysRoute: typeof DashboardSettingsSshKeysRoute + DashboardSettingsSsoRoute: typeof DashboardSettingsSsoRoute + DashboardSettingsTagsRoute: typeof DashboardSettingsTagsRoute + DashboardSettingsUsersRoute: typeof DashboardSettingsUsersRoute + DashboardSettingsWhitelabelingRoute: typeof DashboardSettingsWhitelabelingRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute: typeof DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute +} + +const DashboardRouteRouteChildren: DashboardRouteRouteChildren = { + DashboardDeploymentsRoute: DashboardDeploymentsRoute, + DashboardDockerRoute: DashboardDockerRoute, + DashboardHomeRoute: DashboardHomeRoute, + DashboardMonitoringRoute: DashboardMonitoringRoute, + DashboardNetworksRoute: DashboardNetworksRoute, + DashboardProjectsRoute: DashboardProjectsRoute, + DashboardRequestsRoute: DashboardRequestsRoute, + DashboardSchedulesRoute: DashboardSchedulesRoute, + DashboardSwarmRoute: DashboardSwarmRoute, + DashboardTraefikRoute: DashboardTraefikRoute, + DashboardSettingsAiRoute: DashboardSettingsAiRoute, + DashboardSettingsAuditLogsRoute: DashboardSettingsAuditLogsRoute, + DashboardSettingsBillingRoute: DashboardSettingsBillingRoute, + DashboardSettingsCertificatesRoute: DashboardSettingsCertificatesRoute, + DashboardSettingsClusterRoute: DashboardSettingsClusterRoute, + DashboardSettingsDeploymentsRoute: DashboardSettingsDeploymentsRoute, + DashboardSettingsDestinationsRoute: DashboardSettingsDestinationsRoute, + DashboardSettingsGitProvidersRoute: DashboardSettingsGitProvidersRoute, + DashboardSettingsInvoicesRoute: DashboardSettingsInvoicesRoute, + DashboardSettingsLicenseRoute: DashboardSettingsLicenseRoute, + DashboardSettingsNotificationsRoute: DashboardSettingsNotificationsRoute, + DashboardSettingsProfileRoute: DashboardSettingsProfileRoute, + DashboardSettingsRegistryRoute: DashboardSettingsRegistryRoute, + DashboardSettingsSecretsRoute: DashboardSettingsSecretsRoute, + DashboardSettingsServerRoute: DashboardSettingsServerRoute, + DashboardSettingsServersRoute: DashboardSettingsServersRoute, + DashboardSettingsSshKeysRoute: DashboardSettingsSshKeysRoute, + DashboardSettingsSsoRoute: DashboardSettingsSsoRoute, + DashboardSettingsTagsRoute: DashboardSettingsTagsRoute, + DashboardSettingsUsersRoute: DashboardSettingsUsersRoute, + DashboardSettingsWhitelabelingRoute: DashboardSettingsWhitelabelingRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdIndexRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesApplicationApplicationIdRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesComposeComposeIdRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesLibsqlLibsqlIdRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMariadbMariadbIdRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMongoMongoIdRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesMysqlMysqlIdRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesPostgresPostgresIdRoute, + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute: + DashboardProjectProjectIdEnvironmentEnvironmentIdServicesRedisRedisIdRoute, +} + +const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren( + DashboardRouteRouteChildren, +) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DashboardRouteRoute: DashboardRouteRouteWithChildren, + InvitationRoute: InvitationRoute, + RegisterRoute: RegisterRoute, + ResetPasswordRoute: ResetPasswordRoute, + SendResetPasswordRoute: SendResetPasswordRoute, + SwaggerRoute: SwaggerRoute, + AcceptInvitationInvitationIdRoute: AcceptInvitationInvitationIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/apps/vite/src/routes/__root.tsx b/apps/vite/src/routes/__root.tsx new file mode 100644 index 000000000..b753b3394 --- /dev/null +++ b/apps/vite/src/routes/__root.tsx @@ -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 ( + + + + + + + + + + ); +}; + +export const Route = createRootRoute({ + component: RootComponent, +}); diff --git a/apps/vite/src/routes/accept-invitation/$invitationId.tsx b/apps/vite/src/routes/accept-invitation/$invitationId.tsx new file mode 100644 index 000000000..5521b4a7a --- /dev/null +++ b/apps/vite/src/routes/accept-invitation/$invitationId.tsx @@ -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 ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/accept-invitation/$invitationId")({ + component: AcceptInvitation, +}); diff --git a/apps/vite/src/routes/dashboard/deployments.tsx b/apps/vite/src/routes/dashboard/deployments.tsx new file mode 100644 index 000000000..bb6215101 --- /dev/null +++ b/apps/vite/src/routes/dashboard/deployments.tsx @@ -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 ( +
+ +
+ +
+
+ + + Deployments + + + All application and compose deployments in one place. + +
+
+ + + Deployments + Queue + + + + + + + + +
+
+
+
+ ); +} + +export const Route = createFileRoute("/dashboard/deployments")({ + component: DeploymentsPage, +}); diff --git a/apps/vite/src/routes/dashboard/docker.tsx b/apps/vite/src/routes/dashboard/docker.tsx new file mode 100644 index 000000000..4e7b593fd --- /dev/null +++ b/apps/vite/src/routes/dashboard/docker.tsx @@ -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 ( + + {(serverId) => ( + + + Containers + Swarm + Volumes + {!isCloud && Networks} + + + + + + + + Overview + Containers + + + + + + +
+ +
+
+
+
+
+ + + + {!isCloud && ( + + + + )} +
+ )} +
+ ); +}; + +export const Route = createFileRoute("/dashboard/docker")({ + component: Dashboard, +}); diff --git a/apps/vite/src/routes/dashboard/home.tsx b/apps/vite/src/routes/dashboard/home.tsx new file mode 100644 index 000000000..b98dadfc0 --- /dev/null +++ b/apps/vite/src/routes/dashboard/home.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowHome } from "@/components/dashboard/home/show-home"; + +const Home = () => { + return ; +}; + +export const Route = createFileRoute("/dashboard/home")({ + component: Home, +}); diff --git a/apps/vite/src/routes/dashboard/monitoring.tsx b/apps/vite/src/routes/dashboard/monitoring.tsx new file mode 100644 index 000000000..e61978657 --- /dev/null +++ b/apps/vite/src/routes/dashboard/monitoring.tsx @@ -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 ( +
+ {/* + You are watching the Free plan.{" "} + + Upgrade + {" "} + to get more features. + */} + {isPending ? ( + +
+ Loading... +
+
+ ) : ( + <> + {/* {monitoring?.enabledFeatures && ( +
+ + +
+ )} */} + {toggleMonitoring ? ( + +
+ +
+
+ ) : ( + +
+ +
+
+ )} + + )} +
+ ); +}; + +export const Route = createFileRoute("/dashboard/monitoring")({ + component: Dashboard, +}); diff --git a/apps/vite/src/routes/dashboard/networks.tsx b/apps/vite/src/routes/dashboard/networks.tsx new file mode 100644 index 000000000..d758ba05a --- /dev/null +++ b/apps/vite/src/routes/dashboard/networks.tsx @@ -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).serverId; + throw redirect({ + href: `/dashboard/docker?tab=networks${ + typeof serverId === "string" + ? `&serverId=${encodeURIComponent(serverId)}` + : "" + }`, + }); + }, + component: Networks, +}); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/index.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/index.tsx new file mode 100644 index 000000000..b4ec33feb --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/index.tsx @@ -0,0 +1,1823 @@ +import type { findEnvironmentById } from "@dokploy/server"; +import { createFileRoute } from "@tanstack/react-router"; +import { + Ban, + Check, + CheckCircle2, + ChevronsUpDown, + CircuitBoard, + FolderInput, + GlobeIcon, + Loader2, + Play, + PlusIcon, + RefreshCw, + Search, + ServerIcon, + SquareTerminal, + Trash2, + X, +} from "lucide-react"; +import Head from "next/head"; +import Link from "next/link"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant"; +import { AddApplication } from "@/components/dashboard/project/add-application"; +import { AddCompose } from "@/components/dashboard/project/add-compose"; +import { AddDatabase } from "@/components/dashboard/project/add-database"; +import { AddImport } from "@/components/dashboard/project/add-import"; +import { AddTemplate } from "@/components/dashboard/project/add-template"; +import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector"; +import { DuplicateProject } from "@/components/dashboard/project/duplicate-project"; +import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables"; +import { ProjectEnvironment } from "@/components/dashboard/projects/project-environment"; +import { + LibsqlIcon, + MariadbIcon, + MongodbIcon, + MysqlIcon, + PostgresqlIcon, + RedisIcon, +} from "@/components/icons/data-tools-icons"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { DateTooltip } from "@/components/shared/date-tooltip"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; +import { StatusTooltip } from "@/components/shared/status-tooltip"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { api } from "@/utils/api"; +import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling"; + +export type Services = { + serverId?: string | null; + serverName?: string | null; + name: string; + type: + | "mariadb" + | "application" + | "postgres" + | "mysql" + | "mongo" + | "redis" + | "compose" + | "libsql"; + description?: string | null; + id: string; + createdAt: string; + status?: "idle" | "running" | "done" | "error"; + lastDeployDate?: Date | null; + icon?: string | null; +}; + +type Environment = Awaited>; + +export const extractServicesFromEnvironment = ( + environment: Environment | undefined, +) => { + if (!environment) return []; + + const allServices: Services[] = []; + + const applications: Services[] = + environment.applications?.map((item) => { + // Get the most recent deployment date + let lastDeployDate: Date | null = null; + const deployments = (item as any).deployments; + if (deployments && deployments.length > 0) { + for (const deployment of deployments) { + const deployDate = new Date( + deployment.finishedAt || + deployment.startedAt || + deployment.createdAt, + ); + if (!lastDeployDate || deployDate > lastDeployDate) { + lastDeployDate = deployDate; + } + } + } + return { + name: item.name, + type: "application", + id: item.applicationId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + lastDeployDate, + icon: item.icon || null, + }; + }) || []; + + const mariadb: Services[] = + environment.mariadb?.map((item) => ({ + name: item.name, + type: "mariadb", + id: item.mariadbId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + })) || []; + + const postgres: Services[] = + environment.postgres?.map((item) => ({ + name: item.name, + type: "postgres", + id: item.postgresId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + })) || []; + + const mongo: Services[] = + environment.mongo?.map((item) => ({ + name: item.name, + type: "mongo", + id: item.mongoId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + })) || []; + + const redis: Services[] = + environment.redis?.map((item) => ({ + name: item.name, + type: "redis", + id: item.redisId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + })) || []; + + const mysql: Services[] = + environment.mysql?.map((item) => ({ + name: item.name, + type: "mysql", + id: item.mysqlId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + })) || []; + + const compose: Services[] = + environment.compose?.map((item) => { + // Get the most recent deployment date + let lastDeployDate: Date | null = null; + const deployments = (item as any).deployments; + if (deployments && deployments.length > 0) { + for (const deployment of deployments) { + const deployDate = new Date( + deployment.finishedAt || + deployment.startedAt || + deployment.createdAt, + ); + if (!lastDeployDate || deployDate > lastDeployDate) { + lastDeployDate = deployDate; + } + } + } + return { + name: item.name, + type: "compose", + id: item.composeId, + createdAt: item.createdAt, + status: item.composeStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + lastDeployDate, + icon: item.icon || null, + }; + }) || []; + + const libsql: Services[] = + environment.libsql?.map((item) => ({ + name: item.name, + type: "libsql", + id: item.libsqlId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + })) || []; + + allServices.push( + ...applications, + ...compose, + ...libsql, + ...mysql, + ...redis, + ...mongo, + ...postgres, + ...mariadb, + ); + + allServices.sort((a, b) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); + + return allServices; +}; + +const EnvironmentPage = () => { + const utils = api.useUtils(); + const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); + const { projectId, environmentId } = Route.useParams(); + const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); + + const { data: environments } = api.environment.byProjectId.useQuery({ + projectId: projectId, + }); + const environmentDropdownItems = + environments?.map((env) => ({ + name: env.name, + href: `/dashboard/project/${projectId}/environment/${env.environmentId}`, + })) || []; + + const [sortBy, setSortBy] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem("servicesSort") || "lastDeploy-desc"; + } + return "lastDeploy-desc"; + }); + + useEffect(() => { + localStorage.setItem("servicesSort", sortBy); + }, [sortBy]); + + const sortServices = (services: Services[]) => { + const [field, direction] = sortBy.split("-"); + return [...services].sort((a, b) => { + let comparison = 0; + switch (field) { + case "name": + comparison = a.name.localeCompare(b.name); + break; + case "type": + comparison = a.type.localeCompare(b.type); + break; + case "createdAt": + comparison = + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + case "lastDeploy": { + const aLastDeploy = a.lastDeployDate; + const bLastDeploy = b.lastDeployDate; + + if (direction === "desc") { + // For "desc" (newest first): services with deployments first, then those without + if (!aLastDeploy && !bLastDeploy) { + comparison = 0; + } else if (!aLastDeploy) { + comparison = 1; // a (no deploy) goes after b (has deploy) + } else if (!bLastDeploy) { + comparison = -1; // a (has deploy) goes before b (no deploy) + } else { + // Both have deployments: newest first (negative if a is newer) + comparison = bLastDeploy.getTime() - aLastDeploy.getTime(); + } + } else { + // For "asc" (oldest first): services with deployments first, then those without + if (!aLastDeploy && !bLastDeploy) { + comparison = 0; + } else if (!aLastDeploy) { + comparison = 1; // a (no deploy) goes after b (has deploy) + } else if (!bLastDeploy) { + comparison = -1; // a (has deploy) goes before b (no deploy) + } else { + // Both have deployments: oldest first + comparison = aLastDeploy.getTime() - bLastDeploy.getTime(); + } + } + break; + } + default: + comparison = 0; + } + // For other fields, apply direction normally + if (field !== "lastDeploy") { + return direction === "asc" ? comparison : -comparison; + } + return comparison; + }); + }; + + const { + data: projectData, + isLoading, + refetch, + } = api.project.one.useQuery({ projectId }); + const { data: currentEnvironment } = api.environment.one.useQuery({ + environmentId, + }); + const { data: allProjects } = api.project.all.useQuery(); + + const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false); + const [selectedTargetProject, setSelectedTargetProject] = + useState(""); + const [selectedTargetEnvironment, setSelectedTargetEnvironment] = + useState(""); + + const { data: selectedProjectEnvironments } = + api.environment.byProjectId.useQuery( + { projectId: selectedTargetProject }, + { enabled: !!selectedTargetProject }, + ); + const { config: whitelabeling } = useWhitelabeling(); + const appName = whitelabeling?.appName || "Dokploy"; + + const emptyServices = + !currentEnvironment || + ((currentEnvironment.mariadb?.length || 0) === 0 && + (currentEnvironment.mongo?.length || 0) === 0 && + (currentEnvironment.mysql?.length || 0) === 0 && + (currentEnvironment.postgres?.length || 0) === 0 && + (currentEnvironment.redis?.length || 0) === 0 && + (currentEnvironment.applications?.length || 0) === 0 && + (currentEnvironment.compose?.length || 0) === 0 && + (currentEnvironment.libsql?.length || 0) === 0); + + const applications = extractServicesFromEnvironment(currentEnvironment); + + const [searchQuery, setSearchQuery] = useState(""); + const serviceTypes = [ + { value: "application", label: "Application", icon: GlobeIcon }, + { value: "postgres", label: "PostgreSQL", icon: PostgresqlIcon }, + { value: "mariadb", label: "MariaDB", icon: MariadbIcon }, + { value: "mongo", label: "MongoDB", icon: MongodbIcon }, + { value: "mysql", label: "MySQL", icon: MysqlIcon }, + { value: "redis", label: "Redis", icon: RedisIcon }, + { value: "compose", label: "Compose", icon: CircuitBoard }, + { value: "libsql", label: "Libsql", icon: LibsqlIcon }, + ]; + + const [selectedTypes, setSelectedTypes] = useState([]); + const [openCombobox, setOpenCombobox] = useState(false); + const [selectedServices, setSelectedServices] = useState([]); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [deleteVolumes, setDeleteVolumes] = useState(false); + const [selectedServerId, setSelectedServerId] = useState("all"); + const [serviceToDelete, setServiceToDelete] = useState(null); + + const handleSelectAll = () => { + if (selectedServices.length === filteredServices.length) { + setSelectedServices([]); + } else { + setSelectedServices(filteredServices.map((service) => service.id)); + } + }; + + const handleServiceSelect = (serviceId: string, event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setSelectedServices((prev) => + prev.includes(serviceId) + ? prev.filter((id) => id !== serviceId) + : [...prev, serviceId], + ); + }; + + const composeActions = { + start: api.compose.start.useMutation(), + stop: api.compose.stop.useMutation(), + move: api.compose.move.useMutation(), + delete: api.compose.delete.useMutation(), + deploy: api.compose.deploy.useMutation(), + }; + + const applicationActions = { + start: api.application.start.useMutation(), + stop: api.application.stop.useMutation(), + move: api.application.move.useMutation(), + delete: api.application.delete.useMutation(), + deploy: api.application.deploy.useMutation(), + }; + + const postgresActions = { + start: api.postgres.start.useMutation(), + stop: api.postgres.stop.useMutation(), + move: api.postgres.move.useMutation(), + delete: api.postgres.remove.useMutation(), + deploy: api.postgres.deploy.useMutation(), + }; + + const mysqlActions = { + start: api.mysql.start.useMutation(), + stop: api.mysql.stop.useMutation(), + move: api.mysql.move.useMutation(), + delete: api.mysql.remove.useMutation(), + deploy: api.mysql.deploy.useMutation(), + }; + + const mariadbActions = { + start: api.mariadb.start.useMutation(), + stop: api.mariadb.stop.useMutation(), + move: api.mariadb.move.useMutation(), + delete: api.mariadb.remove.useMutation(), + deploy: api.mariadb.deploy.useMutation(), + }; + + const redisActions = { + start: api.redis.start.useMutation(), + stop: api.redis.stop.useMutation(), + move: api.redis.move.useMutation(), + delete: api.redis.remove.useMutation(), + deploy: api.redis.deploy.useMutation(), + }; + + const mongoActions = { + start: api.mongo.start.useMutation(), + stop: api.mongo.stop.useMutation(), + move: api.mongo.move.useMutation(), + delete: api.mongo.remove.useMutation(), + deploy: api.mongo.deploy.useMutation(), + }; + + const libsqlActions = { + start: api.libsql.start.useMutation(), + stop: api.libsql.stop.useMutation(), + move: api.libsql.move.useMutation(), + delete: api.libsql.remove.useMutation(), + deploy: api.libsql.deploy.useMutation(), + }; + + const handleBulkStart = async () => { + let success = 0; + setIsBulkActionLoading(true); + for (const serviceId of selectedServices) { + try { + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.start.mutateAsync({ + applicationId: serviceId, + }); + break; + case "compose": + await composeActions.start.mutateAsync({ composeId: serviceId }); + break; + case "postgres": + await postgresActions.start.mutateAsync({ postgresId: serviceId }); + break; + case "mysql": + await mysqlActions.start.mutateAsync({ mysqlId: serviceId }); + break; + case "mariadb": + await mariadbActions.start.mutateAsync({ mariadbId: serviceId }); + break; + case "redis": + await redisActions.start.mutateAsync({ redisId: serviceId }); + break; + case "mongo": + await mongoActions.start.mutateAsync({ mongoId: serviceId }); + break; + case "libsql": + await libsqlActions.start.mutateAsync({ libsqlId: serviceId }); + break; + } + success++; + } catch { + toast.error(`Error starting service ${serviceId}`); + } + } + if (success > 0) { + toast.success(`${success} services started successfully`); + refetch(); + } + setIsBulkActionLoading(false); + setSelectedServices([]); + setIsDropdownOpen(false); + }; + + const handleBulkStop = async () => { + let success = 0; + setIsBulkActionLoading(true); + for (const serviceId of selectedServices) { + try { + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.stop.mutateAsync({ + applicationId: serviceId, + }); + break; + case "compose": + await composeActions.stop.mutateAsync({ composeId: serviceId }); + break; + case "postgres": + await postgresActions.stop.mutateAsync({ postgresId: serviceId }); + break; + case "mysql": + await mysqlActions.stop.mutateAsync({ mysqlId: serviceId }); + break; + case "mariadb": + await mariadbActions.stop.mutateAsync({ mariadbId: serviceId }); + break; + case "redis": + await redisActions.stop.mutateAsync({ redisId: serviceId }); + break; + case "mongo": + await mongoActions.stop.mutateAsync({ mongoId: serviceId }); + break; + case "libsql": + await libsqlActions.stop.mutateAsync({ libsqlId: serviceId }); + break; + } + success++; + } catch { + toast.error(`Error stopping service ${serviceId}`); + } + } + if (success > 0) { + toast.success(`${success} services stopped successfully`); + refetch(); + } + setSelectedServices([]); + setIsDropdownOpen(false); + setIsBulkActionLoading(false); + }; + + const handleBulkMove = async () => { + if (!selectedTargetProject) { + toast.error("Please select a target project"); + return; + } + if (!selectedTargetEnvironment) { + toast.error("Please select a target environment"); + return; + } + + let success = 0; + setIsBulkActionLoading(true); + for (const serviceId of selectedServices) { + try { + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + // TODO: Update move APIs to use targetEnvironmentId instead of targetProjectId + switch (service.type) { + case "application": + await applicationActions.move.mutateAsync({ + applicationId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + case "compose": + await composeActions.move.mutateAsync({ + composeId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + case "postgres": + await postgresActions.move.mutateAsync({ + postgresId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + case "mysql": + await mysqlActions.move.mutateAsync({ + mysqlId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + case "mariadb": + await mariadbActions.move.mutateAsync({ + mariadbId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + case "redis": + await redisActions.move.mutateAsync({ + redisId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + case "mongo": + await mongoActions.move.mutateAsync({ + mongoId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + case "libsql": + await libsqlActions.move.mutateAsync({ + libsqlId: serviceId, + targetEnvironmentId: selectedTargetEnvironment, + }); + break; + } + await utils.environment.one.invalidate({ + environmentId, + }); + success++; + } catch (error) { + toast.error( + `Error moving service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + } + if (success > 0) { + toast.success(`${success} services moved successfully`); + refetch(); + } + setSelectedServices([]); + setIsDropdownOpen(false); + setIsMoveDialogOpen(false); + setIsBulkActionLoading(false); + // Reset move dialog state + setSelectedTargetProject(""); + setSelectedTargetEnvironment(""); + }; + + const handleBulkDelete = async (deleteVolumes = false) => { + let success = 0; + setIsBulkActionLoading(true); + for (const serviceId of selectedServices) { + try { + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.delete.mutateAsync({ + applicationId: serviceId, + }); + break; + case "compose": + await composeActions.delete.mutateAsync({ + composeId: serviceId, + deleteVolumes, + }); + break; + case "postgres": + await postgresActions.delete.mutateAsync({ + postgresId: serviceId, + }); + break; + case "mysql": + await mysqlActions.delete.mutateAsync({ + mysqlId: serviceId, + }); + break; + case "mariadb": + await mariadbActions.delete.mutateAsync({ + mariadbId: serviceId, + }); + break; + case "redis": + await redisActions.delete.mutateAsync({ + redisId: serviceId, + }); + break; + case "mongo": + await mongoActions.delete.mutateAsync({ + mongoId: serviceId, + }); + break; + case "libsql": + await libsqlActions.delete.mutateAsync({ + libsqlId: serviceId, + }); + break; + } + await utils.environment.one.invalidate({ + environmentId, + }); + success++; + } catch (error) { + toast.error( + `Error deleting service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + } + if (success > 0) { + toast.success(`${success} services deleted successfully`); + refetch(); + } + setSelectedServices([]); + setIsDropdownOpen(false); + setIsBulkActionLoading(false); + }; + + const handleBulkDeploy = async () => { + let success = 0; + let failed = 0; + setIsBulkActionLoading(true); + + for (const serviceId of selectedServices) { + try { + const service = filteredServices.find((s) => s.id === serviceId); + if (!service) continue; + + switch (service.type) { + case "application": + await applicationActions.deploy.mutateAsync({ + applicationId: serviceId, + }); + break; + case "compose": + await composeActions.deploy.mutateAsync({ + composeId: serviceId, + }); + break; + case "postgres": + await postgresActions.deploy.mutateAsync({ + postgresId: serviceId, + }); + break; + case "mysql": + await mysqlActions.deploy.mutateAsync({ + mysqlId: serviceId, + }); + break; + case "mariadb": + await mariadbActions.deploy.mutateAsync({ + mariadbId: serviceId, + }); + break; + case "redis": + await redisActions.deploy.mutateAsync({ + redisId: serviceId, + }); + break; + case "mongo": + await mongoActions.deploy.mutateAsync({ + mongoId: serviceId, + }); + break; + case "libsql": + await libsqlActions.deploy.mutateAsync({ + libsqlId: serviceId, + }); + break; + } + success++; + } catch (error) { + failed++; + toast.error( + `Error deploying service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + } + if (success > 0) { + toast.success( + `${success} service${success !== 1 ? "s" : ""} queued for deployment`, + ); + } + if (failed > 0) { + toast.error( + `${failed} service${failed !== 1 ? "s" : ""} failed to deploy`, + ); + } + + setSelectedServices([]); + setIsDropdownOpen(false); + setIsBulkActionLoading(false); + }; + + const getServiceActions = (service: Services) => { + switch (service.type) { + case "application": + return applicationActions; + case "compose": + return composeActions; + case "postgres": + return postgresActions; + case "mysql": + return mysqlActions; + case "mariadb": + return mariadbActions; + case "redis": + return redisActions; + case "mongo": + return mongoActions; + default: + return null; + } + }; + + const getServiceIdKey = (service: Services) => { + switch (service.type) { + case "application": + return "applicationId"; + case "compose": + return "composeId"; + case "postgres": + return "postgresId"; + case "mysql": + return "mysqlId"; + case "mariadb": + return "mariadbId"; + case "redis": + return "redisId"; + case "mongo": + return "mongoId"; + default: + return null; + } + }; + + const handleServiceAction = async ( + service: Services, + action: "start" | "stop" | "deploy", + ) => { + const actions = getServiceActions(service); + const idKey = getServiceIdKey(service); + if (!actions || !idKey) return; + + const actionLabels = { + start: { loading: "Starting", success: "started", error: "starting" }, + stop: { loading: "Stopping", success: "stopped", error: "stopping" }, + deploy: { + loading: "Deploying", + success: "queued for deployment", + error: "deploying", + }, + }; + + const labels = actionLabels[action]; + + toast.promise( + (async () => { + await actions[action].mutateAsync({ + [idKey]: service.id, + } as any); + })(), + { + loading: `${labels.loading} ${service.name}...`, + success: () => { + utils.environment.one.invalidate({ environmentId }); + return `${service.name} ${labels.success} successfully`; + }, + error: (error) => + `Error ${labels.error} ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ); + }; + + const handleServiceDelete = async (service: Services) => { + const actions = getServiceActions(service); + const idKey = getServiceIdKey(service); + if (!actions || !idKey) return; + + toast.promise( + (async () => { + await actions.delete.mutateAsync({ + [idKey]: service.id, + } as any); + })(), + { + loading: `Deleting ${service.name}...`, + success: () => { + utils.environment.one.invalidate({ environmentId }); + return `${service.name} deleted successfully`; + }, + error: (error) => + `Error deleting ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ); + setServiceToDelete(null); + }; + + // Get unique servers from services + const availableServers = useMemo(() => { + if (!applications) return []; + const servers = new Map(); + applications.forEach((service) => { + if (service.serverId && service.serverName) { + servers.set(service.serverId, { + serverId: service.serverId, + serverName: service.serverName, + }); + } + }); + return Array.from(servers.values()); + }, [applications]); + + // Check if there are services without a server (Dokploy server) + const hasServicesWithoutServer = useMemo(() => { + if (!applications) return false; + return applications.some((service) => !service.serverId); + }, [applications]); + + const filteredServices = useMemo(() => { + if (!applications) return []; + const filtered = applications.filter( + (service) => + (service.name.toLowerCase().includes(searchQuery.toLowerCase()) || + service.description + ?.toLowerCase() + .includes(searchQuery.toLowerCase())) && + (selectedTypes.length === 0 || selectedTypes.includes(service.type)) && + (selectedServerId === "" || + selectedServerId === "all" || + (selectedServerId === "dokploy-server" && !service.serverId) || + service.serverId === selectedServerId), + ); + return sortServices(filtered); + }, [applications, searchQuery, selectedTypes, selectedServerId, sortBy]); + + const selectedServicesWithRunningStatus = useMemo(() => { + return filteredServices.filter( + (service) => + selectedServices.includes(service.id) && service.status === "running", + ); + }, [filteredServices, selectedServices]); + + if (isLoading) { + return ( +
+ Loading... + +
+ ); + } + + if (!currentEnvironment) { + return ( +
+ + Environment not found + +
+ ); + } + + return ( +
+ + + + Environment: {currentEnvironment.name} | {projectData?.name} |{" "} + {appName} + + +
+ +
+
+ + + +

+ {currentEnvironment.project.name} +

+ + + + +
+ + {currentEnvironment.description || "No description provided"} + +
+
+
+ + + + {permissions?.service.create && ( + + + + + + + Actions + + + + + + + + + + + )} +
+
+
+ + <> +
+
+
+ 0} + className={cn( + "data-[state=checked]:bg-primary", + selectedServices.length > 0 && + selectedServices.length < filteredServices.length && + "bg-primary/50", + )} + onCheckedChange={handleSelectAll} + /> + + Select All{" "} + {selectedServices.length > 0 && + `(${selectedServices.length}/${filteredServices.length})`} + +
+ + + + + + + Actions + + + + + + + + + + + {permissions?.service.delete && ( + <> + +

+ Are you sure you want to delete{" "} + {selectedServices.length} services? This + action cannot be undone. +

+ {selectedServicesWithRunningStatus.length > + 0 && ( + + Warning:{" "} + {selectedServicesWithRunningStatus.length}{" "} + of the selected services are currently + running. Please stop these services first + before deleting:{" "} + {selectedServicesWithRunningStatus + .map((s) => s.name) + .join(", ")} + + )} +
+ } + type="destructive" + disabled={ + selectedServicesWithRunningStatus.length > 0 + } + onClick={() => setIsBulkDeleteDialogOpen(true)} + > + + + + + )} + + + + + + + + Move Services + + Select the target project and environment to + move {selectedServices.length} services + + +
+ {allProjects?.length === 0 ? ( +
+ +

+ No other projects available. Create a new + project first to move services. +

+
+ ) : ( + <> + {/* Step 1: Select Project */} +
+ + +
+ + {/* Step 2: Select Environment (only show if project is selected) */} + {selectedTargetProject && ( +
+ + +
+ )} + + )} +
+ + + + +
+
+ + {/* Bulk Delete Dialog */} + + + + Delete Services + + Are you sure you want to delete{" "} + {selectedServices.length} service + {selectedServices.length !== 1 ? "s" : ""}? This + action cannot be undone. + + + +
+ {/* Show services to be deleted */} +
+ {selectedServices.map((serviceId) => { + const service = filteredServices.find( + (s) => s.id === serviceId, + ); + return service ? ( +
+ + {service.type} + + {service.name} +
+ ) : null; + })} +
+ + {/* Volume deletion option for compose services */} + {(() => { + const servicesWithVolumeSupport = + selectedServices.filter((serviceId) => { + const service = filteredServices.find( + (s) => s.id === serviceId, + ); + // Currently only compose services support volume deletion + return service?.type === "compose"; + }); + + if (servicesWithVolumeSupport.length === 0) + return null; + + return ( +
+
+ + setDeleteVolumes(checked === true) + } + /> + +
+

+ Volume deletion is available for:{" "} + {servicesWithVolumeSupport.length} compose + service + {servicesWithVolumeSupport.length !== 1 + ? "s" + : ""} +

+
+ ); + })()} +
+ + + + + +
+
+ + +
+ +
+
+ setSearchQuery(e.target.value)} + className="pr-10" + /> + +
+ + + + + + + + + No type found. + + {serviceTypes.map((type) => ( + { + setSelectedTypes((prev) => + prev.includes(type.value) + ? prev.filter((t) => t !== type.value) + : [...prev, type.value], + ); + setOpenCombobox(false); + }} + > +
+ + {type.icon && ( + + )} + {type.label} +
+
+ ))} + { + setSelectedTypes([]); + setOpenCombobox(false); + }} + className="border-t" + > +
+ + Clear filters +
+
+
+
+
+
+ {(availableServers.length > 0 || + hasServicesWithoutServer) && ( + + )} +
+
+ +
+ {emptyServices ? ( +
+ + + No services added yet. Click on Create Service. + +
+ ) : filteredServices.length === 0 ? ( +
+ + + No services found with the current filters + + + Try adjusting your search or filters + +
+ ) : ( +
+
+ {filteredServices?.map((service) => ( + + + + + {service.serverId && ( +
+ +
+ )} +
+ +
+ +
+ handleServiceSelect(service.id, e) + } + > +
+ +
+
+ + + +
+
+ + {service.name} + + {service.description && ( + + {service.description} + + )} +
+ + + {service.type === "postgres" && ( + + )} + {service.type === "redis" && ( + + )} + {service.type === "mariadb" && ( + + )} + {service.type === "mongo" && ( + + )} + {service.type === "mysql" && ( + + )} + {service.type === "application" && + (service.icon ? ( + // biome-ignore lint/performance/noImgElement: application icon is data URL + {service.name} + ) : ( + + ))} + {service.type === "compose" && + (service.icon ? ( + // biome-ignore lint/performance/noImgElement: compose icon is data URL + {service.name} + ) : ( + + ))} + {service.type === "libsql" && ( + + )} + +
+
+
+ +
+ {service.serverName && ( +
+ + + {service.serverName} + +
+ )} + + Created + +
+
+
+ +
+ {service.type !== "libsql" && ( + + + {service.name} + + + + handleServiceAction(service, "start") + } + > + + Start + + + handleServiceAction(service, "deploy") + } + > + + Deploy + + + handleServiceAction(service, "stop") + } + > + + Stop + + + setServiceToDelete(service)} + > + + Delete + + + )} +
+ ))} +
+
+ )} +
+ + +
+ +
+ + {/* Single Service Delete Dialog */} + !open && setServiceToDelete(null)} + > + + + Delete Service + + Are you sure you want to delete{" "} + {serviceToDelete?.name}? + This action cannot be undone. + + + + + + + + + + ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/", +)({ component: EnvironmentPage }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId.tsx new file mode 100644 index 000000000..c9261f44f --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId.tsx @@ -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( + (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 ( +
+ + + + + Application: {data?.name} - {data?.environment.project.name} |{" "} + {appName} + + +
+ +
+ +
+ +
+ +
+ +
+
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to add + more servers. + + + + + )} +
+ +
+ {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( + + )} +
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setTab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/application/${applicationId}?tab=${e}`; + router.push(newPath); + }} + > +
+ + General + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.domain.read && ( + Domains + )} + {permissions?.deployment.read && ( + + Deployments + + )} + {permissions?.deployment.read && ( + + Preview Deployments + + )} + {permissions?.schedule.read && ( + Schedules + )} + {permissions?.volumeBackup.read && ( + + Volume Backups + + )} + {permissions?.logs.read && ( + Logs + )} + {data?.sourceType !== "docker" && ( + Patches + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + {permissions?.service.create && ( + Advanced + )} + +
+ + +
+ +
+
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && + isCloud && + data?.serverId && ( +
+ + +
+ )} */} + + {/* {toggleMonitoring ? ( + + ) : ( */} +
+ +
+ {/* )} */} + + )} +
+
+
+ )} + + {permissions?.logs.read && ( + +
+ +
+
+ )} + {permissions?.schedule.read && ( + +
+ +
+
+ )} + {permissions?.deployment.read && ( + +
+ +
+
+ )} + {permissions?.volumeBackup.read && ( + +
+ +
+
+ )} + {permissions?.deployment.read && ( + +
+ +
+
+ )} + {permissions?.domain.read && ( + +
+ +
+
+ )} + +
+ +
+
+ {permissions?.service.create && ( + +
+ + + + + + + + + + +
+
+ )} +
+ )} +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/application/$applicationId", +)({ component: Service }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId.tsx new file mode 100644 index 000000000..ef0f11c4a --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId.tsx @@ -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( + (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 ( +
+ + + + + Compose: {data?.name} - {data?.environment?.project?.name} | {appName} + + +
+ +
+
+ +
+ +
+ +
+ +
+
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to + add more servers. + + + + + )} +
+
+ {permissions?.service.create && ( + + )} + + {permissions?.service.delete && ( + + )} +
+
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setTab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/compose/${composeId}?tab=${e}`; + router.push(newPath); + }} + > +
+ + General + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.domain.read && ( + Domains + )} + {permissions?.deployment.read && ( + + Deployments + + )} + {permissions?.service.read && ( + Containers + )} + {permissions?.service.create && ( + Backups + )} + {permissions?.schedule.read && ( + Schedules + )} + {permissions?.volumeBackup.read && ( + + Volume Backups + + )} + {permissions?.logs.read && ( + Logs + )} + {data?.sourceType !== "raw" && ( + Patches + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + {permissions?.service.create && ( + Advanced + )} + +
+ + +
+ +
+
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.service.create && ( + +
+ +
+
+ )} + + {permissions?.schedule.read && ( + +
+ +
+
+ )} + {permissions?.volumeBackup.read && ( + +
+ +
+
+ )} + {permissions?.service.read && ( + +
+ +
+
+ )} + + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && + isCloud && + data?.serverId && ( +
+ + +
+ )} + + {toggleMonitoring ? ( + + ) : ( */} + {/*
*/} + + {/*
*/} + {/* )} */} + + )} +
+
+
+ )} + + {permissions?.logs.read && ( + +
+ {data?.composeType === "docker-compose" ? ( + + ) : ( + + )} +
+
+ )} + + {permissions?.deployment.read && ( + +
+ +
+
+ )} + + {permissions?.domain.read && ( + +
+ +
+
+ )} + + +
+ +
+
+ + {permissions?.service.create && ( + +
+ + + + + +
+
+ )} +
+ )} +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/compose/$composeId", +)({ component: Service }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId.tsx new file mode 100644 index 000000000..6fcca93d1 --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId.tsx @@ -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( + (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 ( +
+ + + +
+ + + Database: {data?.name} - {data?.environment?.project?.name} | + Dokploy + + + +
+ +
+ +
+
+ +
+ + +
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to add + more servers. + + + + + )} +
+
+ + {(auth?.role === "owner" || auth?.canDeleteServices) && ( + + )} +
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setSab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/libsql/${libsqlId}?tab=${e}`; + + router.push(newPath, undefined, { shallow: true }); + }} + > +
+ + General + Environment + Logs + {((data?.serverId && isCloud) || !data?.server) && ( + Monitoring + )} + Backups + Advanced + +
+ + +
+ + + +
+
+ +
+ +
+
+ +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && ( +
+ + +
+ )} + + {toggleMonitoring ? ( + + ) : ( +
*/} + + {/*
*/} + {/* )} */} + + )} +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ )} +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/libsql/$libsqlId", +)({ component: Libsql }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId.tsx new file mode 100644 index 000000000..5d206dfbd --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId.tsx @@ -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( + (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 ( +
+ + +
+ + + Database: {data?.name} - {data?.environment?.project?.name} | + {appName} + + + +
+ +
+ +
+
+ +
+ + +
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to add + more servers. + + + + + )} +
+
+ {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( + + )} +
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setSab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mariadb/${mariadbId}?tab=${e}`; + + router.push(newPath, undefined, { shallow: true }); + }} + > +
+ + General + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + Backups + {permissions?.service.create && ( + Advanced + )} + +
+ + +
+ + + +
+
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && ( +
+ + +
+ )} + + {toggleMonitoring ? ( + + ) : ( +
*/} + + {/*
*/} + {/* )} */} + + )} +
+
+
+ )} + {permissions?.logs.read && ( + +
+ +
+
+ )} + +
+ +
+
+ {permissions?.service.create && ( + +
+ +
+
+ )} +
+ )} +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/mariadb/$mariadbId", +)({ component: Mariadb }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId.tsx new file mode 100644 index 000000000..52f35decf --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId.tsx @@ -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( + (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 ( +
+ + + + + Database: {data?.name} - {data?.environment?.project?.name} |{" "} + {appName} + + +
+ +
+ +
+ +
+
+ +
+ + +
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to add + more servers. + + + + + )} +
+ +
+ {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( + + )} +
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setSab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mongo/${mongoId}?tab=${e}`; + + router.push(newPath, undefined, { shallow: true }); + }} + > +
+ + General + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + Backups + {permissions?.service.create && ( + Advanced + )} + +
+ + +
+ + + +
+
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && ( +
+ + +
+ )} + + {toggleMonitoring ? ( + + ) : ( +
*/} + + {/*
*/} + {/* )} */} + + )} +
+
+
+ )} + {permissions?.logs.read && ( + +
+ +
+
+ )} + +
+ +
+
+ {permissions?.service.create && ( + +
+ +
+
+ )} +
+ )} +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/mongo/$mongoId", +)({ component: Mongo }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId.tsx new file mode 100644 index 000000000..c0f16a517 --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId.tsx @@ -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( + (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 ( +
+ + +
+ + + Database: {data?.name} - {data?.environment?.project?.name} | + {appName} + + +
+ +
+ +
+ +
+
+ +
+ + +
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to + add more servers. + + + + + )} +
+ +
+ {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( + + )} +
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setSab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/mysql/${mysqlId}?tab=${e}`; + + router.push(newPath, undefined, { shallow: true }); + }} + > +
+ + General + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + Backups + {permissions?.service.create && ( + Advanced + )} + +
+ + +
+ + + +
+
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + + + )} +
+
+
+ )} + {permissions?.logs.read && ( + +
+ +
+
+ )} + +
+ +
+
+ {permissions?.service.create && ( + +
+ +
+
+ )} +
+ )} +
+
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/mysql/$mysqlId", +)({ component: MySql }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId.tsx new file mode 100644 index 000000000..fb84da440 --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId.tsx @@ -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( + (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 ( +
+ + + + + Database: {data?.name} - {data?.environment?.project?.name} |{" "} + {appName} + + +
+ +
+ +
+ +
+
+ +
+ + +
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to add + more servers. + + + + + )} +
+ +
+ {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( + + )} +
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setSab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/postgres/${postgresId}?tab=${e}`; + + router.push(newPath, undefined, { + shallow: true, + }); + }} + > +
+ + General + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + Backups + {permissions?.service.create && ( + Advanced + )} + +
+ + +
+ + + +
+
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + + + )} +
+
+
+ )} + {permissions?.logs.read && ( + +
+ +
+
+ )} + +
+ +
+
+ {permissions?.service.create && ( + +
+ +
+
+ )} +
+ )} +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/postgres/$postgresId", +)({ component: Postgresql }); diff --git a/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId.tsx b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId.tsx new file mode 100644 index 000000000..b002ce94a --- /dev/null +++ b/apps/vite/src/routes/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId.tsx @@ -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( + (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 ( +
+ + + + + Database: {data?.name} - {data?.environment?.project?.name} |{" "} + {appName} + + +
+ +
+ +
+ +
+
+ +
+ + +
+ {data?.name} +
+ {data?.description && ( + {data?.description} + )} + + + {data?.appName} + +
+
+
+ { + 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"} + + {data?.server?.serverStatus === "inactive" && ( + + + + + + + + You cannot, deploy this application because the + server is inactive, please upgrade your plan to add + more servers. + + + + + )} +
+ +
+ {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( + + )} +
+
+
+ + {data?.server?.serverStatus === "inactive" ? ( +
+
+ + + 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. + + + Go to{" "} + + Billing + + +
+
+ ) : ( + { + setSab(e as TabState); + const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/redis/${redisId}?tab=${e}`; + + router.push(newPath, undefined, { shallow: true }); + }} + > +
+ + General + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + {permissions?.service.create && ( + Advanced + )} + +
+ + +
+ + + +
+
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && ( +
+ + +
+ )} + + {toggleMonitoring ? ( + + ) : ( +
*/} + + {/*
*/} + {/* )} */} + + )} +
+
+
+ )} + {permissions?.logs.read && ( + +
+ +
+
+ )} + {permissions?.service.create && ( + +
+ +
+
+ )} +
+ )} +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute( + "/dashboard/project/$projectId/environment/$environmentId/services/redis/$redisId", +)({ component: Redis }); diff --git a/apps/vite/src/routes/dashboard/projects.tsx b/apps/vite/src/routes/dashboard/projects.tsx new file mode 100644 index 000000000..1c129d23f --- /dev/null +++ b/apps/vite/src/routes/dashboard/projects.tsx @@ -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 && } + + + + ); +}; + +export const Route = createFileRoute("/dashboard/projects")({ + component: Dashboard, +}); diff --git a/apps/vite/src/routes/dashboard/requests.tsx b/apps/vite/src/routes/dashboard/requests.tsx new file mode 100644 index 000000000..104aaaaec --- /dev/null +++ b/apps/vite/src/routes/dashboard/requests.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowRequests } from "@/components/dashboard/requests/show-requests"; + +function Requests() { + return ; +} + +export const Route = createFileRoute("/dashboard/requests")({ + component: Requests, +}); diff --git a/apps/vite/src/routes/dashboard/route.tsx b/apps/vite/src/routes/dashboard/route.tsx new file mode 100644 index 000000000..2a6c238db --- /dev/null +++ b/apps/vite/src/routes/dashboard/route.tsx @@ -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 ( + + + + ); +}; + +export const Route = createFileRoute("/dashboard")({ + beforeLoad: async () => { + const session = await getCachedSession(); + if (!session?.session) { + throw redirect({ to: "/" }); + } + }, + component: DashboardRouteComponent, +}); diff --git a/apps/vite/src/routes/dashboard/schedules.tsx b/apps/vite/src/routes/dashboard/schedules.tsx new file mode 100644 index 000000000..3f56ec9c4 --- /dev/null +++ b/apps/vite/src/routes/dashboard/schedules.tsx @@ -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 ( + + {(serverId) => ( +
+ +
+ +
+
+
+ )} +
+ ); +} + +export const Route = createFileRoute("/dashboard/schedules")({ + component: SchedulesPage, +}); diff --git a/apps/vite/src/routes/dashboard/settings/ai.tsx b/apps/vite/src/routes/dashboard/settings/ai.tsx new file mode 100644 index 000000000..ba7fe728e --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/ai.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { AiForm } from "@/components/dashboard/settings/ai-form"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/ai")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/audit-logs.tsx b/apps/vite/src/routes/dashboard/settings/audit-logs.tsx new file mode 100644 index 000000000..d125b408d --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/audit-logs.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowAuditLogs } from "@/components/proprietary/audit-logs/show-audit-logs"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/audit-logs")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/billing.tsx b/apps/vite/src/routes/dashboard/settings/billing.tsx new file mode 100644 index 000000000..1b9ef7db1 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/billing.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowBilling } from "@/components/dashboard/settings/billing/show-billing"; + +const Page = () => { + return ; +}; + +export const Route = createFileRoute("/dashboard/settings/billing")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/certificates.tsx b/apps/vite/src/routes/dashboard/settings/certificates.tsx new file mode 100644 index 000000000..c83b071a8 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/certificates.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/certificates")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/cluster.tsx b/apps/vite/src/routes/dashboard/settings/cluster.tsx new file mode 100644 index 000000000..36347c016 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/cluster.tsx @@ -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 ( + + {(serverId) => ( +
+ +
+ )} +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/cluster")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/deployments.tsx b/apps/vite/src/routes/dashboard/settings/deployments.tsx new file mode 100644 index 000000000..8cd97959c --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/deployments.tsx @@ -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 ( +
+
+ +
+ + Concurrent Builds + + Configure how many deployments can build at the same time on + each server. Builds of the same service are always serialized. + + + + + 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. + +
+

+ Dokploy Server +

+ +
+ +
+

+ Remote Servers +

+ {servers && servers.length > 0 ? ( +
+ {servers.map((server) => ( + + ))} +
+ ) : ( +

+ No remote servers added yet. +

+ )} +
+
+
+
+
+
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/deployments")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/destinations.tsx b/apps/vite/src/routes/dashboard/settings/destinations.tsx new file mode 100644 index 000000000..01cf1c2d6 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/destinations.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/destinations")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/git-providers.tsx b/apps/vite/src/routes/dashboard/settings/git-providers.tsx new file mode 100644 index 000000000..7d185ad25 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/git-providers.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowGitProviders } from "@/components/dashboard/settings/git/show-git-providers"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/git-providers")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/invoices.tsx b/apps/vite/src/routes/dashboard/settings/invoices.tsx new file mode 100644 index 000000000..61c290585 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/invoices.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices"; + +const Page = () => { + return ; +}; + +export const Route = createFileRoute("/dashboard/settings/invoices")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/license.tsx b/apps/vite/src/routes/dashboard/settings/license.tsx new file mode 100644 index 000000000..14d91fa5e --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/license.tsx @@ -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 ( +
+
+ +
+
+ +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/license")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/notifications.tsx b/apps/vite/src/routes/dashboard/settings/notifications.tsx new file mode 100644 index 000000000..a24fc4515 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/notifications.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/notifications")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/profile.tsx b/apps/vite/src/routes/dashboard/settings/profile.tsx new file mode 100644 index 000000000..b39de7997 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/profile.tsx @@ -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 ( +
+
+ + {isCloud && } + {permissions?.api.read && } +
+
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/profile")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/registry.tsx b/apps/vite/src/routes/dashboard/settings/registry.tsx new file mode 100644 index 000000000..bc5aec55d --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/registry.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/registry")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/secrets.tsx b/apps/vite/src/routes/dashboard/settings/secrets.tsx new file mode 100644 index 000000000..611e33815 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/secrets.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowVaultProviders } from "@/components/dashboard/settings/vault/show-vault-providers"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/secrets")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/server.tsx b/apps/vite/src/routes/dashboard/settings/server.tsx new file mode 100644 index 000000000..06ddc5cbd --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/server.tsx @@ -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 ( +
+
+ + +
+ + + +
+
+
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/server")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/servers.tsx b/apps/vite/src/routes/dashboard/settings/servers.tsx new file mode 100644 index 000000000..1c2bbded3 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/servers.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/servers")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/ssh-keys.tsx b/apps/vite/src/routes/dashboard/settings/ssh-keys.tsx new file mode 100644 index 000000000..31f736596 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/ssh-keys.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/ssh-keys")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/sso.tsx b/apps/vite/src/routes/dashboard/settings/sso.tsx new file mode 100644 index 000000000..ce6d78584 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/sso.tsx @@ -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 ( +
+
+ +
+ + + +
+
+ +
+ + + +
+
+ {!isCloud && ( + +
+ + + + Self-hosted Restrictions + + + Control deployment targets and authentication behavior. + + + + + + + +
+
+ )} +
+
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/sso")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/tags.tsx b/apps/vite/src/routes/dashboard/settings/tags.tsx new file mode 100644 index 000000000..ee3070232 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/tags.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { TagManager } from "@/components/dashboard/settings/tags/tag-manager"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/tags")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/users.tsx b/apps/vite/src/routes/dashboard/settings/users.tsx new file mode 100644 index 000000000..3aac918b7 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/users.tsx @@ -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 ( +
+ + {canCreateMembers && } + {isOwnerOrAdmin && } +
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/users")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/settings/whitelabeling.tsx b/apps/vite/src/routes/dashboard/settings/whitelabeling.tsx new file mode 100644 index 000000000..acecfbb57 --- /dev/null +++ b/apps/vite/src/routes/dashboard/settings/whitelabeling.tsx @@ -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 ( +
+
+ +
+
+ + + +
+
+
+
+
+ ); +}; + +export const Route = createFileRoute("/dashboard/settings/whitelabeling")({ + component: Page, +}); diff --git a/apps/vite/src/routes/dashboard/swarm.tsx b/apps/vite/src/routes/dashboard/swarm.tsx new file mode 100644 index 000000000..167646cf9 --- /dev/null +++ b/apps/vite/src/routes/dashboard/swarm.tsx @@ -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).serverId; + throw redirect({ + href: `/dashboard/docker?tab=swarm${ + typeof serverId === "string" + ? `&serverId=${encodeURIComponent(serverId)}` + : "" + }`, + }); + }, + component: Swarm, +}); diff --git a/apps/vite/src/routes/dashboard/traefik.tsx b/apps/vite/src/routes/dashboard/traefik.tsx new file mode 100644 index 000000000..9c27914af --- /dev/null +++ b/apps/vite/src/routes/dashboard/traefik.tsx @@ -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 ( + + {(serverId) => } + + ); +}; + +export const Route = createFileRoute("/dashboard/traefik")({ + component: Dashboard, +}); diff --git a/apps/vite/src/routes/index.tsx b/apps/vite/src/routes/index.tsx new file mode 100644 index 000000000..548dcb10f --- /dev/null +++ b/apps/vite/src/routes/index.tsx @@ -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; + +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(null); + const [twoFactorCode, setTwoFactorCode] = useState(""); + const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false); + const [backupCode, setBackupCode] = useState(""); + const loginForm = useForm({ + 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 && } + {IS_CLOUD && } +
+ + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + + + + + + ); + + return ( + <> +
+

+
+ + Sign in +
+

+

+ Enter your email and password to sign in +

+
+ {error && ( + + {error} + + )} + + {!isTwoFactor ? ( + <> + {enforceSSO ? ( + + ) : showSignInWithSSO ? ( + {loginContent} + ) : ( + loginContent + )} + + ) : ( + <> +
+
+ + + + + + + + + + + + + + + + Enter the 6-digit code from your authenticator app + + +
+ +
+ + +
+
+ + + + + Enter Backup Code + + Enter one of your backup codes to access your account + + + +
+
+ + setBackupCode(e.target.value)} + placeholder="Enter your backup code" + className="font-mono" + /> + + Enter one of the backup codes you received when setting up + 2FA + +
+ +
+ + +
+
+
+
+ + )} + +
+
+ {IS_CLOUD && ( + + Create an account + + )} +
+ +
+ {IS_CLOUD ? ( + + Lost your password? + + ) : ( + + Lost your password? + + )} +
+
+
+ + + ); +} + +const HomePage = () => { + return ( + + + + ); +}; + +export const Route = createFileRoute("/")({ + component: HomePage, + beforeLoad: async () => { + const session = await getCachedSession(); + if (session?.session) { + throw redirect({ to: "/dashboard/projects" }); + } + }, +}); diff --git a/apps/vite/src/routes/invitation.tsx b/apps/vite/src/routes/invitation.tsx new file mode 100644 index 000000000..0d9d4fa6b --- /dev/null +++ b/apps/vite/src/routes/invitation.tsx @@ -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; + +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({ + 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 ( +
+
+
+ + + + + Invitation + + {userAlreadyExists ? ( +
+ +
+ Valid Invitation! + + We detected that you already have an account with this + email. Please sign in to accept the invitation. + +
+
+ + +
+ ) : ( + <> + + Fill the form below to create your account + +
+
+ + {/* {isError && ( +
+ + + {error?.message} + +
+ )} */} + + +
+ +
+ ( + + First Name + + + + + + )} + /> + ( + + Last Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + + ( + + Confirm Password + + + + + + )} + /> + + +
+ +
+ {isCloud && ( + <> + + Login + + + Lost your password? + + + )} +
+
+ +
+
+ + )} +
+
+
+ ); +}; + +const InvitationPage = () => { + return ( + + + + ); +}; + +export const Route = createFileRoute("/invitation")({ + component: InvitationPage, +}); diff --git a/apps/vite/src/routes/register.tsx b/apps/vite/src/routes/register.tsx new file mode 100644 index 000000000..daec3675c --- /dev/null +++ b/apps/vite/src/routes/register.tsx @@ -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; + +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(null); + const [data, setData] = useState(null); + + const form = useForm({ + 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 ( +
+
+
+ + + + + {isCloud ? "Sign Up" : "Setup the server"} + + + Enter your email and password to{" "} + {isCloud ? "create an account" : "setup the server"} + +
+ {isError && ( +
+ + + {error} + +
+ )} + {isCloud && data && ( + + + Registered successfully, please check your inbox or spam + folder to confirm your account. + + + )} + + {isCloud && ( +
+ + +
+ )} + {isCloud && ( +

+ Or register with email +

+ )} +
+ +
+ ( + + First Name + + + + + + )} + /> + ( + + Last Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + + ( + + Confirm Password + + + + + + )} + /> + + +
+
+ +
+ {isCloud && ( +
+ Already have account? + + Sign in + +
+ )} + +
+ Need help? + + Contact us + +
+
+
+
+
+
+
+ ); +}; + +const RegisterPage = () => { + return ( + + + + ); +}; + +export const Route = createFileRoute("/register")({ + component: RegisterPage, + beforeLoad: async () => { + const session = await getCachedSession(); + if (session?.session) { + throw redirect({ to: "/dashboard/projects" }); + } + }, +}); diff --git a/apps/vite/src/routes/reset-password.tsx b/apps/vite/src/routes/reset-password.tsx new file mode 100644 index 000000000..62b56b661 --- /dev/null +++ b/apps/vite/src/routes/reset-password.tsx @@ -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; + +function Home() { + const { config: whitelabeling } = useWhitelabelingPublic(); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + const form = useForm({ + 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 ( +
+
+ + + + + Reset Password + + + Enter your email to reset your password + + +
+ + {error && ( + + {error} + + )} +
+ +
+ ( + + Password + + + + + + )} + /> + ( + + Confirm Password + + + + + + )} + /> + + +
+ +
+ Sign in +
+
+ +
+
+
+
+ ); +} + +const ResetPasswordPage = () => { + return ( + + + + ); +}; + +export const Route = createFileRoute("/reset-password")({ + component: ResetPasswordPage, +}); diff --git a/apps/vite/src/routes/send-reset-password.tsx b/apps/vite/src/routes/send-reset-password.tsx new file mode 100644 index 000000000..77f73c979 --- /dev/null +++ b/apps/vite/src/routes/send-reset-password.tsx @@ -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; + +type AuthResponse = { + is2FAEnabled: boolean; + authId: string; +}; + +function Home() { + const { config: whitelabeling } = useWhitelabelingPublic(); + const [temp, _setTemp] = useState({ + is2FAEnabled: false, + authId: "", + }); + + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const _router = useRouter(); + const form = useForm({ + 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 ( +
+
+ + + + {whitelabeling?.appName || "Dokploy"} + + + Reset Password + + Enter your email to reset your password + + +
+ + {error && ( + + {error} + + )} + {!temp.is2FAEnabled ? ( +
+ +
+ ( + + Email + + + + + + )} + /> + + +
+
+ + ) : null} + +
+
+ + Login + +
+
+
+
+
+
+ ); +} + +const SendResetPasswordPage = () => { + return ( + + + + ); +}; + +export const Route = createFileRoute("/send-reset-password")({ + component: SendResetPasswordPage, +}); diff --git a/apps/vite/src/routes/swagger.tsx b/apps/vite/src/routes/swagger.tsx new file mode 100644 index 000000000..cbe4df4c6 --- /dev/null +++ b/apps/vite/src/routes/swagger.tsx @@ -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).jsonSchemaDialect; + } + setSpec(newSpec); + } + }, [data]); + + return ( +
+ (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; + }} + /> +
+ ); +}; + +export const Route = createFileRoute("/swagger")({ + component: Home, + beforeLoad: async () => { + const session = await getCachedSession(); + if (!session?.session) { + throw redirect({ to: "/" }); + } + }, +}); diff --git a/apps/vite/src/shims/next-dynamic.tsx b/apps/vite/src/shims/next-dynamic.tsx new file mode 100644 index 000000000..f40c349cd --- /dev/null +++ b/apps/vite/src/shims/next-dynamic.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; + +type ComponentModule

= + | { default: React.ComponentType

} + | React.ComponentType

; + +interface DynamicOptions { + ssr?: boolean; + loading?: React.ComponentType; +} + +const dynamic =

( + loader: () => Promise>, + 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

}; + } + return { default: mod as React.ComponentType

}; + }) as unknown as React.ComponentType

; + + const Loading = options?.loading; + + const DynamicComponent = (props: P) => ( + : null}> + + + ); + + return DynamicComponent; +}; + +export default dynamic; diff --git a/apps/vite/src/shims/next-head.tsx b/apps/vite/src/shims/next-head.tsx new file mode 100644 index 000000000..c56ae45a4 --- /dev/null +++ b/apps/vite/src/shims/next-head.tsx @@ -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; diff --git a/apps/vite/src/shims/next-link.tsx b/apps/vite/src/shims/next-link.tsx new file mode 100644 index 000000000..ce060a82d --- /dev/null +++ b/apps/vite/src/shims/next-link.tsx @@ -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, "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( + ( + { + 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 ; + } + + return ( + )} + /> + ); + }, +); + +Link.displayName = "NextLinkShim"; + +export default Link; diff --git a/apps/vite/src/shims/next-navigation.ts b/apps/vite/src/shims/next-navigation.ts new file mode 100644 index 000000000..e48ef8e2d --- /dev/null +++ b/apps/vite/src/shims/next-navigation.ts @@ -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) => {}, + }; +}; diff --git a/apps/vite/src/shims/next-router.ts b/apps/vite/src/shims/next-router.ts new file mode 100644 index 000000000..b6c50a48f --- /dev/null +++ b/apps/vite/src/shims/next-router.ts @@ -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 | 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; + const search = useSearch({ strict: false }) as Record; + + return useMemo(() => { + const pathname = location.pathname; + return { + pathname, + route: pathname, + asPath: `${pathname}${location.searchStr ?? ""}${location.hash ? `#${location.hash}` : ""}`, + query: { ...search, ...params } as Record, + 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; diff --git a/apps/vite/src/shims/next-script.tsx b/apps/vite/src/shims/next-script.tsx new file mode 100644 index 000000000..28f1afa9b --- /dev/null +++ b/apps/vite/src/shims/next-script.tsx @@ -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; diff --git a/apps/vite/src/shims/toploader.tsx b/apps/vite/src/shims/toploader.tsx new file mode 100644 index 000000000..54742522c --- /dev/null +++ b/apps/vite/src/shims/toploader.tsx @@ -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 ( +

+ ); +}; + +export default TopLoader; diff --git a/apps/vite/src/styles/globals.css b/apps/vite/src/styles/globals.css new file mode 100644 index 000000000..5e8316bfe --- /dev/null +++ b/apps/vite/src/styles/globals.css @@ -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); +} diff --git a/apps/vite/src/utils/api.ts b/apps/vite/src/utils/api.ts new file mode 100644 index 000000000..aa14f8dc0 --- /dev/null +++ b/apps/vite/src/utils/api.ts @@ -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(); + +export type RouterInputs = inferRouterInputs; +export type RouterOutputs = inferRouterOutputs; diff --git a/apps/vite/src/utils/session.ts b/apps/vite/src/utils/session.ts new file mode 100644 index 000000000..66b9ab311 --- /dev/null +++ b/apps/vite/src/utils/session.ts @@ -0,0 +1,20 @@ +import { authClient } from "@/lib/auth-client"; + +type SessionData = Awaited>["data"]; + +let cached: { at: number; session: SessionData } | null = null; + +const TTL_MS = 30_000; + +export const getCachedSession = async (): Promise => { + 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; +}; diff --git a/apps/vite/src/utils/trpc-provider.tsx b/apps/vite/src/utils/trpc-provider.tsx new file mode 100644 index 000000000..7ca583029 --- /dev/null +++ b/apps/vite/src/utils/trpc-provider.tsx @@ -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 | 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 ( + + {children} + + ); +}; diff --git a/apps/vite/tsconfig.json b/apps/vite/tsconfig.json new file mode 100644 index 000000000..e77ee6305 --- /dev/null +++ b/apps/vite/tsconfig.json @@ -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"] +} diff --git a/apps/vite/vite.config.ts b/apps/vite/vite.config.ts new file mode 100644 index 000000000..c379bc632 --- /dev/null +++ b/apps/vite/vite.config.ts @@ -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 }]), + ), + }, + }, +}); diff --git a/biome.json b/biome.json index f75baac2b..05cff59a5 100644 --- a/biome.json +++ b/biome.json @@ -13,6 +13,7 @@ "!**/.next/**", "!**/dist", "!**/drizzle/**", + "!**/routeTree.gen.ts", "!node_modules/**", "!packages/server/package.json" ], diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index e5de5a6dc..0637f803b 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -48,6 +48,7 @@ const resolveTrustedOrigins = async () => { ? [ "http://localhost:3000", "https://absolutely-handy-falcon.ngrok-free.app", + "http://localhost:5173" ] : []; return [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44dba075a..20fe3c202 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -562,6 +562,373 @@ importers: specifier: ^5.8.3 version: 5.9.3 + apps/vite: + dependencies: + '@ai-sdk/anthropic': + specifier: ^3.0.44 + version: 3.0.46(zod@4.3.6) + '@ai-sdk/azure': + specifier: ^3.0.30 + version: 3.0.32(zod@4.3.6) + '@ai-sdk/cohere': + specifier: ^3.0.21 + version: 3.0.21(zod@4.3.6) + '@ai-sdk/deepinfra': + specifier: ^2.0.34 + version: 2.0.34(zod@4.3.6) + '@ai-sdk/mistral': + specifier: ^3.0.20 + version: 3.0.20(zod@4.3.6) + '@ai-sdk/openai': + specifier: ^3.0.29 + version: 3.0.31(zod@4.3.6) + '@ai-sdk/openai-compatible': + specifier: ^2.0.30 + version: 2.0.30(zod@4.3.6) + '@aws-sdk/client-secrets-manager': + specifier: ^3.1097.0 + version: 3.1097.0 + '@better-auth/api-key': + specifier: 1.6.23 + version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6)) + '@better-auth/passkey': + specifier: 1.6.23 + version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))(nanostores@1.1.1) + '@better-auth/scim': + specifier: 1.6.23 + version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6)) + '@better-auth/sso': + specifier: 1.6.23 + version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6)) + '@dokploy/server': + specifier: workspace:* + version: link:../../packages/server + '@dokploy/trpc-openapi': + specifier: 0.0.18 + version: 0.0.18(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6) + '@faker-js/faker': + specifier: ^8.4.1 + version: 8.4.1 + '@octokit/auth-app': + specifier: ^6.1.3 + version: 6.1.4 + '@octokit/webhooks': + specifier: ^13.9.0 + version: 13.9.1 + '@react-email/components': + specifier: ^1.0.12 + version: 1.0.12(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@trpc/server': + specifier: ^11.10.0 + version: 11.10.0(typescript@5.9.3) + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 + ai: + specifier: ^6.0.86 + version: 6.0.97(zod@4.3.6) + ai-sdk-ollama: + specifier: ^3.7.0 + version: 3.7.1(ai@6.0.97(zod@4.3.6))(zod@4.3.6) + bcrypt: + specifier: 5.1.1 + version: 5.1.1(encoding@0.1.13) + better-auth: + specifier: 1.6.23 + version: 1.6.23(c1a003db19823d196c7d6468bf307df5) + bl: + specifier: 6.0.11 + version: 6.0.11 + boxen: + specifier: ^7.1.1 + version: 7.1.1 + date-fns: + specifier: 3.6.0 + version: 3.6.0 + dockerode: + specifier: 4.0.2 + version: 4.0.2 + dotenv: + specifier: 16.4.5 + version: 16.4.5 + drizzle-orm: + specifier: 0.45.2 + version: 0.45.2(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.29.3)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)) + drizzle-zod: + specifier: 0.8.3 + version: 0.8.3(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.29.3)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)))(zod@4.3.6) + lodash: + specifier: 4.17.21 + version: 4.17.21 + micromatch: + specifier: 4.0.8 + version: 4.0.8 + nanoid: + specifier: 3.3.11 + version: 3.3.11 + node-os-utils: + specifier: 2.0.1 + version: 2.0.1 + node-pty: + specifier: 1.1.0 + version: 1.1.0 + node-schedule: + specifier: 2.1.1 + version: 2.1.1 + nodemailer: + specifier: 6.9.14 + version: 6.9.14 + octokit: + specifier: 3.1.2 + version: 3.1.2 + pino: + specifier: 9.4.0 + version: 9.4.0 + pino-pretty: + specifier: 11.2.2 + version: 11.2.2 + postgres: + specifier: 3.4.4 + version: 3.4.4 + public-ip: + specifier: 6.0.2 + version: 6.0.2 + react: + specifier: 19.2.7 + version: 19.2.7 + react-dom: + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) + resend: + specifier: ^6.0.2 + version: 6.9.2(@react-email/render@2.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + semver: + specifier: 7.7.3 + version: 7.7.3 + shell-quote: + specifier: ^1.8.1 + version: 1.8.3 + slugify: + specifier: ^1.6.6 + version: 1.6.6 + ssh2: + specifier: ~1.16.0 + version: 1.16.0 + stripe: + specifier: 17.2.0 + version: 17.2.0 + superjson: + specifier: ^2.2.2 + version: 2.2.6 + toml: + specifier: 3.0.0 + version: 3.0.0 + undici: + specifier: ^6.21.3 + version: 6.23.0 + ws: + specifier: 8.16.0 + version: 8.16.0 + yaml: + specifier: 2.8.1 + version: 2.8.1 + zod: + specifier: ^4.3.6 + version: 4.3.6 + zod-form-data: + specifier: ^3.0.1 + version: 3.0.1(zod@4.3.6) + devDependencies: + '@codemirror/autocomplete': + specifier: ^6.18.6 + version: 6.20.3 + '@codemirror/lang-css': + specifier: ^6.3.1 + version: 6.3.1 + '@codemirror/lang-json': + specifier: ^6.0.1 + version: 6.0.2 + '@codemirror/lang-yaml': + specifier: ^6.1.2 + version: 6.1.2 + '@codemirror/language': + specifier: ^6.11.0 + version: 6.12.4 + '@codemirror/legacy-modes': + specifier: 6.4.0 + version: 6.4.0 + '@codemirror/search': + specifier: ^6.6.0 + version: 6.7.1 + '@codemirror/view': + specifier: ^6.39.15 + version: 6.43.6 + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.71.2(react@19.2.7)) + '@stepperize/react': + specifier: 4.0.1 + version: 4.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@stripe/stripe-js': + specifier: 4.8.0 + version: 4.8.0 + '@tailwindcss/typography': + specifier: 0.5.16 + version: 0.5.16(tailwindcss@4.3.1) + '@tailwindcss/vite': + specifier: ^4.3.1 + version: 4.3.3(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1)) + '@tanstack/react-query': + specifier: ^5.90.21 + version: 5.90.21(react@19.2.7) + '@tanstack/react-router': + specifier: ^1.132.0 + version: 1.170.25(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-plugin': + specifier: ^1.132.0 + version: 1.168.29(@tanstack/react-router@1.170.25(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(esbuild@0.20.2)(rollup@4.59.0)(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1)) + '@trpc/client': + specifier: ^11.10.0 + version: 11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3) + '@trpc/react-query': + specifier: ^11.10.0 + version: 11.10.0(@tanstack/react-query@5.90.21(react@19.2.7))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(react@19.2.7)(typescript@5.9.3) + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/lodash': + specifier: 4.17.4 + version: 4.17.4 + '@types/node': + specifier: ^24.4.0 + version: 24.10.13 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.6 + '@types/react': + specifier: 18.3.5 + version: 18.3.5 + '@types/react-dom': + specifier: 18.3.0 + version: 18.3.0 + '@types/semver': + specifier: 7.7.1 + version: 7.7.1 + '@types/swagger-ui-react': + specifier: ^4.19.0 + version: 4.19.0 + '@uiw/codemirror-theme-github': + specifier: ^4.23.12 + version: 4.25.4(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6) + '@uiw/react-codemirror': + specifier: ^4.23.12 + version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.4)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.6)(codemirror@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.0.0 + version: 5.2.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1)) + '@xterm/addon-attach': + specifier: 0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/addon-clipboard': + specifier: 0.1.0 + version: 0.1.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^0.2.1 + version: 0.2.1(@types/react@18.3.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + copy-to-clipboard: + specifier: ^3.3.3 + version: 3.3.3 + dompurify: + specifier: ^3.3.3 + version: 3.4.10 + esbuild: + specifier: 0.20.2 + version: 0.20.2 + fancy-ansi: + specifier: ^0.1.3 + version: 0.1.3 + input-otp: + specifier: ^1.4.2 + version: 1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + lucide-react: + specifier: ^0.469.0 + version: 0.469.0(react@19.2.7) + next-themes: + specifier: ^0.2.1 + version: 0.2.1(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + radix-ui: + specifier: ^1.6.0 + version: 1.6.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-confetti-explosion: + specifier: 3.0.3 + version: 3.0.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-day-picker: + specifier: 10.0.1 + version: 10.0.1(@types/react@18.3.5)(react@19.2.7) + react-hook-form: + specifier: ^7.71.2 + version: 7.71.2(react@19.2.7) + react-markdown: + specifier: ^9.1.0 + version: 9.1.0(@types/react@18.3.5)(react@19.2.7) + recharts: + specifier: ^3.8.0 + version: 3.8.0(@types/react@18.3.5)(react-dom@19.2.7(react@19.2.7))(react-is@18.3.1)(react@19.2.7)(redux@5.0.1) + shadcn: + specifier: ^4.11.0 + version: 4.11.0(typescript@5.9.3) + sonner: + specifier: ^1.7.4 + version: 1.7.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + swagger-ui-react: + specifier: ^5.32.6 + version: 5.32.6(@types/react@18.3.5)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tailwind-merge: + specifier: ^2.6.1 + version: 2.6.1 + tailwindcss: + specifier: ^4.3.1 + version: 4.3.1 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.3.1) + tsx: + specifier: ^4.22.4 + version: 4.22.4 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + use-resize-observer: + specifier: 9.1.0 + version: 9.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + vite: + specifier: ^7.1.0 + version: 7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1) + xterm-addon-fit: + specifier: ^0.8.0 + version: 0.8.0(xterm@5.3.0) + packages/server: dependencies: '@ai-sdk/anthropic': @@ -997,6 +1364,10 @@ packages: resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + '@babel/helper-replace-supers@7.28.6': resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} engines: {node: '>=6.9.0'} @@ -1046,6 +1417,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.6': resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} engines: {node: '>=6.9.0'} @@ -3700,6 +4083,9 @@ packages: react-redux: optional: true + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -4026,60 +4412,117 @@ packages: '@tailwindcss/node@4.3.1': resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + '@tailwindcss/oxide-android-arm64@4.3.1': resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} engines: {node: '>= 20'} cpu: [arm64] os: [android] + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + '@tailwindcss/oxide-darwin-arm64@4.3.1': resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.3.1': resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + '@tailwindcss/oxide-freebsd-x64@4.3.1': resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} engines: {node: '>= 20'} cpu: [arm] os: [linux] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + '@tailwindcss/oxide-linux-x64-musl@4.3.1': resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + '@tailwindcss/oxide-wasm32-wasi@4.3.1': resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} engines: {node: '>=14.0.0'} @@ -4092,22 +4535,50 @@ packages: - '@emnapi/wasi-threads' - tslib + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} engines: {node: '>= 20'} cpu: [x64] os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + '@tailwindcss/oxide@4.3.1': resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} engines: {node: '>= 20'} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + '@tailwindcss/postcss@4.3.1': resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==} @@ -4116,6 +4587,15 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/history@1.162.1': + resolution: {integrity: sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==} + engines: {node: '>=20.19'} + '@tanstack/query-core@5.90.20': resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} @@ -4124,6 +4604,19 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-router@1.170.25': + resolution: {integrity: sha512-XiWYvkLAGhcZHhV2xUvicpF/VfTVSnewP6CqviDONAjmD50tBob5x/93u2W8QZAc/JgkPd+jijCmu6t4NCzmew==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} engines: {node: '>=12'} @@ -4131,10 +4624,50 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/router-core@1.171.21': + resolution: {integrity: sha512-t6xUHBO94nQDrPHPh6ag5UuKHWIkVjq9s63q2ctbxgzq3vtSoGKmAIdLD5o+NU6JUS1ppXRHgL0YGzEHvH32Ng==} + engines: {node: '>=20.19'} + + '@tanstack/router-generator@1.167.27': + resolution: {integrity: sha512-bfV44Nyy7XeSCCuvQWGc3qudqXfOqAwKiWXNymAuK+hy24vs3E75Aw1zMz97ajHXMjxm4nFVVjEnwZ/cDR74ug==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.168.29': + resolution: {integrity: sha512-h1ZbnIvbAKIFeQ6JBRJ/xBP17tBdwjwzPBKVKUdVNYiob07SkrQLhqz+s41WtbNmHey5kQWN3HqP16Vl062bqg==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.25 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.162.2': + resolution: {integrity: sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + '@tree-sitter-grammars/tree-sitter-yaml@0.7.1': resolution: {integrity: sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==} peerDependencies: @@ -4192,6 +4725,18 @@ packages: '@types/aws-lambda@8.10.160': resolution: {integrity: sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcrypt@5.0.2': resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} @@ -4416,6 +4961,12 @@ packages: resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} @@ -4574,6 +5125,10 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -4652,6 +5207,9 @@ packages: axios@1.18.0: resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==} + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4922,6 +5480,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -5078,6 +5640,9 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -5631,6 +6196,10 @@ packages: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -6420,6 +6989,10 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbot@5.2.1: + resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -7758,6 +8331,10 @@ packages: redux: optional: true + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -7827,6 +8404,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -8037,6 +8618,16 @@ packages: resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} engines: {node: '>=10'} + seroval-plugins@1.6.2: + resolution: {integrity: sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.6.2: + resolution: {integrity: sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ==} + engines: {node: '>=10'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -8362,6 +8953,9 @@ packages: tailwindcss@4.3.1: resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} @@ -8621,6 +9215,39 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin@3.3.0: + resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: 0.20.2 + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + unraw@3.0.0: resolution: {integrity: sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==} @@ -8805,6 +9432,9 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} @@ -8995,6 +9625,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -9307,6 +9940,8 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -9356,6 +9991,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -9503,7 +10148,7 @@ snapshots: '@better-auth/utils': 0.4.2 better-auth: 1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193) better-call: 1.3.7(zod@4.3.6) - zod: 4.3.6 + zod: 4.4.3 '@better-auth/scim@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))': dependencies: @@ -9511,7 +10156,7 @@ snapshots: '@better-auth/utils': 0.4.2 better-auth: 1.6.23(c1a003db19823d196c7d6468bf307df5) better-call: 1.3.7(zod@4.3.6) - zod: 4.3.6 + zod: 4.4.3 '@better-auth/sso@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193))(better-call@1.3.7(zod@4.3.6))': dependencies: @@ -12398,6 +13043,8 @@ snapshots: react: 19.2.7 react-redux: 9.2.0(@types/react@18.3.5)(react@19.2.7)(redux@5.0.1) + '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -13001,42 +13648,88 @@ snapshots: source-map-js: 1.2.1 tailwindcss: 4.3.1 + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + '@tailwindcss/oxide-android-arm64@4.3.1': optional: true + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + '@tailwindcss/oxide-darwin-arm64@4.3.1': optional: true + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + '@tailwindcss/oxide-darwin-x64@4.3.1': optional: true + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + '@tailwindcss/oxide-freebsd-x64@4.3.1': optional: true + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + '@tailwindcss/oxide-linux-x64-musl@4.3.1': optional: true + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + '@tailwindcss/oxide-wasm32-wasi@4.3.1': optional: true + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + '@tailwindcss/oxide@4.3.1': optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.3.1 @@ -13052,6 +13745,21 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + '@tailwindcss/postcss@4.3.1': dependencies: '@alloc/quick-lru': 5.2.0 @@ -13068,6 +13776,15 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.3.1 + '@tailwindcss/vite@4.3.3(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1) + + '@tanstack/history@1.162.1': {} + '@tanstack/query-core@5.90.20': {} '@tanstack/react-query@5.90.21(react@19.2.7)': @@ -13075,14 +13792,91 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.7 + '@tanstack/react-router@1.170.25(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/history': 1.162.1 + '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.21 + isbot: 5.2.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + '@tanstack/react-table@8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@tanstack/table-core': 8.21.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + '@tanstack/router-core@1.171.21': + dependencies: + '@tanstack/history': 1.162.1 + cookie-es: 3.1.1 + seroval: 1.6.2 + seroval-plugins: 1.6.2(seroval@1.6.2) + + '@tanstack/router-generator@1.167.27': + dependencies: + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.171.21 + '@tanstack/router-utils': 1.162.2 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.8.1 + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.168.29(@tanstack/react-router@1.170.25(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(esbuild@0.20.2)(rollup@4.59.0)(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.171.21 + '@tanstack/router-generator': 1.167.27 + '@tanstack/router-utils': 1.162.2 + chokidar: 5.0.0 + unplugin: 3.3.0(esbuild@0.20.2)(rollup@4.59.0)(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1)) + zod: 4.4.3 + optionalDependencies: + '@tanstack/react-router': 1.170.25(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - supports-color + - unloader + + '@tanstack/router-utils@1.162.2': + dependencies: + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + ansis: 4.3.1 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.9.3': {} + '@tanstack/table-core@8.21.3': {} + '@tanstack/virtual-file-routes@1.162.0': {} + '@tree-sitter-grammars/tree-sitter-yaml@0.7.1(tree-sitter@0.22.4)': dependencies: node-addon-api: 8.5.0 @@ -13134,6 +13928,27 @@ snapshots: '@types/aws-lambda@8.10.160': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/bcrypt@5.0.2': dependencies: '@types/node': 24.10.13 @@ -13385,6 +14200,18 @@ snapshots: '@vercel/oidc@3.1.0': {} + '@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -13542,6 +14369,8 @@ snapshots: ansi-styles@6.2.3: {} + ansis@4.3.1: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -13617,6 +14446,15 @@ snapshots: - debug - supports-color + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -13943,6 +14781,10 @@ snapshots: readdirp: 4.1.2 optional: true + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + chownr@1.1.4: {} chownr@2.0.0: {} @@ -14087,6 +14929,8 @@ snapshots: cookie-es@1.2.2: {} + cookie-es@3.1.1: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -14442,6 +15286,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -15303,6 +16152,8 @@ snapshots: isarray@2.0.5: {} + isbot@5.2.1: {} + isexe@2.0.0: {} isexe@3.1.5: {} @@ -16774,6 +17625,8 @@ snapshots: '@types/react': 18.3.5 redux: 5.0.1 + react-refresh@0.18.0: {} + react-remove-scroll-bar@2.3.8(@types/react@18.3.5)(react@19.2.7): dependencies: react: 19.2.7 @@ -16849,6 +17702,8 @@ snapshots: readdirp@4.1.2: optional: true + readdirp@5.1.1: {} + real-require@0.2.0: {} recast@0.23.11: @@ -17109,6 +17964,12 @@ snapshots: dependencies: type-fest: 0.20.2 + seroval-plugins@1.6.2(seroval@1.6.2): + dependencies: + seroval: 1.6.2 + + seroval@1.6.2: {} + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -17572,6 +18433,8 @@ snapshots: tailwindcss@4.3.1: {} + tailwindcss@4.3.3: {} + tapable@2.3.3: {} tar-fs@2.0.1: @@ -17838,6 +18701,16 @@ snapshots: unpipe@1.0.0: {} + unplugin@3.3.0(esbuild@0.20.2)(rollup@4.59.0)(vite@7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + esbuild: 0.20.2 + rollup: 4.59.0 + vite: 7.3.1(@types/node@24.10.13)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.8.1) + unraw@3.0.0: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -18053,6 +18926,8 @@ snapshots: webidl-conversions@7.0.0: optional: true + webpack-virtual-modules@0.6.2: {} + whatwg-fetch@3.6.20: {} whatwg-url@14.2.0: @@ -18236,4 +19111,6 @@ snapshots: zod@4.3.6: {} + zod@4.4.3: {} + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4c1316ebc..5b8433d7e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - "apps/api" - "apps/dokploy" - "apps/schedules" + - "apps/vite" - "packages/server"