Compare commits

..

27 Commits

Author SHA1 Message Date
Mauricio Siu
63e7eacae9 chore(version): bump version 2024-09-19 16:37:00 -06:00
Mauricio Siu
f4ab588516 Merge pull request #466 from Dokploy/canary
v0.8.3
2024-09-19 16:01:27 -06:00
Mauricio Siu
4d8a0ba58f Merge pull request #457 from Dokploy/canary
v0.8.2
2024-09-16 15:57:20 -06:00
Mauricio Siu
e88cd11041 Merge pull request #427 from Dokploy/canary
v0.8.1
2024-09-07 13:25:36 -06:00
Mauricio Siu
5f174a883b Merge pull request #424 from Dokploy/canary
v0.8.0
2024-09-07 00:55:08 -06:00
Mauricio Siu
536a6ba2ff Merge pull request #397 from Dokploy/canary
v0.7.3
2024-08-30 00:27:59 -06:00
Mauricio Siu
213fa08210 Merge pull request #382 from Dokploy/canary
v0.7.2
2024-08-26 15:52:49 -06:00
Mauricio Siu
d5c6a601d8 Merge pull request #367 from Dokploy/canary
v0.7.1
2024-08-19 16:03:39 -06:00
Mauricio Siu
452793c8e5 Merge pull request #359 from Dokploy/canary
v0.7.0
2024-08-18 10:26:52 -06:00
Mauricio Siu
385fbf4af5 Merge pull request #355 from Dokploy/canary
v0.6.3
2024-08-16 22:26:35 -06:00
Mauricio Siu
3590f3bed2 Merge pull request #332 from Dokploy/canary
v0.6.2
2024-08-07 21:48:49 -06:00
Mauricio Siu
9b2fcaea31 Merge pull request #317 from Dokploy/canary
v0.6.1
2024-08-03 15:46:05 -06:00
Mauricio Siu
5abcc82215 Merge pull request #312 from Dokploy/canary
v0.6.0
2024-08-02 10:47:43 -06:00
Mauricio Siu
ee855452e3 Merge pull request #303 from Dokploy/canary
chore: add slash to version
2024-08-01 02:06:43 -06:00
Mauricio Siu
d000b526d3 Merge pull request #302 from Dokploy/canary
v0.5.1
2024-08-01 01:58:15 -06:00
Mauricio Siu
9bf88b90c3 Merge pull request #280 from Dokploy/canary
v0.5.0
2024-07-27 15:20:43 -06:00
Mauricio Siu
b1a48d4636 refactor: update job 2024-07-22 03:51:07 -06:00
Mauricio Siu
c34c4b244e Merge pull request #251 from Dokploy/canary
v0.4.0
2024-07-22 03:38:47 -06:00
Mauricio Siu
bb59a0cd3f Merge pull request #230 from Dokploy/canary
v0.3.3
2024-07-18 00:11:10 -06:00
Mauricio Siu
44e6a117dd Merge pull request #208 from Dokploy/canary
v0.3.2
2024-07-11 23:21:32 -06:00
Mauricio Siu
bfdc73f8d1 Merge pull request #197 from Dokploy/canary
v0.3.1
2024-07-06 12:01:07 -06:00
Mauricio Siu
64ada7020a Merge pull request #185 from Dokploy/canary
v0.3.0
2024-07-01 00:01:16 -06:00
Mauricio Siu
4706adc0c0 Merge pull request #174 from Dokploy/canary
v0.2.5
2024-06-29 13:29:39 -06:00
Mauricio Siu
e01d92d1d9 Merge pull request #161 from Dokploy/canary
v0.2.4
2024-06-23 19:40:45 -06:00
Mauricio Siu
fe22890311 Merge pull request #156 from Dokploy/canary
v0.2.3
2024-06-21 11:50:40 -06:00
Mauricio Siu
2b7c7632f4 Merge pull request #136 from Dokploy/canary
v0.2.2
2024-06-08 22:06:39 -06:00
Mauricio Siu
1b7244e841 Merge pull request #127 from Dokploy/canary
v0.2.1
2024-06-07 02:52:03 -06:00
199 changed files with 3540 additions and 15770 deletions

View File

@@ -15,9 +15,7 @@ jobs:
name: Build and push AMD64 image
command: |
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
TAG="feature"
elif [ "${CIRCLE_BRANCH}" == "main" ]; then
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
else
TAG="canary"
@@ -40,9 +38,7 @@ jobs:
name: Build and push ARM64 image
command: |
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
TAG="feature"
elif [ "${CIRCLE_BRANCH}" == "main" ]; then
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
else
TAG="canary"
@@ -75,12 +71,6 @@ jobs:
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${VERSION}
elif [ "${CIRCLE_BRANCH}" == "139-multi-server-feature" ]; then
TAG="feature"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
else
TAG="canary"
docker manifest create dokploy/dokploy:${TAG} \
@@ -98,14 +88,12 @@ workflows:
only:
- main
- canary
- 139-multi-server-feature
- build-arm64:
filters:
branches:
only:
- main
- canary
- 139-multi-server-feature
- combine-manifests:
requires:
- build-amd64
@@ -115,4 +103,3 @@ workflows:
only:
- main
- canary
- 139-multi-server-feature

View File

@@ -27,7 +27,7 @@ WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y curl apache2-utils && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
@@ -42,7 +42,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
# Install docker
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
@@ -55,4 +55,4 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]

View File

