mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'Dokploy:canary' into canary
This commit is contained in:
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -72,6 +72,8 @@ jobs:
|
||||
tags: |
|
||||
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ github.ref_name == 'main' && 'pk_live_51QAm7bF3cxQuHeOzMpfNfJIch6oLif8rS32pRE392CdTbBf0MYBdbapAxarQGspqJBWT2nVOxu8e6ZHrHB4NhVHG008DE2A90d' || 'pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1' }}
|
||||
|
||||
build-and-push-schedule-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -71,10 +71,9 @@ Run the command that will spin up all the required services and files.
|
||||
pnpm run dokploy:setup
|
||||
```
|
||||
|
||||
Build the server package (If you make any changes after in the packages/server folder, you need to rebuild and run this command)
|
||||
|
||||
Run this script
|
||||
```bash
|
||||
pnpm run server:build
|
||||
pnpm run server:script
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
|
||||
@@ -15,6 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server switch:prod
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/dokploy run build
|
||||
|
||||
|
||||
@@ -12,9 +12,12 @@ RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
# Deploy only the dokploy app
|
||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server switch:prod
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/dokploy run build
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server switch:prod
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/schedules run build
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server switch:prod
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/api run build
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
||||
|
||||
## Additional Terms for Specific Features
|
||||
|
||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||
|
||||
For further inquiries or permissions, please contact us directly.
|
||||
|
||||
24
README.md
24
README.md
@@ -15,29 +15,29 @@
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
|
||||
### Features
|
||||
|
||||
Dokploy include multiples features to make your life easier.
|
||||
Dokploy includes multiple features to make your life easier.
|
||||
|
||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
||||
- **Backups**: Automate backups for databases to a external storage destination.
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||
- **Backups**: Automate backups for databases to an external storage destination.
|
||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||
- **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
||||
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
||||
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
|
||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
||||
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
||||
- **Multi Server**: Deploy and manager your applications remotely to external servers.
|
||||
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
||||
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
|
||||
- **Multi Server**: Deploy and manage your applications remotely to external servers.
|
||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
To get started run the following command in a VPS:
|
||||
To get started, run the following command on a VPS:
|
||||
|
||||
```bash
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
@@ -90,6 +90,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
#### Organizations:
|
||||
|
||||
@@ -8,7 +8,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
serverId: z.string(),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
@@ -17,7 +17,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
serverId: z.string(),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
rebuildRemoteCompose,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
} from "@dokploy/server";
|
||||
} from "@dokploy/server/dist";
|
||||
import type { DeployJob } from "./schema";
|
||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ description: '学习如何在服务器上手动安装 Dokploy。'
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 确保以根用户身份运行脚本
|
||||
# Ensure the script is run as root
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "This script must be run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 Linux 操作系统(非 macOS 或 Docker 容器内的操作系统)
|
||||
# Check for Linux OS (not macOS or inside a Docker container)
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
echo "This script must be run on Linux" >&2
|
||||
exit 1
|
||||
@@ -32,7 +32,7 @@ if [ -f /.dockerenv ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查端口是否被占用
|
||||
# Check for occupied ports
|
||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||
echo "Error: Port 80 is already in use" >&2
|
||||
exit 1
|
||||
@@ -43,32 +43,53 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查命令是否存在
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# 如果未安装 Docker 则安装
|
||||
# Install Docker if it is not installed
|
||||
if command_exists docker; then
|
||||
echo "Docker already installed"
|
||||
else
|
||||
curl -sSL https://get.docker.com | sh
|
||||
fi
|
||||
|
||||
# 初始化 Docker Swarm
|
||||
# Initialize Docker Swarm
|
||||
docker swarm leave --force 2>/dev/null
|
||||
advertise_addr=$(curl -s ifconfig.me)
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
echo "Swarm initialized"
|
||||
|
||||
# 创建网络
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
echo "Network created"
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# 准备配置目录
|
||||
mkdir -p /etc/dokploy
|
||||
chmod -R 777 /etc/dokploy
|
||||
if [ -n "$ipv4" ]; then
|
||||
echo "$ipv4"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
|
||||
echo "Network created"
|
||||
|
||||
mkdir -p /etc/dokploy
|
||||
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
# Pull and deploy Dokploy
|
||||
docker pull dokploy/dokploy:latest
|
||||
@@ -84,9 +105,10 @@ docker service create \
|
||||
-e PORT=<Value For PORT eg(3000)> \
|
||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||
-e ADVERTISE_ADDR=$advertise_addr \
|
||||
dokploy/dokploy:latest
|
||||
|
||||
# 输出成功消息
|
||||
# Output success message
|
||||
GREEN="\033[0;32m"
|
||||
YELLOW="\033[1;33m"
|
||||
BLUE="\033[0;34m"
|
||||
|
||||
@@ -57,18 +57,39 @@ fi
|
||||
|
||||
# Initialize Docker Swarm
|
||||
docker swarm leave --force 2>/dev/null
|
||||
advertise_addr=$(curl -s ifconfig.me)
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
echo "Swarm initialized"
|
||||
|
||||
# Create network
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
echo "Network created"
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# Prepare configuration directory
|
||||
mkdir -p /etc/dokploy
|
||||
chmod -R 777 /etc/dokploy
|
||||
if [ -n "$ipv4" ]; then
|
||||
echo "$ipv4"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
|
||||
echo "Network created"
|
||||
|
||||
mkdir -p /etc/dokploy
|
||||
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
# Pull and deploy Dokploy
|
||||
docker pull dokploy/dokploy:latest
|
||||
@@ -84,6 +105,7 @@ docker service create \
|
||||
-e PORT=<Value For PORT eg(3000)> \
|
||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||
-e ADVERTISE_ADDR=$advertise_addr \
|
||||
dokploy/dokploy:latest
|
||||
|
||||
# Output success message
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/dist/constants";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { unzipDrop } from "@dokploy/server";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
// @ts-ignore
|
||||
...actual,
|
||||
paths: () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
const undici = require("undici");
|
||||
globalThis.File = undici.File as any;
|
||||
@@ -82,16 +93,6 @@ const baseApp: ApplicationNested = {
|
||||
dockerContextPath: null,
|
||||
};
|
||||
|
||||
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
// @ts-ignore
|
||||
...actual,
|
||||
paths: () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}),
|
||||
};
|
||||
});
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
// const { APPLICATIONS_PATH } = paths();
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -24,6 +24,9 @@ const baseAdmin: Admin = {
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
enableLogRotation: false,
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: "",
|
||||
stripeSubscriptionId: "",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import path from "node:path";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
root: "./",
|
||||
projects: ["tsconfig.json"],
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||
@@ -18,4 +13,12 @@ export default defineConfig({
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@dokploy/server": path.resolve(
|
||||
__dirname,
|
||||
"../../../packages/server/src",
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NumberInput } from "@/components/ui/input";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import clsx from "clsx";
|
||||
import { AlertTriangle, CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const stripePromise = loadStripe(
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || "",
|
||||
);
|
||||
|
||||
export const calculatePrice = (count: number, isAnnual = false) => {
|
||||
if (isAnnual) {
|
||||
if (count <= 1) return 45.9;
|
||||
return 35.7 * count;
|
||||
}
|
||||
if (count <= 1) return 4.5;
|
||||
return count * 3.5;
|
||||
};
|
||||
export const ShowBilling = () => {
|
||||
const { data: servers } = api.server.all.useQuery(undefined);
|
||||
const { data: admin } = api.admin.one.useQuery();
|
||||
const { data } = api.stripe.getProducts.useQuery();
|
||||
const { mutateAsync: createCheckoutSession } =
|
||||
api.stripe.createCheckoutSession.useMutation();
|
||||
|
||||
const { mutateAsync: createCustomerPortalSession } =
|
||||
api.stripe.createCustomerPortalSession.useMutation();
|
||||
|
||||
const [serverQuantity, setServerQuantity] = useState(3);
|
||||
const [isAnnual, setIsAnnual] = useState(false);
|
||||
|
||||
const handleCheckout = async (productId: string) => {
|
||||
const stripe = await stripePromise;
|
||||
if (data && data.subscriptions.length === 0) {
|
||||
createCheckoutSession({
|
||||
productId,
|
||||
serverQuantity: serverQuantity,
|
||||
isAnnual,
|
||||
}).then(async (session) => {
|
||||
await stripe?.redirectToCheckout({
|
||||
sessionId: session.sessionId,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
const products = data?.products.filter((product) => {
|
||||
// @ts-ignore
|
||||
const interval = product?.default_price?.recurring?.interval;
|
||||
return isAnnual ? interval === "year" : interval === "month";
|
||||
});
|
||||
|
||||
const maxServers = admin?.serversQuantity ?? 1;
|
||||
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
||||
const safePercentage = Math.min(percentage, 100);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<Tabs
|
||||
defaultValue="monthly"
|
||||
value={isAnnual ? "annual" : "monthly"}
|
||||
className="w-full"
|
||||
onValueChange={(e) => setIsAnnual(e === "annual")}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{admin?.stripeSubscriptionId && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You have {servers?.length} server on your plan of{" "}
|
||||
{admin?.serversQuantity} servers
|
||||
</p>
|
||||
<div className="pb-5">
|
||||
<Progress value={safePercentage} className="max-w-lg" />
|
||||
</div>
|
||||
{admin && (
|
||||
<>
|
||||
{admin.serversQuantity! <= servers?.length! && (
|
||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
You have reached the maximum number of servers you can
|
||||
create, please upgrade your plan to add more servers.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{products?.map((product) => {
|
||||
const featured = true;
|
||||
return (
|
||||
<div key={product.id}>
|
||||
<section
|
||||
className={clsx(
|
||||
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||
featured
|
||||
? "order-first bg-black border py-8 lg:order-none"
|
||||
: "lg:py-8",
|
||||
)}
|
||||
>
|
||||
{isAnnual ? (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||
</p>
|
||||
|
|
||||
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||
${" "}
|
||||
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(2)}{" "}
|
||||
/ Month USD
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||
</p>
|
||||
)}
|
||||
<h3 className="mt-5 font-medium text-lg text-white">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-sm",
|
||||
featured ? "text-white" : "text-slate-400",
|
||||
)}
|
||||
>
|
||||
{product.description}
|
||||
</p>
|
||||
|
||||
<ul
|
||||
role="list"
|
||||
className={clsx(
|
||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||
featured ? "text-white" : "text-slate-200",
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"All the features of Dokploy",
|
||||
"Unlimited deployments",
|
||||
"Self-hosted on your own infrastructure",
|
||||
"Full access to all deployment features",
|
||||
"Dokploy integration",
|
||||
"Backups",
|
||||
"All Incoming features",
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex text-muted-foreground">
|
||||
<CheckIcon />
|
||||
<span className="ml-4">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{serverQuantity} Servers
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (serverQuantity <= 1) return;
|
||||
|
||||
setServerQuantity(serverQuantity - 1);
|
||||
}}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
onChange={(e) => {
|
||||
setServerQuantity(e.target.value as unknown as number);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setServerQuantity(serverQuantity + 1);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
data?.subscriptions && data?.subscriptions?.length > 0
|
||||
? "justify-between"
|
||||
: "justify-end",
|
||||
"flex flex-row items-center gap-2 mt-4",
|
||||
)}
|
||||
>
|
||||
{admin?.stripeCustomerId && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
const session = await createCustomerPortalSession();
|
||||
|
||||
window.open(session.url);
|
||||
}}
|
||||
>
|
||||
Manage Subscription
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{data?.subscriptions?.length === 0 && (
|
||||
<div className="justify-end w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
handleCheckout(product.id);
|
||||
}}
|
||||
disabled={serverQuantity < 1}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,181 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Container } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const AddRegistrySchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Username is required",
|
||||
})
|
||||
.regex(/^[a-zA-Z0-9]+$/, {
|
||||
message: "Username can only contain letters and numbers",
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||
|
||||
export const AddSelfHostedRegistry = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.registry.enableSelfHostedRegistry.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<AddRegistry>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
},
|
||||
resolver: zodResolver(AddRegistrySchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
registryUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (data: AddRegistry) => {
|
||||
await mutateAsync({
|
||||
registryUrl: data.registryUrl,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.registry.all.invalidate();
|
||||
toast.success("Self Hosted Registry Created");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create a self hosted registry");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="max-sm:w-full">
|
||||
<Container className="h-4 w-4" />
|
||||
Enable Self Hosted Registry
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:m:max-w-lg ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a self hosted registry</DialogTitle>
|
||||
<DialogDescription>
|
||||
Fill the next fields to add a self hosted registry.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Username" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="registry.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Point a DNS record to the VPS IP address.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { Server } from "lucide-react";
|
||||
import { AddRegistry } from "./add-docker-registry";
|
||||
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
|
||||
import { DeleteRegistry } from "./delete-registry";
|
||||
import { UpdateDockerRegistry } from "./update-docker-registry";
|
||||
|
||||
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
|
||||
<div className="flex flex-row gap-2">
|
||||
{data && data?.length > 0 && (
|
||||
<>
|
||||
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
|
||||
|
||||
<AddRegistry />
|
||||
</>
|
||||
)}
|
||||
@@ -47,7 +44,6 @@ export const ShowRegistry = () => {
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||
<AddSelfHostedRegistry />
|
||||
<AddRegistry />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,16 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
@@ -32,12 +42,15 @@ const addDestination = z.object({
|
||||
bucket: z.string(),
|
||||
region: z.string(),
|
||||
endpoint: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddDestination = z.infer<typeof addDestination>;
|
||||
|
||||
export const AddDestination = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.destination.create.useMutation();
|
||||
@@ -189,30 +202,106 @@ export const AddDestination = () => {
|
||||
/>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
isCloud ? "!flex-col" : "flex-row",
|
||||
"flex w-full !justify-between pt-3 gap-4",
|
||||
)}
|
||||
>
|
||||
{isCloud ? (
|
||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Select a server to test the destination. If you don't have a
|
||||
server choose the default one.
|
||||
</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Servers</SelectLabel>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
serverId: form.getValues("serverId"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error("Error to connect the provider", {
|
||||
description: e.message,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-destination-add"
|
||||
|
||||
@@ -18,6 +18,16 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
@@ -33,6 +43,7 @@ const updateDestination = z.object({
|
||||
bucket: z.string(),
|
||||
region: z.string(),
|
||||
endpoint: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateDestination = z.infer<typeof updateDestination>;
|
||||
@@ -43,6 +54,8 @@ interface Props {
|
||||
|
||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data, refetch } = api.destination.one.useQuery(
|
||||
{
|
||||
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
isCloud ? "!flex-col" : "flex-row",
|
||||
"flex w-full !justify-between pt-3 gap-4",
|
||||
)}
|
||||
>
|
||||
{isCloud ? (
|
||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Select a server to test the destination. If you don't have a
|
||||
server choose the default one.
|
||||
</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Servers</SelectLabel>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
serverId: form.getValues("serverId"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||
export const AddNotification = () => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||
api.notification.testSlackConnection.useMutation();
|
||||
|
||||
@@ -660,26 +660,28 @@ export const AddNotification = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||
const emailMutation = api.notification.updateEmail.useMutation();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const form = useForm<NotificationSchema>({
|
||||
defaultValues: {
|
||||
type: "slack",
|
||||
@@ -618,27 +618,29 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.dokployRestart}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.dokployRestart}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -57,6 +58,9 @@ type Schema = z.infer<typeof Schema>;
|
||||
export const AddServer = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: canCreateMoreServers, refetch } =
|
||||
api.stripe.canCreateMoreServers.useQuery();
|
||||
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
@@ -82,6 +86,10 @@ export const AddServer = () => {
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
@@ -116,6 +124,14 @@ export const AddServer = () => {
|
||||
Add a server to deploy your applications remotely.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{!canCreateMoreServers && (
|
||||
<AlertBlock type="warning">
|
||||
You cannot create more servers,{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Please upgrade your plan
|
||||
</Link>
|
||||
</AlertBlock>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -254,6 +270,7 @@ export const AddServer = () => {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
disabled={!canCreateMoreServers}
|
||||
form="hook-form-add-server"
|
||||
type="submit"
|
||||
>
|
||||
|
||||
@@ -36,6 +36,9 @@ export const ShowServers = () => {
|
||||
const { data, refetch } = api.server.all.useQuery();
|
||||
const { mutateAsync } = api.server.remove.useMutation();
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: canCreateMoreServers } =
|
||||
api.stripe.canCreateMoreServers.useQuery();
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
@@ -74,8 +77,22 @@ export const ShowServers = () => {
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<ServerIcon className="size-8" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No Servers found. Add a server to deploy your applications
|
||||
remotely.
|
||||
{!canCreateMoreServers ? (
|
||||
<div>
|
||||
You cannot create more servers,{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/billing"
|
||||
className="text-primary"
|
||||
>
|
||||
Please upgrade your plan
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
No Servers found. Add a server to deploy your applications
|
||||
remotely.
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@@ -87,6 +104,9 @@ export const ShowServers = () => {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Name</TableHead>
|
||||
{isCloud && (
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
)}
|
||||
<TableHead className="text-center">IP Address</TableHead>
|
||||
<TableHead className="text-center">Port</TableHead>
|
||||
<TableHead className="text-center">Username</TableHead>
|
||||
@@ -98,9 +118,23 @@ export const ShowServers = () => {
|
||||
<TableBody>
|
||||
{data?.map((server) => {
|
||||
const canDelete = server.totalSum === 0;
|
||||
const isActive = server.serverStatus === "active";
|
||||
return (
|
||||
<TableRow key={server.serverId}>
|
||||
<TableCell className="w-[100px]">{server.name}</TableCell>
|
||||
{isCloud && (
|
||||
<TableHead className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
server.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{server.serverStatus}
|
||||
</Badge>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<Badge>{server.ipAddress}</Badge>
|
||||
</TableCell>
|
||||
@@ -131,18 +165,25 @@ export const ShowServers = () => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
{server.sshKeyId && (
|
||||
<TerminalModal serverId={server.serverId}>
|
||||
<span>Enter the terminal</span>
|
||||
</TerminalModal>
|
||||
|
||||
{isActive && (
|
||||
<>
|
||||
{server.sshKeyId && (
|
||||
<TerminalModal serverId={server.serverId}>
|
||||
<span>Enter the terminal</span>
|
||||
</TerminalModal>
|
||||
)}
|
||||
<SetupServer serverId={server.serverId} />
|
||||
|
||||
<UpdateServer serverId={server.serverId} />
|
||||
{server.sshKeyId && (
|
||||
<ShowServerActions
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SetupServer serverId={server.serverId} />
|
||||
|
||||
<UpdateServer serverId={server.serverId} />
|
||||
{server.sshKeyId && (
|
||||
<ShowServerActions serverId={server.serverId} />
|
||||
)}
|
||||
<DialogAction
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
@@ -187,17 +228,21 @@ export const ShowServers = () => {
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
|
||||
{server.sshKeyId && (
|
||||
{isActive && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
{server.sshKeyId && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -80,6 +80,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: ListMusic,
|
||||
href: "/dashboard/settings/registry",
|
||||
},
|
||||
|
||||
...(!isCloud
|
||||
? [
|
||||
{
|
||||
@@ -102,6 +103,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: Server,
|
||||
href: "/dashboard/settings/servers",
|
||||
},
|
||||
...(isCloud
|
||||
? [
|
||||
{
|
||||
title: "Billing",
|
||||
label: "",
|
||||
icon: CreditCardIcon,
|
||||
href: "/dashboard/settings/billing",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...(user?.canAccessToSSHKeys
|
||||
@@ -137,6 +148,7 @@ import {
|
||||
Activity,
|
||||
Bell,
|
||||
BoxesIcon,
|
||||
CreditCardIcon,
|
||||
Database,
|
||||
GitBranch,
|
||||
KeyIcon,
|
||||
|
||||
12
apps/dokploy/drizzle/0041_huge_bruce_banner.sql
Normal file
12
apps/dokploy/drizzle/0041_huge_bruce_banner.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."serverStatus" AS ENUM('active', 'inactive');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "admin" ADD COLUMN "stripeCustomerId" text;--> statement-breakpoint
|
||||
ALTER TABLE "admin" ADD COLUMN "stripeSubscriptionId" text;--> statement-breakpoint
|
||||
ALTER TABLE "admin" ADD COLUMN "serversQuantity" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "auth" ADD COLUMN "resetPasswordToken" text;--> statement-breakpoint
|
||||
ALTER TABLE "auth" ADD COLUMN "resetPasswordExpiresAt" text;--> statement-breakpoint
|
||||
ALTER TABLE "server" ADD COLUMN "serverStatus" "serverStatus" DEFAULT 'active' NOT NULL;
|
||||
3956
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
3956
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -288,6 +288,13 @@
|
||||
"when": 1728780577084,
|
||||
"tag": "0040_graceful_wolfsbane",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 41,
|
||||
"version": "6",
|
||||
"when": 1729667438853,
|
||||
"tag": "0041_huge_bruce_banner",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.10.1",
|
||||
"version": "v0.10.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -10,8 +10,8 @@
|
||||
"build-server": "tsx esbuild.config.ts",
|
||||
"build-next": "next build",
|
||||
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
||||
"reset-password": "node dist/reset-password.mjs",
|
||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||
@@ -34,12 +34,12 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/view": "6.29.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@octokit/webhooks": "^13.2.7",
|
||||
@@ -61,6 +61,7 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@stripe/stripe-js": "4.8.0",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.16.0",
|
||||
"@trpc/client": "^10.43.6",
|
||||
@@ -89,7 +90,7 @@
|
||||
"lucia": "^3.0.1",
|
||||
"lucide-react": "^0.312.0",
|
||||
"nanoid": "3",
|
||||
"next": "^14.1.3",
|
||||
"next": "^15.0.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
@@ -102,6 +103,8 @@
|
||||
"recharts": "^2.12.7",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.4.0",
|
||||
"ssh2": "1.15.0",
|
||||
"stripe": "17.2.0",
|
||||
"superjson": "^2.2.1",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
@@ -111,11 +114,9 @@
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^3.23.4",
|
||||
"zod-form-data": "^2.0.2",
|
||||
"ssh2": "1.15.0"
|
||||
"zod-form-data": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "10.4.12",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
@@ -124,8 +125,10 @@
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/ssh2": "1.15.1",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/ws": "8.5.10",
|
||||
"autoprefixer": "10.4.12",
|
||||
"drizzle-kit": "^0.21.1",
|
||||
"esbuild": "0.20.2",
|
||||
"lint-staged": "^15.2.7",
|
||||
@@ -134,8 +137,7 @@
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.4.2",
|
||||
"vite-tsconfig-paths": "4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"@types/ssh2": "1.15.1"
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.25.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@/server/db";
|
||||
import { applications } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@/server/db";
|
||||
import { compose } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@/server/db";
|
||||
import { applications, compose, github } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
|
||||
274
apps/dokploy/pages/api/stripe/webhook.ts
Normal file
274
apps/dokploy/pages/api/stripe/webhook.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { buffer } from "node:stream/consumers";
|
||||
import { db } from "@/server/db";
|
||||
import { admins, server } from "@/server/db/schema";
|
||||
import { findAdminById } from "@dokploy/server";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (!endpointSecret) {
|
||||
return res.status(400).send("Webhook Error: Missing Stripe Secret Key");
|
||||
}
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
maxNetworkRetries: 3,
|
||||
});
|
||||
|
||||
const buf = await buffer(req);
|
||||
const sig = req.headers["stripe-signature"] as string;
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(buf, sig, endpointSecret);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Webhook signature verification failed.",
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
return res.status(400).send("Webhook Error: ");
|
||||
}
|
||||
|
||||
const webhooksAllowed = [
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.deleted",
|
||||
"customer.subscription.updated",
|
||||
"invoice.payment_succeeded",
|
||||
"invoice.payment_failed",
|
||||
"customer.deleted",
|
||||
"checkout.session.completed",
|
||||
];
|
||||
|
||||
if (!webhooksAllowed.includes(event.type)) {
|
||||
return res.status(400).send("Webhook Error: Invalid Event Type");
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed": {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
const adminId = session?.metadata?.adminId as string;
|
||||
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string,
|
||||
);
|
||||
await db
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeCustomerId: session.customer as string,
|
||||
stripeSubscriptionId: session.subscription as string,
|
||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(admins.adminId, adminId))
|
||||
.returning();
|
||||
|
||||
const admin = await findAdminById(adminId);
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.created": {
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeSubscriptionId: newSubscription.id,
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: newSubscription.customer as string,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
||||
.returning();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "customer.subscription.deleted": {
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await db
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
await disableServers(admin.adminId);
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.updated": {
|
||||
const newSubscription = event.data.object as Stripe.Subscription;
|
||||
await db
|
||||
.update(admins)
|
||||
.set({
|
||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
newSubscription.customer as string,
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
|
||||
break;
|
||||
}
|
||||
case "invoice.payment_succeeded": {
|
||||
const newInvoice = event.data.object as Stripe.Invoice;
|
||||
|
||||
const suscription = await stripe.subscriptions.retrieve(
|
||||
newInvoice.subscription as string,
|
||||
);
|
||||
|
||||
await db
|
||||
.update(admins)
|
||||
.set({
|
||||
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
suscription.customer as string,
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
const newServersQuantity = admin.serversQuantity;
|
||||
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||
break;
|
||||
}
|
||||
case "invoice.payment_failed": {
|
||||
const newInvoice = event.data.object as Stripe.Invoice;
|
||||
await db
|
||||
.update(admins)
|
||||
.set({
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(
|
||||
newInvoice.customer as string,
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
await disableServers(admin.adminId);
|
||||
break;
|
||||
}
|
||||
|
||||
case "customer.deleted": {
|
||||
const customer = event.data.object as Stripe.Customer;
|
||||
|
||||
const admin = await findAdminByStripeCustomerId(customer.id);
|
||||
if (!admin) {
|
||||
return res.status(400).send("Webhook Error: Admin not found");
|
||||
}
|
||||
|
||||
await disableServers(admin.adminId);
|
||||
await db
|
||||
.update(admins)
|
||||
.set({
|
||||
stripeCustomerId: null,
|
||||
stripeSubscriptionId: null,
|
||||
serversQuantity: 0,
|
||||
})
|
||||
.where(eq(admins.stripeCustomerId, customer.id));
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
|
||||
return res.status(200).json({ received: true });
|
||||
}
|
||||
|
||||
const disableServers = async (adminId: string) => {
|
||||
await db
|
||||
.update(server)
|
||||
.set({
|
||||
serverStatus: "inactive",
|
||||
})
|
||||
.where(eq(server.adminId, adminId));
|
||||
};
|
||||
|
||||
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||
const admin = db.query.admins.findFirst({
|
||||
where: eq(admins.stripeCustomerId, stripeCustomerId),
|
||||
});
|
||||
return admin;
|
||||
};
|
||||
|
||||
const activateServer = async (serverId: string) => {
|
||||
await db
|
||||
.update(server)
|
||||
.set({ serverStatus: "active" })
|
||||
.where(eq(server.serverId, serverId));
|
||||
};
|
||||
|
||||
const deactivateServer = async (serverId: string) => {
|
||||
await db
|
||||
.update(server)
|
||||
.set({ serverStatus: "inactive" })
|
||||
.where(eq(server.serverId, serverId));
|
||||
};
|
||||
|
||||
export const findServersByAdminIdSorted = async (adminId: string) => {
|
||||
const servers = await db.query.server.findMany({
|
||||
where: eq(server.adminId, adminId),
|
||||
orderBy: asc(server.createdAt),
|
||||
});
|
||||
|
||||
return servers;
|
||||
};
|
||||
export const updateServersBasedOnQuantity = async (
|
||||
adminId: string,
|
||||
newServersQuantity: number,
|
||||
) => {
|
||||
const servers = await findServersByAdminIdSorted(adminId);
|
||||
|
||||
if (servers.length > newServersQuantity) {
|
||||
for (const [index, server] of servers.entries()) {
|
||||
if (index < newServersQuantity) {
|
||||
await activateServer(server.serverId);
|
||||
} else {
|
||||
await deactivateServer(server.serverId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const server of servers) {
|
||||
await activateServer(server.serverId);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -22,13 +22,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -100,8 +107,40 @@ const Service = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{data?.description && (
|
||||
@@ -119,90 +158,111 @@ const Service = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteApplication applicationId={applicationId} />
|
||||
)}
|
||||
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteApplication applicationId={applicationId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralApplication applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralApplication applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeployments applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="domains" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommand applicationId={applicationId} />
|
||||
<ShowClusterSettings applicationId={applicationId} />
|
||||
<ShowApplicationResources applicationId={applicationId} />
|
||||
<ShowVolumes applicationId={applicationId} />
|
||||
<ShowRedirects applicationId={applicationId} />
|
||||
<ShowSecurity applicationId={applicationId} />
|
||||
<ShowPorts applicationId={applicationId} />
|
||||
<ShowTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeployments applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="domains" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommand applicationId={applicationId} />
|
||||
<ShowClusterSettings applicationId={applicationId} />
|
||||
<ShowApplicationResources applicationId={applicationId} />
|
||||
<ShowVolumes applicationId={applicationId} />
|
||||
<ShowRedirects applicationId={applicationId} />
|
||||
<ShowSecurity applicationId={applicationId} />
|
||||
<ShowPorts applicationId={applicationId} />
|
||||
<ShowTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,13 +16,21 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { CircuitBoard } from "lucide-react";
|
||||
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -94,8 +102,40 @@ const Service = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -113,98 +153,118 @@ const Service = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
||||
data?.serverId && data?.composeType === "stack"
|
||||
? "md:grid-cols-5"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
{data?.composeType === "docker-compose" && (
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
)}
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteCompose composeId={composeId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
||||
data?.serverId && data?.composeType === "stack"
|
||||
? "md:grid-cols-5"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
{data?.composeType === "docker-compose" && (
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
)}
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralCompose composeId={composeId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteCompose composeId={composeId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironmentCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMonitoringCompose
|
||||
<ShowGeneralCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironmentCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMonitoringCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogsCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogsCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeploymentsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="deployments">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeploymentsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="domains">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomainsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommandCompose composeId={composeId} />
|
||||
<ShowVolumesCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="domains">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomainsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommandCompose composeId={composeId} />
|
||||
<ShowVolumesCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -82,8 +90,40 @@ const Mariadb = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -99,79 +139,99 @@ const Mariadb = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMariadb mariadbId={mariadbId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralMariadb mariadbId={mariadbId} />
|
||||
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMariadb mariadbId={mariadbId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralMariadb mariadbId={mariadbId} />
|
||||
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -83,8 +91,40 @@ const Mongo = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -100,80 +140,100 @@ const Mongo = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMongo mongoId={mongoId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralMongo mongoId={mongoId} />
|
||||
<ShowInternalMongoCredentials mongoId={mongoId} />
|
||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMongo mongoId={mongoId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMongoEnvironment mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralMongo mongoId={mongoId} />
|
||||
<ShowInternalMongoCredentials mongoId={mongoId} />
|
||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMongoEnvironment mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -81,8 +89,40 @@ const MySql = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -99,80 +139,100 @@ const MySql = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMysql mysqlId={mysqlId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralMysql mysqlId={mysqlId} />
|
||||
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMysql mysqlId={mysqlId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralMysql mysqlId={mysqlId} />
|
||||
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMySql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMysql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMySql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMysql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -82,8 +90,40 @@ const Postgresql = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -100,80 +140,100 @@ const Postgresql = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeletePostgres postgresId={postgresId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralPostgres postgresId={postgresId} />
|
||||
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
||||
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeletePostgres postgresId={postgresId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralPostgres postgresId={postgresId} />
|
||||
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
||||
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,12 +16,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -81,8 +89,40 @@ const Redis = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -99,74 +139,94 @@ const Redis = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteRedis redisId={redisId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralRedis redisId={redisId} />
|
||||
<ShowInternalRedisCredentials redisId={redisId} />
|
||||
<ShowExternalRedisCredentials redisId={redisId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteRedis redisId={redisId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowRedisEnvironment redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralRedis redisId={redisId} />
|
||||
<ShowInternalRedisCredentials redisId={redisId} />
|
||||
<ShowExternalRedisCredentials redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedRedis redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowRedisEnvironment redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedRedis redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Dashboard = () => {
|
||||
return <ShowProjects />;
|
||||
@@ -16,7 +19,22 @@ Dashboard.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
@@ -27,6 +45,8 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,30 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
await helpers.auth.get.prefetch();
|
||||
if (user?.rol === "user") {
|
||||
await helpers.user.byAuthId.prefetch({
|
||||
authId: user.authId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -34,8 +60,9 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
67
apps/dokploy/pages/dashboard/settings/billing.tsx
Normal file
67
apps/dokploy/pages/dashboard/settings/billing.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ShowBilling } from "@/components/dashboard/settings/billing/show-billing";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return <ShowBilling />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Page.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<DashboardLayout tab={"settings"}>
|
||||
<SettingsLayout>{page}</SettingsLayout>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
if (!IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.auth.get.prefetch();
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
|
||||
import superjson from "superjson";
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
@@ -25,7 +27,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +38,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -26,6 +28,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -43,8 +46,22 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,9 +49,10 @@ export async function getServerSideProps(
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.auth.get.prefetch();
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -26,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -36,7 +39,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ import { GenerateToken } from "@/components/dashboard/settings/profile/generate-
|
||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
@@ -37,7 +40,29 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
await helpers.auth.get.prefetch();
|
||||
if (user?.rol === "user") {
|
||||
await helpers.user.byAuthId.prefetch({
|
||||
authId: user.authId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -48,6 +73,8 @@ export async function getServerSideProps(
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -34,8 +38,23 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -27,6 +30,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +39,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -53,7 +57,22 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -43,7 +47,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +55,10 @@ type AuthResponse = {
|
||||
authId: string;
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
interface Props {
|
||||
IS_CLOUD: boolean;
|
||||
}
|
||||
export default function Home({ IS_CLOUD }: Props) {
|
||||
const [temp, setTemp] = useState<AuthResponse>({
|
||||
is2FAEnabled: false,
|
||||
authId: "",
|
||||
@@ -176,13 +179,22 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
||||
target="_blank"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
{IS_CLOUD ? (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
||||
target="_blank"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2" />
|
||||
@@ -212,7 +224,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
} catch (error) {}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
IS_CLOUD: IS_CLOUD,
|
||||
},
|
||||
};
|
||||
}
|
||||
const hasAdmin = await isAdminPresent();
|
||||
|
||||
228
apps/dokploy/pages/reset-password.tsx
Normal file
228
apps/dokploy/pages/reset-password.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
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 {
|
||||
Card,
|
||||
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 { auth } from "@/server/db/schema";
|
||||
import { api } from "@/utils/api";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isBefore } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const loginSchema = z
|
||||
.object({
|
||||
password: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.min(8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Password is required",
|
||||
})
|
||||
.min(8, {
|
||||
message: "Password must be at least 8 characters",
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
type Login = z.infer<typeof loginSchema>;
|
||||
|
||||
interface Props {
|
||||
token: string;
|
||||
}
|
||||
export default function Home({ token }: Props) {
|
||||
const { mutateAsync, isLoading, isError, error } =
|
||||
api.auth.resetPassword.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<Login>({
|
||||
defaultValues: {
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Login) => {
|
||||
await mutateAsync({
|
||||
resetPasswordToken: token,
|
||||
password: values.password,
|
||||
})
|
||||
.then((data) => {
|
||||
toast.success("Password reset succesfully", {
|
||||
duration: 2000,
|
||||
});
|
||||
router.push("/");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reset password", {
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center ">
|
||||
<div className="flex flex-col items-center gap-4 w-full">
|
||||
<Link href="/" className="flex flex-row items-center gap-2">
|
||||
<Logo />
|
||||
<span className="font-medium text-sm">Dokploy</span>
|
||||
</Link>
|
||||
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email to reset your password
|
||||
</CardDescription>
|
||||
|
||||
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
||||
<div className="p-3.5" />
|
||||
<CardContent>
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="my-2">
|
||||
{error?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Home.getLayout = (page: ReactElement) => {
|
||||
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||
};
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
if (!IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { token } = context.query;
|
||||
|
||||
if (typeof token !== "string") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const authR = await db?.query.auth.findFirst({
|
||||
where: eq(auth.resetPasswordToken, token),
|
||||
});
|
||||
|
||||
if (!authR || authR?.resetPasswordExpiresAt === null) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
const isExpired = isBefore(
|
||||
new Date(authR.resetPasswordExpiresAt),
|
||||
new Date(),
|
||||
);
|
||||
|
||||
if (isExpired) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
token: authR.resetPasswordToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
172
apps/dokploy/pages/send-reset-password.tsx
Normal file
172
apps/dokploy/pages/send-reset-password.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Login2FA } from "@/components/auth/login-2fa";
|
||||
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 {
|
||||
Card,
|
||||
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 { api } from "@/utils/api";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Email is required",
|
||||
})
|
||||
.email({
|
||||
message: "Email must be a valid email",
|
||||
}),
|
||||
});
|
||||
|
||||
type Login = z.infer<typeof loginSchema>;
|
||||
|
||||
type AuthResponse = {
|
||||
is2FAEnabled: boolean;
|
||||
authId: string;
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const [temp, setTemp] = useState<AuthResponse>({
|
||||
is2FAEnabled: false,
|
||||
authId: "",
|
||||
});
|
||||
const { mutateAsync, isLoading, isError, error } =
|
||||
api.auth.sendResetPasswordEmail.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<Login>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (values: Login) => {
|
||||
await mutateAsync({
|
||||
email: values.email,
|
||||
})
|
||||
.then((data) => {
|
||||
toast.success("Email sent", {
|
||||
duration: 2000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to send email", {
|
||||
duration: 2000,
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center ">
|
||||
<div className="flex flex-col items-center gap-4 w-full">
|
||||
<Link href="/" className="flex flex-row items-center gap-2">
|
||||
<Logo />
|
||||
<span className="font-medium text-sm">Dokploy</span>
|
||||
</Link>
|
||||
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email to reset your password
|
||||
</CardDescription>
|
||||
|
||||
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
||||
<div className="p-3.5" />
|
||||
<CardContent>
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="my-2">
|
||||
{error?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
{!temp.is2FAEnabled ? (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
Send Reset Link
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<Login2FA authId={temp.authId} />
|
||||
)}
|
||||
|
||||
<div className="flex flex-row justify-between flex-wrap">
|
||||
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Home.getLayout = (page: ReactElement) => {
|
||||
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||
};
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
if (!IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { securityRouter } from "./routers/security";
|
||||
import { serverRouter } from "./routers/server";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
import { sshRouter } from "./routers/ssh-key";
|
||||
import { stripeRouter } from "./routers/stripe";
|
||||
import { userRouter } from "./routers/user";
|
||||
|
||||
/**
|
||||
@@ -69,6 +70,7 @@ export const appRouter = createTRPCRouter({
|
||||
gitlab: gitlabRouter,
|
||||
github: githubRouter,
|
||||
server: serverRouter,
|
||||
stripe: stripeRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -19,11 +19,8 @@ import {
|
||||
apiUpdateApplication,
|
||||
applications,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
type DeploymentJob,
|
||||
cleanQueuesByApplication,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { uploadFileSchema } from "@/utils/schema";
|
||||
import {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
apiUpdateAuthByAdmin,
|
||||
apiVerify2FA,
|
||||
apiVerifyLogin2FA,
|
||||
auth,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
@@ -18,12 +19,17 @@ import {
|
||||
getUserByToken,
|
||||
lucia,
|
||||
luciaToken,
|
||||
sendEmailNotification,
|
||||
updateAuthById,
|
||||
validateRequest,
|
||||
verify2FA,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { isBefore } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db";
|
||||
import {
|
||||
adminProcedure,
|
||||
@@ -233,4 +239,101 @@ export const authRouter = createTRPCRouter({
|
||||
verifyToken: protectedProcedure.mutation(async () => {
|
||||
return true;
|
||||
}),
|
||||
sendResetPasswordEmail: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().min(1).email(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "This feature is only available in the cloud version",
|
||||
});
|
||||
}
|
||||
const authR = await db.query.auth.findFirst({
|
||||
where: eq(auth.email, input.email),
|
||||
});
|
||||
if (!authR) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
const token = nanoid();
|
||||
await updateAuthById(authR.id, {
|
||||
resetPasswordToken: token,
|
||||
// Make resetPassword in 24 hours
|
||||
resetPasswordExpiresAt: new Date(
|
||||
new Date().getTime() + 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
});
|
||||
|
||||
const email = await sendEmailNotification(
|
||||
{
|
||||
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
|
||||
toAddresses: [authR.email],
|
||||
smtpServer: process.env.SMTP_SERVER || "",
|
||||
smtpPort: Number(process.env.SMTP_PORT),
|
||||
username: process.env.SMTP_USERNAME || "",
|
||||
password: process.env.SMTP_PASSWORD || "",
|
||||
},
|
||||
"Reset Password",
|
||||
`
|
||||
Reset your password by clicking the link below:
|
||||
The link will expire in 24 hours.
|
||||
<a href="http://localhost:3000/reset-password?token=${token}">
|
||||
Reset Password
|
||||
</a>
|
||||
|
||||
`,
|
||||
);
|
||||
}),
|
||||
|
||||
resetPassword: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
resetPasswordToken: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "This feature is only available in the cloud version",
|
||||
});
|
||||
}
|
||||
const authR = await db.query.auth.findFirst({
|
||||
where: eq(auth.resetPasswordToken, input.resetPasswordToken),
|
||||
});
|
||||
|
||||
if (!authR || authR.resetPasswordExpiresAt === null) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Token not found",
|
||||
});
|
||||
}
|
||||
|
||||
const isExpired = isBefore(
|
||||
new Date(authR.resetPasswordExpiresAt),
|
||||
new Date(),
|
||||
);
|
||||
|
||||
if (isExpired) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Token expired",
|
||||
});
|
||||
}
|
||||
|
||||
await updateAuthById(authR.id, {
|
||||
resetPasswordExpiresAt: null,
|
||||
resetPasswordToken: null,
|
||||
password: bcrypt.hashSync(input.password, 10),
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
findMongoByBackupId,
|
||||
findMySqlByBackupId,
|
||||
findPostgresByBackupId,
|
||||
findServerById,
|
||||
removeBackupById,
|
||||
removeScheduleBackup,
|
||||
runMariadbBackup,
|
||||
@@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(newBackup.backupId);
|
||||
|
||||
if (IS_CLOUD && backup.enabled) {
|
||||
const databaseType = backup.databaseType;
|
||||
let serverId = "";
|
||||
if (databaseType === "postgres" && backup.postgres?.serverId) {
|
||||
serverId = backup.postgres.serverId;
|
||||
} else if (databaseType === "mysql" && backup.mysql?.serverId) {
|
||||
serverId = backup.mysql.serverId;
|
||||
} else if (databaseType === "mongo" && backup.mongo?.serverId) {
|
||||
serverId = backup.mongo.serverId;
|
||||
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||
serverId = backup.mariadb.serverId;
|
||||
}
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server is inactive",
|
||||
});
|
||||
}
|
||||
await schedule({
|
||||
cronSchedule: backup.schedule,
|
||||
backupId: backup.backupId,
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
apiUpdateCompose,
|
||||
compose,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
type DeploymentJob,
|
||||
cleanQueuesByCompose,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||
import { templates } from "@/templates/templates";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
import {
|
||||
@@ -28,6 +24,7 @@ import _ from "lodash";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
@@ -41,7 +38,6 @@ import {
|
||||
createComposeByTemplate,
|
||||
createDomain,
|
||||
createMount,
|
||||
findAdmin,
|
||||
findAdminById,
|
||||
findComposeById,
|
||||
findDomainsByComposeId,
|
||||
@@ -252,7 +248,6 @@ export const composeRouter = createTRPCRouter({
|
||||
descriptionLog: "",
|
||||
server: !!compose.serverId,
|
||||
};
|
||||
console.log(jobData);
|
||||
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
jobData.serverId = compose.serverId;
|
||||
|
||||
@@ -12,9 +12,10 @@ import {
|
||||
destinations,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createDestintation,
|
||||
execAsync,
|
||||
findAdmin,
|
||||
execAsyncRemote,
|
||||
findDestinationById,
|
||||
removeDestinationById,
|
||||
updateDestinationById,
|
||||
@@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({
|
||||
];
|
||||
const rcloneDestination = `:s3:${bucket}`;
|
||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
await execAsync(rcloneCommand);
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD) {
|
||||
await execAsyncRemote(input.serverId || "", rcloneCommand);
|
||||
} else {
|
||||
await execAsync(rcloneCommand);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to connect to bucket",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error?.message
|
||||
: "Error to connect to bucket",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
deployMariadb,
|
||||
findMariadbById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
removeMariadbById,
|
||||
removeService,
|
||||
startService,
|
||||
@@ -151,6 +152,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
message: "You are not authorized to deploy this mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
return deployMariadb(input.mariadbId);
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
|
||||
@@ -148,12 +148,6 @@ export const notificationRouter = createTRPCRouter({
|
||||
.input(apiCreateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// go to your discord server
|
||||
// go to settings
|
||||
// go to integrations
|
||||
// add a new integration
|
||||
// select webhook
|
||||
// copy the webhook url
|
||||
return await createDiscordNotification(input, ctx.user.adminId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -20,10 +20,12 @@ import { and, desc, eq, sql } from "drizzle-orm";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
|
||||
import {
|
||||
IS_CLOUD,
|
||||
addNewProject,
|
||||
checkProjectAccess,
|
||||
createProject,
|
||||
deleteProject,
|
||||
findAdminById,
|
||||
findProjectById,
|
||||
findUserByAuthId,
|
||||
updateProjectById,
|
||||
@@ -38,6 +40,15 @@ export const projectRouter = createTRPCRouter({
|
||||
await checkProjectAccess(ctx.user.authId, "create");
|
||||
}
|
||||
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
|
||||
if (admin.serversQuantity === 0 && IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "No servers available, Please subscribe to a plan",
|
||||
});
|
||||
}
|
||||
|
||||
const project = await createProject(input, ctx.user.adminId);
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewProject(ctx.user.authId, project.projectId);
|
||||
@@ -47,7 +58,7 @@ export const projectRouter = createTRPCRouter({
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the project",
|
||||
message: `Error to create the project: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -128,9 +139,9 @@ export const projectRouter = createTRPCRouter({
|
||||
)})`,
|
||||
with: {
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accesedServices,
|
||||
where: and(
|
||||
buildServiceFilter(applications.applicationId, accesedServices),
|
||||
eq(projects.adminId, ctx.user.adminId),
|
||||
),
|
||||
with: { domains: true },
|
||||
},
|
||||
@@ -230,5 +241,5 @@ function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
|
||||
accesedServices.map((serviceId) => sql`${serviceId}`),
|
||||
sql`, `,
|
||||
)})`
|
||||
: sql`1 = 0`; // Always false condition
|
||||
: sql`1 = 0`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
apiCreateRegistry,
|
||||
apiEnableSelfHostedRegistry,
|
||||
apiFindOneRegistry,
|
||||
apiRemoveRegistry,
|
||||
apiTestRegistry,
|
||||
@@ -13,8 +12,6 @@ import {
|
||||
execAsyncRemote,
|
||||
findAllRegistryByAdminId,
|
||||
findRegistryById,
|
||||
initializeRegistry,
|
||||
manageRegistry,
|
||||
removeRegistry,
|
||||
updateRegistry,
|
||||
} from "@dokploy/server";
|
||||
@@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({
|
||||
try {
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Select a server to test the registry",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.serverId && input.serverId !== "none") {
|
||||
await execAsyncRemote(input.serverId, loginCommand);
|
||||
} else {
|
||||
@@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
||||
enableSelfHostedRegistry: adminProcedure
|
||||
.input(apiEnableSelfHostedRegistry)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Self Hosted Registry is not available in the cloud version",
|
||||
});
|
||||
}
|
||||
const selfHostedRegistry = await createRegistry(
|
||||
{
|
||||
...input,
|
||||
registryName: "Self Hosted Registry",
|
||||
registryType: "selfHosted",
|
||||
registryUrl:
|
||||
process.env.NODE_ENV === "production"
|
||||
? input.registryUrl
|
||||
: "dokploy-registry.docker.localhost",
|
||||
imagePrefix: null,
|
||||
serverId: undefined,
|
||||
},
|
||||
ctx.user.adminId,
|
||||
);
|
||||
|
||||
await manageRegistry(selfHostedRegistry);
|
||||
await initializeRegistry(input.username, input.password);
|
||||
|
||||
return selfHostedRegistry;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
@@ -15,15 +16,17 @@ import {
|
||||
server,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createServer,
|
||||
deleteServer,
|
||||
findAdminById,
|
||||
findServerById,
|
||||
findServersByAdminId,
|
||||
haveActiveServices,
|
||||
removeDeploymentsByServerId,
|
||||
serverSetup,
|
||||
updateServerById,
|
||||
} from "@dokploy/server";
|
||||
// import { serverSetup } from "@/server/setup/server-setup";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
||||
|
||||
@@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
|
||||
.input(apiCreateServer)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
const servers = await findServersByAdminId(admin.adminId);
|
||||
if (IS_CLOUD && servers.length >= admin.serversQuantity) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot create more servers",
|
||||
});
|
||||
}
|
||||
const project = await createServer(input, ctx.user.adminId);
|
||||
return project;
|
||||
} catch (error) {
|
||||
@@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.server.findMany({
|
||||
const result = await db.query.server.findMany({
|
||||
orderBy: desc(server.createdAt),
|
||||
where: and(
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.adminId, ctx.user.adminId),
|
||||
),
|
||||
where: IS_CLOUD
|
||||
? and(
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.adminId, ctx.user.adminId),
|
||||
eq(server.serverStatus, "active"),
|
||||
)
|
||||
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
setup: protectedProcedure
|
||||
.input(apiFindOneServer)
|
||||
@@ -125,6 +140,15 @@ export const serverRouter = createTRPCRouter({
|
||||
await removeDeploymentsByServerId(currentServer);
|
||||
await deleteServer(input.serverId);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
|
||||
await updateServersBasedOnQuantity(
|
||||
admin.adminId,
|
||||
admin.serversQuantity,
|
||||
);
|
||||
}
|
||||
|
||||
return currentServer;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -141,6 +165,13 @@ export const serverRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this server",
|
||||
});
|
||||
}
|
||||
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server is inactive",
|
||||
});
|
||||
}
|
||||
const currentServer = await updateServerById(input.serverId, {
|
||||
...input,
|
||||
});
|
||||
|
||||
@@ -221,6 +221,13 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
if (server.enableDockerCleanup) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server is inactive",
|
||||
});
|
||||
}
|
||||
if (IS_CLOUD) {
|
||||
await schedule({
|
||||
cronSchedule: "0 0 * * *",
|
||||
@@ -503,7 +510,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (input?.serverId) {
|
||||
const result = await execAsyncRemote(input.serverId, command);
|
||||
stdout = result.stdout;
|
||||
} else {
|
||||
} else if (!IS_CLOUD) {
|
||||
const result = await execAsync(
|
||||
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
||||
);
|
||||
@@ -635,7 +642,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
}),
|
||||
isCloud: adminProcedure.query(async () => {
|
||||
isCloud: protectedProcedure.query(async () => {
|
||||
return IS_CLOUD;
|
||||
}),
|
||||
health: publicProcedure.query(async () => {
|
||||
|
||||
130
apps/dokploy/server/api/routers/stripe.ts
Normal file
130
apps/dokploy/server/api/routers/stripe.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
findAdminById,
|
||||
findServersByAdminId,
|
||||
updateAdmin,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
const stripeCustomerId = admin.stripeCustomerId;
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
});
|
||||
|
||||
const products = await stripe.products.list({
|
||||
expand: ["data.default_price"],
|
||||
active: true,
|
||||
});
|
||||
|
||||
if (!stripeCustomerId) {
|
||||
return {
|
||||
products: products.data,
|
||||
subscriptions: [],
|
||||
};
|
||||
}
|
||||
|
||||
const subscriptions = await stripe.subscriptions.list({
|
||||
customer: stripeCustomerId,
|
||||
status: "active",
|
||||
expand: ["data.items.data.price"],
|
||||
});
|
||||
|
||||
return {
|
||||
products: products.data,
|
||||
subscriptions: subscriptions.data,
|
||||
};
|
||||
}),
|
||||
createCheckoutSession: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
productId: z.string(),
|
||||
serverQuantity: z.number().min(1),
|
||||
isAnnual: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
});
|
||||
|
||||
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
|
||||
let stripeCustomerId = admin.stripeCustomerId;
|
||||
|
||||
if (stripeCustomerId) {
|
||||
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||
|
||||
if (customer.deleted) {
|
||||
await updateAdmin(admin.authId, {
|
||||
stripeCustomerId: null,
|
||||
});
|
||||
stripeCustomerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: "subscription",
|
||||
line_items: items,
|
||||
...(stripeCustomerId && {
|
||||
customer: stripeCustomerId,
|
||||
}),
|
||||
metadata: {
|
||||
adminId: admin.adminId,
|
||||
},
|
||||
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||
});
|
||||
|
||||
return { sessionId: session.id };
|
||||
}),
|
||||
createCustomerPortalSession: adminProcedure.mutation(
|
||||
async ({ ctx, input }) => {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
|
||||
if (!admin.stripeCustomerId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Stripe Customer ID not found",
|
||||
});
|
||||
}
|
||||
const stripeCustomerId = admin.stripeCustomerId;
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
});
|
||||
|
||||
try {
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: stripeCustomerId,
|
||||
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||
});
|
||||
|
||||
return { url: session.url };
|
||||
} catch (error) {
|
||||
return {
|
||||
url: "",
|
||||
};
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
const servers = await findServersByAdminId(admin.adminId);
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return servers.length < admin.serversQuantity;
|
||||
}),
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
export * from "@dokploy/server/dist/db/schema";
|
||||
export * from "@dokploy/server/db/schema";
|
||||
|
||||
@@ -11,29 +11,8 @@ import {
|
||||
updateCompose,
|
||||
} from "@dokploy/server";
|
||||
import { type Job, Worker } from "bullmq";
|
||||
import { myQueue, redisConfig } from "./queueSetup";
|
||||
|
||||
type DeployJob =
|
||||
| {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application";
|
||||
serverId?: string;
|
||||
}
|
||||
| {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "compose";
|
||||
serverId?: string;
|
||||
};
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
import type { DeploymentJob } from "./queue-types";
|
||||
import { redisConfig } from "./redis-connection";
|
||||
|
||||
export const deploymentWorker = new Worker(
|
||||
"deployments",
|
||||
@@ -114,25 +93,3 @@ export const deploymentWorker = new Worker(
|
||||
connection: redisConfig,
|
||||
},
|
||||
);
|
||||
|
||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job?.data?.applicationId === applicationId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job?.data?.composeId === composeId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
21
apps/dokploy/server/queues/queue-types.ts
Normal file
21
apps/dokploy/server/queues/queue-types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
type DeployJob =
|
||||
| {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application";
|
||||
serverId?: string;
|
||||
}
|
||||
| {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "compose";
|
||||
serverId?: string;
|
||||
};
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
@@ -1,8 +1,6 @@
|
||||
import { type ConnectionOptions, Queue } from "bullmq";
|
||||
import { Queue } from "bullmq";
|
||||
import { redisConfig } from "./redis-connection";
|
||||
|
||||
export const redisConfig: ConnectionOptions = {
|
||||
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||
};
|
||||
const myQueue = new Queue("deployments", {
|
||||
connection: redisConfig,
|
||||
});
|
||||
@@ -21,4 +19,26 @@ myQueue.on("error", (error) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job?.data?.applicationId === applicationId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job?.data?.composeId === composeId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { myQueue };
|
||||
|
||||
5
apps/dokploy/server/queues/redis-connection.ts
Normal file
5
apps/dokploy/server/queues/redis-connection.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ConnectionOptions } from "bullmq";
|
||||
|
||||
export const redisConfig: ConnectionOptions = {
|
||||
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||
};
|
||||
@@ -19,15 +19,12 @@ import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-
|
||||
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
||||
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
||||
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
||||
import {
|
||||
getPublicIpWithFallback,
|
||||
setupTerminalWebSocketServer,
|
||||
} from "./wss/terminal";
|
||||
import { setupTerminalWebSocketServer } from "./wss/terminal";
|
||||
|
||||
config({ path: ".env" });
|
||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
const app = next({ dev });
|
||||
const app = next({ dev, turbopack: dev });
|
||||
const handle = app.getRequestHandler();
|
||||
void app.prepare().then(async () => {
|
||||
try {
|
||||
@@ -40,7 +37,9 @@ void app.prepare().then(async () => {
|
||||
setupDockerContainerLogsWebSocketServer(server);
|
||||
setupDockerContainerTerminalWebSocketServer(server);
|
||||
setupTerminalWebSocketServer(server);
|
||||
setupDockerStatsMonitoringSocketServer(server);
|
||||
if (!IS_CLOUD) {
|
||||
setupDockerStatsMonitoringSocketServer(server);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||
setupDirectories();
|
||||
@@ -53,7 +52,6 @@ void app.prepare().then(async () => {
|
||||
await initializeRedis();
|
||||
|
||||
initCronJobs();
|
||||
welcomeServer();
|
||||
|
||||
// Timeout to wait for the database to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||
@@ -76,18 +74,3 @@ void app.prepare().then(async () => {
|
||||
console.error("Main Server Error", e);
|
||||
}
|
||||
});
|
||||
|
||||
async function welcomeServer() {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
console.log(
|
||||
[
|
||||
"",
|
||||
"",
|
||||
"Dokploy server is up and running!",
|
||||
"Please wait for 15 seconds before opening the browser.",
|
||||
` http://${ip}:${PORT}`,
|
||||
"",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { DeploymentJob } from "../queues/deployments-queue";
|
||||
import { findServerById } from "@dokploy/server";
|
||||
import type { DeploymentJob } from "../queues/queue-types";
|
||||
|
||||
export const deploy = async (jobData: DeploymentJob) => {
|
||||
try {
|
||||
const server = await findServerById(jobData.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new Error("Server is inactive");
|
||||
}
|
||||
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
27
apps/dokploy/server/utils/stripe.ts
Normal file
27
apps/dokploy/server/utils/stripe.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const WEBSITE_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: process.env.SITE_URL;
|
||||
|
||||
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID || ""; // $4.00
|
||||
|
||||
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID || ""; // $7.99
|
||||
|
||||
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
|
||||
const items = [];
|
||||
|
||||
if (isAnnual) {
|
||||
items.push({
|
||||
price: BASE_ANNUAL_MONTHLY_ID,
|
||||
quantity: serverQuantity,
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
items.push({
|
||||
price: BASE_PRICE_MONTHLY_ID,
|
||||
quantity: serverQuantity,
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
@@ -3,15 +3,15 @@ import {
|
||||
createDefaultServerTraefikConfig,
|
||||
createDefaultTraefikConfig,
|
||||
initializeTraefik,
|
||||
} from "@dokploy/server/dist/setup/traefik-setup";
|
||||
} from "@dokploy/server/setup/traefik-setup";
|
||||
|
||||
import { setupDirectories } from "@dokploy/server/dist/setup/config-paths";
|
||||
import { initializePostgres } from "@dokploy/server/dist/setup/postgres-setup";
|
||||
import { initializeRedis } from "@dokploy/server/dist/setup/redis-setup";
|
||||
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
||||
import {
|
||||
initializeNetwork,
|
||||
initializeSwarm,
|
||||
} from "@dokploy/server/dist/setup/setup";
|
||||
} from "@dokploy/server/setup/setup";
|
||||
(async () => {
|
||||
try {
|
||||
setupDirectories();
|
||||
|
||||
@@ -1,47 +1,66 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
database:
|
||||
image: postgis/postgis:13-master
|
||||
volumes:
|
||||
- directus:/var/lib/postgresql/data
|
||||
- directus_database:/var/lib/postgresql/data
|
||||
networks:
|
||||
- dokploy-network
|
||||
environment:
|
||||
POSTGRES_USER: "directus"
|
||||
POSTGRES_PASSWORD: "directus"
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: "directus"
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "--host=localhost", "--username=directus"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_interval: 5s
|
||||
start_period: 30s
|
||||
|
||||
cache:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_interval: 5s
|
||||
start_period: 30s
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
directus:
|
||||
image: directus/directus:10.12.1
|
||||
image: directus/directus:11.0.2
|
||||
ports:
|
||||
- 8055
|
||||
volumes:
|
||||
- ../files/uploads:/directus/uploads
|
||||
- ../files/extensions:/directus/extensions
|
||||
- directus_uploads:/directus/uploads
|
||||
- directus_extensions:/directus/extensions
|
||||
depends_on:
|
||||
- cache
|
||||
- database
|
||||
database:
|
||||
condition: service_healthy
|
||||
cache:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
SECRET: "replace-with-secure-random-value"
|
||||
SECRET: ${DIRECTUS_SECRET}
|
||||
|
||||
DB_CLIENT: "pg"
|
||||
DB_HOST: "database"
|
||||
DB_PORT: "5432"
|
||||
DB_DATABASE: "directus"
|
||||
DB_USER: "directus"
|
||||
DB_PASSWORD: "directus"
|
||||
DB_PASSWORD: ${DATABASE_PASSWORD}
|
||||
|
||||
CACHE_ENABLED: "true"
|
||||
CACHE_AUTO_PURGE: "true"
|
||||
CACHE_STORE: "redis"
|
||||
REDIS: "redis://cache:6379"
|
||||
|
||||
# After first successful login, remove the admin email/password env. variables below
|
||||
# as these will now be stored in the database.
|
||||
ADMIN_EMAIL: "admin@example.com"
|
||||
ADMIN_PASSWORD: "d1r3ctu5"
|
||||
volumes:
|
||||
directus:
|
||||
directus_uploads:
|
||||
directus_extensions:
|
||||
directus_database:
|
||||
@@ -2,10 +2,15 @@ import {
|
||||
type DomainSchema,
|
||||
type Schema,
|
||||
type Template,
|
||||
generateBase64,
|
||||
generatePassword,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const directusSecret = generateBase64(64);
|
||||
const databasePassword = generatePassword();
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: generateRandomDomain(schema),
|
||||
@@ -14,7 +19,13 @@ export function generate(schema: Schema): Template {
|
||||
},
|
||||
];
|
||||
|
||||
const envs = [
|
||||
`DATABASE_PASSWORD=${databasePassword}`,
|
||||
`DIRECTUS_SECRET=${directusSecret}`,
|
||||
];
|
||||
|
||||
return {
|
||||
domains,
|
||||
envs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export const templates: TemplateData[] = [
|
||||
{
|
||||
id: "directus",
|
||||
name: "Directus",
|
||||
version: "10.12.1",
|
||||
version: "11.0.2",
|
||||
description:
|
||||
"Directus is an open source headless CMS that provides an API-first solution for building custom backends.",
|
||||
logo: "directus.jpg",
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./*"],
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"moduleResolution": "Node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./*"],
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "./server/**/*"]
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
findBackupById,
|
||||
findServerById,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
runPostgresBackup,
|
||||
} from "@dokploy/server";
|
||||
} from "@dokploy/server/dist";
|
||||
import { db } from "@dokploy/server/dist/db";
|
||||
import { backups, server } from "@dokploy/server/dist/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -21,22 +22,47 @@ export const runJobs = async (job: QueueJob) => {
|
||||
const { backupId } = job;
|
||||
const backup = await findBackupById(backupId);
|
||||
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
|
||||
|
||||
if (databaseType === "postgres" && postgres) {
|
||||
const server = await findServerById(postgres.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runPostgresBackup(postgres, backup);
|
||||
} else if (databaseType === "mysql" && mysql) {
|
||||
const server = await findServerById(mysql.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runMySqlBackup(mysql, backup);
|
||||
} else if (databaseType === "mongo" && mongo) {
|
||||
const server = await findServerById(mongo.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runMongoBackup(mongo, backup);
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
const server = await findServerById(mariadb.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
}
|
||||
}
|
||||
if (job.type === "server") {
|
||||
const { serverId } = job;
|
||||
const server = await findServerById(serverId);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await cleanUpUnusedImages(serverId);
|
||||
await cleanUpDockerBuilder(serverId);
|
||||
await cleanUpSystemPrune(serverId);
|
||||
// await sendDockerCleanupNotifications();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
@@ -305,6 +305,19 @@ export const ShowSponsors = () => {
|
||||
alt="Rivo.gg"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://photoquest.wedding/?ref=dokploy"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
src="https://photoquest.wedding/favicon/android-chrome-512x512.png"
|
||||
className="rounded-xl"
|
||||
width="60px"
|
||||
alt="Rivo.gg"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Footer } from "./Footer";
|
||||
import { Header } from "./Header";
|
||||
|
||||
export function SlimLayout() {
|
||||
const t = useTranslations("404");
|
||||
@@ -16,6 +14,7 @@ export function SlimLayout() {
|
||||
<Link href="/" className="text-primary">
|
||||
{t("action")}
|
||||
</Link>
|
||||
p{" "}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MinusIcon, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Container } from "./Container";
|
||||
import { trackGAEvent } from "./analitycs";
|
||||
import { Button } from "./ui/button";
|
||||
import { Switch } from "./ui/switch";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button, buttonVariants } from "./ui/button";
|
||||
import { NumberInput } from "./ui/input";
|
||||
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
|
||||
function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
@@ -55,7 +59,14 @@ function CheckIcon({
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const calculatePrice = (count: number, isAnnual = false) => {
|
||||
if (isAnnual) {
|
||||
if (count <= 1) return 45.9;
|
||||
return 35.7 * count;
|
||||
}
|
||||
if (count <= 1) return 4.5;
|
||||
return count * 3.5;
|
||||
};
|
||||
function Plan({
|
||||
name,
|
||||
price,
|
||||
@@ -63,6 +74,7 @@ function Plan({
|
||||
href,
|
||||
features,
|
||||
featured = false,
|
||||
buttonText = "Get Started",
|
||||
}: {
|
||||
name: string;
|
||||
price: string;
|
||||
@@ -70,6 +82,7 @@ function Plan({
|
||||
href: string;
|
||||
features: Array<string>;
|
||||
featured?: boolean;
|
||||
buttonText?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
@@ -116,23 +129,17 @@ function Plan({
|
||||
}}
|
||||
className="rounded-full mt-8"
|
||||
>
|
||||
Get started
|
||||
{buttonText}
|
||||
</Button>
|
||||
{/* <Button
|
||||
href={href}
|
||||
variant={featured ? "solid" : "outline"}
|
||||
color="white"
|
||||
className="mt-8"
|
||||
aria-label={`Get started with the ${name} plan for ${price}`}
|
||||
>
|
||||
Get started
|
||||
</Button> */}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function Pricing() {
|
||||
const [monthly, setMonthly] = useState(false);
|
||||
const router = useRouter();
|
||||
const [isAnnual, setIsAnnual] = useState(true);
|
||||
const [serverQuantity, setServerQuantity] = useState(3);
|
||||
const featured = true;
|
||||
return (
|
||||
<section
|
||||
id="pricing"
|
||||
@@ -152,54 +159,200 @@ export function Pricing() {
|
||||
Deploy Smarter, Scale Faster – Without Breaking the Bank
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-10 flex flex-row gap-x-4 justify-center">
|
||||
<Switch checked={monthly} onCheckedChange={(e) => setMonthly(e)} />
|
||||
{!monthly ? "Monthly" : "Yearly"}
|
||||
</div>
|
||||
|
||||
<div className=" mt-10 mx-auto">
|
||||
<div className="mt-16 grid md:grid-cols-2 gap-y-10 mx-auto w-full lg:-mx-8 xl:mx-0 xl:gap-x-8">
|
||||
<Plan
|
||||
name="Free"
|
||||
price="$0"
|
||||
description="Perfect for developers who prefer to manage their own servers."
|
||||
href="https://docs.dokploy.com/en/docs/core/get-started/installation#docker"
|
||||
features={[
|
||||
"Unlimited deployments",
|
||||
"Self-hosted on your own infrastructure",
|
||||
"Full access to all deployment features",
|
||||
"Docker Swarm and Docker Compose support",
|
||||
"Community support",
|
||||
"Custom domains and SSL integration",
|
||||
"No feature limitations on the core platform",
|
||||
]}
|
||||
/>
|
||||
<Plan
|
||||
featured
|
||||
name="General"
|
||||
price={!monthly ? "$6.99" : "$5.49"}
|
||||
description="Ideal for indie hackers, freelancers, agencies, and businesses looking for a managed solution."
|
||||
href="/register"
|
||||
features={[
|
||||
"1 free server included (user-provided)",
|
||||
"All self-hosted features without hosting the UI",
|
||||
"Dokploy infrastructure managed by us",
|
||||
"$3.99 per additional server (user-provided)",
|
||||
]}
|
||||
/>
|
||||
{/* <Plan
|
||||
name="Enterprise"
|
||||
price="$39"
|
||||
description="For even the biggest enterprise companies."
|
||||
href="/register"
|
||||
features={[
|
||||
"Send unlimited quotes and invoices",
|
||||
"Connect up to 15 bank accounts",
|
||||
"Track up to 200 expenses per month",
|
||||
"Automated payroll support",
|
||||
"Export up to 25 reports, including TPS",
|
||||
]}
|
||||
/> */}
|
||||
<div className="mt-16 flex flex-col gap-10 mx-auto w-full lg:-mx-8 xl:mx-0 xl:gap-x-8 justify-center items-center">
|
||||
<Tabs
|
||||
defaultValue="monthly"
|
||||
value={isAnnual ? "annual" : "monthly"}
|
||||
// className="w-full"
|
||||
onValueChange={(e) => setIsAnnual(e === "annual")}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex flex-row max-w-4xl gap-4 mx-auto">
|
||||
<section
|
||||
className={clsx(
|
||||
"flex flex-col rounded-3xl border-dashed border-muted border-2 px-4 max-w-sm",
|
||||
featured
|
||||
? "order-first bg-black border py-8 lg:order-none"
|
||||
: "lg:py-8",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||
Free
|
||||
</p>
|
||||
|
|
||||
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||
Open Source
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-5 font-medium text-lg text-white">
|
||||
Dokploy Open Source
|
||||
</h3>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-sm",
|
||||
featured ? "text-white" : "text-slate-400",
|
||||
)}
|
||||
>
|
||||
Manager your own infrastructure installing dokploy ui in your
|
||||
own server.
|
||||
</p>
|
||||
|
||||
<ul
|
||||
role="list"
|
||||
className={clsx(
|
||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||
featured ? "text-white" : "text-slate-200",
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"Complete Flexibility: Install Dokploy UI on your own infrastructure",
|
||||
"Unlimited Deployments",
|
||||
"Self-hosted Infrastructure",
|
||||
"Community Support",
|
||||
"Access to Core Features",
|
||||
"Dokploy Integration",
|
||||
"Basic Backups",
|
||||
"Access to All Updates",
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex text-muted-foreground">
|
||||
<CheckIcon />
|
||||
<span className="ml-2">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Unlimited Servers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
className={clsx(
|
||||
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||
featured
|
||||
? "order-first bg-black border py-8 lg:order-none"
|
||||
: "lg:py-8",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center mb-4">
|
||||
<Badge>Recommended 🚀</Badge>
|
||||
</div>
|
||||
{isAnnual ? (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)}{" "}
|
||||
USD
|
||||
</p>
|
||||
|
|
||||
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||
${" "}
|
||||
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(
|
||||
2,
|
||||
)}{" "}
|
||||
/ Month USD
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||
</p>
|
||||
)}
|
||||
<h3 className="mt-5 font-medium text-lg text-white">
|
||||
Dokploy Plan
|
||||
</h3>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-sm",
|
||||
featured ? "text-white" : "text-slate-400",
|
||||
)}
|
||||
>
|
||||
No need to manage Dokploy UI infrastructure, we take care of
|
||||
it for you.
|
||||
</p>
|
||||
|
||||
<ul
|
||||
role="list"
|
||||
className={clsx(
|
||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||
featured ? "text-white" : "text-slate-200",
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"Managed Hosting: No need to manage your own servers",
|
||||
"Priority Support",
|
||||
"Future-Proof Features",
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex text-muted-foreground">
|
||||
<CheckIcon />
|
||||
<span className="ml-2">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{serverQuantity} Servers (You bring the servers)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (serverQuantity <= 1) return;
|
||||
|
||||
setServerQuantity(serverQuantity - 1);
|
||||
}}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
onChange={(e) => {
|
||||
setServerQuantity(e.target.value as unknown as number);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setServerQuantity(serverQuantity + 1);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"justify-between",
|
||||
// : "justify-end",
|
||||
"flex flex-row items-center gap-2 mt-4",
|
||||
)}
|
||||
>
|
||||
<div className="justify-end w-full">
|
||||
<Link
|
||||
href="https://app.dokploy.com/register"
|
||||
target="_blank"
|
||||
className={buttonVariants({ className: "w-full" })}
|
||||
>
|
||||
Subscribe
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
@@ -212,48 +365,43 @@ export function Pricing() {
|
||||
const faqs = [
|
||||
[
|
||||
{
|
||||
question: "How does Dokploy's free plan work?",
|
||||
question: "How does Dokploy's Open Source plan work?",
|
||||
answer:
|
||||
"The free plan allows you to self-host Dokploy on your own infrastructure with unlimited deployments and full access to all features.",
|
||||
"You can host Dokploy UI on your own infrastructure and you will be responsible for the maintenance and updates.",
|
||||
},
|
||||
{
|
||||
question: "Do I need to provide my own server for the managed plan?",
|
||||
answer:
|
||||
"Yes, in the managed plan, you provide your own server, and we manage the Dokploy UI infrastructure for you.",
|
||||
"Yes, in the managed plan, you provide your own server eg(Hetzner, Hostinger, AWS, ETC.) VPS, and we manage the Dokploy UI infrastructure for you.",
|
||||
},
|
||||
{
|
||||
question: "What happens if I need more than one server?",
|
||||
answer:
|
||||
"Each additional server costs $3.99/month and can be easily added to your account.",
|
||||
"The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
question: "Can I use my custom domain with Dokploy?",
|
||||
answer:
|
||||
"Yes, custom domain support is available on all plans, including the free version.",
|
||||
},
|
||||
{
|
||||
question: "Is there a limit on the number of deployments?",
|
||||
answer:
|
||||
"No, there is no limit on the number of deployments in any of the plans.",
|
||||
},
|
||||
{
|
||||
question: "Do I have to manually configure Traefik?",
|
||||
question: "What happens if I exceed my purchased server limit?",
|
||||
answer:
|
||||
"Dokploy offers dynamic Traefik configuration out-of-the-box, so no manual setup is needed.",
|
||||
"The most recently added servers will be deactivated. You won't be able to create services on inactive servers until they are reactivated.",
|
||||
},
|
||||
{
|
||||
question: "Do you offer a refunds?",
|
||||
answer:
|
||||
"We do not offer refunds. However, you can cancel your subscription at any time. Feel free to try our open-source version for free before making a purchase.",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
question: "How do automated backups work?",
|
||||
answer:
|
||||
"Automated backups are included in the managed plan and are limited to database backups only.",
|
||||
},
|
||||
{
|
||||
question: "What kind of support do you offer?",
|
||||
answer:
|
||||
"We offer community support for the free plan and priority support for paid plans.",
|
||||
"We offer community support for the open source version and priority support for paid plans.",
|
||||
},
|
||||
{
|
||||
question: "Is Dokploy open-source?",
|
||||
@@ -279,7 +427,7 @@ export function Faqs() {
|
||||
{"Frequently asked questions"}
|
||||
</h2>
|
||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground">
|
||||
If you can’t find what you’re looking for, please submit an issue
|
||||
If you can’t find what you’re looking for, please send us an email
|
||||
to:{" "}
|
||||
<Link href={"mailto:support@dokploy.com"} className="text-primary">
|
||||
support@dokploy.com
|
||||
|
||||
36
apps/website/components/ui/badge.tsx
Normal file
36
apps/website/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
69
apps/website/components/ui/input.tsx
Normal file
69
apps/website/components/ui/input.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from "react";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, errorMessage, type, ...props }, ref) => {
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
// bg-gray
|
||||
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<span className="text-sm text-red-600 text-secondary-foreground">
|
||||
{errorMessage}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, errorMessage, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
className={cn("text-left", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
value={props.value === undefined ? undefined : String(props.value)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
props.onChange?.(e);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
const syntheticEvent = {
|
||||
...e,
|
||||
target: {
|
||||
...e.target,
|
||||
value: number,
|
||||
},
|
||||
};
|
||||
props.onChange?.(
|
||||
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
NumberInput.displayName = "NumberInput";
|
||||
|
||||
export { Input, NumberInput };
|
||||
53
apps/website/components/ui/tabs.tsx
Normal file
53
apps/website/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
@@ -24,9 +24,9 @@
|
||||
"projectsDes": "Manage and organize all your projects in one place, keeping detailed track of progress and resource allocation.",
|
||||
"applications": "Applications & Databases",
|
||||
"applicationsDes": "Centralize control over your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.",
|
||||
"compose": "compose",
|
||||
"compose": "Compose",
|
||||
"composeDes": "Native Docker Compose support for manage complex applications and services with ease.",
|
||||
"multinode": "multinode",
|
||||
"multinode": "Multinode",
|
||||
"multinodeDes": "Scale applications to multiples nodes using docker swarm to manage the cluster.",
|
||||
"monitoring": "Monitoring",
|
||||
"monitoringDes": "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
|
||||
@@ -56,23 +56,23 @@
|
||||
},
|
||||
"faq": {
|
||||
"title": "Frequently asked questions",
|
||||
"des": "If you can’t find what you’re looking for, please submit an issue through our GitHub repository or ask questions on our Discord.",
|
||||
"des": "If you can't find what you're looking for, please submit an issue through our GitHub repository or ask questions on our Discord.",
|
||||
"q1": "What is dokploy?",
|
||||
"a1": "Dokploy is a stable, easy-to-use deployment solution designed to simplify the application management process. Think of Dokploy as a free alternative self-hostable solution to platforms like Heroku, Vercel, and Netlify.",
|
||||
"q2": "Why Choose Dokploy?",
|
||||
"a2": "Simplicity, Flexibility, and Fast",
|
||||
"q3": "Is free?",
|
||||
"a3": "Yes, dokploy is totally free. You can use it for personal projects, small teams, or even for large-scale applications.",
|
||||
"a2": "Dokploy offers simplicity, flexibility, and speed in application deployment and management.",
|
||||
"q3": "Is Dokploy free?",
|
||||
"a3": "Yes, Dokploy is totally free. You can use it for personal projects, small teams, or even for large-scale applications.",
|
||||
"q4": "Is it open source?",
|
||||
"a4": "Yes, dokploy is open source and free to use.",
|
||||
"q5": "What types of languages can i deploy with dokploy?",
|
||||
"a5": "Dokploy do not restrict programming languages. you are free to choose your preferred language and framework.",
|
||||
"a4": "Yes, Dokploy is open source and free to use.",
|
||||
"q5": "What types of languages can I deploy with Dokploy?",
|
||||
"a5": "Dokploy does not restrict programming languages. You are free to choose your preferred language and framework.",
|
||||
"q6": "How do I request a feature or report a bug?",
|
||||
"a6": "Currently we are working on fixing bug fixes, but we will be releasing new features soon. You can also request features or report bugs.",
|
||||
"a6": "To request a feature or report a bug, please create an issue on our GitHub repository or ask in our Discord channel. We are currently focused on addressing existing bugs and plan to release new features soon.",
|
||||
"q7": "Do you track the usage of Dokploy?",
|
||||
"a7": "No, we don't track any usage data.",
|
||||
"q8": "Are there any user forums or communities where I can interact with other users?",
|
||||
"a8": "Yes, we have active github discussions where you can share ideas, ask for help, and connect with other users.",
|
||||
"a8": "Yes, we have active GitHub discussions where you can share ideas, ask for help, and connect with other users.",
|
||||
"q9": "What types of applications can I deploy with Dokploy?",
|
||||
"a9": "Dokploy supports a variety of applications, including those built with Docker, as well as applications from any Git repository, offering custom builds with Nixpacks, Dockerfiles, or Buildpacks like Heroku and Paketo.",
|
||||
"q10": "How does Dokploy handle database management?",
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.1.6",
|
||||
"@radix-ui/react-tabs": "1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.7.0",
|
||||
|
||||
@@ -57,10 +57,15 @@ install_dokploy() {
|
||||
fi
|
||||
}
|
||||
|
||||
advertise_addr=$(get_ip)
|
||||
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Swarm initialized"
|
||||
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
@@ -87,6 +92,7 @@ install_dokploy() {
|
||||
--update-order stop-first \
|
||||
--constraint 'node.role == manager' \
|
||||
-e RELEASE_TAG=canary \
|
||||
-e ADVERTISE_ADDR=$advertise_addr \
|
||||
dokploy/dokploy:canary
|
||||
|
||||
GREEN="\033[0;32m"
|
||||
|
||||
@@ -41,10 +41,31 @@ install_dokploy() {
|
||||
fi
|
||||
|
||||
docker swarm leave --force 2>/dev/null
|
||||
advertise_addr=$(curl -s ifconfig.me)
|
||||
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
|
||||
if [ -n "$ipv4" ]; then
|
||||
echo "$ipv4"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Swarm initialized"
|
||||
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
@@ -71,6 +92,7 @@ install_dokploy() {
|
||||
--update-order stop-first \
|
||||
--constraint 'node.role == manager' \
|
||||
-e RELEASE_TAG=feature \
|
||||
-e ADVERTISE_ADDR=$advertise_addr \
|
||||
dokploy/dokploy:feature
|
||||
|
||||
GREEN="\033[0;32m"
|
||||
|
||||
@@ -56,9 +56,14 @@ install_dokploy() {
|
||||
fi
|
||||
}
|
||||
|
||||
advertise_addr=$(get_ip)
|
||||
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Swarm initialized"
|
||||
|
||||
@@ -85,6 +90,7 @@ install_dokploy() {
|
||||
--update-parallelism 1 \
|
||||
--update-order stop-first \
|
||||
--constraint 'node.role == manager' \
|
||||
-e ADVERTISE_ADDR=$advertise_addr \
|
||||
dokploy/dokploy:latest
|
||||
|
||||
GREEN="\033[0;32m"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"dokploy:build": "pnpm --filter=dokploy run build",
|
||||
"dokploy:start": "pnpm --filter=dokploy run start",
|
||||
"test": "pnpm --filter=dokploy run test",
|
||||
"server:script": "pnpm --filter=server run switch:dev",
|
||||
"server:dev": "pnpm --filter=server run dev",
|
||||
"server:build": "pnpm --filter=server run build",
|
||||
"docker:build:canary": "./apps/dokploy/docker/build.sh canary",
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
// import { build } from "esbuild";
|
||||
// import alias from "esbuild-plugin-alias";
|
||||
// import path from "node:path";
|
||||
// import { fileURLToPath } from "node:url";
|
||||
// const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
|
||||
// const __dirname = path.dirname(__filename);
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { build } from "esbuild";
|
||||
import alias from "esbuild-plugin-alias";
|
||||
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// build({
|
||||
// entryPoints: ["./src/**/*.ts"],
|
||||
// // outfile: "./dist/index.js",
|
||||
// outdir: "./dist",
|
||||
// bundle: true,
|
||||
// minify: false,
|
||||
// platform: "node",
|
||||
// target: "esnext",
|
||||
// format: "esm",
|
||||
// plugins: [
|
||||
// alias({
|
||||
// "@/server": path.resolve(__dirname, "src"),
|
||||
// }),
|
||||
// ],
|
||||
// packages: "external",
|
||||
// // Opcional: si deseas emitir declaraciones de tipos con esbuild-plugin-dts
|
||||
// })
|
||||
// .then(() => {
|
||||
// console.log("Build successful");
|
||||
// })
|
||||
// .catch(() => process.exit(1));
|
||||
build({
|
||||
entryPoints: ["./src/**/*.ts"],
|
||||
// outfile: "./dist/index.js",
|
||||
outdir: "./dist",
|
||||
bundle: true,
|
||||
minify: false,
|
||||
platform: "node",
|
||||
target: "esnext",
|
||||
format: "esm",
|
||||
plugins: [
|
||||
alias({
|
||||
"@dokploy/server": path.resolve(__dirname, "src"),
|
||||
}),
|
||||
],
|
||||
packages: "external",
|
||||
// Opcional: si deseas emitir declaraciones de tipos con esbuild-plugin-dts
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Build successful");
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
@@ -1,14 +1,42 @@
|
||||
{
|
||||
"name": "@dokploy/server",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./db": {
|
||||
"import": "./src/db/index.ts",
|
||||
"require": "./dist/db/index.cjs.js"
|
||||
},
|
||||
"./setup/*": {
|
||||
"import": "./src/setup/*.ts",
|
||||
"require": "./dist/setup/index.cjs.js"
|
||||
},
|
||||
"./constants": {
|
||||
"import": "./src/constants/index.ts",
|
||||
"require": "./dist/constants.cjs.js"
|
||||
},
|
||||
"./dist": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs.js"
|
||||
},
|
||||
"./dist/db": {
|
||||
"import": "./dist/db/index.js",
|
||||
"require": "./dist/db/*.cjs"
|
||||
},
|
||||
"./dist/db/schema": {
|
||||
"import": "./dist/db/schema/index.js",
|
||||
"require": "./dist/db/schema/*.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf ./dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
|
||||
"build:types": "tsc --emitDeclarationOnly --experimenta-dts",
|
||||
"switch:dev": "node scripts/switchToSrc.js",
|
||||
"switch:prod": "node scripts/switchToDist.js",
|
||||
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
|
||||
"esbuild": "tsx ./esbuild.config.ts",
|
||||
"esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -49,6 +77,7 @@
|
||||
"ssh2": "1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild-plugin-alias": "0.2.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsx": "^4.7.1",
|
||||
"tsc-alias": "1.8.10",
|
||||
|
||||
27
packages/server/scripts/switchToDist.js
Normal file
27
packages/server/scripts/switchToDist.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const packagePath = path.resolve(__dirname, "../package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
||||
|
||||
pkg.exports = {
|
||||
".": {
|
||||
import: "./dist/index.js",
|
||||
require: "./dist/index.cjs.js",
|
||||
},
|
||||
"./db": {
|
||||
import: "./dist/db/index.js",
|
||||
require: "./dist/db/index.cjs.js",
|
||||
},
|
||||
"./*": {
|
||||
import: "./dist/*",
|
||||
require: "./dist/*.cjs",
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
|
||||
console.log("Switched exports to use dist for production");
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user