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