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

View File

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

View File

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

View File

@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
## Additional Terms for Specific Features ## Additional Terms for Specific Features
The following additional terms apply to the multi-node support, 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. - **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, 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. - **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, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service. - **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. For further inquiries or permissions, please contact us directly.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,8 @@ interface Props {
logPath: string | null; logPath: string | null;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
serverId?: string;
} }
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
const [data, setData] = useState(""); const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null); 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 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); const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => { ws.onmessage = (e) => {

View File

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

View File

@@ -175,7 +175,6 @@ export const AddDomain = ({
onClick={() => { onClick={() => {
generateDomain({ generateDomain({
appName: application?.appName || "", appName: application?.appName || "",
serverId: application?.serverId || "",
}) })
.then((domain) => { .then((domain) => {
field.onChange(domain); field.onChange(domain);
@@ -297,7 +296,11 @@ export const AddDomain = ({
</form> </form>
<DialogFooter> <DialogFooter>
<Button isLoading={isLoading} form="hook-form" type="submit"> <Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
>
{dictionary.submit} {dictionary.submit}
</Button> </Button>
</DialogFooter> </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> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={async () => { onClick={async () => {
toast.success("Deploying Application....");
await refetch();
await deploy({ await deploy({
applicationId, applicationId,
}) }).catch(() => {
.then(async () => { toast.error("Error to deploy Application");
toast.success("Application deployed succesfully"); });
await refetch();
})
.catch(() => {
toast.error("Error to deploy Application");
});
await refetch(); await refetch();
}} }}

View File

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

View File

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

View File

@@ -16,7 +16,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const DockerLogs = dynamic( export const DockerLogs = dynamic(
@@ -31,14 +30,12 @@ export const DockerLogs = dynamic(
interface Props { interface Props {
appName: string; appName: string;
serverId?: string;
} }
export const ShowDockerLogs = ({ appName, serverId }: Props) => { export const ShowDockerLogs = ({ appName }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery( const { data } = api.docker.getContainersByAppNameMatch.useQuery(
{ {
appName, appName,
serverId,
}, },
{ {
enabled: !!appName, enabled: !!appName,
@@ -65,14 +62,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <SelectTrigger>
{isLoading ? ( <SelectValue placeholder="Select a container" />
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@@ -89,7 +79,6 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
</SelectContent> </SelectContent>
</Select> </Select>
<DockerLogs <DockerLogs
serverId={serverId || ""}
id="terminal" id="terminal"
containerId={containerId || "select-a-container"} containerId={containerId || "select-a-container"}
/> />

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader, Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const DockerLogs = dynamic( export const DockerLogs = dynamic(
@@ -31,20 +30,14 @@ export const DockerLogs = dynamic(
interface Props { interface Props {
appName: string; appName: string;
serverId?: string;
appType: "stack" | "docker-compose"; appType: "stack" | "docker-compose";
} }
export const ShowDockerLogsCompose = ({ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
appName, const { data } = api.docker.getContainersByAppNameMatch.useQuery(
appType,
serverId,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{ {
appName, appName,
appType, appType,
serverId,
}, },
{ {
enabled: !!appName, enabled: !!appName,
@@ -71,14 +64,7 @@ export const ShowDockerLogsCompose = ({
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <SelectTrigger>
{isLoading ? ( <SelectValue placeholder="Select a container" />
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@@ -95,7 +81,6 @@ export const ShowDockerLogsCompose = ({
</SelectContent> </SelectContent>
</Select> </Select>
<DockerLogs <DockerLogs
serverId={serverId || ""}
id="terminal" id="terminal"
containerId={containerId || "select-a-container"} containerId={containerId || "select-a-container"}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,29 +17,13 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import {
getObjectSchema,
mergeFormValues,
providerSchemas,
providersData,
} from "../../application/domains/schema";
import { capitalize } from "lodash";
const addDestination = z.object({ const addDestination = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
@@ -54,45 +38,43 @@ type AddDestination = z.infer<typeof addDestination>;
export const AddDestination = () => { export const AddDestination = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [provider, setProviders] = useState<keyof typeof providerSchemas>("s3");
const { mutateAsync, isError, error, isLoading } = const { mutateAsync, isError, error, isLoading } =
api.destination.create.useMutation(); api.destination.create.useMutation();
const { mutateAsync: testConnection, isLoading: isLoadingConnection } = const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
api.destination.testConnection.useMutation(); api.destination.testConnection.useMutation();
const schema = providerSchemas[provider]; const form = useForm<AddDestination>({
const form = useForm<z.infer<typeof schema>>({
defaultValues: { defaultValues: {
...getObjectSchema(schema), accessKeyId: "",
bucket: "",
name: "",
region: "",
secretAccessKey: "",
endpoint: "",
}, },
resolver: zodResolver(schema), resolver: zodResolver(addDestination),
}); });
const { useEffect(() => {
register, form.reset();
handleSubmit, }, [form, form.reset, form.formState.isSubmitSuccessful]);
control,
formState: { errors },
} = form;
const onSubmit = async (data: z.infer<typeof schema>) => { const onSubmit = async (data: AddDestination) => {
// await mutateAsync({ await mutateAsync({
// accessKey: data.accessKeyId, accessKey: data.accessKeyId,
// bucket: data.bucket, bucket: data.bucket,
// endpoint: data.endpoint, endpoint: data.endpoint,
// name: data.name, name: data.name,
// region: data.region, region: data.region,
// secretAccessKey: data.secretAccessKey, secretAccessKey: data.secretAccessKey,
// }) })
// .then(async () => { .then(async () => {
// toast.success("Destination Created"); toast.success("Destination Created");
// await utils.destination.all.invalidate(); await utils.destination.all.invalidate();
// }) })
// .catch(() => { .catch(() => {
// toast.error("Error to create the Destination"); toast.error("Error to create the Destination");
// }); });
}; };
const fields = Object.keys(schema.shape);
return ( return (
<Dialog> <Dialog>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
@@ -106,117 +88,140 @@ export const AddDestination = () => {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}> <Form {...form}>
<form <form
id="hook-form-destination-add" id="hook-form-destination-add"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 " className="grid w-full gap-4 "
> >
<div className="flex flex-col gap-2"> <FormField
{fields.map((input) => ( control={form.control}
<FormField name="name"
control={control} render={({ field }) => {
key={`${provider}.${input}`} return (
name={`${provider}.${input}`} <FormItem>
render={({ field }) => { <FormLabel>Name</FormLabel>
return ( <FormControl>
<FormItem> <Input placeholder={"S3 Bucket"} {...field} />
<FormLabel>{capitalize(input)}</FormLabel> </FormControl>
<FormControl> <FormMessage />
<Input placeholder={"Value"} {...field} /> </FormItem>
</FormControl> );
<span className="text-sm font-medium text-destructive"> }}
{errors[input]?.message} />
</span>
</FormItem> <FormField
); control={form.control}
}} name="accessKeyId"
/> render={({ field }) => {
))} return (
</div> <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>
</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) => { <DialogFooter className="flex w-full flex-row !justify-between pt-3">
// const initialValues = getInitialValues(schema); <Button
// console.log(form, initialValues); isLoading={isLoadingConnection}
// const properties: any = {}; type="button"
// for (const key in form) { variant="secondary"
// const keysMatch = Object.keys(initialValues).filter( onClick={async () => {
// (k) => k === key, await testConnection({
// ); accessKey: form.getValues("accessKeyId"),
// if (keysMatch.length === 0) { bucket: form.getValues("bucket"),
// continue; endpoint: form.getValues("endpoint"),
// } name: "Test",
region: form.getValues("region"),
// properties[keysMatch[0]] = form[key] || ""; secretAccessKey: form.getValues("secretAccessKey"),
// console.log(key);
// }
// return properties;
// };
// const result = form.getValues();
// const properties = getPropertiesByForm(result);
// console.log(properties);
await testConnection({
json: {
...hola,
provider: provider,
},
// accessKey: form.getValues("accessKeyId"),
// bucket: form.getValues("bucket"),
// endpoint: form.getValues("endpoint"),
// name: "Test",
// region: form.getValues("region"),
// secretAccessKey: form.getValues("secretAccessKey"),
})
.then(async () => {
toast.success("Connection Success");
}) })
.catch(() => { .then(async () => {
toast.error("Error to connect the provider"); toast.success("Connection Success");
}); })
}} .catch(() => {
> toast.error("Error to connect the provider");
Test connection });
</Button> }}
<Button >
// isLoading={isLoading} Test connection
form="hook-form-destination-add" </Button>
type="submit" <Button
> isLoading={isLoading}
Create form="hook-form-destination-add"
</Button> type="submit"
{/* */} >
</DialogFooter> Create
</Button>
</DialogFooter>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

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

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

View File

@@ -1,3 +1,4 @@
import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
@@ -5,34 +6,334 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { Switch } from "@/components/ui/switch";
import React from "react"; import React from "react";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions"; import {
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions"; DropdownMenu,
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup"; 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"; import { UpdateServer } from "./web-server/update-server";
interface Props { export const WebServer = () => {
className?: string; const { data, refetch } = api.admin.one.useQuery();
} const { mutateAsync: reloadServer, isLoading } =
export const WebServer = ({ className }: Props) => { api.settings.reloadServer.useMutation();
const { data } = api.admin.one.useQuery(); 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 { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { mutateAsync: updateDockerCleanup } =
api.settings.updateDockerCleanup.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
return ( return (
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}> <Card className="rounded-lg w-full bg-transparent">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Web server settings</CardTitle> <CardTitle className="text-xl">Web server settings</CardTitle>
<CardDescription>Reload or clean the web server.</CardDescription> <CardDescription>Reload or clean the web server.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 "> <CardContent className="flex flex-col gap-4">
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<ShowDokployActions /> <DropdownMenu>
<ShowTraefikActions /> <DropdownMenuTrigger asChild disabled={isLoading}>
<ShowStorageActions /> <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 /> <UpdateServer />
</div> </div>
@@ -44,8 +345,25 @@ export const WebServer = ({ className }: Props) => {
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Version: {dokployVersion} Version: {dokployVersion}
</span> </span>
<div className="flex items-center gap-4">
<Switch
checked={data?.enableDockerCleanup}
onCheckedChange={async (e) => {
await updateDockerCleanup({
enableDockerCleanup: e,
})
.then(async () => {
toast.success("Docker Cleanup Enabled");
})
.catch(() => {
toast.error("Docker Cleanup Error");
});
<ToggleDockerCleanup /> refetch();
}}
/>
<Label className="text-primary">Daily Docker Cleanup</Label>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -17,7 +17,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -35,14 +34,12 @@ const Terminal = dynamic(
interface Props { interface Props {
appName: string; appName: string;
children?: React.ReactNode; children?: React.ReactNode;
serverId?: string;
} }
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => { export const DockerTerminalModal = ({ children, appName }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery( const { data } = api.docker.getContainersByAppNameMatch.useQuery(
{ {
appName, appName,
serverId,
}, },
{ {
enabled: !!appName, enabled: !!appName,
@@ -68,14 +65,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <SelectTrigger>
{isLoading ? ( <SelectValue placeholder="Select a container" />
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@@ -92,7 +82,6 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
</SelectContent> </SelectContent>
</Select> </Select>
<Terminal <Terminal
serverId={serverId || ""}
id="terminal" id="terminal"
containerId={containerId || "select-a-container"} containerId={containerId || "select-a-container"}
/> />

View File

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

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, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -36,14 +35,12 @@ export const DockerLogsId = dynamic(
interface Props { interface Props {
appName: string; appName: string;
children?: React.ReactNode; children?: React.ReactNode;
serverId?: string;
} }
export const ShowModalLogs = ({ appName, children, serverId }: Props) => { export const ShowModalLogs = ({ appName, children }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery( const { data } = api.docker.getContainersByAppLabel.useQuery(
{ {
appName, appName,
serverId,
}, },
{ {
enabled: !!appName, enabled: !!appName,
@@ -75,14 +72,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
<Label>Select a container to view logs</Label> <Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}> <Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger> <SelectTrigger>
{isLoading ? ( <SelectValue placeholder="Select a container" />
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@@ -98,11 +88,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
<DockerLogsId <DockerLogsId id="terminal" containerId={containerId || ""} />
id="terminal"
containerId={containerId || ""}
serverId={serverId}
/>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </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 { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -7,27 +8,79 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { RemoveSSHPrivateKey } from "./remove-ssh-private-key";
const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), { const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), {
ssr: false, ssr: false,
}); });
const addSSHPrivateKey = z.object({
sshPrivateKey: z
.string({
required_error: "SSH private key is required",
})
.min(1, "SSH private key is required"),
});
type AddSSHPrivateKey = z.infer<typeof addSSHPrivateKey>;
interface Props { interface Props {
children?: React.ReactNode; children?: React.ReactNode;
serverId: string;
} }
export const TerminalModal = ({ children, serverId }: Props) => { export const TerminalModal = ({ children }: Props) => {
const { data } = api.server.one.useQuery( const { data, refetch } = api.admin.one.useQuery();
{ const [user, setUser] = useState("root");
serverId, const [terminalUser, setTerminalUser] = useState("root");
},
{ enabled: !!serverId },
);
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 ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -39,14 +92,75 @@ export const TerminalModal = ({ children, serverId }: Props) => {
</DropdownMenuItem> </DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl"> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogHeader className="flex flex-col gap-1"> <DialogHeader className="flex flex-row justify-between pt-4">
<DialogTitle>Terminal ({data?.name})</DialogTitle> <div>
<DialogDescription>Easy way to access the server</DialogDescription> <DialogTitle>Terminal</DialogTitle>
<DialogDescription>Easy way to access the server</DialogDescription>
</div>
{data?.haveSSH && (
<div>
<RemoveSSHPrivateKey />
</div>
)}
</DialogHeader> </DialogHeader>
{!data?.haveSSH ? (
<div>
<div className="flex flex-col gap-4">
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="grid w-full">
<FormField
control={form.control}
name="sshPrivateKey"
render={({ field }) => {
return (
<FormItem>
<FormLabel>SSH Private Key</FormLabel>
<FormDescription>
In order to access the server you need to add an
ssh private key
</FormDescription>
<FormControl>
<Textarea
placeholder={
"-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----"
}
className="h-32"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save
</Button>
</div>
</form>
</Form>
</div>
</div>
) : (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label>Log in as</Label>
<div className="flex flex-row gap-4">
<Input value={user} onChange={(e) => setUser(e.target.value)} />
<Button onClick={() => setTerminalUser(user)}>Login</Button>
</div>
</div>
<div className="flex flex-col gap-4"> <Terminal id="terminal" userSSH={terminalUser} />
<Terminal id="terminal" serverId={serverId} /> </div>
</div> )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

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

View File

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

View File

@@ -11,11 +11,10 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
interface Props { interface Props {
title?: string | React.ReactNode; title?: string;
description?: string | React.ReactNode; description?: string;
onClick: () => void; onClick: () => void;
children?: React.ReactNode; children?: React.ReactNode;
disabled?: boolean;
} }
export const DialogAction = ({ export const DialogAction = ({
@@ -23,7 +22,6 @@ export const DialogAction = ({
children, children,
description, description,
title, title,
disabled,
}: Props) => { }: Props) => {
return ( return (
<AlertDialog> <AlertDialog>
@@ -39,9 +37,7 @@ export const DialogAction = ({
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction disabled={disabled} onClick={onClick}> <AlertDialogAction onClick={onClick}>Confirm</AlertDialogAction>
Confirm
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </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, "when": 1725519351871,
"tag": "0036_tired_ronan", "tag": "0036_tired_ronan",
"breakpoints": true "breakpoints": true
},
{
"idx": 37,
"version": "6",
"when": 1726988289562,
"tag": "0037_legal_namor",
"breakpoints": true
},
{
"idx": 38,
"version": "6",
"when": 1727036227151,
"tag": "0038_familiar_shockwave",
"breakpoints": true
} }
] ]
} }

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -9,16 +9,15 @@ import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/g
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb"; import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { MariadbIcon } from "@/components/icons/data-tools-icons"; import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -82,9 +81,7 @@ const Mariadb = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">
{data?.description} {data?.description}
@@ -111,17 +108,10 @@ const Mariadb = (
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && ( <TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger> <TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -146,19 +136,14 @@ const Mariadb = (
<ShowMariadbEnvironment mariadbId={mariadbId} /> <ShowMariadbEnvironment mariadbId={mariadbId} />
</div> </div>
</TabsContent> </TabsContent>
{!data?.serverId && ( <TabsContent value="monitoring">
<TabsContent value="monitoring"> <div className="flex flex-col gap-4 pt-2.5">
<div className="flex flex-col gap-4 pt-2.5"> <DockerMonitoring appName={data?.appName || ""} />
<DockerMonitoring appName={data?.appName || ""} /> </div>
</div> </TabsContent>
</TabsContent>
)}
<TabsContent value="logs"> <TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs <ShowDockerLogs appName={data?.appName || ""} />
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="backups"> <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 { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show"; import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { MongodbIcon } from "@/components/icons/data-tools-icons"; import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -83,9 +82,7 @@ const Mongo = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">
{data?.description} {data?.description}
@@ -112,17 +109,10 @@ const Mongo = (
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && ( <TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger> <TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -148,19 +138,14 @@ const Mongo = (
<ShowMongoEnvironment mongoId={mongoId} /> <ShowMongoEnvironment mongoId={mongoId} />
</div> </div>
</TabsContent> </TabsContent>
{!data?.serverId && ( <TabsContent value="monitoring">
<TabsContent value="monitoring"> <div className="flex flex-col gap-4 pt-2.5">
<div className="flex flex-col gap-4 pt-2.5"> <DockerMonitoring appName={data?.appName || ""} />
<DockerMonitoring appName={data?.appName || ""} /> </div>
</div> </TabsContent>
</TabsContent>
)}
<TabsContent value="logs"> <TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs <ShowDockerLogs appName={data?.appName || ""} />
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="backups"> <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 { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql"; import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { MysqlIcon } from "@/components/icons/data-tools-icons"; import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -81,9 +80,7 @@ const MySql = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">
{data?.description} {data?.description}
@@ -111,17 +108,10 @@ const MySql = (
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && ( <TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger> <TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -147,19 +137,14 @@ const MySql = (
<ShowMysqlEnvironment mysqlId={mysqlId} /> <ShowMysqlEnvironment mysqlId={mysqlId} />
</div> </div>
</TabsContent> </TabsContent>
{!data?.serverId && ( <TabsContent value="monitoring">
<TabsContent value="monitoring"> <div className="flex flex-col gap-4 pt-2.5">
<div className="flex flex-col gap-4 pt-2.5"> <DockerMonitoring appName={data?.appName || ""} />
<DockerMonitoring appName={data?.appName || ""} /> </div>
</div> </TabsContent>
</TabsContent>
)}
<TabsContent value="logs"> <TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs <ShowDockerLogs appName={data?.appName || ""} />
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="backups"> <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 { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres"; import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons"; import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -82,9 +81,7 @@ const Postgresql = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">
{data?.description} {data?.description}
@@ -112,17 +109,10 @@ const Postgresql = (
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && ( <TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger> <TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -148,19 +138,14 @@ const Postgresql = (
<ShowPostgresEnvironment postgresId={postgresId} /> <ShowPostgresEnvironment postgresId={postgresId} />
</div> </div>
</TabsContent> </TabsContent>
{!data?.serverId && ( <TabsContent value="monitoring">
<TabsContent value="monitoring"> <div className="flex flex-col gap-4 pt-2.5">
<div className="flex flex-col gap-4 pt-2.5"> <DockerMonitoring appName={data?.appName || ""} />
<DockerMonitoring appName={data?.appName || ""} /> </div>
</div> </TabsContent>
</TabsContent>
)}
<TabsContent value="logs"> <TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs <ShowDockerLogs appName={data?.appName || ""} />
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="backups"> <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 { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
import { UpdateRedis } from "@/components/dashboard/redis/update-redis"; import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { RedisIcon } from "@/components/icons/data-tools-icons"; import { RedisIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth"; import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -81,9 +80,7 @@ const Redis = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">
{data?.description} {data?.description}
@@ -111,17 +108,10 @@ const Redis = (
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-y-scroll justify-start">
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
)}
>
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && ( <TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger> <TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList> </TabsList>
@@ -146,19 +136,14 @@ const Redis = (
<ShowRedisEnvironment redisId={redisId} /> <ShowRedisEnvironment redisId={redisId} />
</div> </div>
</TabsContent> </TabsContent>
{!data?.serverId && ( <TabsContent value="monitoring">
<TabsContent value="monitoring"> <div className="flex flex-col gap-4 pt-2.5">
<div className="flex flex-col gap-4 pt-2.5"> <DockerMonitoring appName={data?.appName || ""} />
<DockerMonitoring appName={data?.appName || ""} /> </div>
</div> </TabsContent>
</TabsContent>
)}
<TabsContent value="logs"> <TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs <ShowDockerLogs appName={data?.appName || ""} />
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="advanced"> <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"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { isAdminPresent } from "@/server/api/services/admin"; import { isAdminPresent } from "@/server/api/services/admin";
// import { IS_CLOUD } from "@/server/constants";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
@@ -221,11 +220,6 @@ const Register = ({ hasAdmin }: Props) => {
export default Register; export default Register;
export async function getServerSideProps() { export async function getServerSideProps() {
// if (IS_CLOUD) {
// return {
// props: {},
// };
// }
const hasAdmin = await isAdminPresent(); const hasAdmin = await isAdminPresent();
if (hasAdmin) { if (hasAdmin) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import {
apiRemoveDestination, apiRemoveDestination,
apiUpdateDestination, apiUpdateDestination,
} from "@/server/db/schema"; } 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 { TRPCError } from "@trpc/server";
import { findAdmin } from "../services/admin"; import { findAdmin } from "../services/admin";
import { import {
@@ -38,31 +38,29 @@ export const destinationRouter = createTRPCRouter({
testConnection: adminProcedure testConnection: adminProcedure
.input(apiCreateDestination) .input(apiCreateDestination)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
console.log(input); const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
// const { secretAccessKey, bucket, region, endpoint, accessKey } = input; const s3Client = new S3Client({
// try { region: region,
// const rcloneFlags = [ ...(endpoint && {
// // `--s3-provider=Cloudflare`, endpoint: endpoint,
// `--s3-access-key-id=${accessKey}`, }),
// `--s3-secret-access-key=${secretAccessKey}`, credentials: {
// `--s3-region=${region}`, accessKeyId: accessKey,
// `--s3-endpoint=${endpoint}`, secretAccessKey: secretAccessKey,
// "--s3-no-check-bucket", },
// "--s3-force-path-style", forcePathStyle: true,
// ]; });
const connextion = buildRcloneCommand(input.json.provider, input.json); const headBucketCommand = new HeadBucketCommand({ Bucket: bucket });
console.log(connextion);
// const rcloneDestination = `:s3:${bucket}`; try {
// const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; await s3Client.send(headBucketCommand);
await execAsync(connextion); } catch (error) {
// } catch (error) { throw new TRPCError({
// console.log(error); code: "BAD_REQUEST",
// throw new TRPCError({ message: "Error to connect to bucket",
// code: "BAD_REQUEST", cause: error,
// message: "Error to connect to bucket", });
// cause: error, }
// });
// }
}), }),
one: protectedProcedure one: protectedProcedure
.input(apiFindOneDestination) .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"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const dockerRouter = createTRPCRouter({ export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure getContainers: protectedProcedure.query(async () => {
.input( return await getContainers();
z.object({ }),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getContainers(input.serverId);
}),
restartContainer: protectedProcedure restartContainer: protectedProcedure
.input( .input(
@@ -33,11 +27,10 @@ export const dockerRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
containerId: z.string().min(1), containerId: z.string().min(1),
serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
return await getConfig(input.containerId, input.serverId); return await getConfig(input.containerId);
}), }),
getContainersByAppNameMatch: protectedProcedure getContainersByAppNameMatch: protectedProcedure
@@ -47,25 +40,19 @@ export const dockerRouter = createTRPCRouter({
.union([z.literal("stack"), z.literal("docker-compose")]) .union([z.literal("stack"), z.literal("docker-compose")])
.optional(), .optional(),
appName: z.string().min(1), appName: z.string().min(1),
serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
return await getContainersByAppNameMatch( return await getContainersByAppNameMatch(input.appName, input.appType);
input.appName,
input.appType,
input.serverId,
);
}), }),
getContainersByAppLabel: protectedProcedure getContainersByAppLabel: protectedProcedure
.input( .input(
z.object({ z.object({
appName: z.string().min(1), appName: z.string().min(1),
serverId: z.string().optional(),
}), }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
return await getContainersByAppLabel(input.appName, input.serverId); return await getContainersByAppLabel(input.appName);
}), }),
}); });

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