mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
62 Commits
v0.8.3
...
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 | ||
|
|
54f855e738 | ||
|
|
79f39db502 | ||
|
|
a46e7759b2 | ||
|
|
f001a50278 | ||
|
|
4c3bc8efdc | ||
|
|
a591e02ffa | ||
|
|
abe787593c | ||
|
|
9b312cd9d7 | ||
|
|
c84d39a20f | ||
|
|
d8d0b60cb3 | ||
|
|
19295ba746 | ||
|
|
0d3c978aad | ||
|
|
d2c8632c4f | ||
|
|
033bf66405 | ||
|
|
c549ea17d8 | ||
|
|
c412dabc54 | ||
|
|
0bd0da2ee4 | ||
|
|
bf58ae0f0f | ||
|
|
a8d714c20d | ||
|
|
86f1bf31b8 | ||
|
|
95f75fdccb | ||
|
|
5afe1645a0 | ||
|
|
cf06162be7 | ||
|
|
ea5349c844 | ||
|
|
6007427a6c | ||
|
|
0a889c5db1 | ||
|
|
3d60236b36 | ||
|
|
1a34ba175e | ||
|
|
bd0bbdea26 |
@@ -15,7 +15,9 @@ jobs:
|
|||||||
name: Build and push AMD64 image
|
name: Build and push AMD64 image
|
||||||
command: |
|
command: |
|
||||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
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"
|
TAG="latest"
|
||||||
else
|
else
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
@@ -38,7 +40,9 @@ jobs:
|
|||||||
name: Build and push ARM64 image
|
name: Build and push ARM64 image
|
||||||
command: |
|
command: |
|
||||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
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"
|
TAG="latest"
|
||||||
else
|
else
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
@@ -71,6 +75,12 @@ jobs:
|
|||||||
dokploy/dokploy:${TAG}-amd64 \
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
dokploy/dokploy:${TAG}-arm64
|
dokploy/dokploy:${TAG}-arm64
|
||||||
docker manifest push dokploy/dokploy:${VERSION}
|
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
|
else
|
||||||
TAG="canary"
|
TAG="canary"
|
||||||
docker manifest create dokploy/dokploy:${TAG} \
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
@@ -88,12 +98,14 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- 139-multi-server-feature
|
||||||
- build-arm64:
|
- build-arm64:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- 139-multi-server-feature
|
||||||
- combine-manifests:
|
- combine-manifests:
|
||||||
requires:
|
requires:
|
||||||
- build-amd64
|
- build-amd64
|
||||||
@@ -103,3 +115,4 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
|
- 139-multi-server-feature
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=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 only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
@@ -42,7 +42,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
|
|||||||
|
|
||||||
|
|
||||||
# Install docker
|
# 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
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | 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
|
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD [ "pnpm", "start" ]
|
CMD [ "pnpm", "start" ]
|
||||||
@@ -29,7 +29,7 @@ We have tested on the following Linux Distros:
|
|||||||
|
|
||||||
### Providers
|
### Providers
|
||||||
|
|
||||||
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://hostinger.com?REFERRALCODE=1SIUMAURICI97)
|
- [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)
|
- [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)
|
- [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)
|
- [Linode](https://www.linode.com/es/pricing/#compute-shared)
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## 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.
|
- **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 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.
|
- **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 and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
- **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.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
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 { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
@@ -11,11 +13,84 @@ if (typeof window === "undefined") {
|
|||||||
globalThis.FileList = undici.FileList as any;
|
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", () => ({
|
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", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
|
// const { APPLICATIONS_PATH } = paths();
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
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 () => {
|
it("should correctly extract a zip with a single root folder", async () => {
|
||||||
const appName = "single-file";
|
baseApp.appName = "single-file";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "single-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
expect(files.some((f) => f.name === "test.txt")).toBe(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 () => {
|
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
const appName = "folderwithfile";
|
baseApp.appName = "folderwithfile";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "folderwithfile";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with multiple root folders", async () => {
|
it("should correctly extract a zip with multiple root folders", async () => {
|
||||||
const appName = "two-folders";
|
baseApp.appName = "two-folders";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "two-folders";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
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 () => {
|
it("should correctly extract a zip with a single root with a file", async () => {
|
||||||
const appName = "nested";
|
baseApp.appName = "nested";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "nested";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
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 () => {
|
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||||
const appName = "folder-with-sibling-file";
|
baseApp.appName = "folder-with-sibling-file";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// 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 zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const baseApp: ApplicationNested = {
|
|||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
|||||||
@@ -278,6 +278,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{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 {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { File } from "lucide-react";
|
import { File, Loader2 } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -15,7 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.readTraefikConfig.useQuery(
|
const { data, isLoading } = api.application.readTraefikConfig.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
@@ -35,7 +35,12 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<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">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<File className="size-8 text-muted-foreground" />
|
<File className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertTriangle, Package } from "lucide-react";
|
import { Package } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
import { DeleteVolume } from "./delete-volume";
|
import { DeleteVolume } from "./delete-volume";
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ interface Props {
|
|||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
|
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
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 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);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
{ applicationId },
|
{ applicationId },
|
||||||
{
|
{
|
||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
refetchInterval: 5000,
|
refetchInterval: 1000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
@@ -110,6 +110,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
open={activeLog !== null}
|
open={activeLog !== null}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog}
|
logPath={activeLog}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export const AddDomain = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
generateDomain({
|
generateDomain({
|
||||||
appName: application?.appName || "",
|
appName: application?.appName || "",
|
||||||
|
serverId: application?.serverId || "",
|
||||||
})
|
})
|
||||||
.then((domain) => {
|
.then((domain) => {
|
||||||
field.onChange(domain);
|
field.onChange(domain);
|
||||||
@@ -296,11 +297,7 @@ export const AddDomain = ({
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{dictionary.submit}
|
{dictionary.submit}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</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>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
toast.success("Deploying Application....");
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
await deploy({
|
await deploy({
|
||||||
applicationId,
|
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();
|
await refetch();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!zip}
|
disabled={!zip || isLoading}
|
||||||
>
|
>
|
||||||
Deploy{" "}
|
Deploy{" "}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<StopApplication applicationId={applicationId} />
|
<StopApplication applicationId={applicationId} />
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
export const DockerLogs = dynamic(
|
export const DockerLogs = dynamic(
|
||||||
@@ -30,12 +31,14 @@ export const DockerLogs = dynamic(
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogs = ({ appName }: Props) => {
|
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -62,7 +65,14 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -79,6 +89,7 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
id="terminal"
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
|
serverId?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
export const ShowDeploymentCompose = ({
|
||||||
|
logPath,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
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 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);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeploymentCompose
|
<ShowDeploymentCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
open={activeLog !== null}
|
open={activeLog !== null}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog}
|
logPath={activeLog}
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ export const AddDomainCompose = ({
|
|||||||
isLoading={isLoadingGenerate}
|
isLoading={isLoadingGenerate}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
generateDomain({
|
generateDomain({
|
||||||
|
serverId: compose?.serverId || "",
|
||||||
appName: compose?.appName || "",
|
appName: compose?.appName || "",
|
||||||
})
|
})
|
||||||
.then((domain) => {
|
.then((domain) => {
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
<StopCompose composeId={composeId} />
|
<StopCompose composeId={composeId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
@@ -116,6 +119,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
{data?.server?.name}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader, Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
export const DockerLogs = dynamic(
|
export const DockerLogs = dynamic(
|
||||||
@@ -30,14 +31,20 @@ export const DockerLogs = dynamic(
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
appType: "stack" | "docker-compose";
|
appType: "stack" | "docker-compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
export const ShowDockerLogsCompose = ({
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
appName,
|
||||||
|
appType,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
appType,
|
appType,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -64,7 +71,14 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -81,6 +95,7 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
id="terminal"
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,23 +17,27 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerMonitoring } from "../../monitoring/docker/show";
|
import { DockerMonitoring } from "../../monitoring/docker/show";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
appType: "stack" | "docker-compose";
|
appType: "stack" | "docker-compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowMonitoringCompose = ({
|
export const ShowMonitoringCompose = ({
|
||||||
appName,
|
appName,
|
||||||
appType = "stack",
|
appType = "stack",
|
||||||
|
serverId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName: appName,
|
appName: appName,
|
||||||
appType,
|
appType,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -46,7 +50,7 @@ export const ShowMonitoringCompose = ({
|
|||||||
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
|
||||||
const { mutateAsync: restart, isLoading } =
|
const { mutateAsync: restart, isLoading: isRestarting } =
|
||||||
api.docker.restartContainer.useMutation();
|
api.docker.restartContainer.useMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,7 +81,14 @@ export const ShowMonitoringCompose = ({
|
|||||||
value={containerAppName}
|
value={containerAppName}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -95,7 +106,7 @@ export const ShowMonitoringCompose = ({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isRestarting}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!containerId) return;
|
if (!containerId) return;
|
||||||
toast.success(`Restarting container ${containerAppName}`);
|
toast.success(`Restarting container ${containerAppName}`);
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import { api } from "@/utils/api";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowContainerConfig = ({ containerId }: Props) => {
|
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||||
const { data } = api.docker.getConfig.useQuery(
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
{
|
{
|
||||||
containerId,
|
containerId,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!containerId,
|
enabled: !!containerId,
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import "@xterm/xterm/css/xterm.css";
|
|||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
containerId: 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 [term, setTerm] = React.useState<Terminal>();
|
||||||
const [lines, setLines] = React.useState<number>(40);
|
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 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 ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
|
|||||||
@@ -22,9 +22,14 @@ export const DockerLogsId = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
export const ShowDockerModalLogs = ({
|
||||||
|
containerId,
|
||||||
|
children,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -41,7 +46,11 @@ export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
|||||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
<DockerLogsId
|
||||||
|
id="terminal"
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -114,11 +114,20 @@ export const columns: ColumnDef<Container>[] = [
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<ShowDockerModalLogs containerId={container.containerId}>
|
<ShowDockerModalLogs
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId}
|
||||||
|
>
|
||||||
View Logs
|
View Logs
|
||||||
</ShowDockerModalLogs>
|
</ShowDockerModalLogs>
|
||||||
<ShowContainerConfig containerId={container.containerId} />
|
<ShowContainerConfig
|
||||||
<DockerTerminalModal containerId={container.containerId}>
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId || ""}
|
||||||
|
/>
|
||||||
|
<DockerTerminalModal
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId || ""}
|
||||||
|
>
|
||||||
Terminal
|
Terminal
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -34,8 +34,15 @@ export type Container = NonNullable<
|
|||||||
RouterOutputs["docker"]["getContainers"]
|
RouterOutputs["docker"]["getContainers"]
|
||||||
>[0];
|
>[0];
|
||||||
|
|
||||||
export const ShowContainers = () => {
|
interface Props {
|
||||||
const { data, isLoading } = api.docker.getContainers.useQuery();
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowContainers = ({ serverId }: Props) => {
|
||||||
|
const { data, isLoading } = api.docker.getContainers.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||||
[],
|
[],
|
||||||
@@ -103,83 +110,99 @@ export const ShowContainers = () => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
{isLoading ? (
|
||||||
<TableHeader>
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
<TableRow key={headerGroup.id}>
|
Loading...
|
||||||
{headerGroup.headers.map((header) => {
|
</span>
|
||||||
return (
|
</div>
|
||||||
<TableHead key={header.id}>
|
) : data?.length === 0 ? (
|
||||||
{header.isPlaceholder
|
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
? null
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
: flexRender(
|
No results.
|
||||||
header.column.columnDef.header,
|
</span>
|
||||||
header.getContext(),
|
</div>
|
||||||
)}
|
) : (
|
||||||
</TableHead>
|
<Table>
|
||||||
);
|
<TableHeader>
|
||||||
})}
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
</TableRow>
|
<TableRow key={headerGroup.id}>
|
||||||
))}
|
{headerGroup.headers.map((header) => {
|
||||||
</TableHeader>
|
return (
|
||||||
<TableBody>
|
<TableHead key={header.id}>
|
||||||
{table.getRowModel().rows?.length ? (
|
{header.isPlaceholder
|
||||||
table.getRowModel().rows.map((row) => (
|
? null
|
||||||
<TableRow
|
: flexRender(
|
||||||
key={row.id}
|
header.column.columnDef.header,
|
||||||
data-state={row.getIsSelected() && "selected"}
|
header.getContext(),
|
||||||
>
|
)}
|
||||||
{row.getVisibleCells().map((cell) => (
|
</TableHead>
|
||||||
<TableCell key={cell.id}>
|
);
|
||||||
{flexRender(
|
})}
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext(),
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
) : (
|
</TableHeader>
|
||||||
<TableRow>
|
<TableBody>
|
||||||
<TableCell
|
{table?.getRowModel()?.rows?.length ? (
|
||||||
colSpan={columns.length}
|
table.getRowModel().rows.map((row) => (
|
||||||
className="h-24 text-center"
|
<TableRow
|
||||||
>
|
key={row.id}
|
||||||
{isLoading ? (
|
data-state={row.getIsSelected() && "selected"}
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
>
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
{row.getVisibleCells().map((cell) => (
|
||||||
Loading...
|
<TableCell key={cell.id}>
|
||||||
</span>
|
{flexRender(
|
||||||
</div>
|
cell.column.columnDef.cell,
|
||||||
) : (
|
cell.getContext(),
|
||||||
<>No results.</>
|
)}
|
||||||
)}
|
</TableCell>
|
||||||
</TableCell>
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
))
|
||||||
</TableBody>
|
) : (
|
||||||
</Table>
|
<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>
|
||||||
<div className="flex items-center justify-end space-x-2 py-4">
|
{data && data?.length > 0 && (
|
||||||
<div className="space-x-2 flex flex-wrap">
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
<Button
|
<div className="space-x-2 flex flex-wrap">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => table.previousPage()}
|
size="sm"
|
||||||
disabled={!table.getCanPreviousPage()}
|
onClick={() => table.previousPage()}
|
||||||
>
|
disabled={!table.getCanPreviousPage()}
|
||||||
Previous
|
>
|
||||||
</Button>
|
Previous
|
||||||
<Button
|
</Button>
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => table.nextPage()}
|
size="sm"
|
||||||
disabled={!table.getCanNextPage()}
|
onClick={() => table.nextPage()}
|
||||||
>
|
disabled={!table.getCanNextPage()}
|
||||||
Next
|
>
|
||||||
</Button>
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ const Terminal = dynamic(
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
|
serverId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
export const DockerTerminalModal = ({
|
||||||
|
children,
|
||||||
|
containerId,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -40,7 +45,11 @@ export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Terminal id="terminal" containerId={containerId} />
|
<Terminal
|
||||||
|
id="terminal"
|
||||||
|
containerId={containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import { AttachAddon } from "@xterm/addon-attach";
|
|||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
containerId: 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 termRef = useRef(null);
|
||||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,7 +38,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
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);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer<
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowTraefikFile = ({ path }: Props) => {
|
export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||||
const { data, refetch } = api.settings.readTraefikFile.useQuery(
|
const {
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
isLoading: isLoadingFile,
|
||||||
|
} = api.settings.readTraefikFile.useQuery(
|
||||||
{
|
{
|
||||||
path,
|
path,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!path,
|
enabled: !!path,
|
||||||
@@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
form.reset({
|
||||||
form.reset({
|
traefikConfig: data || "",
|
||||||
traefikConfig: data || "",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
||||||
@@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
traefikConfig: data.traefikConfig,
|
traefikConfig: data.traefikConfig,
|
||||||
path,
|
path,
|
||||||
|
serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Traefik config Updated");
|
toast.success("Traefik config Updated");
|
||||||
@@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
className="w-full relative z-[5]"
|
className="w-full relative z-[5]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col overflow-auto">
|
<div className="flex flex-col overflow-auto">
|
||||||
<FormField
|
{isLoadingFile ? (
|
||||||
control={form.control}
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
name="traefikConfig"
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
render={({ field }) => (
|
Loading...
|
||||||
<FormItem className="relative">
|
</span>
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||||
<FormDescription className="break-all">
|
</div>
|
||||||
{path}
|
) : (
|
||||||
</FormDescription>
|
<FormField
|
||||||
<FormControl>
|
control={form.control}
|
||||||
<CodeEditor
|
name="traefikConfig"
|
||||||
lineWrapping
|
render={({ field }) => (
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
<FormItem className="relative">
|
||||||
placeholder={`http:
|
<FormLabel>Traefik config</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
{path}
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
router-name:
|
router-name:
|
||||||
rule: Host('domain.com')
|
rule: Host('domain.com')
|
||||||
@@ -116,31 +130,36 @@ routers:
|
|||||||
tls: false
|
tls: false
|
||||||
middlewares: []
|
middlewares: []
|
||||||
`}
|
`}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||||
<Button
|
<Button
|
||||||
className="shadow-sm"
|
className="shadow-sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setCanEdit(!canEdit);
|
setCanEdit(!canEdit);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{canEdit ? "Unlock" : "Lock"}
|
{canEdit ? "Unlock" : "Lock"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button isLoading={isLoading} disabled={canEdit} type="submit">
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={canEdit || isLoading}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,47 @@
|
|||||||
import React from "react";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
import { Tree } from "@/components/ui/file-tree";
|
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 { 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";
|
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 [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 (
|
return (
|
||||||
<div className={cn("mt-6 md:grid gap-4")}>
|
<div className={cn("mt-6 md:grid gap-4")}>
|
||||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
<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 && (
|
{directories?.length === 0 && (
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
@@ -34,7 +61,7 @@ export const ShowTraefikSystem = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{file ? (
|
{file ? (
|
||||||
<ShowTraefikFile path={file} />
|
<ShowTraefikFile path={file} serverId={serverId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<StopMariadb mariadbId={mariadbId} />
|
<StopMariadb mariadbId={mariadbId} />
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<StopMongo mongoId={mongoId} />
|
<StopMongo mongoId={mongoId} />
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const DockerMemoryChart = ({
|
|||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
usage: (item.value.used / 1024).toFixed(2),
|
usage: (item.value.used / 1024 ** 3).toFixed(2),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -208,9 +208,7 @@ export const DockerMonitoring = ({
|
|||||||
<div className="flex flex-col gap-2 w-full ">
|
<div className="flex flex-col gap-2 w-full ">
|
||||||
<span className="text-base font-medium">Memory</span>
|
<span className="text-base font-medium">Memory</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Used: ${(currentData.memory.value.used / 1024).toFixed(
|
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
|
||||||
2,
|
|
||||||
)} GB / Limit: ${(currentData.memory.value.total / 1024).toFixed(2)} GB`}
|
|
||||||
</span>
|
</span>
|
||||||
<Progress
|
<Progress
|
||||||
value={currentData.memory.value.usedPercentage}
|
value={currentData.memory.value.usedPercentage}
|
||||||
@@ -218,7 +216,7 @@ export const DockerMonitoring = ({
|
|||||||
/>
|
/>
|
||||||
<DockerMemoryChart
|
<DockerMemoryChart
|
||||||
acummulativeData={acummulativeData.memory}
|
acummulativeData={acummulativeData.memory}
|
||||||
memoryLimitGB={currentData.memory.value.total / 1024}
|
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{appName === "dokploy" && (
|
{appName === "dokploy" && (
|
||||||
@@ -240,9 +238,9 @@ export const DockerMonitoring = ({
|
|||||||
<div className="flex flex-col gap-2 w-full ">
|
<div className="flex flex-col gap-2 w-full ">
|
||||||
<span className="text-base font-medium">Block I/O</span>
|
<span className="text-base font-medium">Block I/O</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Used: ${currentData.block.value.readMb.toFixed(
|
{`Read: ${currentData.block.value.readMb.toFixed(
|
||||||
2,
|
2,
|
||||||
)} MB / Limit: ${currentData.block.value.writeMb.toFixed(
|
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
|
||||||
3,
|
3,
|
||||||
)} MB`}
|
)} MB`}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
<StopMysql mysqlId={mysqlId} />
|
<StopMysql mysqlId={mysqlId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
<StopPostgres postgresId={postgresId} />
|
<StopPostgres postgresId={postgresId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -19,11 +19,26 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Folder } from "lucide-react";
|
import { Folder, HelpCircle } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
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 '-'",
|
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
||||||
@@ -54,8 +70,10 @@ interface Props {
|
|||||||
|
|
||||||
export const AddApplication = ({ projectId, projectName }: Props) => {
|
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.application.create.useMutation();
|
api.application.create.useMutation();
|
||||||
@@ -75,6 +93,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Created");
|
toast.success("Service Created");
|
||||||
@@ -135,6 +154,57 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="appName"
|
name="appName"
|
||||||
|
|||||||
@@ -22,15 +22,23 @@ import { Input } from "@/components/ui/input";
|
|||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CircuitBoard, Folder } from "lucide-react";
|
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
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 '-'",
|
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCompose = z.infer<typeof AddComposeSchema>;
|
type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||||
@@ -63,6 +72,7 @@ interface Props {
|
|||||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
@@ -87,6 +97,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
projectId,
|
projectId,
|
||||||
composeType: data.composeType,
|
composeType: data.composeType,
|
||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
@@ -148,6 +159,57 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="appName"
|
name="appName"
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
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 { Textarea } from "@/components/ui/textarea";
|
||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -71,6 +80,7 @@ const baseDatabaseSchema = z.object({
|
|||||||
databasePassword: z.string(),
|
databasePassword: z.string(),
|
||||||
dockerImage: z.string(),
|
dockerImage: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
|
serverId: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("type", [
|
const mySchema = z.discriminatedUnion("type", [
|
||||||
@@ -145,6 +155,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const postgresMutation = api.postgres.create.useMutation();
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
const mongoMutation = api.mongo.create.useMutation();
|
const mongoMutation = api.mongo.create.useMutation();
|
||||||
const redisMutation = api.redis.create.useMutation();
|
const redisMutation = api.redis.create.useMutation();
|
||||||
@@ -161,6 +172,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
description: "",
|
description: "",
|
||||||
databaseName: "",
|
databaseName: "",
|
||||||
databaseUser: "",
|
databaseUser: "",
|
||||||
|
serverId: null,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(mySchema),
|
resolver: zodResolver(mySchema),
|
||||||
});
|
});
|
||||||
@@ -183,6 +195,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
dockerImage: defaultDockerImage,
|
dockerImage: defaultDockerImage,
|
||||||
projectId,
|
projectId,
|
||||||
|
serverId: data.serverId,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,8 +204,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
...commonParams,
|
...commonParams,
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
|
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mongo") {
|
} else if (data.type === "mongo") {
|
||||||
promise = mongoMutation.mutateAsync({
|
promise = mongoMutation.mutateAsync({
|
||||||
@@ -200,11 +215,13 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "redis") {
|
} else if (data.type === "redis") {
|
||||||
promise = redisMutation.mutateAsync({
|
promise = redisMutation.mutateAsync({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
|
serverId: data.serverId,
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mariadb") {
|
} else if (data.type === "mariadb") {
|
||||||
@@ -215,6 +232,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mysql") {
|
} else if (data.type === "mysql") {
|
||||||
promise = mysqlMutation.mutateAsync({
|
promise = mysqlMutation.mutateAsync({
|
||||||
@@ -224,6 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +371,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="appName"
|
name="appName"
|
||||||
|
|||||||
@@ -29,11 +29,27 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} 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 { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ScrollArea } from "@radix-ui/react-scroll-area";
|
import { ScrollArea } from "@radix-ui/react-scroll-area";
|
||||||
@@ -43,6 +59,7 @@ import {
|
|||||||
Code,
|
Code,
|
||||||
Github,
|
Github,
|
||||||
Globe,
|
Globe,
|
||||||
|
HelpCircle,
|
||||||
PuzzleIcon,
|
PuzzleIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -58,9 +75,12 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { data } = api.compose.templates.useQuery();
|
const { data } = api.compose.templates.useQuery();
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { data: tags, isLoading: isLoadingTags } =
|
const { data: tags, isLoading: isLoadingTags } =
|
||||||
api.compose.getTags.useQuery();
|
api.compose.getTags.useQuery();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const [serverId, setServerId] = useState<string | undefined>(undefined);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.deployTemplate.useMutation();
|
api.compose.deployTemplate.useMutation();
|
||||||
|
|
||||||
@@ -109,7 +129,6 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:max-w-[15rem] w-full justify-between !bg-input",
|
"md:max-w-[15rem] w-full justify-between !bg-input",
|
||||||
// !field.value && "text-muted-foreground",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoadingTags
|
{isLoadingTags
|
||||||
@@ -268,30 +287,79 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
{template.name} template and add it to your
|
{template.name} template and add it to your
|
||||||
project.
|
project.
|
||||||
</AlertDialogDescription>
|
</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>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
|
disabled={isLoading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
const promise = mutateAsync({
|
||||||
projectId,
|
projectId,
|
||||||
|
serverId: serverId || undefined,
|
||||||
id: template.id,
|
id: template.id,
|
||||||
})
|
});
|
||||||
.then(async () => {
|
toast.promise(promise, {
|
||||||
toast.success(
|
loading: "Setting up...",
|
||||||
`Succesfully created ${template.name} application from template`,
|
success: (data) => {
|
||||||
);
|
|
||||||
|
|
||||||
utils.project.one.invalidate({
|
utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
})
|
return `${template.name} template created succesfully`;
|
||||||
.catch(() => {
|
},
|
||||||
toast.error(
|
error: (err) => {
|
||||||
`Error creating ${template.name} application from template`,
|
return `Ocurred an error deploying ${template.name} template`;
|
||||||
);
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -109,6 +110,7 @@ export const AddProject = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
<StopRedis redisId={redisId} />
|
<StopRedis redisId={redisId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -17,13 +17,29 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
getObjectSchema,
|
||||||
|
mergeFormValues,
|
||||||
|
providerSchemas,
|
||||||
|
providersData,
|
||||||
|
} from "../../application/domains/schema";
|
||||||
|
import { capitalize } from "lodash";
|
||||||
|
|
||||||
const addDestination = z.object({
|
const addDestination = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@@ -38,43 +54,45 @@ type AddDestination = z.infer<typeof addDestination>;
|
|||||||
|
|
||||||
export const AddDestination = () => {
|
export const AddDestination = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [provider, setProviders] = useState<keyof typeof providerSchemas>("s3");
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.destination.create.useMutation();
|
api.destination.create.useMutation();
|
||||||
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
|
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
|
||||||
api.destination.testConnection.useMutation();
|
api.destination.testConnection.useMutation();
|
||||||
const form = useForm<AddDestination>({
|
const schema = providerSchemas[provider];
|
||||||
|
const form = useForm<z.infer<typeof schema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
accessKeyId: "",
|
...getObjectSchema(schema),
|
||||||
bucket: "",
|
|
||||||
name: "",
|
|
||||||
region: "",
|
|
||||||
secretAccessKey: "",
|
|
||||||
endpoint: "",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addDestination),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
const {
|
||||||
form.reset();
|
register,
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = form;
|
||||||
|
|
||||||
const onSubmit = async (data: AddDestination) => {
|
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||||
await mutateAsync({
|
// await mutateAsync({
|
||||||
accessKey: data.accessKeyId,
|
// accessKey: data.accessKeyId,
|
||||||
bucket: data.bucket,
|
// bucket: data.bucket,
|
||||||
endpoint: data.endpoint,
|
// endpoint: data.endpoint,
|
||||||
name: data.name,
|
// name: data.name,
|
||||||
region: data.region,
|
// region: data.region,
|
||||||
secretAccessKey: data.secretAccessKey,
|
// secretAccessKey: data.secretAccessKey,
|
||||||
})
|
// })
|
||||||
.then(async () => {
|
// .then(async () => {
|
||||||
toast.success("Destination Created");
|
// toast.success("Destination Created");
|
||||||
await utils.destination.all.invalidate();
|
// await utils.destination.all.invalidate();
|
||||||
})
|
// })
|
||||||
.catch(() => {
|
// .catch(() => {
|
||||||
toast.error("Error to create the Destination");
|
// toast.error("Error to create the Destination");
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fields = Object.keys(schema.shape);
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
@@ -88,140 +106,117 @@ export const AddDestination = () => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-destination-add"
|
id="hook-form-destination-add"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-8 "
|
||||||
>
|
>
|
||||||
<FormField
|
<div className="flex flex-col gap-2">
|
||||||
control={form.control}
|
{fields.map((input) => (
|
||||||
name="name"
|
<FormField
|
||||||
render={({ field }) => {
|
control={control}
|
||||||
return (
|
key={`${provider}.${input}`}
|
||||||
<FormItem>
|
name={`${provider}.${input}`}
|
||||||
<FormLabel>Name</FormLabel>
|
render={({ field }) => {
|
||||||
<FormControl>
|
return (
|
||||||
<Input placeholder={"S3 Bucket"} {...field} />
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>{capitalize(input)}</FormLabel>
|
||||||
<FormMessage />
|
<FormControl>
|
||||||
</FormItem>
|
<Input placeholder={"Value"} {...field} />
|
||||||
);
|
</FormControl>
|
||||||
}}
|
<span className="text-sm font-medium text-destructive">
|
||||||
/>
|
{errors[input]?.message}
|
||||||
|
</span>
|
||||||
<FormField
|
</FormItem>
|
||||||
control={form.control}
|
);
|
||||||
name="accessKeyId"
|
}}
|
||||||
render={({ field }) => {
|
/>
|
||||||
return (
|
))}
|
||||||
<FormItem>
|
</div>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
</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>
|
</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>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Edit } from "lucide-react";
|
import { Edit } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
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 (
|
return (
|
||||||
<div className="h-full col-span-2">
|
<div className=" col-span-2">
|
||||||
<Card className="bg-transparent h-full ">
|
<Card className="bg-transparent ">
|
||||||
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
|
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl">Users</CardTitle>
|
<CardTitle className="text-xl">Users</CardTitle>
|
||||||
@@ -55,9 +55,9 @@ export const ShowUsers = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 h-full">
|
<CardContent className="space-y-2">
|
||||||
{data?.length === 0 ? (
|
{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" />
|
<Users className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a user, you need to add:
|
To create a user, you need to add:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -6,334 +5,34 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { cn } from "@/lib/utils";
|
||||||
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 { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import React from "react";
|
||||||
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||||
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
|
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||||
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
|
||||||
import { ShowModalLogs } from "./web-server/show-modal-logs";
|
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
|
||||||
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 { UpdateServer } from "./web-server/update-server";
|
import { UpdateServer } from "./web-server/update-server";
|
||||||
|
|
||||||
export const WebServer = () => {
|
interface Props {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
className?: string;
|
||||||
const { mutateAsync: reloadServer, isLoading } =
|
}
|
||||||
api.settings.reloadServer.useMutation();
|
export const WebServer = ({ className }: Props) => {
|
||||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
const { data } = api.admin.one.useQuery();
|
||||||
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();
|
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.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 (
|
return (
|
||||||
<Card className="rounded-lg w-full bg-transparent">
|
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Web server settings</CardTitle>
|
<CardTitle className="text-xl">Web server settings</CardTitle>
|
||||||
<CardDescription>Reload or clean the web server.</CardDescription>
|
<CardDescription>Reload or clean the web server.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4 ">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<DropdownMenu>
|
<ShowDokployActions />
|
||||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
<ShowTraefikActions />
|
||||||
<Button isLoading={isLoading} variant="outline">
|
<ShowStorageActions />
|
||||||
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>
|
|
||||||
|
|
||||||
<UpdateServer />
|
<UpdateServer />
|
||||||
</div>
|
</div>
|
||||||
@@ -345,25 +44,8 @@ export const WebServer = () => {
|
|||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version: {dokployVersion}
|
Version: {dokployVersion}
|
||||||
</span>
|
</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();
|
<ToggleDockerCleanup />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label className="text-primary">Daily Docker Cleanup</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -34,12 +35,14 @@ const Terminal = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerTerminalModal = ({ children, appName }: Props) => {
|
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -65,7 +68,14 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -82,6 +92,7 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Terminal
|
<Terminal
|
||||||
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
id="terminal"
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -33,12 +33,15 @@ type Schema = z.infer<typeof schema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditTraefikEnv = ({ children }: Props) => {
|
export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
||||||
const [canEdit, setCanEdit] = useState(true);
|
const [canEdit, setCanEdit] = useState(true);
|
||||||
|
|
||||||
const { data } = api.settings.readTraefikEnv.useQuery();
|
const { data } = api.settings.readTraefikEnv.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.settings.writeTraefikEnv.useMutation();
|
api.settings.writeTraefikEnv.useMutation();
|
||||||
@@ -62,6 +65,7 @@ export const EditTraefikEnv = ({ children }: Props) => {
|
|||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
env: data.env,
|
env: data.env,
|
||||||
|
serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Traefik Env Updated");
|
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,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -35,12 +36,14 @@ export const DockerLogsId = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowModalLogs = ({ appName, children }: Props) => {
|
export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppLabel.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -72,7 +75,14 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -88,7 +98,11 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
<DockerLogsId
|
||||||
|
id="terminal"
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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 {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,79 +7,27 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
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 { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
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), {
|
const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), {
|
||||||
ssr: false,
|
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 {
|
interface Props {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TerminalModal = ({ children }: Props) => {
|
export const TerminalModal = ({ children, serverId }: Props) => {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
const { data } = api.server.one.useQuery(
|
||||||
const [user, setUser] = useState("root");
|
{
|
||||||
const [terminalUser, setTerminalUser] = useState("root");
|
serverId,
|
||||||
|
|
||||||
const { mutateAsync, isLoading } =
|
|
||||||
api.settings.saveSSHPrivateKey.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddSSHPrivateKey>({
|
|
||||||
defaultValues: {
|
|
||||||
sshPrivateKey: "",
|
|
||||||
},
|
},
|
||||||
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 (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -92,75 +39,14 @@ export const TerminalModal = ({ children }: Props) => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||||
<DialogHeader className="flex flex-row justify-between pt-4">
|
<DialogHeader className="flex flex-col gap-1">
|
||||||
<div>
|
<DialogTitle>Terminal ({data?.name})</DialogTitle>
|
||||||
<DialogTitle>Terminal</DialogTitle>
|
<DialogDescription>Easy way to access the server</DialogDescription>
|
||||||
<DialogDescription>Easy way to access the server</DialogDescription>
|
|
||||||
</div>
|
|
||||||
{data?.haveSSH && (
|
|
||||||
<div>
|
|
||||||
<RemoveSSHPrivateKey />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogHeader>
|
</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 className="flex flex-col gap-4">
|
||||||
</div>
|
<Terminal id="terminal" serverId={serverId} />
|
||||||
)}
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { AttachAddon } from "@xterm/addon-attach";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
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);
|
const termRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,7 +33,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
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 ws = new WebSocket(wsUrl);
|
||||||
const addonAttach = new AttachAddon(ws);
|
const addonAttach = new AttachAddon(ws);
|
||||||
@@ -46,7 +46,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
ws.readyState === WebSocket.OPEN && ws.close();
|
ws.readyState === WebSocket.OPEN && ws.close();
|
||||||
};
|
};
|
||||||
}, [id, userSSH]);
|
}, [id, serverId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
{
|
{
|
||||||
title: "Cluster",
|
title: "Cluster",
|
||||||
label: "",
|
label: "",
|
||||||
icon: Server,
|
icon: BoxesIcon,
|
||||||
href: "/dashboard/settings/cluster",
|
href: "/dashboard/settings/cluster",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,6 +83,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: Bell,
|
icon: Bell,
|
||||||
href: "/dashboard/settings/notifications",
|
href: "/dashboard/settings/notifications",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Servers",
|
||||||
|
label: "",
|
||||||
|
icon: Server,
|
||||||
|
href: "/dashboard/settings/servers",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(user?.canAccessToSSHKeys
|
...(user?.canAccessToSSHKeys
|
||||||
@@ -117,6 +123,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Bell,
|
Bell,
|
||||||
|
BoxesIcon,
|
||||||
Database,
|
Database,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string | React.ReactNode;
|
||||||
description?: string;
|
description?: string | React.ReactNode;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DialogAction = ({
|
export const DialogAction = ({
|
||||||
@@ -22,6 +23,7 @@ export const DialogAction = ({
|
|||||||
children,
|
children,
|
||||||
description,
|
description,
|
||||||
title,
|
title,
|
||||||
|
disabled,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@@ -37,7 +39,9 @@ export const DialogAction = ({
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={onClick}>Confirm</AlertDialogAction>
|
<AlertDialogAction disabled={disabled} onClick={onClick}>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</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,
|
"when": 1725519351871,
|
||||||
"tag": "0036_tired_ronan",
|
"tag": "0036_tired_ronan",
|
||||||
"breakpoints": true
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rotating-file-stream": "3.2.3",
|
"rotating-file-stream": "3.2.3",
|
||||||
"@aws-sdk/client-s3": "3.515.0",
|
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
@@ -130,7 +129,8 @@
|
|||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4",
|
||||||
"zod-form-data": "^2.0.2",
|
"zod-form-data": "^2.0.2",
|
||||||
"@radix-ui/react-primitive": "2.0.0",
|
"@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": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.8.3",
|
||||||
@@ -167,7 +167,8 @@
|
|||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"vite-tsconfig-paths": "4.3.2",
|
"vite-tsconfig-paths": "4.3.2",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"xterm-readline": "1.1.1"
|
"xterm-readline": "1.1.1",
|
||||||
|
"@types/ssh2": "1.15.1"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.25.2"
|
"initVersion": "7.25.2"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export default async function handler(
|
|||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!application.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default async function handler(
|
|||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
server: !!composeResult.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export default async function handler(
|
|||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"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 { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -98,6 +100,9 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<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">
|
<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="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</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="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
@@ -152,14 +164,20 @@ const Service = (
|
|||||||
<ShowEnvironment applicationId={applicationId} />
|
<ShowEnvironment applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments" className="w-full">
|
<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 { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -92,13 +94,16 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex flex-row gap-4">
|
<div className="relative flex flex-row gap-4">
|
||||||
<div className="absolute -right-1 -top-2">
|
<div className="absolute -right-1 -top-2">
|
||||||
<StatusTooltip status={data?.composeStatus} />
|
<StatusTooltip status={data?.composeStatus} />
|
||||||
@@ -119,10 +124,23 @@ const Service = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<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="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{data?.composeType === "docker-compose" && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
@@ -147,19 +165,22 @@ const Service = (
|
|||||||
<ShowEnvironmentCompose composeId={composeId} />
|
<ShowEnvironmentCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
{!data?.serverId && (
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowMonitoringCompose
|
<ShowMonitoringCompose
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appName={data?.appName || ""}
|
||||||
/>
|
appType={data?.composeType || "docker-compose"}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogsCompose
|
<ShowDockerLogsCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
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 { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
|
||||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -81,7 +82,9 @@ const Mariadb = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -108,10 +111,17 @@ const Mariadb = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<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="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -136,14 +146,19 @@ const Mariadb = (
|
|||||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/gener
|
|||||||
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
|
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
|
||||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -82,7 +83,9 @@ const Mongo = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -109,10 +112,17 @@ const Mongo = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<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="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -138,14 +148,19 @@ const Mongo = (
|
|||||||
<ShowMongoEnvironment mongoId={mongoId} />
|
<ShowMongoEnvironment mongoId={mongoId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<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 { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
|
||||||
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
||||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -80,7 +81,9 @@ const MySql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -108,10 +111,17 @@ const MySql = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<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="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -137,14 +147,19 @@ const MySql = (
|
|||||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<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 { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
||||||
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
||||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -81,7 +82,9 @@ const Postgresql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -109,10 +112,17 @@ const Postgresql = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<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="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</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="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -138,14 +148,19 @@ const Postgresql = (
|
|||||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<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 { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
|
||||||
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
||||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -80,7 +81,9 @@ const Redis = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -108,10 +111,17 @@ const Redis = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<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="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</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="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -136,14 +146,19 @@ const Redis = (
|
|||||||
<ShowRedisEnvironment redisId={redisId} />
|
<ShowRedisEnvironment redisId={redisId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<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";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { isAdminPresent } from "@/server/api/services/admin";
|
import { isAdminPresent } from "@/server/api/services/admin";
|
||||||
|
// import { IS_CLOUD } from "@/server/constants";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
@@ -220,6 +221,11 @@ const Register = ({ hasAdmin }: Props) => {
|
|||||||
|
|
||||||
export default Register;
|
export default Register;
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
|
// if (IS_CLOUD) {
|
||||||
|
// return {
|
||||||
|
// props: {},
|
||||||
|
// };
|
||||||
|
// }
|
||||||
const hasAdmin = await isAdminPresent();
|
const hasAdmin = await isAdminPresent();
|
||||||
|
|
||||||
if (hasAdmin) {
|
if (hasAdmin) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { redirectsRouter } from "./routers/redirects";
|
|||||||
import { redisRouter } from "./routers/redis";
|
import { redisRouter } from "./routers/redis";
|
||||||
import { registryRouter } from "./routers/registry";
|
import { registryRouter } from "./routers/registry";
|
||||||
import { securityRouter } from "./routers/security";
|
import { securityRouter } from "./routers/security";
|
||||||
|
import { serverRouter } from "./routers/server";
|
||||||
import { settingsRouter } from "./routers/settings";
|
import { settingsRouter } from "./routers/settings";
|
||||||
import { sshRouter } from "./routers/ssh-key";
|
import { sshRouter } from "./routers/ssh-key";
|
||||||
import { userRouter } from "./routers/user";
|
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.
|
* All routers added in /api/routers should be manually added here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
admin: adminRouter,
|
admin: adminRouter,
|
||||||
docker: dockerRouter,
|
docker: dockerRouter,
|
||||||
@@ -66,6 +68,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
bitbucket: bitbucketRouter,
|
bitbucket: bitbucketRouter,
|
||||||
gitlab: gitlabRouter,
|
gitlab: gitlabRouter,
|
||||||
github: githubRouter,
|
github: githubRouter,
|
||||||
|
server: serverRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
|
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
export const adminRouter = createTRPCRouter({
|
export const adminRouter = createTRPCRouter({
|
||||||
one: adminProcedure.query(async () => {
|
one: adminProcedure.query(async ({ ctx }) => {
|
||||||
const { sshPrivateKey, ...rest } = await findAdmin();
|
const { sshPrivateKey, ...rest } = await findAdmin();
|
||||||
return {
|
return {
|
||||||
haveSSH: !!sshPrivateKey,
|
haveSSH: !!sshPrivateKey,
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ import {
|
|||||||
cleanQueuesByApplication,
|
cleanQueuesByApplication,
|
||||||
} from "@/server/queues/deployments-queue";
|
} from "@/server/queues/deployments-queue";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
import {
|
import {
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
stopService,
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import {
|
import {
|
||||||
removeDirectoryCode,
|
removeDirectoryCode,
|
||||||
@@ -35,10 +38,13 @@ import {
|
|||||||
} from "@/server/utils/filesystem/directory";
|
} from "@/server/utils/filesystem/directory";
|
||||||
import {
|
import {
|
||||||
readConfig,
|
readConfig,
|
||||||
|
readRemoteConfig,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
|
writeConfigRemote,
|
||||||
} from "@/server/utils/traefik/application";
|
} from "@/server/utils/traefik/application";
|
||||||
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware";
|
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware";
|
||||||
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@@ -53,9 +59,6 @@ import {
|
|||||||
import { removeDeployments } from "../services/deployment";
|
import { removeDeployments } from "../services/deployment";
|
||||||
import { addNewService, checkServiceAccess } from "../services/user";
|
import { addNewService, checkServiceAccess } from "../services/user";
|
||||||
|
|
||||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
|
||||||
import { uploadFileSchema } from "@/utils/schema";
|
|
||||||
|
|
||||||
export const applicationRouter = createTRPCRouter({
|
export const applicationRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
@@ -96,9 +99,19 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiReloadApplication)
|
.input(apiReloadApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await stopService(input.appName);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
if (application.serverId) {
|
||||||
|
await stopServiceRemote(application.serverId, input.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(input.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "idle");
|
await updateApplicationStatus(input.applicationId, "idle");
|
||||||
await startService(input.appName);
|
|
||||||
|
if (application.serverId) {
|
||||||
|
await startServiceRemote(application.serverId, input.appName);
|
||||||
|
} else {
|
||||||
|
await startService(input.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "done");
|
await updateApplicationStatus(input.applicationId, "done");
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
@@ -121,12 +134,19 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => deleteAllMiddlewares(application),
|
async () => await deleteAllMiddlewares(application),
|
||||||
async () => await removeDeployments(application),
|
async () => await removeDeployments(application),
|
||||||
async () => await removeDirectoryCode(application?.appName),
|
async () =>
|
||||||
async () => await removeMonitoringDirectory(application?.appName),
|
await removeDirectoryCode(application.appName, application.serverId),
|
||||||
async () => await removeTraefikConfig(application?.appName),
|
async () =>
|
||||||
async () => await removeService(application?.appName),
|
await removeMonitoringDirectory(
|
||||||
|
application.appName,
|
||||||
|
application.serverId,
|
||||||
|
),
|
||||||
|
async () =>
|
||||||
|
await removeTraefikConfig(application.appName, application.serverId),
|
||||||
|
async () =>
|
||||||
|
await removeService(application?.appName, application.serverId),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const operation of cleanupOperations) {
|
for (const operation of cleanupOperations) {
|
||||||
@@ -142,7 +162,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
await stopService(service.appName);
|
if (service.serverId) {
|
||||||
|
await stopServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(service.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "idle");
|
await updateApplicationStatus(input.applicationId, "idle");
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
@@ -152,8 +176,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
|
if (service.serverId) {
|
||||||
await startService(service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await startService(service.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "done");
|
await updateApplicationStatus(input.applicationId, "done");
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
@@ -162,12 +189,14 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!application.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -306,13 +335,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!application.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -334,8 +365,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
let traefikConfig = null;
|
||||||
const traefikConfig = readConfig(application.appName);
|
if (application.serverId) {
|
||||||
|
traefikConfig = await readRemoteConfig(
|
||||||
|
application.serverId,
|
||||||
|
application.appName,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
traefikConfig = readConfig(application.appName);
|
||||||
|
}
|
||||||
return traefikConfig;
|
return traefikConfig;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -359,7 +397,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const app = await findApplicationById(input.applicationId as string);
|
const app = await findApplicationById(input.applicationId as string);
|
||||||
await unzipDrop(zipFile, app.appName);
|
await unzipDrop(zipFile, app);
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: app.applicationId,
|
applicationId: app.applicationId,
|
||||||
@@ -367,6 +405,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -382,7 +421,16 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
writeConfig(application.appName, input.traefikConfig);
|
|
||||||
|
if (application.serverId) {
|
||||||
|
await writeConfigRemote(
|
||||||
|
application.serverId,
|
||||||
|
application.appName,
|
||||||
|
input.traefikConfig,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
writeConfig(application.appName, input.traefikConfig);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
readAppMonitoring: protectedProcedure
|
readAppMonitoring: protectedProcedure
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { lucia, validateRequest } from "@/server/auth/auth";
|
import { lucia, validateRequest } from "@/server/auth/auth";
|
||||||
import { luciaToken } from "@/server/auth/token";
|
import { luciaToken } from "@/server/auth/token";
|
||||||
|
// import { IS_CLOUD } from "@/server/constants";
|
||||||
import {
|
import {
|
||||||
apiCreateAdmin,
|
apiCreateAdmin,
|
||||||
apiCreateUser,
|
apiCreateUser,
|
||||||
@@ -35,14 +36,16 @@ export const authRouter = createTRPCRouter({
|
|||||||
.input(apiCreateAdmin)
|
.input(apiCreateAdmin)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
|
// if (!IS_CLOUD) {
|
||||||
const admin = await db.query.admins.findFirst({});
|
const admin = await db.query.admins.findFirst({});
|
||||||
|
|
||||||
if (admin) {
|
if (admin) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Admin already exists",
|
message: "Admin already exists",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
const newAdmin = await createAdmin(input);
|
const newAdmin = await createAdmin(input);
|
||||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
const session = await lucia.createSession(newAdmin.id || "", {});
|
||||||
ctx.res.appendHeader(
|
ctx.res.appendHeader(
|
||||||
@@ -51,6 +54,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to create the main admin",
|
message: "Error to create the main admin",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
|
||||||
if (backup.enabled) {
|
if (backup.enabled) {
|
||||||
|
removeScheduleBackup(input.backupId);
|
||||||
scheduleBackup(backup);
|
scheduleBackup(backup);
|
||||||
} else {
|
} else {
|
||||||
removeScheduleBackup(input.backupId);
|
removeScheduleBackup(input.backupId);
|
||||||
@@ -90,7 +91,6 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||||
await runPostgresBackup(postgres, backup);
|
await runPostgresBackup(postgres, backup);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import {
|
|||||||
} from "@/server/queues/deployments-queue";
|
} from "@/server/queues/deployments-queue";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { createCommand } from "@/server/utils/builders/compose";
|
import { createCommand } from "@/server/utils/builders/compose";
|
||||||
|
import { randomizeComposeFile } from "@/server/utils/docker/compose";
|
||||||
import {
|
import {
|
||||||
randomizeComposeFile,
|
addDomainToCompose,
|
||||||
randomizeSpecificationFile,
|
cloneCompose,
|
||||||
} from "@/server/utils/docker/compose";
|
cloneComposeRemote,
|
||||||
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
|
} from "@/server/utils/docker/domain";
|
||||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
@@ -33,7 +34,7 @@ import { eq } from "drizzle-orm";
|
|||||||
import { dump } from "js-yaml";
|
import { dump } from "js-yaml";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin, findAdminById } from "../services/admin";
|
||||||
import {
|
import {
|
||||||
createCompose,
|
createCompose,
|
||||||
createComposeByTemplate,
|
createComposeByTemplate,
|
||||||
@@ -47,6 +48,7 @@ import { removeDeploymentsByComposeId } from "../services/deployment";
|
|||||||
import { createDomain, findDomainsByComposeId } from "../services/domain";
|
import { createDomain, findDomainsByComposeId } from "../services/domain";
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
import { findProjectById } from "../services/project";
|
import { findProjectById } from "../services/project";
|
||||||
|
import { findServerById } from "../services/server";
|
||||||
import { addNewService, checkServiceAccess } from "../services/user";
|
import { addNewService, checkServiceAccess } from "../services/user";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
@@ -130,7 +132,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
await cloneCompose(compose);
|
if (compose.serverId) {
|
||||||
|
await cloneComposeRemote(compose);
|
||||||
|
} else {
|
||||||
|
await cloneCompose(compose);
|
||||||
|
}
|
||||||
return compose.sourceType;
|
return compose.sourceType;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -151,9 +157,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
const domains = await findDomainsByComposeId(input.composeId);
|
const domains = await findDomainsByComposeId(input.composeId);
|
||||||
|
|
||||||
const composeFile = await addDomainToCompose(compose, domains);
|
const composeFile = await addDomainToCompose(compose, domains);
|
||||||
|
|
||||||
return dump(composeFile, {
|
return dump(composeFile, {
|
||||||
lineWidth: 1000,
|
lineWidth: 1000,
|
||||||
});
|
});
|
||||||
@@ -162,12 +166,14 @@ export const composeRouter = createTRPCRouter({
|
|||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -181,12 +187,14 @@ export const composeRouter = createTRPCRouter({
|
|||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -227,7 +235,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||||
|
|
||||||
const admin = await findAdmin();
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
let serverIp = admin.serverIp;
|
||||||
|
|
||||||
if (!admin.serverIp) {
|
if (!admin.serverIp) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -239,9 +248,15 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
serverIp = server.ipAddress;
|
||||||
|
} else if (process.env.NODE_ENV === "development") {
|
||||||
|
serverIp = "127.0.0.1";
|
||||||
|
}
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const { envs, mounts, domains } = generate({
|
const { envs, mounts, domains } = generate({
|
||||||
serverIp: admin.serverIp,
|
serverIp: serverIp || "",
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,6 +264,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
...input,
|
...input,
|
||||||
composeFile: composeFile,
|
composeFile: composeFile,
|
||||||
env: envs?.join("\n"),
|
env: envs?.join("\n"),
|
||||||
|
serverId: input.serverId,
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: `${projectName}-${generatePassword(6)}`,
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
apiFindAllByApplication,
|
apiFindAllByApplication,
|
||||||
apiFindAllByCompose,
|
apiFindAllByCompose,
|
||||||
|
apiFindAllByServer,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
findAllDeploymentsByApplicationId,
|
findAllDeploymentsByApplicationId,
|
||||||
findAllDeploymentsByComposeId,
|
findAllDeploymentsByComposeId,
|
||||||
|
findAllDeploymentsByServerId,
|
||||||
} from "../services/deployment";
|
} from "../services/deployment";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
@@ -20,4 +22,9 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await findAllDeploymentsByComposeId(input.composeId);
|
return await findAllDeploymentsByComposeId(input.composeId);
|
||||||
}),
|
}),
|
||||||
|
allByServer: protectedProcedure
|
||||||
|
.input(apiFindAllByServer)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await findAllDeploymentsByServerId(input.serverId);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
apiRemoveDestination,
|
apiRemoveDestination,
|
||||||
apiUpdateDestination,
|
apiUpdateDestination,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
|
import { execAsync } from "@/server/utils/process/execAsync";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin } from "../services/admin";
|
||||||
import {
|
import {
|
||||||
@@ -38,29 +38,31 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
testConnection: adminProcedure
|
testConnection: adminProcedure
|
||||||
.input(apiCreateDestination)
|
.input(apiCreateDestination)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
console.log(input);
|
||||||
const s3Client = new S3Client({
|
// const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
||||||
region: region,
|
// try {
|
||||||
...(endpoint && {
|
// const rcloneFlags = [
|
||||||
endpoint: endpoint,
|
// // `--s3-provider=Cloudflare`,
|
||||||
}),
|
// `--s3-access-key-id=${accessKey}`,
|
||||||
credentials: {
|
// `--s3-secret-access-key=${secretAccessKey}`,
|
||||||
accessKeyId: accessKey,
|
// `--s3-region=${region}`,
|
||||||
secretAccessKey: secretAccessKey,
|
// `--s3-endpoint=${endpoint}`,
|
||||||
},
|
// "--s3-no-check-bucket",
|
||||||
forcePathStyle: true,
|
// "--s3-force-path-style",
|
||||||
});
|
// ];
|
||||||
const headBucketCommand = new HeadBucketCommand({ Bucket: bucket });
|
const connextion = buildRcloneCommand(input.json.provider, input.json);
|
||||||
|
console.log(connextion);
|
||||||
try {
|
// const rcloneDestination = `:s3:${bucket}`;
|
||||||
await s3Client.send(headBucketCommand);
|
// const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
} catch (error) {
|
await execAsync(connextion);
|
||||||
throw new TRPCError({
|
// } catch (error) {
|
||||||
code: "BAD_REQUEST",
|
// console.log(error);
|
||||||
message: "Error to connect to bucket",
|
// throw new TRPCError({
|
||||||
cause: error,
|
// code: "BAD_REQUEST",
|
||||||
});
|
// message: "Error to connect to bucket",
|
||||||
}
|
// cause: error,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneDestination)
|
.input(apiFindOneDestination)
|
||||||
@@ -97,3 +99,151 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
function buildRcloneCommand(
|
||||||
|
providerType: string,
|
||||||
|
credentials: Record<string, string>,
|
||||||
|
): string {
|
||||||
|
let rcloneFlags: string[] = [];
|
||||||
|
let rcloneDestination = "";
|
||||||
|
let rcloneCommand = "";
|
||||||
|
|
||||||
|
switch (providerType) {
|
||||||
|
case "s3":
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
accessKey,
|
||||||
|
secretAccessKey,
|
||||||
|
region,
|
||||||
|
endpoint,
|
||||||
|
bucket,
|
||||||
|
provider,
|
||||||
|
storageClass,
|
||||||
|
acl,
|
||||||
|
} = credentials;
|
||||||
|
|
||||||
|
if (!accessKey || !secretAccessKey || !region || !endpoint || !bucket) {
|
||||||
|
throw new Error("Missing required S3 credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneFlags.push(`--s3-access-key-id=${accessKey}`);
|
||||||
|
rcloneFlags.push(`--s3-secret-access-key=${secretAccessKey}`);
|
||||||
|
rcloneFlags.push(`--s3-region=${region}`);
|
||||||
|
rcloneFlags.push(`--s3-endpoint=${endpoint}`);
|
||||||
|
rcloneFlags.push("--s3-no-check-bucket");
|
||||||
|
rcloneFlags.push("--s3-force-path-style");
|
||||||
|
|
||||||
|
if (provider && provider !== "AWS") {
|
||||||
|
rcloneFlags.push(`--s3-provider=${provider}`);
|
||||||
|
}
|
||||||
|
if (storageClass) {
|
||||||
|
rcloneFlags.push(`--s3-storage-class=${storageClass}`);
|
||||||
|
}
|
||||||
|
if (acl) {
|
||||||
|
rcloneFlags.push(`--s3-acl=${acl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:s3:${bucket}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "azureblob":
|
||||||
|
{
|
||||||
|
const { account, key, endpoint, container } = credentials;
|
||||||
|
|
||||||
|
if (!account || !key || !container) {
|
||||||
|
throw new Error("Missing required Azure Blob Storage credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneFlags.push(`--azureblob-account=${account}`);
|
||||||
|
rcloneFlags.push(`--azureblob-key=${key}`);
|
||||||
|
if (endpoint) {
|
||||||
|
rcloneFlags.push(`--azureblob-endpoint=${endpoint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:azureblob:${container}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ftp":
|
||||||
|
{
|
||||||
|
const { host, port, user, pass, secure, path } = credentials;
|
||||||
|
|
||||||
|
if (!host || !user || !pass) {
|
||||||
|
throw new Error("Missing required FTP credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneFlags.push(`--ftp-host=${host}`);
|
||||||
|
rcloneFlags.push(`--ftp-user=${user}`);
|
||||||
|
rcloneFlags.push(`--ftp-pass=${pass}`);
|
||||||
|
if (port) {
|
||||||
|
rcloneFlags.push(`--ftp-port=${port}`);
|
||||||
|
}
|
||||||
|
if (secure === "true" || secure === "1") {
|
||||||
|
rcloneFlags.push("--ftp-tls");
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:ftp:${path || "/"}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "gcs":
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
serviceAccountFile,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
projectNumber,
|
||||||
|
bucket,
|
||||||
|
objectAcl,
|
||||||
|
bucketAcl,
|
||||||
|
} = credentials;
|
||||||
|
|
||||||
|
if (serviceAccountFile) {
|
||||||
|
rcloneFlags.push(`--gcs-service-account-file=${serviceAccountFile}`);
|
||||||
|
} else if (clientId && clientSecret && projectNumber) {
|
||||||
|
rcloneFlags.push(`--gcs-client-id=${clientId}`);
|
||||||
|
rcloneFlags.push(`--gcs-client-secret=${clientSecret}`);
|
||||||
|
rcloneFlags.push(`--gcs-project-number=${projectNumber}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Missing required GCS credentials. Provide either serviceAccountFile or clientId, clientSecret, and projectNumber.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bucket) {
|
||||||
|
throw new Error("Bucket name is required for GCS.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectAcl) {
|
||||||
|
rcloneFlags.push(`--gcs-object-acl=${objectAcl}`);
|
||||||
|
}
|
||||||
|
if (bucketAcl) {
|
||||||
|
rcloneFlags.push(`--gcs-bucket-acl=${bucketAcl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rcloneDestination = `:gcs:${bucket}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "dropbox":
|
||||||
|
{
|
||||||
|
const { token, path } = credentials;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Access token is required for Dropbox.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning: Passing tokens via command line can be insecure.
|
||||||
|
rcloneFlags.push(`--dropbox-token='{"access_token":"${token}"}'`);
|
||||||
|
rcloneDestination = `:dropbox:${path || "/"}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported provider type: ${providerType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the Rclone command
|
||||||
|
rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
|
return rcloneCommand;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,9 +9,15 @@ import {
|
|||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
export const dockerRouter = createTRPCRouter({
|
export const dockerRouter = createTRPCRouter({
|
||||||
getContainers: protectedProcedure.query(async () => {
|
getContainers: protectedProcedure
|
||||||
return await getContainers();
|
.input(
|
||||||
}),
|
z.object({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getContainers(input.serverId);
|
||||||
|
}),
|
||||||
|
|
||||||
restartContainer: protectedProcedure
|
restartContainer: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -27,10 +33,11 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
containerId: z.string().min(1),
|
containerId: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getConfig(input.containerId);
|
return await getConfig(input.containerId, input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getContainersByAppNameMatch: protectedProcedure
|
getContainersByAppNameMatch: protectedProcedure
|
||||||
@@ -40,19 +47,25 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
.union([z.literal("stack"), z.literal("docker-compose")])
|
.union([z.literal("stack"), z.literal("docker-compose")])
|
||||||
.optional(),
|
.optional(),
|
||||||
appName: z.string().min(1),
|
appName: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getContainersByAppNameMatch(input.appName, input.appType);
|
return await getContainersByAppNameMatch(
|
||||||
|
input.appName,
|
||||||
|
input.appType,
|
||||||
|
input.serverId,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getContainersByAppLabel: protectedProcedure
|
getContainersByAppLabel: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appName: z.string().min(1),
|
appName: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getContainersByAppLabel(input.appName);
|
return await getContainersByAppLabel(input.appName, input.serverId);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user