@@ -29,7 +29,7 @@ We have tested on the following Linux Distros:
### Providers
- [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)
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://hostinger.com?REFERRALCODE=1SIUMAURICI97)
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get 200$ credits for free with this referral link: [Referral Link](https://m.do.co/c/db24efd43f35)
- [Hetzner](https://www.hetzner.com/cloud/) Get 20€ credits for free with this referral link: [Referral Link](https://hetzner.cloud/?ref=vou4fhxJ1W2D)
- [Linode](https://www.linode.com/es/pricing/#compute-shared)

View File

@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support, 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:
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:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -1,8 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { paths } from "@/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@/server/utils/builders";
import { APPLICATIONS_PATH } from "@/server/constants";
import { unzipDrop } from "@/server/utils/builders/drop";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
@@ -13,84 +11,11 @@ if (typeof window === "undefined") {
globalThis.FileList = undici.FileList as any;
}
const baseApp: ApplicationNested = {
applicationId: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,
serverId: "",
branch: null,
dockerBuildStage: "",
buildArgs: null,
buildPath: "/",
gitlabPathNamespace: "",
buildType: "nixpacks",
bitbucketBranch: "",
bitbucketBuildPath: "",
bitbucketId: "",
bitbucketRepository: "",
bitbucketOwner: "",
githubId: "",
gitlabProjectId: 0,
gitlabBranch: "",
gitlabBuildPath: "",
gitlabId: "",
gitlabRepository: "",
gitlabOwner: "",
command: null,
cpuLimit: null,
cpuReservation: null,
createdAt: "",
customGitBranch: "",
customGitBuildPath: "",
customGitSSHKeyId: null,
customGitUrl: "",
description: "",
dockerfile: null,
dockerImage: null,
dropBuildPath: null,
enabled: null,
env: null,
healthCheckSwarm: null,
labelsSwarm: null,
memoryLimit: null,
memoryReservation: null,
modeSwarm: null,
mounts: [],
name: "",
networkSwarm: null,
owner: null,
password: null,
placementSwarm: null,
ports: [],
projectId: "",
publishDirectory: null,
redirects: [],
refreshToken: "",
registry: null,
registryId: null,
replicas: 1,
repository: null,
restartPolicySwarm: null,
rollbackConfigSwarm: null,
security: [],
sourceType: "git",
subtitle: null,
title: null,
updateConfigSwarm: null,
username: null,
dockerContextPath: null,
};
//
vi.mock("@/server/constants", () => ({
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}));
describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
@@ -100,42 +25,39 @@ describe("unzipDrop using real zip files", () => {
});
it("should correctly extract a zip with a single root folder", async () => {
baseApp.appName = "single-file";
// const appName = "single-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const appName = "single-file";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
await unzipDrop(file, appName);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true);
});
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
baseApp.appName = "folderwithfile";
// const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
await unzipDrop(file, appName);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
});
it("should correctly extract a zip with multiple root folders", async () => {
baseApp.appName = "two-folders";
// const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
await unzipDrop(file, appName);
const files = await fs.readdir(outputPath, { withFileTypes: true });
@@ -144,14 +66,13 @@ describe("unzipDrop using real zip files", () => {
});
it("should correctly extract a zip with a single root with a file", async () => {
baseApp.appName = "nested";
// const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
await unzipDrop(file, appName);
const files = await fs.readdir(outputPath, { withFileTypes: true });
@@ -161,14 +82,13 @@ describe("unzipDrop using real zip files", () => {
});
it("should correctly extract a zip with a single root with a folder", async () => {
baseApp.appName = "folder-with-sibling-file";
// const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
await unzipDrop(file, appName);
const files = await fs.readdir(outputPath, { withFileTypes: true });

View File

@@ -9,7 +9,6 @@ const baseApp: ApplicationNested = {
applicationStatus: "done",
appName: "",
autoDeploy: true,
serverId: "",
branch: null,
dockerBuildStage: "",
buildArgs: null,

View File

@@ -278,12 +278,6 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="px-4">
<AlertBlock type="info">
Changing settings such as placements may cause the logs/monitoring
to be unavailable.
</AlertBlock>
</div>
<Form {...form}>
<form

View File

@@ -7,7 +7,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { File, Loader2 } from "lucide-react";
import { File } from "lucide-react";
import React from "react";
import { UpdateTraefikConfig } from "./update-traefik-config";
interface Props {
@@ -15,7 +15,7 @@ interface Props {
}
export const ShowTraefikConfig = ({ applicationId }: Props) => {
const { data, isLoading } = api.application.readTraefikConfig.useQuery(
const { data } = api.application.readTraefikConfig.useQuery(
{
applicationId,
},
@@ -35,12 +35,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{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 ? (
{data === null ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<File className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">

View File

@@ -7,7 +7,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Package } from "lucide-react";
import { AlertTriangle, Package } from "lucide-react";
import React from "react";
import { AddVolumes } from "./add-volumes";
import { DeleteVolume } from "./delete-volume";

View File

@@ -11,9 +11,8 @@ interface Props {
logPath: string | null;
open: boolean;
onClose: () => void;
serverId?: string;
}
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null);
@@ -22,7 +21,7 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {

View File

@@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
refetchInterval: 1000,
refetchInterval: 5000,
},
);
const [url, setUrl] = React.useState("");
@@ -110,7 +110,6 @@ export const ShowDeployments = ({ applicationId }: Props) => {
</div>
)}
<ShowDeployment
serverId={data?.serverId || ""}
open={activeLog !== null}
onClose={() => setActiveLog(null)}
logPath={activeLog}

View File

@@ -175,7 +175,6 @@ export const AddDomain = ({
onClick={() => {
generateDomain({
appName: application?.appName || "",
serverId: application?.serverId || "",
})
.then((domain) => {
field.onChange(domain);
@@ -297,7 +296,11 @@ export const AddDomain = ({
</form>
<DialogFooter>
<Button isLoading={isLoading} form="hook-form" type="submit">
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
>
{dictionary.submit}
</Button>
</DialogFooter>

View File

@@ -1,358 +0,0 @@
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;
};

View File

@@ -45,17 +45,14 @@ export const DeployApplication = ({ applicationId }: Props) => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
toast.success("Deploying Application....");
await refetch();
await deploy({
applicationId,
})
.then(async () => {
toast.success("Application deployed succesfully");
await refetch();
})
.catch(() => {
toast.error("Error to deploy Application");
});
}).catch(() => {
toast.error("Error to deploy Application");
});
await refetch();
}}

View File

@@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
type="submit"
className="w-fit"
isLoading={isLoading}
disabled={!zip || isLoading}
disabled={!zip}
>
Deploy{" "}
</Button>

View File

@@ -66,10 +66,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
) : (
<StopApplication applicationId={applicationId} />
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -16,7 +16,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic(
@@ -31,14 +30,12 @@ export const DockerLogs = dynamic(
interface Props {
appName: string;
serverId?: string;
}
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
export const ShowDockerLogs = ({ appName }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
@@ -65,14 +62,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{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" />
)}
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -89,7 +79,6 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
</SelectContent>
</Select>
<DockerLogs
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>

View File

@@ -9,16 +9,10 @@ import { useEffect, useRef, useState } from "react";
interface Props {
logPath: string | null;
serverId?: string;
open: boolean;
onClose: () => void;
}
export const ShowDeploymentCompose = ({
logPath,
open,
onClose,
serverId,
}: Props) => {
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null);
@@ -27,7 +21,7 @@ export const ShowDeploymentCompose = ({
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {

View File

@@ -111,7 +111,6 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
</div>
)}
<ShowDeploymentCompose
serverId={data?.serverId || ""}
open={activeLog !== null}
onClose={() => setActiveLog(null)}
logPath={activeLog}

View File

@@ -310,7 +310,6 @@ export const AddDomainCompose = ({
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
serverId: compose?.serverId || "",
appName: compose?.appName || "",
})
.then((domain) => {

View File

@@ -75,10 +75,7 @@ export const ComposeActions = ({ composeId }: Props) => {
<StopCompose composeId={composeId} />
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal
@@ -119,7 +116,6 @@ export const ComposeActions = ({ composeId }: Props) => {
</DropdownMenuContent>
</DropdownMenu>
)}
{data?.server?.name}
</div>
);
};

View File

@@ -16,7 +16,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader, Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic(
@@ -31,20 +30,14 @@ export const DockerLogs = dynamic(
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
}
export const ShowDockerLogsCompose = ({
appName,
appType,
serverId,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType,
serverId,
},
{
enabled: !!appName,
@@ -71,14 +64,7 @@ export const ShowDockerLogsCompose = ({
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{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" />
)}
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -95,7 +81,6 @@ export const ShowDockerLogsCompose = ({
</SelectContent>
</Select>
<DockerLogs
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>

View File

@@ -17,27 +17,23 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { DockerMonitoring } from "../../monitoring/docker/show";
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
}
export const ShowMonitoringCompose = ({
appName,
appType = "stack",
serverId,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
serverId,
},
{
enabled: !!appName,
@@ -50,7 +46,7 @@ export const ShowMonitoringCompose = ({
const [containerId, setContainerId] = useState<string | undefined>();
const { mutateAsync: restart, isLoading: isRestarting } =
const { mutateAsync: restart, isLoading } =
api.docker.restartContainer.useMutation();
useEffect(() => {
@@ -81,14 +77,7 @@ export const ShowMonitoringCompose = ({
value={containerAppName}
>
<SelectTrigger>
{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" />
)}
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -106,7 +95,7 @@ export const ShowMonitoringCompose = ({
</SelectContent>
</Select>
<Button
isLoading={isRestarting}
isLoading={isLoading}
onClick={async () => {
if (!containerId) return;
toast.success(`Restarting container ${containerAppName}`);

View File

@@ -11,14 +11,12 @@ import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
export const ShowContainerConfig = ({ containerId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,

View File

@@ -8,14 +8,9 @@ import "@xterm/xterm/css/xterm.css";
interface Props {
id: string;
containerId: string;
serverId?: string | null;
}
export const DockerLogsId: React.FC<Props> = ({
id,
containerId,
serverId,
}) => {
export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
const [term, setTerm] = React.useState<Terminal>();
const [lines, setLines] = React.useState<number>(40);
@@ -43,7 +38,7 @@ export const DockerLogsId: React.FC<Props> = ({
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}`;
const ws = new WebSocket(wsUrl);
const fitAddon = new FitAddon();

View File

@@ -22,14 +22,9 @@ export const DockerLogsId = dynamic(
interface Props {
containerId: string;
children?: React.ReactNode;
serverId?: string | null;
}
export const ShowDockerModalLogs = ({
containerId,
children,
serverId,
}: Props) => {
export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
@@ -46,11 +41,7 @@ export const ShowDockerModalLogs = ({
<DialogDescription>View the logs for {containerId}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId
id="terminal"
containerId={containerId || ""}
serverId={serverId}
/>
<DockerLogsId id="terminal" containerId={containerId || ""} />
</div>
</DialogContent>
</Dialog>

View File

@@ -114,20 +114,11 @@ export const columns: ColumnDef<Container>[] = [
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<ShowDockerModalLogs
containerId={container.containerId}
serverId={container.serverId}
>
<ShowDockerModalLogs containerId={container.containerId}>
View Logs
</ShowDockerModalLogs>
<ShowContainerConfig
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<DockerTerminalModal
containerId={container.containerId}
serverId={container.serverId || ""}
>
<ShowContainerConfig containerId={container.containerId} />
<DockerTerminalModal containerId={container.containerId}>
Terminal
</DockerTerminalModal>
</DropdownMenuContent>

View File

@@ -34,15 +34,8 @@ export type Container = NonNullable<
RouterOutputs["docker"]["getContainers"]
>[0];
interface Props {
serverId?: string;
}
export const ShowContainers = ({ serverId }: Props) => {
const { data, isLoading } = api.docker.getContainers.useQuery({
serverId,
});
export const ShowContainers = () => {
const { data, isLoading } = api.docker.getContainers.useQuery();
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
@@ -110,99 +103,83 @@ export const ShowContainers = ({ serverId }: Props) => {
</DropdownMenu>
</div>
<div className="rounded-md border">
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : data?.length === 0 ? (
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
No results.
</span>
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table?.getRowModel()?.rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : (
<>No results.</>
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
))
) : (
<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>
{data && data?.length > 0 && (
<div className="flex items-center justify-end space-x-2 py-4">
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
)}
</div>
</div>
</div>
);

View File

@@ -18,15 +18,10 @@ const Terminal = dynamic(
interface Props {
containerId: string;
serverId?: string;
children?: React.ReactNode;
}
export const DockerTerminalModal = ({
children,
containerId,
serverId,
}: Props) => {
export const DockerTerminalModal = ({ children, containerId }: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
@@ -45,11 +40,7 @@ export const DockerTerminalModal = ({
</DialogDescription>
</DialogHeader>
<Terminal
id="terminal"
containerId={containerId}
serverId={serverId || ""}
/>
<Terminal id="terminal" containerId={containerId} />
</DialogContent>
</Dialog>
);

View File

@@ -8,14 +8,9 @@ import { AttachAddon } from "@xterm/addon-attach";
interface Props {
id: string;
containerId: string;
serverId?: string;
}
export const DockerTerminal: React.FC<Props> = ({
id,
containerId,
serverId,
}) => {
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
const termRef = useRef(null);
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
useEffect(() => {
@@ -38,7 +33,7 @@ export const DockerTerminal: React.FC<Props> = ({
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}`;
const ws = new WebSocket(wsUrl);

View File

@@ -13,7 +13,6 @@ import {
} from "@/components/ui/form";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -30,18 +29,12 @@ type UpdateServerMiddlewareConfig = z.infer<
interface Props {
path: string;
serverId?: string;
}
export const ShowTraefikFile = ({ path, serverId }: Props) => {
const {
data,
refetch,
isLoading: isLoadingFile,
} = api.settings.readTraefikFile.useQuery(
export const ShowTraefikFile = ({ path }: Props) => {
const { data, refetch } = api.settings.readTraefikFile.useQuery(
{
path,
serverId,
},
{
enabled: !!path,
@@ -61,9 +54,11 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
});
useEffect(() => {
form.reset({
traefikConfig: data || "",
});
if (data) {
form.reset({
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
@@ -79,7 +74,6 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
await mutateAsync({
traefikConfig: data.traefikConfig,
path,
serverId,
})
.then(async () => {
toast.success("Traefik config Updated");
@@ -99,28 +93,20 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
className="w-full relative z-[5]"
>
<div className="flex flex-col overflow-auto">
{isLoadingFile ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
) : (
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormDescription className="break-all">
{path}
</FormDescription>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormDescription className="break-all">
{path}
</FormDescription>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:
router-name:
rule: Host('domain.com')
@@ -130,36 +116,31 @@ routers:
tls: false
middlewares: []
`}
{...field}
/>
</FormControl>
{...field}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-8">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
)}
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-8">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
</div>
<div className="flex justify-end">
<Button
isLoading={isLoading}
disabled={canEdit || isLoading}
type="submit"
>
<Button isLoading={isLoading} disabled={canEdit} type="submit">
Update
</Button>
</div>

View File

@@ -1,47 +1,20 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Tree } from "@/components/ui/file-tree";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
import React from "react";
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 { ShowTraefikFile } from "./show-traefik-file";
interface Props {
serverId?: string;
}
export const ShowTraefikSystem = ({ serverId }: Props) => {
export const ShowTraefikSystem = () => {
const [file, setFile] = React.useState<null | string>(null);
const {
data: directories,
isLoading,
error,
isError,
} = api.settings.readDirectories.useQuery(
{
serverId,
},
{
retry: 2,
},
);
const { data: directories } = api.settings.readDirectories.useQuery();
return (
<div className={cn("mt-6 md:grid gap-4")}>
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
{isError && (
<AlertBlock type="error" className="w-full">
{error?.message}
</AlertBlock>
)}
{isLoading && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
)}
{directories?.length === 0 && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
@@ -61,7 +34,7 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
/>
<div className="w-full">
{file ? (
<ShowTraefikFile path={file} serverId={serverId} />
<ShowTraefikFile path={file} />
) : (
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
<span className="text-muted-foreground text-lg font-medium">

View File

@@ -36,10 +36,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
) : (
<StopMariadb mariadbId={mariadbId} />
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -34,10 +34,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
) : (
<StopMongo mongoId={mongoId} />
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -23,7 +23,7 @@ export const DockerMemoryChart = ({
return {
time: item.time,
name: `Point ${index + 1}`,
usage: (item.value.used / 1024 ** 3).toFixed(2),
usage: (item.value.used / 1024).toFixed(2),
};
});
return (

View File

@@ -208,7 +208,9 @@ export const DockerMonitoring = ({
<div className="flex flex-col gap-2 w-full ">
<span className="text-base font-medium">Memory</span>
<span className="text-sm text-muted-foreground">
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
{`Used: ${(currentData.memory.value.used / 1024).toFixed(
2,
)} GB / Limit: ${(currentData.memory.value.total / 1024).toFixed(2)} GB`}
</span>
<Progress
value={currentData.memory.value.usedPercentage}
@@ -216,7 +218,7 @@ export const DockerMonitoring = ({
/>
<DockerMemoryChart
acummulativeData={acummulativeData.memory}
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
memoryLimitGB={currentData.memory.value.total / 1024}
/>
</div>
{appName === "dokploy" && (
@@ -238,9 +240,9 @@ export const DockerMonitoring = ({
<div className="flex flex-col gap-2 w-full ">
<span className="text-base font-medium">Block I/O</span>
<span className="text-sm text-muted-foreground">
{`Read: ${currentData.block.value.readMb.toFixed(
{`Used: ${currentData.block.value.readMb.toFixed(
2,
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
)} MB / Limit: ${currentData.block.value.writeMb.toFixed(
3,
)} MB`}
</span>

View File

@@ -35,10 +35,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<StopMysql mysqlId={mysqlId} />
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -38,10 +38,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<StopPostgres postgresId={postgresId} />
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -19,26 +19,11 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { Folder } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -58,7 +43,6 @@ const AddTemplateSchema = z.object({
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
description: z.string().optional(),
serverId: z.string().optional(),
});
type AddTemplate = z.infer<typeof AddTemplateSchema>;
@@ -70,10 +54,8 @@ interface Props {
export const AddApplication = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation();
@@ -93,7 +75,6 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
appName: data.appName,
description: data.description,
projectId,
serverId: data.serverId,
})
.then(async () => {
toast.success("Service Created");
@@ -154,57 +135,6 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If not server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"

View File

@@ -22,23 +22,15 @@ import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { CircuitBoard, Folder } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -59,7 +51,6 @@ const AddComposeSchema = z.object({
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
description: z.string().optional(),
serverId: z.string().optional(),
});
type AddCompose = z.infer<typeof AddComposeSchema>;
@@ -72,7 +63,6 @@ interface Props {
export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
@@ -97,7 +87,6 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
projectId,
composeType: data.composeType,
appName: data.appName,
serverId: data.serverId,
})
.then(async () => {
toast.success("Compose Created");
@@ -159,57 +148,6 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)}
/>
</div>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If not server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"

View File

@@ -26,15 +26,6 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
@@ -80,7 +71,6 @@ const baseDatabaseSchema = z.object({
databasePassword: z.string(),
dockerImage: z.string(),
description: z.string().nullable(),
serverId: z.string().nullable(),
});
const mySchema = z.discriminatedUnion("type", [
@@ -155,7 +145,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const postgresMutation = api.postgres.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
const redisMutation = api.redis.create.useMutation();
@@ -172,7 +161,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
description: "",
databaseName: "",
databaseUser: "",
serverId: null,
},
resolver: zodResolver(mySchema),
});
@@ -195,7 +183,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
appName: data.appName,
dockerImage: defaultDockerImage,
projectId,
serverId: data.serverId,
description: data.description,
};
@@ -204,10 +191,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "mongo") {
promise = mongoMutation.mutateAsync({
@@ -215,13 +200,11 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databasePassword: data.databasePassword,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "redis") {
promise = redisMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
serverId: data.serverId,
projectId,
});
} else if (data.type === "mariadb") {
@@ -232,7 +215,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "mysql") {
promise = mysqlMutation.mutateAsync({
@@ -242,7 +224,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
databaseRootPassword: data.databaseRootPassword,
serverId: data.serverId,
});
}
@@ -371,39 +352,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"

View File

@@ -29,27 +29,11 @@ import {
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { ScrollArea } from "@radix-ui/react-scroll-area";
@@ -59,7 +43,6 @@ import {
Code,
Github,
Globe,
HelpCircle,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
@@ -75,12 +58,9 @@ export const AddTemplate = ({ projectId }: Props) => {
const [open, setOpen] = useState(false);
const { data } = api.compose.templates.useQuery();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: tags, isLoading: isLoadingTags } =
api.compose.getTags.useQuery();
const utils = api.useUtils();
const [serverId, setServerId] = useState<string | undefined>(undefined);
const { mutateAsync, isLoading, error, isError } =
api.compose.deployTemplate.useMutation();
@@ -129,6 +109,7 @@ export const AddTemplate = ({ projectId }: Props) => {
role="combobox"
className={cn(
"md:max-w-[15rem] w-full justify-between !bg-input",
// !field.value && "text-muted-foreground",
)}
>
{isLoadingTags
@@ -287,79 +268,30 @@ export const AddTemplate = ({ projectId }: Props) => {
{template.name} template and add it to your
project.
</AlertDialogDescription>
<div>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If not server is selected, the
application will be deployed on the
server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={(e) => {
setServerId(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isLoading}
onClick={async () => {
const promise = mutateAsync({
await mutateAsync({
projectId,
serverId: serverId || undefined,
id: template.id,
});
toast.promise(promise, {
loading: "Setting up...",
success: (data) => {
})
.then(async () => {
toast.success(
`Succesfully created ${template.name} application from template`,
);
utils.project.one.invalidate({
projectId,
});
setOpen(false);
return `${template.name} template created succesfully`;
},
error: (err) => {
return `Ocurred an error deploying ${template.name} template`;
},
});
})
.catch(() => {
toast.error(
`Error creating ${template.name} application from template`,
);
});
}}
>
Confirm

View File

@@ -17,7 +17,6 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
@@ -110,7 +109,6 @@ export const AddProject = () => {
)}
/>
</div>
<FormField
control={form.control}
name="description"

View File

@@ -37,10 +37,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<StopRedis redisId={redisId} />
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -17,29 +17,13 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import {
getObjectSchema,
mergeFormValues,
providerSchemas,
providersData,
} from "../../application/domains/schema";
import { capitalize } from "lodash";
const addDestination = z.object({
name: z.string().min(1, "Name is required"),
@@ -54,45 +38,43 @@ type AddDestination = z.infer<typeof addDestination>;
export const AddDestination = () => {
const utils = api.useUtils();
const [provider, setProviders] = useState<keyof typeof providerSchemas>("s3");
const { mutateAsync, isError, error, isLoading } =
api.destination.create.useMutation();
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
api.destination.testConnection.useMutation();
const schema = providerSchemas[provider];
const form = useForm<z.infer<typeof schema>>({
const form = useForm<AddDestination>({
defaultValues: {
...getObjectSchema(schema),
accessKeyId: "",
bucket: "",
name: "",
region: "",
secretAccessKey: "",
endpoint: "",
},
resolver: zodResolver(schema),
resolver: zodResolver(addDestination),
});
const {
register,
handleSubmit,
control,
formState: { errors },
} = form;
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: z.infer<typeof schema>) => {
// await mutateAsync({
// accessKey: data.accessKeyId,
// bucket: data.bucket,
// endpoint: data.endpoint,
// name: data.name,
// region: data.region,
// secretAccessKey: data.secretAccessKey,
// })
// .then(async () => {
// toast.success("Destination Created");
// await utils.destination.all.invalidate();
// })
// .catch(() => {
// toast.error("Error to create the Destination");
// });
const onSubmit = async (data: AddDestination) => {
await mutateAsync({
accessKey: data.accessKeyId,
bucket: data.bucket,
endpoint: data.endpoint,
name: data.name,
region: data.region,
secretAccessKey: data.secretAccessKey,
})
.then(async () => {
toast.success("Destination Created");
await utils.destination.all.invalidate();
})
.catch(() => {
toast.error("Error to create the Destination");
});
};
const fields = Object.keys(schema.shape);
return (
<Dialog>
<DialogTrigger className="" asChild>
@@ -106,117 +88,140 @@ export const AddDestination = () => {
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-destination-add"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
className="grid w-full gap-4 "
>
<div className="flex flex-col gap-2">
{fields.map((input) => (
<FormField
control={control}
key={`${provider}.${input}`}
name={`${provider}.${input}`}
render={({ field }) => {
return (
<FormItem>
<FormLabel>{capitalize(input)}</FormLabel>
<FormControl>
<Input placeholder={"Value"} {...field} />
</FormControl>
<span className="text-sm font-medium text-destructive">
{errors[input]?.message}
</span>
</FormItem>
);
}}
/>
))}
</div>
<FormField
control={form.control}
name="name"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder={"S3 Bucket"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Access Key Id</FormLabel>
<FormControl>
<Input placeholder={"xcas41dasde"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Secret Access Key</FormLabel>
</div>
<FormControl>
<Input placeholder={"asd123asdasw"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Bucket</FormLabel>
</div>
<FormControl>
<Input placeholder={"dokploy-bucket"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="region"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Region</FormLabel>
</div>
<FormControl>
<Input placeholder={"us-east-1"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint</FormLabel>
<FormControl>
<Input
placeholder={"https://us.bucket.aws/s3"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</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");
<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"),
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
<Button
// isLoading={isLoading}
form="hook-form-destination-add"
type="submit"
>
Create
</Button>
{/* */}
</DialogFooter>
.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>
</DialogContent>
</Dialog>
);

View File

@@ -19,8 +19,10 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { Edit } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -1,52 +0,0 @@
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>
);
};

View File

@@ -1,44 +0,0 @@
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>
);
};

View File

@@ -1,177 +0,0 @@
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>
);
};

View File

@@ -1,125 +0,0 @@
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>
);
};

View File

@@ -1,52 +0,0 @@
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>
);
};

View File

@@ -1,253 +0,0 @@
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>
);
};

View File

@@ -1,301 +0,0 @@
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>
);
};

View File

@@ -1,48 +0,0 @@
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>
);
};

View File

@@ -1,216 +0,0 @@
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>
);
};

View File

@@ -1,48 +0,0 @@
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>
);
};

View File

@@ -1,269 +0,0 @@
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>
);
};

View File

@@ -41,8 +41,8 @@ export const ShowUsers = () => {
}, []);
return (
<div className=" col-span-2">
<Card className="bg-transparent ">
<div className="h-full col-span-2">
<Card className="bg-transparent h-full ">
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
<div className="flex flex-col gap-2">
<CardTitle className="text-xl">Users</CardTitle>
@@ -55,9 +55,9 @@ export const ShowUsers = () => {
</div>
)}
</CardHeader>
<CardContent className="space-y-2">
<CardContent className="space-y-2 h-full">
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 h-full">
<div className="flex flex-col items-center gap-3">
<Users className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
To create a user, you need to add:

View File

@@ -1,3 +1,4 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -5,34 +6,334 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import React from "react";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
import { ShowModalLogs } from "./web-server/show-modal-logs";
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
import { TerminalModal } from "./web-server/terminal-modal";
import { UpdateServer } from "./web-server/update-server";
interface Props {
className?: string;
}
export const WebServer = ({ className }: Props) => {
const { data } = api.admin.one.useQuery();
export const WebServer = () => {
const { data, refetch } = api.admin.one.useQuery();
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
const {
mutateAsync: cleanDockerBuilder,
isLoading: cleanDockerBuilderIsLoading,
} = api.settings.cleanDockerBuilder.useMutation();
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
api.settings.cleanMonitoring.useMutation();
const {
mutateAsync: cleanUnusedImages,
isLoading: cleanUnusedImagesIsLoading,
} = api.settings.cleanUnusedImages.useMutation();
const {
mutateAsync: cleanUnusedVolumes,
isLoading: cleanUnusedVolumesIsLoading,
} = api.settings.cleanUnusedVolumes.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { mutateAsync: updateDockerCleanup } =
api.settings.updateDockerCleanup.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
return (
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
<Card className="rounded-lg w-full bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Web server settings</CardTitle>
<CardDescription>Reload or clean the web server.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4 ">
<CardContent className="flex flex-col gap-4">
<div className="grid md:grid-cols-2 gap-4">
<ShowDokployActions />
<ShowTraefikActions />
<ShowStorageActions />
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
<Button isLoading={isLoading} variant="outline">
Server
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadServer()
.then(async () => {
toast.success("Server Reloaded");
})
.catch(() => {
toast.success("Server Reloaded");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy">
<span>Watch logs</span>
</ShowModalLogs>
<ShowServerTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowServerTraefikConfig>
<ShowServerMiddlewareConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View middlewares config</span>
</DropdownMenuItem>
</ShowServerMiddlewareConfig>
<TerminalModal>
<span>Enter the terminal</span>
</TerminalModal>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
>
<Button
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
variant="outline"
>
Traefik
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadTraefik()
.then(async () => {
toast.success("Traefik Reloaded");
})
.catch(() => {
toast.error("Error to reload the traefik");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik">
<span>Watch logs</span>
</ShowModalLogs>
<ShowMainTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowMainTraefikConfig>
<EditTraefikEnv>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
</DropdownMenuItem>
</EditTraefikEnv>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: !haveTraefikDashboardPortEnabled,
})
.then(async () => {
toast.success(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
refetchDashboard();
})
.catch(() => {
toast.error(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
Dashboard
</span>
</DropdownMenuItem>
<DockerTerminalModal appName="dokploy-traefik">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<span>Enter the terminal</span>
</DropdownMenuItem>
</DockerTerminalModal>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
>
<Button
isLoading={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
variant="outline"
>
Space
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedImages()
.then(async () => {
toast.success("Cleaned images");
})
.catch(() => {
toast.error("Error to clean images");
});
}}
>
<span>Clean unused images</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedVolumes()
.then(async () => {
toast.success("Cleaned volumes");
})
.catch(() => {
toast.error("Error to clean volumes");
});
}}
>
<span>Clean unused volumes</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanStoppedContainers()
.then(async () => {
toast.success("Stopped containers cleaned");
})
.catch(() => {
toast.error("Error to clean stopped containers");
});
}}
>
<span>Clean stopped containers</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanDockerBuilder()
.then(async () => {
toast.success("Cleaned Docker Builder");
})
.catch(() => {
toast.error("Error to clean Docker Builder");
});
}}
>
<span>Clean Docker Builder & System</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanMonitoring()
.then(async () => {
toast.success("Cleaned Monitoring");
})
.catch(() => {
toast.error("Error to clean Monitoring");
});
}}
>
<span>Clean Monitoring </span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanAll()
.then(async () => {
toast.success("Cleaned all");
})
.catch(() => {
toast.error("Error to clean all");
});
}}
>
<span>Clean all</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<UpdateServer />
</div>
@@ -44,8 +345,25 @@ export const WebServer = ({ className }: Props) => {
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}
</span>
<div className="flex items-center gap-4">
<Switch
checked={data?.enableDockerCleanup}
onCheckedChange={async (e) => {
await updateDockerCleanup({
enableDockerCleanup: e,
})
.then(async () => {
toast.success("Docker Cleanup Enabled");
})
.catch(() => {
toast.error("Docker Cleanup Error");
});
<ToggleDockerCleanup />
refetch();
}}
/>
<Label className="text-primary">Daily Docker Cleanup</Label>
</div>
</div>
</CardContent>
</Card>

View File

@@ -17,7 +17,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
@@ -35,14 +34,12 @@ const Terminal = dynamic(
interface Props {
appName: string;
children?: React.ReactNode;
serverId?: string;
}
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
export const DockerTerminalModal = ({ children, appName }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
@@ -68,14 +65,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{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" />
)}
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -92,7 +82,6 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
</SelectContent>
</Select>
<Terminal
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>

View File

@@ -33,15 +33,12 @@ type Schema = z.infer<typeof schema>;
interface Props {
children?: React.ReactNode;
serverId?: string;
}
export const EditTraefikEnv = ({ children, serverId }: Props) => {
export const EditTraefikEnv = ({ children }: Props) => {
const [canEdit, setCanEdit] = useState(true);
const { data } = api.settings.readTraefikEnv.useQuery({
serverId,
});
const { data } = api.settings.readTraefikEnv.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.settings.writeTraefikEnv.useMutation();
@@ -65,7 +62,6 @@ export const EditTraefikEnv = ({ children, serverId }: Props) => {
const onSubmit = async (data: Schema) => {
await mutateAsync({
env: data.env,
serverId,
})
.then(async () => {
toast.success("Traefik Env Updated");

View File

@@ -0,0 +1,165 @@
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>
);
};

View File

@@ -18,7 +18,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
@@ -36,14 +35,12 @@ export const DockerLogsId = dynamic(
interface Props {
appName: string;
children?: React.ReactNode;
serverId?: string;
}
export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
export const ShowModalLogs = ({ appName, children }: Props) => {
const { data } = api.docker.getContainersByAppLabel.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
@@ -75,14 +72,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{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" />
)}
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -98,11 +88,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
</SelectGroup>
</SelectContent>
</Select>
<DockerLogsId
id="terminal"
containerId={containerId || ""}
serverId={serverId}
/>
<DockerLogsId id="terminal" containerId={containerId || ""} />
</div>
</DialogContent>
</Dialog>

View File

@@ -0,0 +1,162 @@
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>
);
};

View File

@@ -0,0 +1,163 @@
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>
);
};

View File

@@ -1,3 +1,4 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -7,27 +8,79 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { RemoveSSHPrivateKey } from "./remove-ssh-private-key";
const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), {
ssr: false,
});
const addSSHPrivateKey = z.object({
sshPrivateKey: z
.string({
required_error: "SSH private key is required",
})
.min(1, "SSH private key is required"),
});
type AddSSHPrivateKey = z.infer<typeof addSSHPrivateKey>;
interface Props {
children?: React.ReactNode;
serverId: string;
}
export const TerminalModal = ({ children, serverId }: Props) => {
const { data } = api.server.one.useQuery(
{
serverId,
},
{ enabled: !!serverId },
);
export const TerminalModal = ({ children }: Props) => {
const { data, refetch } = api.admin.one.useQuery();
const [user, setUser] = useState("root");
const [terminalUser, setTerminalUser] = useState("root");
const { mutateAsync, isLoading } =
api.settings.saveSSHPrivateKey.useMutation();
const form = useForm<AddSSHPrivateKey>({
defaultValues: {
sshPrivateKey: "",
},
resolver: zodResolver(addSSHPrivateKey),
});
useEffect(() => {
if (data) {
form.reset({});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: AddSSHPrivateKey) => {
await mutateAsync({
sshPrivateKey: formData.sshPrivateKey,
})
.then(async () => {
toast.success("SSH Key Updated");
await refetch();
})
.catch(() => {
toast.error("Error to Update the ssh key");
});
};
return (
<Dialog>
<DialogTrigger asChild>
@@ -39,14 +92,75 @@ export const TerminalModal = ({ children, serverId }: Props) => {
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogHeader className="flex flex-col gap-1">
<DialogTitle>Terminal ({data?.name})</DialogTitle>
<DialogDescription>Easy way to access the server</DialogDescription>
<DialogHeader className="flex flex-row justify-between pt-4">
<div>
<DialogTitle>Terminal</DialogTitle>
<DialogDescription>Easy way to access the server</DialogDescription>
</div>
{data?.haveSSH && (
<div>
<RemoveSSHPrivateKey />
</div>
)}
</DialogHeader>
{!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>
<div className="flex flex-col gap-4">
<Terminal id="terminal" serverId={serverId} />
</div>
<Terminal id="terminal" userSSH={terminalUser} />
</div>
)}
</DialogContent>
</Dialog>
);

View File

@@ -7,10 +7,10 @@ import { AttachAddon } from "@xterm/addon-attach";
interface Props {
id: string;
serverId: string;
userSSH?: string;
}
export const Terminal: React.FC<Props> = ({ id, serverId }) => {
export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
const termRef = useRef(null);
useEffect(() => {
@@ -33,7 +33,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/terminal?serverId=${serverId}`;
const wsUrl = `${protocol}//${window.location.host}/terminal?userSSH=${userSSH}`;
const ws = new WebSocket(wsUrl);
const addonAttach = new AttachAddon(ws);
@@ -46,7 +46,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
return () => {
ws.readyState === WebSocket.OPEN && ws.close();
};
}, [id, serverId]);
}, [id, userSSH]);
return (
<div className="flex flex-col gap-4">

View File

@@ -74,7 +74,7 @@ export const SettingsLayout = ({ children }: Props) => {
{
title: "Cluster",
label: "",
icon: BoxesIcon,
icon: Server,
href: "/dashboard/settings/cluster",
},
{
@@ -83,12 +83,6 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Bell,
href: "/dashboard/settings/notifications",
},
{
title: "Servers",
label: "",
icon: Server,
href: "/dashboard/settings/servers",
},
]
: []),
...(user?.canAccessToSSHKeys
@@ -123,7 +117,6 @@ export const SettingsLayout = ({ children }: Props) => {
import {
Activity,
Bell,
BoxesIcon,
Database,
GitBranch,
KeyIcon,

View File

@@ -11,11 +11,10 @@ import {
} from "@/components/ui/alert-dialog";
interface Props {
title?: string | React.ReactNode;
description?: string | React.ReactNode;
title?: string;
description?: string;
onClick: () => void;
children?: React.ReactNode;
disabled?: boolean;
}
export const DialogAction = ({
@@ -23,7 +22,6 @@ export const DialogAction = ({
children,
description,
title,
disabled,
}: Props) => {
return (
<AlertDialog>
@@ -39,9 +37,7 @@ export const DialogAction = ({
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction disabled={disabled} onClick={onClick}>
Confirm
</AlertDialogAction>
<AlertDialogAction onClick={onClick}>Confirm</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -0,0 +1,35 @@
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>
);
};

View File

@@ -1,81 +0,0 @@
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 $$;

View File

@@ -1 +0,0 @@
ALTER TABLE "destination" ADD COLUMN "schema" json;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -260,20 +260,6 @@
"when": 1725519351871,
"tag": "0036_tired_ronan",
"breakpoints": true
},
{
"idx": 37,
"version": "6",
"when": 1726988289562,
"tag": "0037_legal_namor",
"breakpoints": true
},
{
"idx": 38,
"version": "6",
"when": 1727036227151,
"tag": "0038_familiar_shockwave",
"breakpoints": true
}
]
}

View File

@@ -35,6 +35,7 @@
},
"dependencies": {
"rotating-file-stream": "3.2.3",
"@aws-sdk/client-s3": "3.515.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
@@ -129,8 +130,7 @@
"zod": "^3.23.4",
"zod-form-data": "^2.0.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"ssh2": "1.15.0"
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
@@ -167,8 +167,7 @@
"typescript": "^5.4.2",
"vite-tsconfig-paths": "4.3.2",
"vitest": "^1.6.0",
"xterm-readline": "1.1.1",
"@types/ssh2": "1.15.1"
"xterm-readline": "1.1.1"
},
"ct3aMetadata": {
"initVersion": "7.25.2"

View File

@@ -87,7 +87,6 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!application.serverId,
};
await myQueue.add(
"deployments",

View File

@@ -63,7 +63,6 @@ export default async function handler(
type: "deploy",
applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeResult.serverId,
};
await myQueue.add(
"deployments",

View File

@@ -86,7 +86,6 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
await myQueue.add(
"deployments",

View File

@@ -1,18 +0,0 @@
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;

View File

@@ -16,14 +16,12 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -100,9 +98,6 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -130,17 +125,10 @@ const Service = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
)}
>
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
@@ -164,20 +152,14 @@ const Service = (
<ShowEnvironment applicationId={applicationId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
/>
<ShowDockerLogs appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="deployments" className="w-full">

View File

@@ -10,14 +10,12 @@ import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -94,16 +92,13 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
</p>
)}
</div>
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.composeStatus} />
@@ -124,23 +119,10 @@ const Service = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
data?.serverId && data?.composeType === "stack"
? "md:grid-cols-5"
: "",
)}
>
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
{data?.composeType === "docker-compose" && (
<TabsTrigger value="environment">Environment</TabsTrigger>
)}
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
@@ -165,22 +147,19 @@ const Service = (
<ShowEnvironmentCompose composeId={composeId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
)}
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogsCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>

View File

@@ -9,16 +9,15 @@ import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/g
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -82,9 +81,7 @@ const Mariadb = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -111,17 +108,10 @@ const Mariadb = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -146,19 +136,14 @@ const Mariadb = (
<ShowMariadbEnvironment mariadbId={mariadbId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
<ShowDockerLogs appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="backups">

View File

@@ -9,16 +9,15 @@ import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/gener
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -83,9 +82,7 @@ const Mongo = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -112,17 +109,10 @@ const Mongo = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -148,19 +138,14 @@ const Mongo = (
<ShowMongoEnvironment mongoId={mongoId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
<ShowDockerLogs appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="backups">

View File

@@ -9,16 +9,15 @@ import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-gene
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -81,9 +80,7 @@ const MySql = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -111,17 +108,10 @@ const MySql = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -147,19 +137,14 @@ const MySql = (
<ShowMysqlEnvironment mysqlId={mysqlId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
<ShowDockerLogs appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="backups">

View File

@@ -9,16 +9,15 @@ import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/sho
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -82,9 +81,7 @@ const Postgresql = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -112,17 +109,10 @@ const Postgresql = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -148,19 +138,14 @@ const Postgresql = (
<ShowPostgresEnvironment postgresId={postgresId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
<ShowDockerLogs appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="backups">

View File

@@ -8,16 +8,15 @@ import { ShowGeneralRedis } from "@/components/dashboard/redis/general/show-gene
import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { RedisIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -81,9 +80,7 @@ const Redis = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -111,17 +108,10 @@ const Redis = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
)}
>
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
@@ -146,19 +136,14 @@ const Redis = (
<ShowRedisEnvironment redisId={redisId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
<ShowDockerLogs appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="advanced">

View File

@@ -1,49 +0,0 @@
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: {},
};
}

View File

@@ -1,3 +0,0 @@
export default function hola() {
return <div>hola</div>;
}

View File

@@ -16,7 +16,6 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { isAdminPresent } from "@/server/api/services/admin";
// import { IS_CLOUD } from "@/server/constants";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
@@ -221,11 +220,6 @@ const Register = ({ hasAdmin }: Props) => {
export default Register;
export async function getServerSideProps() {
// if (IS_CLOUD) {
// return {
// props: {},
// };
// }
const hasAdmin = await isAdminPresent();
if (hasAdmin) {

View File

@@ -26,7 +26,6 @@ import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry";
import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings";
import { sshRouter } from "./routers/ssh-key";
import { userRouter } from "./routers/user";
@@ -36,7 +35,6 @@ import { userRouter } from "./routers/user";
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
admin: adminRouter,
docker: dockerRouter,
@@ -68,7 +66,6 @@ export const appRouter = createTRPCRouter({
bitbucket: bitbucketRouter,
gitlab: gitlabRouter,
github: githubRouter,
server: serverRouter,
});
// export type definition of API

View File

@@ -17,7 +17,7 @@ import {
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
export const adminRouter = createTRPCRouter({
one: adminProcedure.query(async ({ ctx }) => {
one: adminProcedure.query(async () => {
const { sshPrivateKey, ...rest } = await findAdmin();
return {
haveSSH: !!sshPrivateKey,

View File

@@ -24,13 +24,10 @@ import {
cleanQueuesByApplication,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { unzipDrop } from "@/server/utils/builders/drop";
import {
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
} from "@/server/utils/docker/utils";
import {
removeDirectoryCode,
@@ -38,13 +35,10 @@ import {
} from "@/server/utils/filesystem/directory";
import {
readConfig,
readRemoteConfig,
removeTraefikConfig,
writeConfig,
writeConfigRemote,
} from "@/server/utils/traefik/application";
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware";
import { uploadFileSchema } from "@/utils/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
@@ -59,6 +53,9 @@ import {
import { removeDeployments } from "../services/deployment";
import { addNewService, checkServiceAccess } from "../services/user";
import { unzipDrop } from "@/server/utils/builders/drop";
import { uploadFileSchema } from "@/utils/schema";
export const applicationRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateApplication)
@@ -99,19 +96,9 @@ export const applicationRouter = createTRPCRouter({
reload: protectedProcedure
.input(apiReloadApplication)
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
if (application.serverId) {
await stopServiceRemote(application.serverId, input.appName);
} else {
await stopService(input.appName);
}
await stopService(input.appName);
await updateApplicationStatus(input.applicationId, "idle");
if (application.serverId) {
await startServiceRemote(application.serverId, input.appName);
} else {
await startService(input.appName);
}
await startService(input.appName);
await updateApplicationStatus(input.applicationId, "done");
return true;
}),
@@ -134,19 +121,12 @@ export const applicationRouter = createTRPCRouter({
.returning();
const cleanupOperations = [
async () => await deleteAllMiddlewares(application),
async () => deleteAllMiddlewares(application),
async () => await removeDeployments(application),
async () =>
await removeDirectoryCode(application.appName, application.serverId),
async () =>
await removeMonitoringDirectory(
application.appName,
application.serverId,
),
async () =>
await removeTraefikConfig(application.appName, application.serverId),
async () =>
await removeService(application?.appName, application.serverId),
async () => await removeDirectoryCode(application?.appName),
async () => await removeMonitoringDirectory(application?.appName),
async () => await removeTraefikConfig(application?.appName),
async () => await removeService(application?.appName),
];
for (const operation of cleanupOperations) {
@@ -162,11 +142,7 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const service = await findApplicationById(input.applicationId);
if (service.serverId) {
await stopServiceRemote(service.serverId, service.appName);
} else {
await stopService(service.appName);
}
await stopService(service.appName);
await updateApplicationStatus(input.applicationId, "idle");
return service;
@@ -176,11 +152,8 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const service = await findApplicationById(input.applicationId);
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
await startService(service.appName);
}
await startService(service.appName);
await updateApplicationStatus(input.applicationId, "done");
return service;
@@ -189,14 +162,12 @@ export const applicationRouter = createTRPCRouter({
redeploy: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Rebuild deployment",
descriptionLog: "",
type: "redeploy",
applicationType: "application",
server: !!application.serverId,
};
await myQueue.add(
"deployments",
@@ -335,15 +306,13 @@ export const applicationRouter = createTRPCRouter({
}),
deploy: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
.mutation(async ({ input }) => {
const jobData: DeploymentJob = {
applicationId: input.applicationId,
titleLog: "Manual deployment",
descriptionLog: "",
type: "deploy",
applicationType: "application",
server: !!application.serverId,
};
await myQueue.add(
"deployments",
@@ -365,15 +334,8 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication)
.query(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
let traefikConfig = null;
if (application.serverId) {
traefikConfig = await readRemoteConfig(
application.serverId,
application.appName,
);
} else {
traefikConfig = readConfig(application.appName);
}
const traefikConfig = readConfig(application.appName);
return traefikConfig;
}),
@@ -397,7 +359,7 @@ export const applicationRouter = createTRPCRouter({
});
const app = await findApplicationById(input.applicationId as string);
await unzipDrop(zipFile, app);
await unzipDrop(zipFile, app.appName);
const jobData: DeploymentJob = {
applicationId: app.applicationId,
@@ -405,7 +367,6 @@ export const applicationRouter = createTRPCRouter({
descriptionLog: "",
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
await myQueue.add(
"deployments",
@@ -421,16 +382,7 @@ export const applicationRouter = createTRPCRouter({
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
if (application.serverId) {
await writeConfigRemote(
application.serverId,
application.appName,
input.traefikConfig,
);
} else {
writeConfig(application.appName, input.traefikConfig);
}
writeConfig(application.appName, input.traefikConfig);
return true;
}),
readAppMonitoring: protectedProcedure

View File

@@ -1,6 +1,5 @@
import { lucia, validateRequest } from "@/server/auth/auth";
import { luciaToken } from "@/server/auth/token";
// import { IS_CLOUD } from "@/server/constants";
import {
apiCreateAdmin,
apiCreateUser,
@@ -36,16 +35,14 @@ export const authRouter = createTRPCRouter({
.input(apiCreateAdmin)
.mutation(async ({ ctx, input }) => {
try {
// if (!IS_CLOUD) {
const admin = await db.query.admins.findFirst({});
if (admin) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin already exists",
});
}
// }
const newAdmin = await createAdmin(input);
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(
@@ -54,7 +51,6 @@ export const authRouter = createTRPCRouter({
);
return true;
} catch (error) {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the main admin",

View File

@@ -57,7 +57,6 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
if (backup.enabled) {
removeScheduleBackup(input.backupId);
scheduleBackup(backup);
} else {
removeScheduleBackup(input.backupId);
@@ -91,6 +90,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup);
return true;
} catch (error) {
console.log(error);

View File

@@ -15,12 +15,11 @@ import {
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { createCommand } from "@/server/utils/builders/compose";
import { randomizeComposeFile } from "@/server/utils/docker/compose";
import {
addDomainToCompose,
cloneCompose,
cloneComposeRemote,
} from "@/server/utils/docker/domain";
randomizeComposeFile,
randomizeSpecificationFile,
} from "@/server/utils/docker/compose";
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
@@ -34,7 +33,7 @@ import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { findAdmin, findAdminById } from "../services/admin";
import { findAdmin } from "../services/admin";
import {
createCompose,
createComposeByTemplate,
@@ -48,7 +47,6 @@ import { removeDeploymentsByComposeId } from "../services/deployment";
import { createDomain, findDomainsByComposeId } from "../services/domain";
import { createMount } from "../services/mount";
import { findProjectById } from "../services/project";
import { findServerById } from "../services/server";
import { addNewService, checkServiceAccess } from "../services/user";
import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -132,11 +130,7 @@ export const composeRouter = createTRPCRouter({
.mutation(async ({ input }) => {
try {
const compose = await findComposeById(input.composeId);
if (compose.serverId) {
await cloneComposeRemote(compose);
} else {
await cloneCompose(compose);
}
await cloneCompose(compose);
return compose.sourceType;
} catch (err) {
throw new TRPCError({
@@ -157,7 +151,9 @@ export const composeRouter = createTRPCRouter({
.query(async ({ input }) => {
const compose = await findComposeById(input.composeId);
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, {
lineWidth: 1000,
});
@@ -166,14 +162,12 @@ export const composeRouter = createTRPCRouter({
deploy: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input }) => {
const compose = await findComposeById(input.composeId);
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: "Manual deployment",
type: "deploy",
applicationType: "compose",
descriptionLog: "",
server: !!compose.serverId,
};
await myQueue.add(
"deployments",
@@ -187,14 +181,12 @@ export const composeRouter = createTRPCRouter({
redeploy: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input }) => {
const compose = await findComposeById(input.composeId);
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: "Rebuild deployment",
type: "redeploy",
applicationType: "compose",
descriptionLog: "",
server: !!compose.serverId,
};
await myQueue.add(
"deployments",
@@ -235,8 +227,7 @@ export const composeRouter = createTRPCRouter({
const generate = await loadTemplateModule(input.id as TemplatesKeys);
const admin = await findAdminById(ctx.user.adminId);
let serverIp = admin.serverIp;
const admin = await findAdmin();
if (!admin.serverIp) {
throw new TRPCError({
@@ -248,15 +239,9 @@ export const composeRouter = createTRPCRouter({
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 { envs, mounts, domains } = generate({
serverIp: serverIp || "",
serverIp: admin.serverIp,
projectName: projectName,
});
@@ -264,7 +249,6 @@ export const composeRouter = createTRPCRouter({
...input,
composeFile: composeFile,
env: envs?.join("\n"),
serverId: input.serverId,
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,

View File

@@ -1,12 +1,10 @@
import {
apiFindAllByApplication,
apiFindAllByCompose,
apiFindAllByServer,
} from "@/server/db/schema";
import {
findAllDeploymentsByApplicationId,
findAllDeploymentsByComposeId,
findAllDeploymentsByServerId,
} from "../services/deployment";
import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -22,9 +20,4 @@ export const deploymentRouter = createTRPCRouter({
.query(async ({ input }) => {
return await findAllDeploymentsByComposeId(input.composeId);
}),
allByServer: protectedProcedure
.input(apiFindAllByServer)
.query(async ({ input }) => {
return await findAllDeploymentsByServerId(input.serverId);
}),
});

View File

@@ -10,7 +10,7 @@ import {
apiRemoveDestination,
apiUpdateDestination,
} from "@/server/db/schema";
import { execAsync } from "@/server/utils/process/execAsync";
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
import { TRPCError } from "@trpc/server";
import { findAdmin } from "../services/admin";
import {
@@ -38,31 +38,29 @@ export const destinationRouter = createTRPCRouter({
testConnection: adminProcedure
.input(apiCreateDestination)
.mutation(async ({ input }) => {
console.log(input);
// const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
// try {
// const rcloneFlags = [
// // `--s3-provider=Cloudflare`,
// `--s3-access-key-id=${accessKey}`,
// `--s3-secret-access-key=${secretAccessKey}`,
// `--s3-region=${region}`,
// `--s3-endpoint=${endpoint}`,
// "--s3-no-check-bucket",
// "--s3-force-path-style",
// ];
const connextion = buildRcloneCommand(input.json.provider, input.json);
console.log(connextion);
// const rcloneDestination = `:s3:${bucket}`;
// const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
await execAsync(connextion);
// } catch (error) {
// console.log(error);
// throw new TRPCError({
// code: "BAD_REQUEST",
// message: "Error to connect to bucket",
// cause: error,
// });
// }
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
const s3Client = new S3Client({
region: region,
...(endpoint && {
endpoint: endpoint,
}),
credentials: {
accessKeyId: accessKey,
secretAccessKey: secretAccessKey,
},
forcePathStyle: true,
});
const headBucketCommand = new HeadBucketCommand({ Bucket: bucket });
try {
await s3Client.send(headBucketCommand);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to connect to bucket",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneDestination)
@@ -99,151 +97,3 @@ 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;
}

View File

@@ -9,15 +9,9 @@ import {
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainers(input.serverId);
}),
getContainers: protectedProcedure.query(async () => {
return await getContainers();
}),
restartContainer: protectedProcedure
.input(
@@ -33,11 +27,10 @@ export const dockerRouter = createTRPCRouter({
.input(
z.object({
containerId: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getConfig(input.containerId, input.serverId);
return await getConfig(input.containerId);
}),
getContainersByAppNameMatch: protectedProcedure
@@ -47,25 +40,19 @@ export const dockerRouter = createTRPCRouter({
.union([z.literal("stack"), z.literal("docker-compose")])
.optional(),
appName: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainersByAppNameMatch(
input.appName,
input.appType,
input.serverId,
);
return await getContainersByAppNameMatch(input.appName, input.appType);
}),
getContainersByAppLabel: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainersByAppLabel(input.appName, input.serverId);
return await getContainersByAppLabel(input.appName);
}),
});

Some files were not shown because too many files have changed in this diff Show More