mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
104 Commits
v0.8.1
...
feat/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
525b420e75 | ||
|
|
f0f34df13c | ||
|
|
1a877340d3 | ||
|
|
f7e43fa1c1 | ||
|
|
906906102b | ||
|
|
245a5175a8 | ||
|
|
f427014f52 | ||
|
|
0465a71d86 | ||
|
|
3de8a18ef9 | ||
|
|
e317d0c808 | ||
|
|
ff482ffe28 | ||
|
|
82588f3e16 | ||
|
|
069f1a7b7a | ||
|
|
807137d3b1 | ||
|
|
c03c154fc4 | ||
|
|
698ff9e918 | ||
|
|
8bf6a22db8 | ||
|
|
497d45129c | ||
|
|
0b22b694e6 | ||
|
|
ee5516bb91 | ||
|
|
e90b98e629 | ||
|
|
ff382d2029 | ||
|
|
4a37f85a51 | ||
|
|
6bdc833413 | ||
|
|
17a64a9402 | ||
|
|
a22b0797b1 | ||
|
|
f3b351245a | ||
|
|
0cb74c5fde | ||
|
|
9a828d4966 | ||
|
|
4845c1ad5d | ||
|
|
6159786dfe | ||
|
|
b473062f40 | ||
|
|
6c08f33ebb | ||
|
|
3ded0d21d0 | ||
|
|
9ee8fb1894 | ||
|
|
72b1600cd4 | ||
|
|
04a59c5e21 | ||
|
|
c8f990d541 | ||
|
|
54f855e738 | ||
|
|
65a70c09c1 | ||
|
|
f25d78a87d | ||
|
|
79f39db502 | ||
|
|
a46e7759b2 | ||
|
|
f001a50278 | ||
|
|
4c3bc8efdc | ||
|
|
a591e02ffa | ||
|
|
abe787593c | ||
|
|
9b312cd9d7 | ||
|
|
66a4e86209 | ||
|
|
92df2472ae | ||
|
|
5f558f3773 | ||
|
|
754bb75e2a | ||
|
|
c84d39a20f | ||
|
|
6415a66603 | ||
|
|
66567c8f2b | ||
|
|
5b5aeb545a | ||
|
|
12c263c1ce | ||
|
|
d8d0b60cb3 | ||
|
|
19295ba746 | ||
|
|
0d3c978aad | ||
|
|
d2c8632c4f | ||
|
|
b419da427f | ||
|
|
033bf66405 | ||
|
|
c549ea17d8 | ||
|
|
c412dabc54 | ||
|
|
0bd0da2ee4 | ||
|
|
bf58ae0f0f | ||
|
|
e7ed3c300b | ||
|
|
f876457fbd | ||
|
|
a8d714c20d | ||
|
|
86f1bf31b8 | ||
|
|
95f75fdccb | ||
|
|
ac4f327775 | ||
|
|
6c0205c0d9 | ||
|
|
950c0abf9d | ||
|
|
8b66a5ca9e | ||
|
|
5afe1645a0 | ||
|
|
4a82125612 | ||
|
|
3bb19cd324 | ||
|
|
8c121a07aa | ||
|
|
d4c8c63691 | ||
|
|
cf06162be7 | ||
|
|
ea5349c844 | ||
|
|
6007427a6c | ||
|
|
0a889c5db1 | ||
|
|
3d60236b36 | ||
|
|
83009fd0b7 | ||
|
|
b9c7e5f6bb | ||
|
|
1a34ba175e | ||
|
|
bd0bbdea26 | ||
|
|
0b18f86a91 | ||
|
|
6e6df2c771 | ||
|
|
189c2b768d | ||
|
|
c482230995 | ||
|
|
7acb86a83e | ||
|
|
3e6a519c8b | ||
|
|
c3ccd2a6b7 | ||
|
|
94587c3472 | ||
|
|
27b83e471e | ||
|
|
137c219402 | ||
|
|
fe032d3d0f | ||
|
|
d3108ebf65 | ||
|
|
458ddc6e0a | ||
|
|
9c36f30bb0 |
@@ -15,7 +15,9 @@ jobs:
|
||||
name: Build and push AMD64 image
|
||||
command: |
|
||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
if [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
|
||||
TAG="feature"
|
||||
elif [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
TAG="latest"
|
||||
else
|
||||
TAG="canary"
|
||||
@@ -38,7 +40,9 @@ jobs:
|
||||
name: Build and push ARM64 image
|
||||
command: |
|
||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
if [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
|
||||
TAG="feature"
|
||||
elif [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
TAG="latest"
|
||||
else
|
||||
TAG="canary"
|
||||
@@ -71,6 +75,12 @@ jobs:
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
docker manifest push dokploy/dokploy:${VERSION}
|
||||
elif [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
|
||||
TAG="feature"
|
||||
docker manifest create dokploy/dokploy:${TAG} \
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
docker manifest push dokploy/dokploy:${TAG}
|
||||
else
|
||||
TAG="canary"
|
||||
docker manifest create dokploy/dokploy:${TAG} \
|
||||
@@ -88,12 +98,14 @@ workflows:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- 139-multi-server-feature
|
||||
- build-arm64:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- 139-multi-server-feature
|
||||
- combine-manifests:
|
||||
requires:
|
||||
- build-amd64
|
||||
@@ -103,3 +115,4 @@ workflows:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- 139-multi-server-feature
|
||||
|
||||
BIN
.github/sponsors/logo.png
vendored
Normal file
BIN
.github/sponsors/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
.github/sponsors/lxaer.png
vendored
Normal file
BIN
.github/sponsors/lxaer.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 267 KiB |
@@ -27,7 +27,7 @@ WORKDIR /app
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
@@ -42,7 +42,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
|
||||
|
||||
|
||||
# Install docker
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
|
||||
|
||||
# Install Nixpacks and tsx
|
||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||
@@ -55,4 +55,4 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
CMD [ "pnpm", "start" ]
|
||||
@@ -1,8 +1,8 @@
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
<div>
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://dokploy.com/og.png" >
|
||||
|
||||
<a href="https://dokploy.com" target="_blank" rel="noopener">
|
||||
<img style="object-fit: cover;" align="center" width="100%"src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
|
||||
@@ -3,7 +3,6 @@ title: Installation
|
||||
description: "Get Dokploy up and running on your server within minutes with this easy-to-follow installation guide."
|
||||
---
|
||||
|
||||
|
||||
Follow these steps in order to set up Dokploy locally and deploy it to your server, effectively managing Docker containers and applications:
|
||||
|
||||
You need to follow this steps in the same order:
|
||||
@@ -30,8 +29,9 @@ We have tested on the following Linux Distros:
|
||||
|
||||
### Providers
|
||||
|
||||
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets)
|
||||
- [Hetzner](https://www.hetzner.com/cloud/)
|
||||
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97)
|
||||
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get 200$ credits for free with this referral link: [Referral Link](https://m.do.co/c/db24efd43f35)
|
||||
- [Hetzner](https://www.hetzner.com/cloud/) Get 20€ credits for free with this referral link: [Referral Link](https://hetzner.cloud/?ref=vou4fhxJ1W2D)
|
||||
- [Linode](https://www.linode.com/es/pricing/#compute-shared)
|
||||
- [Vultr](https://www.vultr.com/pricing/#cloud-compute)
|
||||
- [Scaleway](https://www.scaleway.com/en/pricing/?tags=baremetal,available)
|
||||
@@ -42,11 +42,12 @@ We have tested on the following Linux Distros:
|
||||
|
||||
To ensure a smooth experience with Dokploy, your server should have at least 2GB of RAM and 30GB of disk space. This specification helps to handle the resources consumed by Docker during builds and prevents system freezes.
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
<Callout>**Suggestion:** For cost efficiency with reliable service, we recommend Hetzner as the best value-for-money VPS provider.</Callout>
|
||||
|
||||
<Callout>
|
||||
**Suggestion:** For cost efficiency with reliable service, we recommend
|
||||
Hetzner as the best value-for-money VPS provider.
|
||||
</Callout>
|
||||
|
||||
### Docker
|
||||
|
||||
@@ -64,9 +65,16 @@ After running the installation script, Dokploy and its dependencies will be set
|
||||
|
||||
Open your web browser and navigate to `http://your-ip-from-your-vps:3000`. You will be directed to the initial setup page where you can configure the administrative account for Dokploy.
|
||||
|
||||
|
||||
### Initial Configuration
|
||||
|
||||
1. **Create an Admin Account:** Fill in the necessary details to set up your administrator account. This account will be the admin account for Dokploy.
|
||||
|
||||
<ImageZoom src="/assets/images/setup.png" width={1300} height={650} alt='home og image' className="rounded-lg" />
|
||||
{" "}
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/images/setup.png"
|
||||
width={1300}
|
||||
height={650}
|
||||
alt="home og image"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,6 @@ description: Deploy open source templates with Dokploy
|
||||
- **AppSmith**: 开源的 CRM 替代方案
|
||||
- **Meilisearch**: 一个快速的搜索 API,轻松集成到您的应用、网站和工作流中
|
||||
- **Odoo**: 开源的 ERP 替代方案
|
||||
- **Plausible**: 开源分析平台
|
||||
- **Rocketchat**: 开源的聊天平台
|
||||
- **Uptime Kuma**: 开源的运行时间监控
|
||||
- **PhpMyAdmin**: 开源数据库管理
|
||||
@@ -28,9 +27,7 @@ description: Deploy open source templates with Dokploy
|
||||
- **excalidraw**: 开源协作绘图工具
|
||||
- **Directus**: 现代数据栈 🐰 — Directus 是一个即时的 REST+GraphQL API 和直观的无代码数据协作应用程序,适用于任何 SQL 数据库
|
||||
- **Baserow**: 构建管理面板、内部工具和仪表板的平台
|
||||
- **Minio**: 开源对象存储
|
||||
- **Metabase**: 开源商业智能
|
||||
- **Grafana**: 开源的指标仪表板
|
||||
- **Wordpress**: 开源内容管理系统
|
||||
|
||||
## 创建您自己的模板
|
||||
|
||||
@@ -19,7 +19,6 @@ The following templates are available:
|
||||
- **AppSmith**: Open Source CRM Alternative
|
||||
- **Meilisearch**: A lightning-fast search API that fits effortlessly into your apps, websites, and workflow
|
||||
- **Odoo**: Open Source ERP Alternative
|
||||
- **Plausible**: Open source analytics platform
|
||||
- **Rocketchat**: Open Source Chat Platform
|
||||
- **Uptime Kuma**: Open Source Uptime Monitoring
|
||||
- **PhpMyAdmin**: Open Source Database Administration
|
||||
@@ -28,9 +27,7 @@ The following templates are available:
|
||||
- **excalidraw**: Open Source Collaborative Drawing Tool
|
||||
- **Directus**: The Modern Data Stack 🐰 — Directus is an instant REST+GraphQL API and intuitive no-code data collaboration app for any SQL database.
|
||||
- **Baserow**: Platform to build admin panels, internal tools, and dashboards
|
||||
- **Minio**: Open Source Object Storage
|
||||
- **Metabase**: Open Source Business Intelligence
|
||||
- **Grafana**: Open Source Dashboard for your metrics
|
||||
- **Wordpress**: Open Source Content Management System
|
||||
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
||||
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||
import { paths } from "@/server/constants";
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
import type { ApplicationNested } from "@/server/utils/builders";
|
||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
@@ -11,11 +13,84 @@ if (typeof window === "undefined") {
|
||||
globalThis.FileList = undici.FileList as any;
|
||||
}
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
serverId: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
buildArgs: null,
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
buildType: "nixpacks",
|
||||
bitbucketBranch: "",
|
||||
bitbucketBuildPath: "",
|
||||
bitbucketId: "",
|
||||
bitbucketRepository: "",
|
||||
bitbucketOwner: "",
|
||||
githubId: "",
|
||||
gitlabProjectId: 0,
|
||||
gitlabBranch: "",
|
||||
gitlabBuildPath: "",
|
||||
gitlabId: "",
|
||||
gitlabRepository: "",
|
||||
gitlabOwner: "",
|
||||
command: null,
|
||||
cpuLimit: null,
|
||||
cpuReservation: null,
|
||||
createdAt: "",
|
||||
customGitBranch: "",
|
||||
customGitBuildPath: "",
|
||||
customGitSSHKeyId: null,
|
||||
customGitUrl: "",
|
||||
description: "",
|
||||
dockerfile: null,
|
||||
dockerImage: null,
|
||||
dropBuildPath: null,
|
||||
enabled: null,
|
||||
env: null,
|
||||
healthCheckSwarm: null,
|
||||
labelsSwarm: null,
|
||||
memoryLimit: null,
|
||||
memoryReservation: null,
|
||||
modeSwarm: null,
|
||||
mounts: [],
|
||||
name: "",
|
||||
networkSwarm: null,
|
||||
owner: null,
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
redirects: [],
|
||||
refreshToken: "",
|
||||
registry: null,
|
||||
registryId: null,
|
||||
replicas: 1,
|
||||
repository: null,
|
||||
restartPolicySwarm: null,
|
||||
rollbackConfigSwarm: null,
|
||||
security: [],
|
||||
sourceType: "git",
|
||||
subtitle: null,
|
||||
title: null,
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
};
|
||||
//
|
||||
vi.mock("@/server/constants", () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
paths: () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}),
|
||||
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}));
|
||||
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
// const { APPLICATIONS_PATH } = paths();
|
||||
beforeAll(async () => {
|
||||
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||
});
|
||||
@@ -25,39 +100,42 @@ describe("unzipDrop using real zip files", () => {
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder", async () => {
|
||||
const appName = "single-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "single-file";
|
||||
// const appName = "single-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||
const appName = "folderwithfile";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "folderwithfile";
|
||||
// const appName = "folderwithfile";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with multiple root folders", async () => {
|
||||
const appName = "two-folders";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "two-folders";
|
||||
// const appName = "two-folders";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
@@ -66,13 +144,14 @@ describe("unzipDrop using real zip files", () => {
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a file", async () => {
|
||||
const appName = "nested";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "nested";
|
||||
// const appName = "nested";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
@@ -82,13 +161,14 @@ describe("unzipDrop using real zip files", () => {
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||
const appName = "folder-with-sibling-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "folder-with-sibling-file";
|
||||
// const appName = "folder-with-sibling-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const baseApp: ApplicationNested = {
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
serverId: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
buildArgs: null,
|
||||
|
||||
@@ -278,6 +278,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="px-4">
|
||||
<AlertBlock type="info">
|
||||
Changing settings such as placements may cause the logs/monitoring
|
||||
to be unavailable.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -81,7 +81,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
registryId: data?.registryId === "none" ? null : data?.registryId,
|
||||
registryId:
|
||||
data?.registryId === "none" || !data?.registryId
|
||||
? null
|
||||
: data?.registryId,
|
||||
replicas: data?.replicas,
|
||||
})
|
||||
.then(async () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { File } from "lucide-react";
|
||||
import { File, Loader2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||
interface Props {
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.readTraefikConfig.useQuery(
|
||||
const { data, isLoading } = api.application.readTraefikConfig.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
@@ -35,7 +35,12 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{data === null ? (
|
||||
{isLoading ? (
|
||||
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center justify-center min-h-[10vh]">
|
||||
Loading...
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
) : !data ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<File className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertTriangle, Package } from "lucide-react";
|
||||
import { Package } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddVolumes } from "./add-volumes";
|
||||
import { DeleteVolume } from "./delete-volume";
|
||||
|
||||
@@ -11,8 +11,9 @@ interface Props {
|
||||
logPath: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
|
||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -21,7 +22,7 @@ export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
{ applicationId },
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
refetchInterval: 5000,
|
||||
refetchInterval: 1000,
|
||||
},
|
||||
);
|
||||
const [url, setUrl] = React.useState("");
|
||||
@@ -110,6 +110,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<ShowDeployment
|
||||
serverId={data?.serverId || ""}
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
|
||||
@@ -175,6 +175,7 @@ export const AddDomain = ({
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
appName: application?.appName || "",
|
||||
serverId: application?.serverId || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
field.onChange(domain);
|
||||
@@ -296,11 +297,7 @@ export const AddDomain = ({
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
>
|
||||
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||
{dictionary.submit}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
358
apps/dokploy/components/dashboard/application/domains/schema.tsx
Normal file
358
apps/dokploy/components/dashboard/application/domains/schema.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import { UseFormGetValues } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
export const providersData = [
|
||||
{
|
||||
name: "S3",
|
||||
type: "s3",
|
||||
properties: [
|
||||
{
|
||||
name: "accessKey",
|
||||
type: "text",
|
||||
label: "Access Key",
|
||||
description: "Your S3 Access Key",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "secretAccessKey",
|
||||
type: "password",
|
||||
label: "Secret Access Key",
|
||||
description: "Your S3 Secret Access Key",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "region",
|
||||
type: "text",
|
||||
label: "Region",
|
||||
description: "AWS Region, e.g., us-east-1",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "endpoint",
|
||||
type: "text",
|
||||
label: "Endpoint",
|
||||
description: "S3 Endpoint URL",
|
||||
required: true,
|
||||
default: "https://s3.amazonaws.com",
|
||||
},
|
||||
{
|
||||
name: "bucket",
|
||||
type: "text",
|
||||
label: "Bucket Name",
|
||||
description: "Name of the S3 bucket",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "provider",
|
||||
type: "select",
|
||||
label: "S3 Provider",
|
||||
description: "Select your S3 provider",
|
||||
required: false,
|
||||
default: "AWS",
|
||||
options: ["AWS", "Ceph", "Minio", "Alibaba", "Other"],
|
||||
},
|
||||
{
|
||||
name: "storageClass",
|
||||
type: "text",
|
||||
label: "Storage Class",
|
||||
description: "S3 Storage Class, e.g., STANDARD, REDUCED_REDUNDANCY",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "acl",
|
||||
type: "text",
|
||||
label: "ACL",
|
||||
description: "Access Control List settings for S3",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "GCS",
|
||||
type: "gcs",
|
||||
properties: [
|
||||
{
|
||||
name: "serviceAccountFile",
|
||||
type: "text",
|
||||
label: "Service Account File",
|
||||
description:
|
||||
"Path to the JSON file containing your service account key",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "clientId",
|
||||
type: "text",
|
||||
label: "Client ID",
|
||||
description:
|
||||
"Your GCS OAuth Client ID (required if Service Account File not provided)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "clientSecret",
|
||||
type: "password",
|
||||
label: "Client Secret",
|
||||
description:
|
||||
"Your GCS OAuth Client Secret (required if Service Account File not provided)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "projectNumber",
|
||||
type: "text",
|
||||
label: "Project Number",
|
||||
description:
|
||||
"Your GCS Project Number (required if Service Account File not provided)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "bucket",
|
||||
type: "text",
|
||||
label: "Bucket Name",
|
||||
description: "Name of the GCS bucket",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "objectAcl",
|
||||
type: "text",
|
||||
label: "Object ACL",
|
||||
description: "Access Control List for objects uploaded to GCS",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "bucketAcl",
|
||||
type: "text",
|
||||
label: "Bucket ACL",
|
||||
description: "Access Control List for the GCS bucket",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Azure Blob",
|
||||
type: "azureblob",
|
||||
properties: [
|
||||
{
|
||||
name: "account",
|
||||
type: "text",
|
||||
label: "Account Name",
|
||||
description: "Your Azure Storage account name",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
type: "password",
|
||||
label: "Account Key",
|
||||
description: "Your Azure Storage account access key",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "endpoint",
|
||||
type: "text",
|
||||
label: "Endpoint",
|
||||
description: "Custom endpoint for Azure Blob Storage (if any)",
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "container",
|
||||
type: "text",
|
||||
label: "Container Name",
|
||||
description: "Name of the Azure Blob container",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dropbox",
|
||||
type: "dropbox",
|
||||
properties: [
|
||||
{
|
||||
name: "token",
|
||||
type: "password",
|
||||
label: "Access Token",
|
||||
description: "Your Dropbox access token",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
type: "text",
|
||||
label: "Destination Path",
|
||||
description: "Path in Dropbox where the files will be uploaded",
|
||||
required: false,
|
||||
default: "/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "FTP",
|
||||
type: "ftp",
|
||||
properties: [
|
||||
{
|
||||
name: "host",
|
||||
type: "text",
|
||||
label: "FTP Host",
|
||||
description: "Hostname or IP address of the FTP server",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "port",
|
||||
type: "number",
|
||||
label: "FTP Port",
|
||||
description: "Port number of the FTP server",
|
||||
required: false,
|
||||
default: 21,
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
type: "text",
|
||||
label: "Username",
|
||||
description: "FTP username",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "pass",
|
||||
type: "password",
|
||||
label: "Password",
|
||||
description: "FTP password",
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "secure",
|
||||
type: "checkbox",
|
||||
label: "Use FTPS",
|
||||
description: "Enable FTPS (FTP over SSL/TLS)",
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
type: "text",
|
||||
label: "Destination Path",
|
||||
description: "Remote path on the FTP server",
|
||||
required: false,
|
||||
default: "/",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* S3 Provider Schema
|
||||
*/
|
||||
export const s3Schema = z.object({
|
||||
accessKey: z.string().nonempty({ message: "Access Key is required" }),
|
||||
secretAccessKey: z
|
||||
.string()
|
||||
.nonempty({ message: "Secret Access Key is required" }),
|
||||
region: z.string().nonempty({ message: "Region is required" }),
|
||||
endpoint: z
|
||||
.string()
|
||||
.nonempty({ message: "Endpoint is required" })
|
||||
.default("https://s3.amazonaws.com"),
|
||||
bucket: z.string().nonempty({ message: "Bucket Name is required" }),
|
||||
provider: z
|
||||
.enum(["AWS", "Ceph", "Minio", "Alibaba", "Other"])
|
||||
.optional()
|
||||
.default("AWS"),
|
||||
storageClass: z.string().optional(),
|
||||
acl: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Azure Blob Storage Provider Schema
|
||||
*/
|
||||
export const azureBlobSchema = z.object({
|
||||
account: z.string().nonempty({ message: "Account Name is required" }),
|
||||
key: z.string().nonempty({ message: "Account Key is required" }),
|
||||
endpoint: z.string().optional(),
|
||||
container: z.string().nonempty({ message: "Container Name is required" }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Dropbox Provider Schema
|
||||
*/
|
||||
export const dropboxSchema = z.object({
|
||||
token: z.string().nonempty({ message: "Access Token is required" }),
|
||||
path: z.string().optional().default("/"),
|
||||
});
|
||||
|
||||
/**
|
||||
* FTP Provider Schema
|
||||
*/
|
||||
export const ftpSchema = z.object({
|
||||
host: z.string().nonempty({ message: "FTP Host is required" }),
|
||||
port: z.number().optional().default(21),
|
||||
user: z.string().nonempty({ message: "Username is required" }),
|
||||
pass: z.string().nonempty({ message: "Password is required" }),
|
||||
secure: z.boolean().optional().default(false),
|
||||
path: z.string().optional().default("/"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Exporting all schemas in a single object for convenience
|
||||
*/
|
||||
|
||||
export const providerSchemas = {
|
||||
s3: s3Schema,
|
||||
azureblob: azureBlobSchema,
|
||||
dropbox: dropboxSchema,
|
||||
ftp: ftpSchema,
|
||||
};
|
||||
|
||||
export const getObjectSchema = (schema: z.ZodTypeAny) => {
|
||||
const initialValues: any = {};
|
||||
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const shape = schema._def.shape();
|
||||
|
||||
for (const [key, fieldSchema] of Object.entries(shape)) {
|
||||
if ("_def" in fieldSchema && "defaultValue" in fieldSchema._def) {
|
||||
initialValues[key] = fieldSchema._def.defaultValue();
|
||||
} else {
|
||||
initialValues[key] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialValues;
|
||||
};
|
||||
|
||||
export const mergeFormValues = (
|
||||
schema: z.ZodTypeAny,
|
||||
values: Record<string, any>,
|
||||
) => {
|
||||
const initialSchemaObj = getObjectSchema(schema);
|
||||
|
||||
const properties: any = {};
|
||||
|
||||
for (const key in values) {
|
||||
const keysMatch = Object.keys(initialSchemaObj).filter((k) => k === key);
|
||||
if (keysMatch.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[keysMatch[0] as keyof typeof initialSchemaObj] =
|
||||
values[key] || "";
|
||||
}
|
||||
|
||||
return properties;
|
||||
};
|
||||
@@ -45,14 +45,17 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
toast.success("Deploying Application....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Application deployed succesfully");
|
||||
await refetch();
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
}}
|
||||
|
||||
@@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
isLoading={isLoading}
|
||||
disabled={!zip}
|
||||
disabled={!zip || isLoading}
|
||||
>
|
||||
Deploy{" "}
|
||||
</Button>
|
||||
|
||||
@@ -66,7 +66,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
) : (
|
||||
<StopApplication applicationId={applicationId} />
|
||||
)}
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
@@ -30,12 +31,14 @@ export const DockerLogs = dynamic(
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogs = ({ appName }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -62,7 +65,14 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -79,6 +89,7 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
id="terminal"
|
||||
containerId={containerId || "select-a-container"}
|
||||
/>
|
||||
|
||||
@@ -9,10 +9,16 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
logPath: string | null;
|
||||
serverId?: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
||||
export const ShowDeploymentCompose = ({
|
||||
logPath,
|
||||
open,
|
||||
onClose,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -21,7 +27,7 @@ export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
|
||||
@@ -111,6 +111,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<ShowDeploymentCompose
|
||||
serverId={data?.serverId || ""}
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
|
||||
@@ -310,6 +310,7 @@ export const AddDomainCompose = ({
|
||||
isLoading={isLoadingGenerate}
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
serverId: compose?.serverId || "",
|
||||
appName: compose?.appName || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
|
||||
@@ -75,7 +75,10 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
<StopCompose composeId={composeId} />
|
||||
)}
|
||||
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
@@ -116,6 +119,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{data?.server?.name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader, Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
@@ -30,14 +31,20 @@ export const DockerLogs = dynamic(
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
}
|
||||
|
||||
export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
export const ShowDockerLogsCompose = ({
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -64,7 +71,14 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -81,6 +95,7 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
id="terminal"
|
||||
containerId={containerId || "select-a-container"}
|
||||
/>
|
||||
|
||||
@@ -17,23 +17,27 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DockerMonitoring } from "../../monitoring/docker/show";
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
}
|
||||
|
||||
export const ShowMonitoringCompose = ({
|
||||
appName,
|
||||
appType = "stack",
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName: appName,
|
||||
appType,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -46,7 +50,7 @@ export const ShowMonitoringCompose = ({
|
||||
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
const { mutateAsync: restart, isLoading } =
|
||||
const { mutateAsync: restart, isLoading: isRestarting } =
|
||||
api.docker.restartContainer.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,7 +81,14 @@ export const ShowMonitoringCompose = ({
|
||||
value={containerAppName}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -95,7 +106,7 @@ export const ShowMonitoringCompose = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isLoading={isRestarting}
|
||||
onClick={async () => {
|
||||
if (!containerId) return;
|
||||
toast.success(`Restarting container ${containerAppName}`);
|
||||
|
||||
@@ -11,12 +11,14 @@ import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowContainerConfig = ({ containerId }: Props) => {
|
||||
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!containerId,
|
||||
|
||||
@@ -8,9 +8,14 @@ import "@xterm/xterm/css/xterm.css";
|
||||
interface Props {
|
||||
id: string;
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
export const DockerLogsId: React.FC<Props> = ({
|
||||
id,
|
||||
containerId,
|
||||
serverId,
|
||||
}) => {
|
||||
const [term, setTerm] = React.useState<Terminal>();
|
||||
const [lines, setLines] = React.useState<number>(40);
|
||||
|
||||
@@ -38,7 +43,7 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
|
||||
@@ -22,9 +22,14 @@ export const DockerLogsId = dynamic(
|
||||
interface Props {
|
||||
containerId: string;
|
||||
children?: React.ReactNode;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
||||
export const ShowDockerModalLogs = ({
|
||||
containerId,
|
||||
children,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
@@ -41,7 +46,11 @@ export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
||||
<DockerLogsId
|
||||
id="terminal"
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -114,11 +114,20 @@ export const columns: ColumnDef<Container>[] = [
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<ShowDockerModalLogs containerId={container.containerId}>
|
||||
<ShowDockerModalLogs
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId}
|
||||
>
|
||||
View Logs
|
||||
</ShowDockerModalLogs>
|
||||
<ShowContainerConfig containerId={container.containerId} />
|
||||
<DockerTerminalModal containerId={container.containerId}>
|
||||
<ShowContainerConfig
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
/>
|
||||
<DockerTerminalModal
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
>
|
||||
Terminal
|
||||
</DockerTerminalModal>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -34,8 +34,15 @@ export type Container = NonNullable<
|
||||
RouterOutputs["docker"]["getContainers"]
|
||||
>[0];
|
||||
|
||||
export const ShowContainers = () => {
|
||||
const { data, isLoading } = api.docker.getContainers.useQuery();
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowContainers = ({ serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainers.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
@@ -103,83 +110,99 @@ export const ShowContainers = () => {
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{isLoading ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
) : data?.length === 0 ? (
|
||||
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No results.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>No results.</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table?.getRowModel()?.rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>No results.</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="space-x-2 flex flex-wrap">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
{data && data?.length > 0 && (
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="space-x-2 flex flex-wrap">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,10 +18,15 @@ const Terminal = dynamic(
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
||||
export const DockerTerminalModal = ({
|
||||
children,
|
||||
containerId,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
@@ -40,7 +45,11 @@ export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Terminal id="terminal" containerId={containerId} />
|
||||
<Terminal
|
||||
id="terminal"
|
||||
containerId={containerId}
|
||||
serverId={serverId || ""}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -8,9 +8,14 @@ import { AttachAddon } from "@xterm/addon-attach";
|
||||
interface Props {
|
||||
id: string;
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||
export const DockerTerminal: React.FC<Props> = ({
|
||||
id,
|
||||
containerId,
|
||||
serverId,
|
||||
}) => {
|
||||
const termRef = useRef(null);
|
||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||
useEffect(() => {
|
||||
@@ -33,7 +38,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer<
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowTraefikFile = ({ path }: Props) => {
|
||||
const { data, refetch } = api.settings.readTraefikFile.useQuery(
|
||||
export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
isLoading: isLoadingFile,
|
||||
} = api.settings.readTraefikFile.useQuery(
|
||||
{
|
||||
path,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!path,
|
||||
@@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
||||
@@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
path,
|
||||
serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
@@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
className="w-full relative z-[5]"
|
||||
>
|
||||
<div className="flex flex-col overflow-auto">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormDescription className="break-all">
|
||||
{path}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
{isLoadingFile ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormDescription className="break-all">
|
||||
{path}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
rule: Host('domain.com')
|
||||
@@ -116,31 +130,36 @@ routers:
|
||||
tls: false
|
||||
middlewares: []
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button isLoading={isLoading} disabled={canEdit} type="submit">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit || isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Tree } from "@/components/ui/file-tree";
|
||||
import { api } from "@/utils/api";
|
||||
import { FileIcon, Folder, Workflow } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
||||
import React from "react";
|
||||
import { ShowTraefikFile } from "./show-traefik-file";
|
||||
|
||||
export const ShowTraefikSystem = () => {
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||
const [file, setFile] = React.useState<null | string>(null);
|
||||
|
||||
const { data: directories } = api.settings.readDirectories.useQuery();
|
||||
const {
|
||||
data: directories,
|
||||
isLoading,
|
||||
error,
|
||||
isError,
|
||||
} = api.settings.readDirectories.useQuery(
|
||||
{
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
retry: 2,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("mt-6 md:grid gap-4")}>
|
||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="w-full">
|
||||
{error?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
@@ -34,7 +61,7 @@ export const ShowTraefikSystem = () => {
|
||||
/>
|
||||
<div className="w-full">
|
||||
{file ? (
|
||||
<ShowTraefikFile path={file} />
|
||||
<ShowTraefikFile path={file} serverId={serverId} />
|
||||
) : (
|
||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
|
||||
@@ -36,7 +36,10 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
) : (
|
||||
<StopMariadb mariadbId={mariadbId} />
|
||||
)}
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -34,7 +34,10 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
) : (
|
||||
<StopMongo mongoId={mongoId} />
|
||||
)}
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -23,7 +23,7 @@ export const DockerMemoryChart = ({
|
||||
return {
|
||||
time: item.time,
|
||||
name: `Point ${index + 1}`,
|
||||
usage: (item.value.used / 1024).toFixed(2),
|
||||
usage: (item.value.used / 1024 ** 3).toFixed(2),
|
||||
};
|
||||
});
|
||||
return (
|
||||
|
||||
@@ -208,9 +208,7 @@ export const DockerMonitoring = ({
|
||||
<div className="flex flex-col gap-2 w-full ">
|
||||
<span className="text-base font-medium">Memory</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{`Used: ${(currentData.memory.value.used / 1024).toFixed(
|
||||
2,
|
||||
)} GB / Limit: ${(currentData.memory.value.total / 1024).toFixed(2)} GB`}
|
||||
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
|
||||
</span>
|
||||
<Progress
|
||||
value={currentData.memory.value.usedPercentage}
|
||||
@@ -218,7 +216,7 @@ export const DockerMonitoring = ({
|
||||
/>
|
||||
<DockerMemoryChart
|
||||
acummulativeData={acummulativeData.memory}
|
||||
memoryLimitGB={currentData.memory.value.total / 1024}
|
||||
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
|
||||
/>
|
||||
</div>
|
||||
{appName === "dokploy" && (
|
||||
@@ -240,9 +238,9 @@ export const DockerMonitoring = ({
|
||||
<div className="flex flex-col gap-2 w-full ">
|
||||
<span className="text-base font-medium">Block I/O</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{`Used: ${currentData.block.value.readMb.toFixed(
|
||||
{`Read: ${currentData.block.value.readMb.toFixed(
|
||||
2,
|
||||
)} MB / Limit: ${currentData.block.value.writeMb.toFixed(
|
||||
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
|
||||
3,
|
||||
)} MB`}
|
||||
</span>
|
||||
|
||||
@@ -35,7 +35,10 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
<StopMysql mysqlId={mysqlId} />
|
||||
)}
|
||||
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -38,7 +38,10 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
<StopPostgres postgresId={postgresId} />
|
||||
)}
|
||||
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -19,11 +19,26 @@ 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 { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Folder } from "lucide-react";
|
||||
import { Folder, HelpCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -43,6 +58,7 @@ const AddTemplateSchema = z.object({
|
||||
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
||||
@@ -54,8 +70,10 @@ interface Props {
|
||||
|
||||
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.application.create.useMutation();
|
||||
@@ -75,6 +93,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||
appName: data.appName,
|
||||
description: data.description,
|
||||
projectId,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Created");
|
||||
@@ -135,6 +154,57 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server (Optional)
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
If not server is selected, the application will be
|
||||
deployed on the server where the user is logged in.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
|
||||
@@ -22,15 +22,23 @@ import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CircuitBoard, Folder } from "lucide-react";
|
||||
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -51,6 +59,7 @@ const AddComposeSchema = z.object({
|
||||
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||
@@ -63,6 +72,7 @@ interface Props {
|
||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const slug = slugify(projectName);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.create.useMutation();
|
||||
|
||||
@@ -87,6 +97,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
projectId,
|
||||
composeType: data.composeType,
|
||||
appName: data.appName,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
@@ -148,6 +159,57 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server (Optional)
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
If not server is selected, the application will be
|
||||
deployed on the server where the user is logged in.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
|
||||
@@ -26,6 +26,15 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -71,6 +80,7 @@ const baseDatabaseSchema = z.object({
|
||||
databasePassword: z.string(),
|
||||
dockerImage: z.string(),
|
||||
description: z.string().nullable(),
|
||||
serverId: z.string().nullable(),
|
||||
});
|
||||
|
||||
const mySchema = z.discriminatedUnion("type", [
|
||||
@@ -145,6 +155,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const postgresMutation = api.postgres.create.useMutation();
|
||||
const mongoMutation = api.mongo.create.useMutation();
|
||||
const redisMutation = api.redis.create.useMutation();
|
||||
@@ -161,6 +172,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
description: "",
|
||||
databaseName: "",
|
||||
databaseUser: "",
|
||||
serverId: null,
|
||||
},
|
||||
resolver: zodResolver(mySchema),
|
||||
});
|
||||
@@ -183,6 +195,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
appName: data.appName,
|
||||
dockerImage: defaultDockerImage,
|
||||
projectId,
|
||||
serverId: data.serverId,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
@@ -191,8 +204,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseName: data.databaseName,
|
||||
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId,
|
||||
});
|
||||
} else if (data.type === "mongo") {
|
||||
promise = mongoMutation.mutateAsync({
|
||||
@@ -200,11 +215,13 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
databasePassword: data.databasePassword,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId,
|
||||
});
|
||||
} else if (data.type === "redis") {
|
||||
promise = redisMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
serverId: data.serverId,
|
||||
projectId,
|
||||
});
|
||||
} else if (data.type === "mariadb") {
|
||||
@@ -215,6 +232,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
databaseName: data.databaseName,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId,
|
||||
});
|
||||
} else if (data.type === "mysql") {
|
||||
promise = mysqlMutation.mutateAsync({
|
||||
@@ -224,6 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
databaseRootPassword: data.databaseRootPassword,
|
||||
serverId: data.serverId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -352,6 +371,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a Server</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
|
||||
@@ -29,11 +29,27 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { ScrollArea } from "@radix-ui/react-scroll-area";
|
||||
@@ -43,6 +59,7 @@ import {
|
||||
Code,
|
||||
Github,
|
||||
Globe,
|
||||
HelpCircle,
|
||||
PuzzleIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
@@ -58,9 +75,12 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data } = api.compose.templates.useQuery();
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: tags, isLoading: isLoadingTags } =
|
||||
api.compose.getTags.useQuery();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [serverId, setServerId] = useState<string | undefined>(undefined);
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.deployTemplate.useMutation();
|
||||
|
||||
@@ -89,9 +109,9 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
||||
<div className="sticky top-0 z-10 flex flex-col gap-4 bg-background p-6 border-b">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Template</DialogTitle>
|
||||
<DialogTitle>Create from Template</DialogTitle>
|
||||
<DialogDescription>
|
||||
Deploy a open source template to your project
|
||||
Create an open source application from a template
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
@@ -109,7 +129,6 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"md:max-w-[15rem] w-full justify-between !bg-input",
|
||||
// !field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingTags
|
||||
@@ -234,7 +253,7 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href={`https://github.com/dokploy/dokploy/tree/canary/templates/${template.id}`}
|
||||
href={`https://github.com/Dokploy/dokploy/tree/canary/apps/dokploy/templates/${template.id}`}
|
||||
target="_blank"
|
||||
className={
|
||||
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
||||
@@ -255,7 +274,7 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button onSelect={(e) => e.preventDefault()}>
|
||||
Deploy
|
||||
Create
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
@@ -264,33 +283,83 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
Are you absolutely sure?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will deploy {template.name} template to
|
||||
your project.
|
||||
This will create an application from the{" "}
|
||||
{template.name} template and add it to your
|
||||
project.
|
||||
</AlertDialogDescription>
|
||||
|
||||
<div>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
|
||||
Select a Server (Optional)
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
If not server is selected, the
|
||||
application will be deployed on the
|
||||
server where the user is logged in.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<Select
|
||||
onValueChange={(e) => {
|
||||
setServerId(e);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers ({servers?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
const promise = mutateAsync({
|
||||
projectId,
|
||||
serverId: serverId || undefined,
|
||||
id: template.id,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(
|
||||
`${template.name} template created succesfully`,
|
||||
);
|
||||
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Setting up...",
|
||||
success: (data) => {
|
||||
utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
`Error to delete ${template.name} template`,
|
||||
);
|
||||
});
|
||||
return `${template.name} template created succesfully`;
|
||||
},
|
||||
error: (err) => {
|
||||
return `Ocurred an error deploying ${template.name} template`;
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -109,6 +110,7 @@ export const AddProject = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
|
||||
@@ -63,8 +63,8 @@ export const ShowProjects = () => {
|
||||
project?.mysql.length === 0 &&
|
||||
project?.postgres.length === 0 &&
|
||||
project?.redis.length === 0 &&
|
||||
project?.applications.length === 0;
|
||||
project?.compose.length === 0;
|
||||
project?.applications.length === 0 &&
|
||||
project?.compose.length === 0;
|
||||
|
||||
const totalServices =
|
||||
project?.mariadb.length +
|
||||
|
||||
@@ -37,7 +37,10 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
<StopRedis redisId={redisId} />
|
||||
)}
|
||||
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -17,13 +17,29 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
getObjectSchema,
|
||||
mergeFormValues,
|
||||
providerSchemas,
|
||||
providersData,
|
||||
} from "../../application/domains/schema";
|
||||
import { capitalize } from "lodash";
|
||||
|
||||
const addDestination = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
@@ -38,43 +54,45 @@ type AddDestination = z.infer<typeof addDestination>;
|
||||
|
||||
export const AddDestination = () => {
|
||||
const utils = api.useUtils();
|
||||
const [provider, setProviders] = useState<keyof typeof providerSchemas>("s3");
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.destination.create.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
|
||||
api.destination.testConnection.useMutation();
|
||||
const form = useForm<AddDestination>({
|
||||
const schema = providerSchemas[provider];
|
||||
const form = useForm<z.infer<typeof schema>>({
|
||||
defaultValues: {
|
||||
accessKeyId: "",
|
||||
bucket: "",
|
||||
name: "",
|
||||
region: "",
|
||||
secretAccessKey: "",
|
||||
endpoint: "",
|
||||
...getObjectSchema(schema),
|
||||
},
|
||||
resolver: zodResolver(addDestination),
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const onSubmit = async (data: AddDestination) => {
|
||||
await mutateAsync({
|
||||
accessKey: data.accessKeyId,
|
||||
bucket: data.bucket,
|
||||
endpoint: data.endpoint,
|
||||
name: data.name,
|
||||
region: data.region,
|
||||
secretAccessKey: data.secretAccessKey,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Destination Created");
|
||||
await utils.destination.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Destination");
|
||||
});
|
||||
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||
// await mutateAsync({
|
||||
// accessKey: data.accessKeyId,
|
||||
// bucket: data.bucket,
|
||||
// endpoint: data.endpoint,
|
||||
// name: data.name,
|
||||
// region: data.region,
|
||||
// secretAccessKey: data.secretAccessKey,
|
||||
// })
|
||||
// .then(async () => {
|
||||
// toast.success("Destination Created");
|
||||
// await utils.destination.all.invalidate();
|
||||
// })
|
||||
// .catch(() => {
|
||||
// toast.error("Error to create the Destination");
|
||||
// });
|
||||
};
|
||||
|
||||
const fields = Object.keys(schema.shape);
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
@@ -88,140 +106,117 @@ export const AddDestination = () => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-destination-add"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"S3 Bucket"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessKeyId"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Access Key Id</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"xcas41dasde"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secretAccessKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Secret Access Key</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder={"asd123asdasw"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bucket"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Bucket</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder={"dokploy-bucket"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="region"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Region</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder={"us-east-1"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Endpoint</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"https://us.bucket.aws/s3"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
{fields.map((input) => (
|
||||
<FormField
|
||||
control={control}
|
||||
key={`${provider}.${input}`}
|
||||
name={`${provider}.${input}`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>{capitalize(input)}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"Value"} {...field} />
|
||||
</FormControl>
|
||||
<span className="text-sm font-medium text-destructive">
|
||||
{errors[input]?.message}
|
||||
</span>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</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");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-destination-add"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
<Select
|
||||
onValueChange={(e) => {
|
||||
setProviders(e as keyof typeof providerSchemas);
|
||||
}}
|
||||
value={provider}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{Object.keys(providerSchemas).map((registry) => (
|
||||
<SelectItem key={registry} value={registry}>
|
||||
{registry}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Providers ({providersData?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const result = form.getValues()[provider];
|
||||
const hola = mergeFormValues(schema, result);
|
||||
console.log(hola);
|
||||
|
||||
// const getPropertiesByForm = (form: any) => {
|
||||
// const initialValues = getInitialValues(schema);
|
||||
// console.log(form, initialValues);
|
||||
// const properties: any = {};
|
||||
// for (const key in form) {
|
||||
// const keysMatch = Object.keys(initialValues).filter(
|
||||
// (k) => k === key,
|
||||
// );
|
||||
// if (keysMatch.length === 0) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// properties[keysMatch[0]] = form[key] || "";
|
||||
// console.log(key);
|
||||
// }
|
||||
// return properties;
|
||||
// };
|
||||
// const result = form.getValues();
|
||||
// const properties = getPropertiesByForm(result);
|
||||
// console.log(properties);
|
||||
await testConnection({
|
||||
json: {
|
||||
...hola,
|
||||
provider: provider,
|
||||
},
|
||||
// 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");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
<Button
|
||||
// isLoading={isLoading}
|
||||
form="hook-form-destination-add"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
{/* */}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -19,10 +19,8 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Edit } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||
|
||||
export const ShowDokployActions = () => {
|
||||
const { mutateAsync: reloadServer, isLoading } =
|
||||
api.settings.reloadServer.useMutation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||
<Button isLoading={isLoading} variant="outline">
|
||||
Server
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadServer()
|
||||
.then(async () => {
|
||||
toast.success("Server Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.success("Server Reloaded");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy">
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CardDescription, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { useState } from "react";
|
||||
import { ShowStorageActions } from "./show-storage-actions";
|
||||
import { ShowTraefikActions } from "./show-traefik-actions";
|
||||
import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ShowServerActions = ({ serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
View Actions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen ">
|
||||
<div className="flex flex-col gap-1">
|
||||
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
||||
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 w-full gap-4">
|
||||
<ShowTraefikActions serverId={serverId} />
|
||||
<ShowStorageActions serverId={serverId} />
|
||||
<ToggleDockerCleanup serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowStorageActions = ({ serverId }: Props) => {
|
||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||
api.settings.cleanAll.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanDockerBuilder,
|
||||
isLoading: cleanDockerBuilderIsLoading,
|
||||
} = api.settings.cleanDockerBuilder.useMutation();
|
||||
|
||||
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
|
||||
api.settings.cleanMonitoring.useMutation();
|
||||
const {
|
||||
mutateAsync: cleanUnusedImages,
|
||||
isLoading: cleanUnusedImagesIsLoading,
|
||||
} = api.settings.cleanUnusedImages.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanUnusedVolumes,
|
||||
isLoading: cleanUnusedVolumesIsLoading,
|
||||
} = api.settings.cleanUnusedVolumes.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanStoppedContainers,
|
||||
isLoading: cleanStoppedContainersIsLoading,
|
||||
} = api.settings.cleanStoppedContainers.useMutation();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isLoading={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
Space
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedImages({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned images");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean images");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused images</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedVolumes({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned volumes");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean volumes");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused volumes</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanStoppedContainers({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Stopped containers cleaned");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean stopped containers");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean stopped containers</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanDockerBuilder({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Docker Builder");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Docker Builder");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Docker Builder & System</span>
|
||||
</DropdownMenuItem>
|
||||
{!serverId && (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanMonitoring()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Monitoring");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Monitoring");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Monitoring </span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanAll({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned all");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean all");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean all</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||
api.settings.reloadTraefik.useMutation();
|
||||
|
||||
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
||||
api.settings.toggleDashboard.useMutation();
|
||||
|
||||
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||
api.settings.haveTraefikDashboardPortEnabled.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||
>
|
||||
<Button
|
||||
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||
variant="outline"
|
||||
>
|
||||
Traefik
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadTraefik({
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the traefik");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
<EditTraefikEnv serverId={serverId}>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>Modify Env</span>
|
||||
</DropdownMenuItem>
|
||||
</EditTraefikEnv>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await toggleDashboard({
|
||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
refetchDashboard();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
});
|
||||
}}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>
|
||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{/*
|
||||
<DockerTerminalModal appName="dokploy-traefik">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<span>Enter the terminal</span>
|
||||
</DropdownMenuItem>
|
||||
</DockerTerminalModal> */}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
const { data, refetch } = api.admin.one.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
|
||||
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
|
||||
{
|
||||
serverId: serverId || "",
|
||||
},
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
|
||||
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||
|
||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={async (e) => {
|
||||
await mutateAsync({
|
||||
enableDockerCleanup: e,
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Docker Cleanup Enabled");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Docker Cleanup Error");
|
||||
});
|
||||
|
||||
if (serverId) {
|
||||
refetchServer();
|
||||
} else {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label className="text-primary">Daily Docker Cleanup</Label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,253 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } 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 Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
ipAddress: z.string().min(1, {
|
||||
message: "IP Address is required",
|
||||
}),
|
||||
port: z.number().optional(),
|
||||
username: z.string().optional(),
|
||||
sshKeyId: z.string().min(1, {
|
||||
message: "SSH Key is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const AddServer = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
description: "",
|
||||
name: "",
|
||||
ipAddress: "",
|
||||
port: 22,
|
||||
username: "root",
|
||||
sshKeyId: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
description: "",
|
||||
name: "",
|
||||
ipAddress: "",
|
||||
port: 22,
|
||||
username: "root",
|
||||
sshKeyId: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
description: data.description || "",
|
||||
ipAddress: data.ipAddress || "",
|
||||
port: data.port || 22,
|
||||
username: data.username || "root",
|
||||
sshKeyId: data.sshKeyId || "",
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.server.all.invalidate();
|
||||
toast.success("Server Created");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create a server");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Server
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-3xl ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Server</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a server to deploy your applications remotely.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-server"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Hostinger Server" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="This server is for databases..."
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshKeyId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a SSH Key</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a SSH Key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{sshKeys?.map((sshKey) => (
|
||||
<SelectItem
|
||||
key={sshKey.sshKeyId}
|
||||
value={sshKey.sshKeyId}
|
||||
>
|
||||
{sshKey.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Registries ({sshKeys?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ipAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>IP Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="192.168.1.100" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="22" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="root" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form-add-server"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,301 @@
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
CopyIcon,
|
||||
ExternalLinkIcon,
|
||||
RocketIcon,
|
||||
ServerIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import copy from "copy-to-clipboard";
|
||||
import Link from "next/link";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const SetupServer = ({ serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: server } = api.server.one.useQuery(
|
||||
{
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const { data: deployments, refetch } = api.deployment.allByServer.useQuery(
|
||||
{ serverId },
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading } = api.server.setup.useMutation();
|
||||
|
||||
console.log(server?.sshKey);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Setup Server
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-4xl overflow-y-auto max-h-screen ">
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ServerIcon className="size-5" /> Setup Server
|
||||
</DialogTitle>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To setup a server, please click on the button below.
|
||||
</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
{!server?.sshKeyId ? (
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||
<AlertBlock type="warning">
|
||||
Please add a SSH Key to your server before setting up the server.
|
||||
you can assign a SSH Key to your server in Edit Server.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
) : (
|
||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||
<Tabs defaultValue="ssh-keys">
|
||||
<TabsList className="grid grid-cols-2 w-[400px]">
|
||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="ssh-keys"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||
<p className="text-primary text-base font-semibold">
|
||||
You have two options to add SSH Keys to your server:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
1. Add the public SSH Key when you create a server in your
|
||||
preffered provider (Hostinger, Digital Ocean, Hetzner,
|
||||
etc){" "}
|
||||
</li>
|
||||
<li>2. Add The SSH Key to Server Manually</li>
|
||||
</ul>
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
||||
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
||||
Copy Public Key ({server?.sshKey?.name})
|
||||
<button
|
||||
type="button"
|
||||
className=" right-2 top-8"
|
||||
onClick={() => {
|
||||
copy(
|
||||
server?.sshKey?.publicKey || "Generate a SSH Key",
|
||||
);
|
||||
toast.success("SSH Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4">
|
||||
<span className="text-base font-semibold text-primary">
|
||||
Automatic process
|
||||
</span>
|
||||
<Link
|
||||
href="https://docs.dokploy.com/en/docs/core/get-started/introduction"
|
||||
target="_blank"
|
||||
className="text-primary flex flex-row gap-2"
|
||||
>
|
||||
View Tutorial <ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
|
||||
<span className="text-base font-semibold text-primary">
|
||||
Manual process
|
||||
</span>
|
||||
<ul>
|
||||
<li className="items-center flex gap-1">
|
||||
1. Login to your server{" "}
|
||||
<span className="text-primary bg-secondary p-1 rounded-lg">
|
||||
ssh {server?.username}@{server?.ipAddress}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
copy(
|
||||
`ssh ${server?.username}@${server?.ipAddress}`,
|
||||
);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
2. When you are logged in run the following command
|
||||
<div className="flex relative flex-col gap-4 w-full mt-2">
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
language="properties"
|
||||
value={`echo "${server?.sshKey?.publicKey}" >> ~/.ssh/authorized_keys`}
|
||||
readOnly
|
||||
className="font-mono opacity-60"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2"
|
||||
onClick={() => {
|
||||
copy(server?.sshKey?.publicKey || "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li className="mt-1">
|
||||
3. You're done, you can test the connection by entering
|
||||
to the terminal or by setting up the server tab.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">
|
||||
Deployments
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
See all the 5 Server Setup
|
||||
</CardDescription>
|
||||
</div>
|
||||
<DialogAction
|
||||
title={"Setup Server?"}
|
||||
description="This will setup the server and all associated data"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
serverId: server?.serverId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
refetch();
|
||||
toast.success("Server setup successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error configuring server");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button isLoading={isLoading}>Setup Server</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{server?.deployments?.length === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<RocketIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No deployments found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{deployments?.map((deployment) => (
|
||||
<div
|
||||
key={deployment.deploymentId}
|
||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
{deployment.status}
|
||||
|
||||
<StatusTooltip
|
||||
status={deployment?.status}
|
||||
className="size-2.5"
|
||||
/>
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="break-all text-sm text-muted-foreground">
|
||||
{deployment.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="text-sm capitalize text-muted-foreground">
|
||||
<DateTooltip date={deployment.createdAt} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment.logPath);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ShowDeployment
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { ContainerIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ShowContainers } from "../../docker/show/show-containers";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ShowDockerContainersModal = ({ serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Show Docker Containers
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ContainerIcon className="size-5" /> Docker Containers
|
||||
</DialogTitle>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
See all the containers of your remote server
|
||||
</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid w-full gap-1">
|
||||
<ShowContainers serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import { KeyIcon, MoreHorizontal, ServerIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { TerminalModal } from "../web-server/terminal-modal";
|
||||
import { ShowServerActions } from "./actions/show-server-actions";
|
||||
import { AddServer } from "./add-server";
|
||||
import { SetupServer } from "./setup-server";
|
||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||
import { UpdateServer } from "./update-server";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
export const ShowServers = () => {
|
||||
const { data, refetch } = api.server.all.useQuery();
|
||||
const { mutateAsync } = api.server.remove.useMutation();
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-2 flex flex-row justify-between items-end">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Servers</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Add servers to deploy your applications remotely.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{sshKeys && sshKeys?.length > 0 && (
|
||||
<div>
|
||||
<AddServer />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-1">
|
||||
{sshKeys?.length === 0 && data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<KeyIcon className="size-8" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No SSH Keys found. Add a SSH Key to start adding servers.{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/ssh-keys"
|
||||
className="text-primary"
|
||||
>
|
||||
Add SSH Key
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
data &&
|
||||
data.length === 0 && (
|
||||
<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.
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{data && data?.length > 0 && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Table>
|
||||
<TableCaption>See all servers</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Name</TableHead>
|
||||
<TableHead className="text-center">IP Address</TableHead>
|
||||
<TableHead className="text-center">Port</TableHead>
|
||||
<TableHead className="text-center">Username</TableHead>
|
||||
<TableHead className="text-center">SSH Key</TableHead>
|
||||
<TableHead className="text-center">Created</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((server) => {
|
||||
const canDelete = server.totalSum === 0;
|
||||
return (
|
||||
<TableRow key={server.serverId}>
|
||||
<TableCell className="w-[100px]">{server.name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge>{server.ipAddress}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{server.port}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{server.username}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{server.sshKeyId ? "Yes" : "No"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(new Date(server.createdAt), "PPpp")}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
{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} />
|
||||
)}
|
||||
<DialogAction
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
canDelete
|
||||
? "Delete Server"
|
||||
: "Server has active services"
|
||||
}
|
||||
description={
|
||||
canDelete ? (
|
||||
"This will delete the server and all associated data"
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
You can not delete this server because it
|
||||
has active services.
|
||||
<AlertBlock type="warning">
|
||||
You have active services associated with
|
||||
this server, please delete them first.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
serverId: server.serverId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
`Server ${server.name} deleted succesfully`,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Delete Server
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
|
||||
{server.sshKeyId && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ShowTraefikFileSystemModal = ({ serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Show Traefik File System
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileTextIcon className="size-5" /> Traefik File System
|
||||
</DialogTitle>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
See all the files and directories of your traefik configuration
|
||||
</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||
<ShowTraefikSystem serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,269 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } 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 Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
ipAddress: z.string().min(1, {
|
||||
message: "IP Address is required",
|
||||
}),
|
||||
port: z.number().optional(),
|
||||
username: z.string().optional(),
|
||||
sshKeyId: z.string().min(1, {
|
||||
message: "SSH Key is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const UpdateServer = ({ serverId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data, isLoading } = api.server.one.useQuery(
|
||||
{
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const { mutateAsync, error, isError } = api.server.update.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
description: "",
|
||||
name: "",
|
||||
ipAddress: "",
|
||||
port: 22,
|
||||
username: "root",
|
||||
sshKeyId: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
description: data?.description || "",
|
||||
name: data?.name || "",
|
||||
ipAddress: data?.ipAddress || "",
|
||||
port: data?.port || 22,
|
||||
username: data?.username || "root",
|
||||
sshKeyId: data?.sshKeyId || "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (formData: Schema) => {
|
||||
await mutateAsync({
|
||||
name: formData.name,
|
||||
description: formData.description || "",
|
||||
ipAddress: formData.ipAddress || "",
|
||||
port: formData.port || 22,
|
||||
username: formData.username || "root",
|
||||
sshKeyId: formData.sshKeyId || "",
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.server.all.invalidate();
|
||||
toast.success("Server Updated");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update a server");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Edit Server
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-3xl ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update Server</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update a server to deploy your applications remotely.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-server"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Hostinger Server" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="This server is for databases..."
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshKeyId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a SSH Key</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a SSH Key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{sshKeys?.map((sshKey) => (
|
||||
<SelectItem
|
||||
key={sshKey.sshKeyId}
|
||||
value={sshKey.sshKeyId}
|
||||
>
|
||||
{sshKey.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Registries ({sshKeys?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ipAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>IP Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="192.168.1.100" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="22" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="root" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form-update-server"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -41,8 +41,8 @@ export const ShowUsers = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full col-span-2">
|
||||
<Card className="bg-transparent h-full ">
|
||||
<div className=" col-span-2">
|
||||
<Card className="bg-transparent ">
|
||||
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Users</CardTitle>
|
||||
@@ -55,9 +55,9 @@ export const ShowUsers = () => {
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 h-full">
|
||||
<CardContent className="space-y-2">
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="flex flex-col items-center gap-3 h-full">
|
||||
<Users className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a user, you need to add:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -6,334 +5,34 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
||||
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
|
||||
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
||||
import { ShowModalLogs } from "./web-server/show-modal-logs";
|
||||
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
|
||||
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
|
||||
import { TerminalModal } from "./web-server/terminal-modal";
|
||||
import React from "react";
|
||||
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
|
||||
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
|
||||
import { UpdateServer } from "./web-server/update-server";
|
||||
|
||||
export const WebServer = () => {
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const { mutateAsync: reloadServer, isLoading } =
|
||||
api.settings.reloadServer.useMutation();
|
||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||
api.settings.reloadTraefik.useMutation();
|
||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||
api.settings.cleanAll.useMutation();
|
||||
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
||||
api.settings.toggleDashboard.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanDockerBuilder,
|
||||
isLoading: cleanDockerBuilderIsLoading,
|
||||
} = api.settings.cleanDockerBuilder.useMutation();
|
||||
|
||||
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
|
||||
api.settings.cleanMonitoring.useMutation();
|
||||
const {
|
||||
mutateAsync: cleanUnusedImages,
|
||||
isLoading: cleanUnusedImagesIsLoading,
|
||||
} = api.settings.cleanUnusedImages.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanUnusedVolumes,
|
||||
isLoading: cleanUnusedVolumesIsLoading,
|
||||
} = api.settings.cleanUnusedVolumes.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: cleanStoppedContainers,
|
||||
isLoading: cleanStoppedContainersIsLoading,
|
||||
} = api.settings.cleanStoppedContainers.useMutation();
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
export const WebServer = ({ className }: Props) => {
|
||||
const { data } = api.admin.one.useQuery();
|
||||
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
|
||||
const { mutateAsync: updateDockerCleanup } =
|
||||
api.settings.updateDockerCleanup.useMutation();
|
||||
|
||||
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||
api.settings.haveTraefikDashboardPortEnabled.useQuery();
|
||||
|
||||
return (
|
||||
<Card className="rounded-lg w-full bg-transparent">
|
||||
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Web server settings</CardTitle>
|
||||
<CardDescription>Reload or clean the web server.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<CardContent className="flex flex-col gap-4 ">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||
<Button isLoading={isLoading} variant="outline">
|
||||
Server
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadServer()
|
||||
.then(async () => {
|
||||
toast.success("Server Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.success("Server Reloaded");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy">
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
|
||||
<ShowServerTraefikConfig>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>View Traefik config</span>
|
||||
</DropdownMenuItem>
|
||||
</ShowServerTraefikConfig>
|
||||
|
||||
<ShowServerMiddlewareConfig>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>View middlewares config</span>
|
||||
</DropdownMenuItem>
|
||||
</ShowServerMiddlewareConfig>
|
||||
|
||||
<TerminalModal>
|
||||
<span>Enter the terminal</span>
|
||||
</TerminalModal>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||
>
|
||||
<Button
|
||||
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||
variant="outline"
|
||||
>
|
||||
Traefik
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await reloadTraefik()
|
||||
.then(async () => {
|
||||
toast.success("Traefik Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to reload the traefik");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Reload</span>
|
||||
</DropdownMenuItem>
|
||||
<ShowModalLogs appName="dokploy-traefik">
|
||||
<span>Watch logs</span>
|
||||
</ShowModalLogs>
|
||||
<ShowMainTraefikConfig>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>View Traefik config</span>
|
||||
</DropdownMenuItem>
|
||||
</ShowMainTraefikConfig>
|
||||
<EditTraefikEnv>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>Modify Env</span>
|
||||
</DropdownMenuItem>
|
||||
</EditTraefikEnv>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await toggleDashboard({
|
||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
refetchDashboard();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||
);
|
||||
});
|
||||
}}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>
|
||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
|
||||
Dashboard
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DockerTerminalModal appName="dokploy-traefik">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<span>Enter the terminal</span>
|
||||
</DropdownMenuItem>
|
||||
</DockerTerminalModal>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
disabled={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
>
|
||||
<Button
|
||||
isLoading={
|
||||
cleanAllIsLoading ||
|
||||
cleanDockerBuilderIsLoading ||
|
||||
cleanUnusedImagesIsLoading ||
|
||||
cleanUnusedVolumesIsLoading ||
|
||||
cleanStoppedContainersIsLoading
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
Space
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedImages()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned images");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean images");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused images</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanUnusedVolumes()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned volumes");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean volumes");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean unused volumes</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanStoppedContainers()
|
||||
.then(async () => {
|
||||
toast.success("Stopped containers cleaned");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean stopped containers");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean stopped containers</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanDockerBuilder()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Docker Builder");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Docker Builder");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Docker Builder & System</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanMonitoring()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned Monitoring");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean Monitoring");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean Monitoring </span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onClick={async () => {
|
||||
await cleanAll()
|
||||
.then(async () => {
|
||||
toast.success("Cleaned all");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to clean all");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Clean all</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ShowDokployActions />
|
||||
<ShowTraefikActions />
|
||||
<ShowStorageActions />
|
||||
|
||||
<UpdateServer />
|
||||
</div>
|
||||
@@ -345,25 +44,8 @@ export const WebServer = () => {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Version: {dokployVersion}
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<Switch
|
||||
checked={data?.enableDockerCleanup}
|
||||
onCheckedChange={async (e) => {
|
||||
await updateDockerCleanup({
|
||||
enableDockerCleanup: e,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Docker Cleanup Enabled");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Docker Cleanup Error");
|
||||
});
|
||||
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
<Label className="text-primary">Daily Docker Cleanup</Label>
|
||||
</div>
|
||||
<ToggleDockerCleanup />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -34,12 +35,14 @@ const Terminal = dynamic(
|
||||
interface Props {
|
||||
appName: string;
|
||||
children?: React.ReactNode;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminalModal = ({ children, appName }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -65,7 +68,14 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -82,6 +92,7 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Terminal
|
||||
serverId={serverId || ""}
|
||||
id="terminal"
|
||||
containerId={containerId || "select-a-container"}
|
||||
/>
|
||||
|
||||
@@ -33,12 +33,15 @@ type Schema = z.infer<typeof schema>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const EditTraefikEnv = ({ children }: Props) => {
|
||||
export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
||||
const [canEdit, setCanEdit] = useState(true);
|
||||
|
||||
const { data } = api.settings.readTraefikEnv.useQuery();
|
||||
const { data } = api.settings.readTraefikEnv.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.settings.writeTraefikEnv.useMutation();
|
||||
@@ -62,6 +65,7 @@ export const EditTraefikEnv = ({ children }: Props) => {
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
env: data.env,
|
||||
serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik Env Updated");
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateMainTraefikConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
});
|
||||
|
||||
type UpdateTraefikConfig = z.infer<typeof UpdateMainTraefikConfigSchema>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
const { data, refetch } = api.settings.readTraefikConfig.useQuery();
|
||||
const [canEdit, setCanEdit] = useState(true);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.settings.updateTraefikConfig.useMutation();
|
||||
|
||||
const form = useForm<UpdateTraefikConfig>({
|
||||
defaultValues: {
|
||||
traefikConfig: "",
|
||||
},
|
||||
disabled: canEdit,
|
||||
resolver: zodResolver(UpdateMainTraefikConfigSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateTraefikConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
form.clearErrors("traefikConfig");
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-main-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4 relative"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`providers:
|
||||
docker:
|
||||
defaultRule: 'Host('dokploy.com')'
|
||||
file:
|
||||
directory: /etc/dokploy/traefik
|
||||
watch: true
|
||||
entryPoints:
|
||||
web:
|
||||
address: ':80'
|
||||
websecure:
|
||||
address: ':443'
|
||||
api:
|
||||
insecure: true
|
||||
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit}
|
||||
form="hook-form-update-main-traefik-config"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -35,12 +36,14 @@ export const DockerLogsId = dynamic(
|
||||
interface Props {
|
||||
appName: string;
|
||||
children?: React.ReactNode;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowModalLogs = ({ appName, children }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppLabel.useQuery(
|
||||
export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -72,7 +75,14 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -88,7 +98,11 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
||||
<DockerLogsId
|
||||
id="terminal"
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
});
|
||||
|
||||
type UpdateServerMiddlewareConfig = z.infer<
|
||||
typeof UpdateServerMiddlewareConfigSchema
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
||||
const { data, refetch } = api.settings.readMiddlewareTraefikConfig.useQuery();
|
||||
const [canEdit, setCanEdit] = useState(true);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.settings.updateMiddlewareTraefikConfig.useMutation();
|
||||
|
||||
const form = useForm<UpdateServerMiddlewareConfig>({
|
||||
defaultValues: {
|
||||
traefikConfig: "",
|
||||
},
|
||||
disabled: canEdit,
|
||||
resolver: zodResolver(UpdateServerMiddlewareConfigSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
console.log(error);
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
form.clearErrors("traefikConfig");
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Middleware config Updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the middleware traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update Middleware config</DialogTitle>
|
||||
<DialogDescription>Update the middleware config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
rule: Host('domain.com')
|
||||
service: container-name
|
||||
entryPoints:
|
||||
- web
|
||||
tls: false
|
||||
middlewares: []
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit}
|
||||
form="hook-form-update-server-traefik-config"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,163 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateServerTraefikConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
});
|
||||
|
||||
type UpdateServerTraefikConfig = z.infer<
|
||||
typeof UpdateServerTraefikConfigSchema
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowServerTraefikConfig = ({ children }: Props) => {
|
||||
const { data, refetch } = api.settings.readWebServerTraefikConfig.useQuery();
|
||||
const [canEdit, setCanEdit] = useState(true);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.settings.updateWebServerTraefikConfig.useMutation();
|
||||
|
||||
const form = useForm<UpdateServerTraefikConfig>({
|
||||
defaultValues: {
|
||||
traefikConfig: "",
|
||||
},
|
||||
disabled: canEdit,
|
||||
resolver: zodResolver(UpdateServerTraefikConfigSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateServerTraefikConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
console.log(error);
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
form.clearErrors("traefikConfig");
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
rule: Host('domain.com')
|
||||
service: container-name
|
||||
entryPoints:
|
||||
- web
|
||||
tls: false
|
||||
middlewares: []
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit}
|
||||
form="hook-form-update-server-traefik-config"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,79 +7,27 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { RemoveSSHPrivateKey } from "./remove-ssh-private-key";
|
||||
|
||||
const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const addSSHPrivateKey = z.object({
|
||||
sshPrivateKey: z
|
||||
.string({
|
||||
required_error: "SSH private key is required",
|
||||
})
|
||||
.min(1, "SSH private key is required"),
|
||||
});
|
||||
|
||||
type AddSSHPrivateKey = z.infer<typeof addSSHPrivateKey>;
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const TerminalModal = ({ children }: Props) => {
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const [user, setUser] = useState("root");
|
||||
const [terminalUser, setTerminalUser] = useState("root");
|
||||
|
||||
const { mutateAsync, isLoading } =
|
||||
api.settings.saveSSHPrivateKey.useMutation();
|
||||
|
||||
const form = useForm<AddSSHPrivateKey>({
|
||||
defaultValues: {
|
||||
sshPrivateKey: "",
|
||||
export const TerminalModal = ({ children, serverId }: Props) => {
|
||||
const { data } = api.server.one.useQuery(
|
||||
{
|
||||
serverId,
|
||||
},
|
||||
resolver: zodResolver(addSSHPrivateKey),
|
||||
});
|
||||
{ enabled: !!serverId },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: AddSSHPrivateKey) => {
|
||||
await mutateAsync({
|
||||
sshPrivateKey: formData.sshPrivateKey,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("SSH Key Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the ssh key");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
@@ -92,75 +39,14 @@ export const TerminalModal = ({ children }: Props) => {
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||
<DialogHeader className="flex flex-row justify-between pt-4">
|
||||
<div>
|
||||
<DialogTitle>Terminal</DialogTitle>
|
||||
<DialogDescription>Easy way to access the server</DialogDescription>
|
||||
</div>
|
||||
{data?.haveSSH && (
|
||||
<div>
|
||||
<RemoveSSHPrivateKey />
|
||||
</div>
|
||||
)}
|
||||
<DialogHeader className="flex flex-col gap-1">
|
||||
<DialogTitle>Terminal ({data?.name})</DialogTitle>
|
||||
<DialogDescription>Easy way to access the server</DialogDescription>
|
||||
</DialogHeader>
|
||||
{!data?.haveSSH ? (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="grid w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshPrivateKey"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>SSH Private Key</FormLabel>
|
||||
<FormDescription>
|
||||
In order to access the server you need to add an
|
||||
ssh private key
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----"
|
||||
}
|
||||
className="h-32"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Log in as</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input value={user} onChange={(e) => setUser(e.target.value)} />
|
||||
<Button onClick={() => setTerminalUser(user)}>Login</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Terminal id="terminal" userSSH={terminalUser} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Terminal id="terminal" serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,10 @@ import { AttachAddon } from "@xterm/addon-attach";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
userSSH?: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
||||
export const Terminal: React.FC<Props> = ({ id, serverId }) => {
|
||||
const termRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,7 +33,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/terminal?userSSH=${userSSH}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/terminal?serverId=${serverId}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
const addonAttach = new AttachAddon(ws);
|
||||
@@ -46,7 +46,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
||||
return () => {
|
||||
ws.readyState === WebSocket.OPEN && ws.close();
|
||||
};
|
||||
}, [id, userSSH]);
|
||||
}, [id, serverId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -74,7 +74,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
{
|
||||
title: "Cluster",
|
||||
label: "",
|
||||
icon: Server,
|
||||
icon: BoxesIcon,
|
||||
href: "/dashboard/settings/cluster",
|
||||
},
|
||||
{
|
||||
@@ -83,6 +83,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: Bell,
|
||||
href: "/dashboard/settings/notifications",
|
||||
},
|
||||
{
|
||||
title: "Servers",
|
||||
label: "",
|
||||
icon: Server,
|
||||
href: "/dashboard/settings/servers",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(user?.canAccessToSSHKeys
|
||||
@@ -117,6 +123,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
import {
|
||||
Activity,
|
||||
Bell,
|
||||
BoxesIcon,
|
||||
Database,
|
||||
GitBranch,
|
||||
KeyIcon,
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
title?: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
onClick: () => void;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const DialogAction = ({
|
||||
@@ -22,6 +23,7 @@ export const DialogAction = ({
|
||||
children,
|
||||
description,
|
||||
title,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
@@ -37,7 +39,9 @@ export const DialogAction = ({
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onClick}>Confirm</AlertDialogAction>
|
||||
<AlertDialogAction disabled={disabled} onClick={onClick}>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { HeartIcon } from "lucide-react";
|
||||
|
||||
export const ShowSupport = () => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="rounded-full">
|
||||
<span className="text-sm font-semibold">Support </span>
|
||||
<HeartIcon className="size-4 text-red-500 fill-red-600 animate-heartbeat " />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl ">
|
||||
<DialogHeader className="text-center flex justify-center items-center">
|
||||
<DialogTitle>Dokploy Support</DialogTitle>
|
||||
<DialogDescription>Consider supporting Dokploy</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid w-full gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-sm font-semibold">Name</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
81
apps/dokploy/drizzle/0037_legal_namor.sql
Normal file
81
apps/dokploy/drizzle/0037_legal_namor.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
CREATE TABLE IF NOT EXISTS "server" (
|
||||
"serverId" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"ipAddress" text NOT NULL,
|
||||
"port" integer NOT NULL,
|
||||
"username" text DEFAULT 'root' NOT NULL,
|
||||
"appName" text NOT NULL,
|
||||
"enableDockerCleanup" boolean DEFAULT false NOT NULL,
|
||||
"createdAt" text NOT NULL,
|
||||
"adminId" text NOT NULL,
|
||||
"sshKeyId" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
ALTER TABLE "deployment" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
ALTER TABLE "redis" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "server" ADD CONSTRAINT "server_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "server" ADD CONSTRAINT "server_sshKeyId_ssh-key_sshKeyId_fk" FOREIGN KEY ("sshKeyId") REFERENCES "public"."ssh-key"("sshKeyId") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "postgres" ADD CONSTRAINT "postgres_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "mariadb" ADD CONSTRAINT "mariadb_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "mongo" ADD CONSTRAINT "mongo_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "mysql" ADD CONSTRAINT "mysql_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "redis" ADD CONSTRAINT "redis_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "compose" ADD CONSTRAINT "compose_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1
apps/dokploy/drizzle/0038_familiar_shockwave.sql
Normal file
1
apps/dokploy/drizzle/0038_familiar_shockwave.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "destination" ADD COLUMN "schema" json;
|
||||
3823
apps/dokploy/drizzle/meta/0037_snapshot.json
Normal file
3823
apps/dokploy/drizzle/meta/0037_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3829
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
3829
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -260,6 +260,20 @@
|
||||
"when": 1725519351871,
|
||||
"tag": "0036_tired_ronan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 37,
|
||||
"version": "6",
|
||||
"when": 1726988289562,
|
||||
"tag": "0037_legal_namor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "6",
|
||||
"when": 1727036227151,
|
||||
"tag": "0038_familiar_shockwave",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.8.1",
|
||||
"version": "v0.8.3",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -35,7 +35,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"@aws-sdk/client-s3": "3.515.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
@@ -130,7 +129,8 @@
|
||||
"zod": "^3.23.4",
|
||||
"zod-form-data": "^2.0.2",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"ssh2": "1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
@@ -167,7 +167,8 @@
|
||||
"typescript": "^5.4.2",
|
||||
"vite-tsconfig-paths": "4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"xterm-readline": "1.1.1"
|
||||
"xterm-readline": "1.1.1",
|
||||
"@types/ssh2": "1.15.1"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.25.2"
|
||||
|
||||
@@ -87,6 +87,7 @@ export default async function handler(
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!application.serverId,
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
@@ -63,6 +63,7 @@ export default async function handler(
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
server: !!composeResult.serverId,
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
@@ -86,6 +86,7 @@ export default async function handler(
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!app.serverId,
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
18
apps/dokploy/pages/api/teapot.ts
Normal file
18
apps/dokploy/pages/api/teapot.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import Page418 from "../hola"; // Importa la página 418
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
// Renderiza el componente de la página 418 como HTML
|
||||
const htmlContent = renderToString(Page418());
|
||||
|
||||
// Devuelve la respuesta con el código de estado HTTP 418
|
||||
return new Response(htmlContent, {
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
},
|
||||
status: 418,
|
||||
});
|
||||
};
|
||||
|
||||
export default GET;
|
||||
@@ -16,12 +16,14 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
|
||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -98,6 +100,9 @@ const Service = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
</div>
|
||||
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -125,10 +130,17 @@ const Service = (
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
|
||||
<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>
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
@@ -152,14 +164,20 @@ const Service = (
|
||||
<ShowEnvironment applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</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 || ""} />
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments" className="w-full">
|
||||
|
||||
@@ -10,12 +10,14 @@ import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring
|
||||
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -92,13 +94,16 @@ const Service = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
{data?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-row gap-4">
|
||||
<div className="absolute -right-1 -top-2">
|
||||
<StatusTooltip status={data?.composeStatus} />
|
||||
@@ -119,10 +124,23 @@ const Service = (
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
|
||||
<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>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="monitoring">Monitoring</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>
|
||||
@@ -147,19 +165,22 @@ const Service = (
|
||||
<ShowEnvironmentCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMonitoringCompose
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</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"}
|
||||
/>
|
||||
|
||||
@@ -9,15 +9,16 @@ import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/g
|
||||
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
|
||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -81,7 +82,9 @@ const Mariadb = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
{data?.description}
|
||||
@@ -108,10 +111,17 @@ const Mariadb = (
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
||||
<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>
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
@@ -136,14 +146,19 @@ const Mariadb = (
|
||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</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 || ""} />
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
|
||||
@@ -9,15 +9,16 @@ import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/gener
|
||||
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
|
||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -82,7 +83,9 @@ const Mongo = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
{data?.description}
|
||||
@@ -109,10 +112,17 @@ const Mongo = (
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
||||
<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>
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
@@ -138,14 +148,19 @@ const Mongo = (
|
||||
<ShowMongoEnvironment mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</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 || ""} />
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
|
||||
@@ -9,15 +9,16 @@ import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-gene
|
||||
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
|
||||
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -80,7 +81,9 @@ const MySql = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
{data?.description}
|
||||
@@ -108,10 +111,17 @@ const MySql = (
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
||||
<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>
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
@@ -137,14 +147,19 @@ const MySql = (
|
||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</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 || ""} />
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
|
||||
@@ -9,15 +9,16 @@ import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/sho
|
||||
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
||||
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -81,7 +82,9 @@ const Postgresql = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
{data?.description}
|
||||
@@ -109,10 +112,17 @@ const Postgresql = (
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
||||
<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>
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
@@ -138,14 +148,19 @@ const Postgresql = (
|
||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</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 || ""} />
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
|
||||
@@ -8,15 +8,16 @@ import { ShowGeneralRedis } from "@/components/dashboard/redis/general/show-gene
|
||||
import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
|
||||
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -80,7 +81,9 @@ const Redis = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
{data?.description}
|
||||
@@ -108,10 +111,17 @@ const Redis = (
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-y-scroll justify-start">
|
||||
<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>
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -136,14 +146,19 @@ const Redis = (
|
||||
<ShowRedisEnvironment redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</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 || ""} />
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
|
||||
49
apps/dokploy/pages/dashboard/settings/servers.tsx
Normal file
49
apps/dokploy/pages/dashboard/settings/servers.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowServers />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Page.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<DashboardLayout tab={"settings"}>
|
||||
<SettingsLayout>{page}</SettingsLayout>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/settings/profile",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
3
apps/dokploy/pages/hola.tsx
Normal file
3
apps/dokploy/pages/hola.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function hola() {
|
||||
return <div>hola</div>;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { isAdminPresent } from "@/server/api/services/admin";
|
||||
// import { IS_CLOUD } from "@/server/constants";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
@@ -220,6 +221,11 @@ const Register = ({ hasAdmin }: Props) => {
|
||||
|
||||
export default Register;
|
||||
export async function getServerSideProps() {
|
||||
// if (IS_CLOUD) {
|
||||
// return {
|
||||
// props: {},
|
||||
// };
|
||||
// }
|
||||
const hasAdmin = await isAdminPresent();
|
||||
|
||||
if (hasAdmin) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { redirectsRouter } from "./routers/redirects";
|
||||
import { redisRouter } from "./routers/redis";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
import { securityRouter } from "./routers/security";
|
||||
import { serverRouter } from "./routers/server";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
import { sshRouter } from "./routers/ssh-key";
|
||||
import { userRouter } from "./routers/user";
|
||||
@@ -35,6 +36,7 @@ import { userRouter } from "./routers/user";
|
||||
*
|
||||
* All routers added in /api/routers should be manually added here.
|
||||
*/
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
admin: adminRouter,
|
||||
docker: dockerRouter,
|
||||
@@ -66,6 +68,7 @@ export const appRouter = createTRPCRouter({
|
||||
bitbucket: bitbucketRouter,
|
||||
gitlab: gitlabRouter,
|
||||
github: githubRouter,
|
||||
server: serverRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
export const adminRouter = createTRPCRouter({
|
||||
one: adminProcedure.query(async () => {
|
||||
one: adminProcedure.query(async ({ ctx }) => {
|
||||
const { sshPrivateKey, ...rest } = await findAdmin();
|
||||
return {
|
||||
haveSSH: !!sshPrivateKey,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user