mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #469 from Dokploy/139-multi-server-feature
139 multi server feature
This commit is contained in:
@@ -27,7 +27,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl apache2-utils && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only the necessary files
|
# Copy only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
@@ -42,7 +42,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
|
|||||||
|
|
||||||
|
|
||||||
# Install docker
|
# Install docker
|
||||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
|
||||||
|
|
||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
@@ -55,4 +55,4 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD [ "pnpm", "start" ]
|
CMD [ "pnpm", "start" ]
|
||||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## Additional Terms for Specific Features
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { APPLICATIONS_PATH } from "@/server/constants";
|
import { paths } from "@/server/constants";
|
||||||
|
const { APPLICATIONS_PATH } = paths();
|
||||||
|
import type { ApplicationNested } from "@/server/utils/builders";
|
||||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
@@ -11,11 +13,84 @@ if (typeof window === "undefined") {
|
|||||||
globalThis.FileList = undici.FileList as any;
|
globalThis.FileList = undici.FileList as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseApp: ApplicationNested = {
|
||||||
|
applicationId: "",
|
||||||
|
applicationStatus: "done",
|
||||||
|
appName: "",
|
||||||
|
autoDeploy: true,
|
||||||
|
serverId: "",
|
||||||
|
branch: null,
|
||||||
|
dockerBuildStage: "",
|
||||||
|
buildArgs: null,
|
||||||
|
buildPath: "/",
|
||||||
|
gitlabPathNamespace: "",
|
||||||
|
buildType: "nixpacks",
|
||||||
|
bitbucketBranch: "",
|
||||||
|
bitbucketBuildPath: "",
|
||||||
|
bitbucketId: "",
|
||||||
|
bitbucketRepository: "",
|
||||||
|
bitbucketOwner: "",
|
||||||
|
githubId: "",
|
||||||
|
gitlabProjectId: 0,
|
||||||
|
gitlabBranch: "",
|
||||||
|
gitlabBuildPath: "",
|
||||||
|
gitlabId: "",
|
||||||
|
gitlabRepository: "",
|
||||||
|
gitlabOwner: "",
|
||||||
|
command: null,
|
||||||
|
cpuLimit: null,
|
||||||
|
cpuReservation: null,
|
||||||
|
createdAt: "",
|
||||||
|
customGitBranch: "",
|
||||||
|
customGitBuildPath: "",
|
||||||
|
customGitSSHKeyId: null,
|
||||||
|
customGitUrl: "",
|
||||||
|
description: "",
|
||||||
|
dockerfile: null,
|
||||||
|
dockerImage: null,
|
||||||
|
dropBuildPath: null,
|
||||||
|
enabled: null,
|
||||||
|
env: null,
|
||||||
|
healthCheckSwarm: null,
|
||||||
|
labelsSwarm: null,
|
||||||
|
memoryLimit: null,
|
||||||
|
memoryReservation: null,
|
||||||
|
modeSwarm: null,
|
||||||
|
mounts: [],
|
||||||
|
name: "",
|
||||||
|
networkSwarm: null,
|
||||||
|
owner: null,
|
||||||
|
password: null,
|
||||||
|
placementSwarm: null,
|
||||||
|
ports: [],
|
||||||
|
projectId: "",
|
||||||
|
publishDirectory: null,
|
||||||
|
redirects: [],
|
||||||
|
refreshToken: "",
|
||||||
|
registry: null,
|
||||||
|
registryId: null,
|
||||||
|
replicas: 1,
|
||||||
|
repository: null,
|
||||||
|
restartPolicySwarm: null,
|
||||||
|
rollbackConfigSwarm: null,
|
||||||
|
security: [],
|
||||||
|
sourceType: "git",
|
||||||
|
subtitle: null,
|
||||||
|
title: null,
|
||||||
|
updateConfigSwarm: null,
|
||||||
|
username: null,
|
||||||
|
dockerContextPath: null,
|
||||||
|
};
|
||||||
|
//
|
||||||
vi.mock("@/server/constants", () => ({
|
vi.mock("@/server/constants", () => ({
|
||||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
paths: () => ({
|
||||||
|
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}),
|
||||||
|
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("unzipDrop using real zip files", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
|
// const { APPLICATIONS_PATH } = paths();
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
@@ -25,39 +100,42 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root folder", async () => {
|
it("should correctly extract a zip with a single root folder", async () => {
|
||||||
const appName = "single-file";
|
baseApp.appName = "single-file";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "single-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
const appName = "folderwithfile";
|
baseApp.appName = "folderwithfile";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "folderwithfile";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with multiple root folders", async () => {
|
it("should correctly extract a zip with multiple root folders", async () => {
|
||||||
const appName = "two-folders";
|
baseApp.appName = "two-folders";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "two-folders";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
@@ -66,13 +144,14 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root with a file", async () => {
|
it("should correctly extract a zip with a single root with a file", async () => {
|
||||||
const appName = "nested";
|
baseApp.appName = "nested";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "nested";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
@@ -82,13 +161,14 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root with a folder", async () => {
|
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||||
const appName = "folder-with-sibling-file";
|
baseApp.appName = "folder-with-sibling-file";
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
// const appName = "folder-with-sibling-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||||
|
|
||||||
const zipBuffer = zip.toBuffer();
|
const zipBuffer = zip.toBuffer();
|
||||||
const file = new File([zipBuffer], "single.zip");
|
const file = new File([zipBuffer], "single.zip");
|
||||||
await unzipDrop(file, appName);
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const baseApp: ApplicationNested = {
|
|||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
|||||||
@@ -278,6 +278,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<div className="px-4">
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Changing settings such as placements may cause the logs/monitoring
|
||||||
|
to be unavailable.
|
||||||
|
</AlertBlock>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { File } from "lucide-react";
|
import { File, Loader2 } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -15,7 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.readTraefikConfig.useQuery(
|
const { data, isLoading } = api.application.readTraefikConfig.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
@@ -35,7 +35,12 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
{data === null ? (
|
{isLoading ? (
|
||||||
|
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center justify-center min-h-[10vh]">
|
||||||
|
Loading...
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
</span>
|
||||||
|
) : !data ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<File className="size-8 text-muted-foreground" />
|
<File className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertTriangle, Package } from "lucide-react";
|
import { Package } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
import { DeleteVolume } from "./delete-volume";
|
import { DeleteVolume } from "./delete-volume";
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ interface Props {
|
|||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
|
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
{ applicationId },
|
{ applicationId },
|
||||||
{
|
{
|
||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
refetchInterval: 5000,
|
refetchInterval: 1000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
@@ -110,6 +110,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
open={activeLog !== null}
|
open={activeLog !== null}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog}
|
logPath={activeLog}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export const AddDomain = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
generateDomain({
|
generateDomain({
|
||||||
appName: application?.appName || "",
|
appName: application?.appName || "",
|
||||||
|
serverId: application?.serverId || "",
|
||||||
})
|
})
|
||||||
.then((domain) => {
|
.then((domain) => {
|
||||||
field.onChange(domain);
|
field.onChange(domain);
|
||||||
@@ -296,11 +297,7 @@ export const AddDomain = ({
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{dictionary.submit}
|
{dictionary.submit}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -45,14 +45,17 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
toast.success("Deploying Application....");
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
await deploy({
|
await deploy({
|
||||||
applicationId,
|
applicationId,
|
||||||
}).catch(() => {
|
})
|
||||||
toast.error("Error to deploy Application");
|
.then(async () => {
|
||||||
});
|
toast.success("Application deployed succesfully");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to deploy Application");
|
||||||
|
});
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!zip}
|
disabled={!zip || isLoading}
|
||||||
>
|
>
|
||||||
Deploy{" "}
|
Deploy{" "}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<StopApplication applicationId={applicationId} />
|
<StopApplication applicationId={applicationId} />
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
export const DockerLogs = dynamic(
|
export const DockerLogs = dynamic(
|
||||||
@@ -30,12 +31,14 @@ export const DockerLogs = dynamic(
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogs = ({ appName }: Props) => {
|
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -62,7 +65,14 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a container" />
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a container" />
|
||||||
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -79,6 +89,7 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
id="terminal"
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
|
serverId?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
export const ShowDeploymentCompose = ({
|
||||||
|
logPath,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -21,7 +27,7 @@ export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeploymentCompose
|
<ShowDeploymentCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
open={activeLog !== null}
|
open={activeLog !== null}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog}
|
logPath={activeLog}
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ export const AddDomainCompose = ({
|
|||||||
isLoading={isLoadingGenerate}
|
isLoading={isLoadingGenerate}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
generateDomain({
|
generateDomain({
|
||||||
|
serverId: compose?.serverId || "",
|
||||||
appName: compose?.appName || "",
|
appName: compose?.appName || "",
|
||||||
})
|
})
|
||||||
.then((domain) => {
|
.then((domain) => {
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
<StopCompose composeId={composeId} />
|
<StopCompose composeId={composeId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
@@ -116,6 +119,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
{data?.server?.name}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader, Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
export const DockerLogs = dynamic(
|
export const DockerLogs = dynamic(
|
||||||
@@ -30,14 +31,20 @@ export const DockerLogs = dynamic(
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
appType: "stack" | "docker-compose";
|
appType: "stack" | "docker-compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
export const ShowDockerLogsCompose = ({
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
appName,
|
||||||
|
appType,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
appType,
|
appType,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -64,7 +71,14 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a container" />
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a container" />
|
||||||
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -81,6 +95,7 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
id="terminal"
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,23 +17,27 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerMonitoring } from "../../monitoring/docker/show";
|
import { DockerMonitoring } from "../../monitoring/docker/show";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
appType: "stack" | "docker-compose";
|
appType: "stack" | "docker-compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowMonitoringCompose = ({
|
export const ShowMonitoringCompose = ({
|
||||||
appName,
|
appName,
|
||||||
appType = "stack",
|
appType = "stack",
|
||||||
|
serverId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName: appName,
|
appName: appName,
|
||||||
appType,
|
appType,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -46,7 +50,7 @@ export const ShowMonitoringCompose = ({
|
|||||||
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
|
||||||
const { mutateAsync: restart, isLoading } =
|
const { mutateAsync: restart, isLoading: isRestarting } =
|
||||||
api.docker.restartContainer.useMutation();
|
api.docker.restartContainer.useMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,7 +81,14 @@ export const ShowMonitoringCompose = ({
|
|||||||
value={containerAppName}
|
value={containerAppName}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a container" />
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a container" />
|
||||||
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -95,7 +106,7 @@ export const ShowMonitoringCompose = ({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isRestarting}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!containerId) return;
|
if (!containerId) return;
|
||||||
toast.success(`Restarting container ${containerAppName}`);
|
toast.success(`Restarting container ${containerAppName}`);
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import { api } from "@/utils/api";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowContainerConfig = ({ containerId }: Props) => {
|
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||||
const { data } = api.docker.getConfig.useQuery(
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
{
|
{
|
||||||
containerId,
|
containerId,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!containerId,
|
enabled: !!containerId,
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import "@xterm/xterm/css/xterm.css";
|
|||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
containerId: string;
|
containerId: string;
|
||||||
|
serverId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
export const DockerLogsId: React.FC<Props> = ({
|
||||||
|
id,
|
||||||
|
containerId,
|
||||||
|
serverId,
|
||||||
|
}) => {
|
||||||
const [term, setTerm] = React.useState<Terminal>();
|
const [term, setTerm] = React.useState<Terminal>();
|
||||||
const [lines, setLines] = React.useState<number>(40);
|
const [lines, setLines] = React.useState<number>(40);
|
||||||
|
|
||||||
@@ -38,7 +43,7 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}`;
|
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
|
|||||||
@@ -22,9 +22,14 @@ export const DockerLogsId = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
export const ShowDockerModalLogs = ({
|
||||||
|
containerId,
|
||||||
|
children,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -41,7 +46,11 @@ export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
|||||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
<DockerLogsId
|
||||||
|
id="terminal"
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -114,11 +114,20 @@ export const columns: ColumnDef<Container>[] = [
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<ShowDockerModalLogs containerId={container.containerId}>
|
<ShowDockerModalLogs
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId}
|
||||||
|
>
|
||||||
View Logs
|
View Logs
|
||||||
</ShowDockerModalLogs>
|
</ShowDockerModalLogs>
|
||||||
<ShowContainerConfig containerId={container.containerId} />
|
<ShowContainerConfig
|
||||||
<DockerTerminalModal containerId={container.containerId}>
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId || ""}
|
||||||
|
/>
|
||||||
|
<DockerTerminalModal
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId || ""}
|
||||||
|
>
|
||||||
Terminal
|
Terminal
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -34,8 +34,15 @@ export type Container = NonNullable<
|
|||||||
RouterOutputs["docker"]["getContainers"]
|
RouterOutputs["docker"]["getContainers"]
|
||||||
>[0];
|
>[0];
|
||||||
|
|
||||||
export const ShowContainers = () => {
|
interface Props {
|
||||||
const { data, isLoading } = api.docker.getContainers.useQuery();
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowContainers = ({ serverId }: Props) => {
|
||||||
|
const { data, isLoading } = api.docker.getContainers.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||||
[],
|
[],
|
||||||
@@ -103,83 +110,99 @@ export const ShowContainers = () => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
{isLoading ? (
|
||||||
<TableHeader>
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
<TableRow key={headerGroup.id}>
|
Loading...
|
||||||
{headerGroup.headers.map((header) => {
|
</span>
|
||||||
return (
|
</div>
|
||||||
<TableHead key={header.id}>
|
) : data?.length === 0 ? (
|
||||||
{header.isPlaceholder
|
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
? null
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
: flexRender(
|
No results.
|
||||||
header.column.columnDef.header,
|
</span>
|
||||||
header.getContext(),
|
</div>
|
||||||
)}
|
) : (
|
||||||
</TableHead>
|
<Table>
|
||||||
);
|
<TableHeader>
|
||||||
})}
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
</TableRow>
|
<TableRow key={headerGroup.id}>
|
||||||
))}
|
{headerGroup.headers.map((header) => {
|
||||||
</TableHeader>
|
return (
|
||||||
<TableBody>
|
<TableHead key={header.id}>
|
||||||
{table.getRowModel().rows?.length ? (
|
{header.isPlaceholder
|
||||||
table.getRowModel().rows.map((row) => (
|
? null
|
||||||
<TableRow
|
: flexRender(
|
||||||
key={row.id}
|
header.column.columnDef.header,
|
||||||
data-state={row.getIsSelected() && "selected"}
|
header.getContext(),
|
||||||
>
|
)}
|
||||||
{row.getVisibleCells().map((cell) => (
|
</TableHead>
|
||||||
<TableCell key={cell.id}>
|
);
|
||||||
{flexRender(
|
})}
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext(),
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
) : (
|
</TableHeader>
|
||||||
<TableRow>
|
<TableBody>
|
||||||
<TableCell
|
{table?.getRowModel()?.rows?.length ? (
|
||||||
colSpan={columns.length}
|
table.getRowModel().rows.map((row) => (
|
||||||
className="h-24 text-center"
|
<TableRow
|
||||||
>
|
key={row.id}
|
||||||
{isLoading ? (
|
data-state={row.getIsSelected() && "selected"}
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
>
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
{row.getVisibleCells().map((cell) => (
|
||||||
Loading...
|
<TableCell key={cell.id}>
|
||||||
</span>
|
{flexRender(
|
||||||
</div>
|
cell.column.columnDef.cell,
|
||||||
) : (
|
cell.getContext(),
|
||||||
<>No results.</>
|
)}
|
||||||
)}
|
</TableCell>
|
||||||
</TableCell>
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
))
|
||||||
</TableBody>
|
) : (
|
||||||
</Table>
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>No results.</>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end space-x-2 py-4">
|
{data && data?.length > 0 && (
|
||||||
<div className="space-x-2 flex flex-wrap">
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
<Button
|
<div className="space-x-2 flex flex-wrap">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => table.previousPage()}
|
size="sm"
|
||||||
disabled={!table.getCanPreviousPage()}
|
onClick={() => table.previousPage()}
|
||||||
>
|
disabled={!table.getCanPreviousPage()}
|
||||||
Previous
|
>
|
||||||
</Button>
|
Previous
|
||||||
<Button
|
</Button>
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => table.nextPage()}
|
size="sm"
|
||||||
disabled={!table.getCanNextPage()}
|
onClick={() => table.nextPage()}
|
||||||
>
|
disabled={!table.getCanNextPage()}
|
||||||
Next
|
>
|
||||||
</Button>
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ const Terminal = dynamic(
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
|
serverId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
export const DockerTerminalModal = ({
|
||||||
|
children,
|
||||||
|
containerId,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -40,7 +45,11 @@ export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Terminal id="terminal" containerId={containerId} />
|
<Terminal
|
||||||
|
id="terminal"
|
||||||
|
containerId={containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import { AttachAddon } from "@xterm/addon-attach";
|
|||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
containerId: string;
|
containerId: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
export const DockerTerminal: React.FC<Props> = ({
|
||||||
|
id,
|
||||||
|
containerId,
|
||||||
|
serverId,
|
||||||
|
}) => {
|
||||||
const termRef = useRef(null);
|
const termRef = useRef(null);
|
||||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,7 +38,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}`;
|
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer<
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowTraefikFile = ({ path }: Props) => {
|
export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||||
const { data, refetch } = api.settings.readTraefikFile.useQuery(
|
const {
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
isLoading: isLoadingFile,
|
||||||
|
} = api.settings.readTraefikFile.useQuery(
|
||||||
{
|
{
|
||||||
path,
|
path,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!path,
|
enabled: !!path,
|
||||||
@@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
form.reset({
|
||||||
form.reset({
|
traefikConfig: data || "",
|
||||||
traefikConfig: data || "",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
||||||
@@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
traefikConfig: data.traefikConfig,
|
traefikConfig: data.traefikConfig,
|
||||||
path,
|
path,
|
||||||
|
serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Traefik config Updated");
|
toast.success("Traefik config Updated");
|
||||||
@@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
className="w-full relative z-[5]"
|
className="w-full relative z-[5]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col overflow-auto">
|
<div className="flex flex-col overflow-auto">
|
||||||
<FormField
|
{isLoadingFile ? (
|
||||||
control={form.control}
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
name="traefikConfig"
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
render={({ field }) => (
|
Loading...
|
||||||
<FormItem className="relative">
|
</span>
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||||
<FormDescription className="break-all">
|
</div>
|
||||||
{path}
|
) : (
|
||||||
</FormDescription>
|
<FormField
|
||||||
<FormControl>
|
control={form.control}
|
||||||
<CodeEditor
|
name="traefikConfig"
|
||||||
lineWrapping
|
render={({ field }) => (
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
<FormItem className="relative">
|
||||||
placeholder={`http:
|
<FormLabel>Traefik config</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
{path}
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
router-name:
|
router-name:
|
||||||
rule: Host('domain.com')
|
rule: Host('domain.com')
|
||||||
@@ -116,31 +130,36 @@ routers:
|
|||||||
tls: false
|
tls: false
|
||||||
middlewares: []
|
middlewares: []
|
||||||
`}
|
`}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||||
<Button
|
<Button
|
||||||
className="shadow-sm"
|
className="shadow-sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setCanEdit(!canEdit);
|
setCanEdit(!canEdit);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{canEdit ? "Unlock" : "Lock"}
|
{canEdit ? "Unlock" : "Lock"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button isLoading={isLoading} disabled={canEdit} type="submit">
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={canEdit || isLoading}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,47 @@
|
|||||||
import React from "react";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
import { Tree } from "@/components/ui/file-tree";
|
import { Tree } from "@/components/ui/file-tree";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { FileIcon, Folder, Workflow } from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { ShowTraefikFile } from "./show-traefik-file";
|
import { ShowTraefikFile } from "./show-traefik-file";
|
||||||
|
|
||||||
export const ShowTraefikSystem = () => {
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||||
const [file, setFile] = React.useState<null | string>(null);
|
const [file, setFile] = React.useState<null | string>(null);
|
||||||
|
|
||||||
const { data: directories } = api.settings.readDirectories.useQuery();
|
const {
|
||||||
|
data: directories,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
|
} = api.settings.readDirectories.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("mt-6 md:grid gap-4")}>
|
<div className={cn("mt-6 md:grid gap-4")}>
|
||||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="w-full">
|
||||||
|
{error?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{directories?.length === 0 && (
|
{directories?.length === 0 && (
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
@@ -34,7 +61,7 @@ export const ShowTraefikSystem = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{file ? (
|
{file ? (
|
||||||
<ShowTraefikFile path={file} />
|
<ShowTraefikFile path={file} serverId={serverId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<StopMariadb mariadbId={mariadbId} />
|
<StopMariadb mariadbId={mariadbId} />
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
) : (
|
) : (
|
||||||
<StopMongo mongoId={mongoId} />
|
<StopMongo mongoId={mongoId} />
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const DockerMemoryChart = ({
|
|||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
usage: (item.value.used / 1024).toFixed(2),
|
usage: (item.value.used / 1024 ** 3).toFixed(2),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -208,9 +208,7 @@ export const DockerMonitoring = ({
|
|||||||
<div className="flex flex-col gap-2 w-full ">
|
<div className="flex flex-col gap-2 w-full ">
|
||||||
<span className="text-base font-medium">Memory</span>
|
<span className="text-base font-medium">Memory</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Used: ${(currentData.memory.value.used / 1024).toFixed(
|
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
|
||||||
2,
|
|
||||||
)} GB / Limit: ${(currentData.memory.value.total / 1024).toFixed(2)} GB`}
|
|
||||||
</span>
|
</span>
|
||||||
<Progress
|
<Progress
|
||||||
value={currentData.memory.value.usedPercentage}
|
value={currentData.memory.value.usedPercentage}
|
||||||
@@ -218,7 +216,7 @@ export const DockerMonitoring = ({
|
|||||||
/>
|
/>
|
||||||
<DockerMemoryChart
|
<DockerMemoryChart
|
||||||
acummulativeData={acummulativeData.memory}
|
acummulativeData={acummulativeData.memory}
|
||||||
memoryLimitGB={currentData.memory.value.total / 1024}
|
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{appName === "dokploy" && (
|
{appName === "dokploy" && (
|
||||||
@@ -240,9 +238,9 @@ export const DockerMonitoring = ({
|
|||||||
<div className="flex flex-col gap-2 w-full ">
|
<div className="flex flex-col gap-2 w-full ">
|
||||||
<span className="text-base font-medium">Block I/O</span>
|
<span className="text-base font-medium">Block I/O</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Used: ${currentData.block.value.readMb.toFixed(
|
{`Read: ${currentData.block.value.readMb.toFixed(
|
||||||
2,
|
2,
|
||||||
)} MB / Limit: ${currentData.block.value.writeMb.toFixed(
|
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
|
||||||
3,
|
3,
|
||||||
)} MB`}
|
)} MB`}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
<StopMysql mysqlId={mysqlId} />
|
<StopMysql mysqlId={mysqlId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
<StopPostgres postgresId={postgresId} />
|
<StopPostgres postgresId={postgresId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -19,11 +19,26 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Folder } from "lucide-react";
|
import { Folder, HelpCircle } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -43,6 +58,7 @@ const AddTemplateSchema = z.object({
|
|||||||
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
||||||
@@ -54,8 +70,10 @@ interface Props {
|
|||||||
|
|
||||||
export const AddApplication = ({ projectId, projectName }: Props) => {
|
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.application.create.useMutation();
|
api.application.create.useMutation();
|
||||||
@@ -75,6 +93,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Created");
|
toast.success("Service Created");
|
||||||
@@ -135,6 +154,57 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
Select a Server (Optional)
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</FormLabel>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
If not server is selected, the application will be
|
||||||
|
deployed on the server where the user is logged in.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="appName"
|
name="appName"
|
||||||
|
|||||||
@@ -22,15 +22,23 @@ import { Input } from "@/components/ui/input";
|
|||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CircuitBoard, Folder } from "lucide-react";
|
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -51,6 +59,7 @@ const AddComposeSchema = z.object({
|
|||||||
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCompose = z.infer<typeof AddComposeSchema>;
|
type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||||
@@ -63,6 +72,7 @@ interface Props {
|
|||||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
@@ -87,6 +97,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
projectId,
|
projectId,
|
||||||
composeType: data.composeType,
|
composeType: data.composeType,
|
||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
|
serverId: data.serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
@@ -148,6 +159,57 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
Select a Server (Optional)
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</FormLabel>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
If not server is selected, the application will be
|
||||||
|
deployed on the server where the user is logged in.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="appName"
|
name="appName"
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -71,6 +80,7 @@ const baseDatabaseSchema = z.object({
|
|||||||
databasePassword: z.string(),
|
databasePassword: z.string(),
|
||||||
dockerImage: z.string(),
|
dockerImage: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
|
serverId: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("type", [
|
const mySchema = z.discriminatedUnion("type", [
|
||||||
@@ -145,6 +155,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const postgresMutation = api.postgres.create.useMutation();
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
const mongoMutation = api.mongo.create.useMutation();
|
const mongoMutation = api.mongo.create.useMutation();
|
||||||
const redisMutation = api.redis.create.useMutation();
|
const redisMutation = api.redis.create.useMutation();
|
||||||
@@ -161,6 +172,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
description: "",
|
description: "",
|
||||||
databaseName: "",
|
databaseName: "",
|
||||||
databaseUser: "",
|
databaseUser: "",
|
||||||
|
serverId: null,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(mySchema),
|
resolver: zodResolver(mySchema),
|
||||||
});
|
});
|
||||||
@@ -183,6 +195,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
dockerImage: defaultDockerImage,
|
dockerImage: defaultDockerImage,
|
||||||
projectId,
|
projectId,
|
||||||
|
serverId: data.serverId,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,8 +204,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
...commonParams,
|
...commonParams,
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
|
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mongo") {
|
} else if (data.type === "mongo") {
|
||||||
promise = mongoMutation.mutateAsync({
|
promise = mongoMutation.mutateAsync({
|
||||||
@@ -200,11 +215,13 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "redis") {
|
} else if (data.type === "redis") {
|
||||||
promise = redisMutation.mutateAsync({
|
promise = redisMutation.mutateAsync({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
|
serverId: data.serverId,
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mariadb") {
|
} else if (data.type === "mariadb") {
|
||||||
@@ -215,6 +232,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mysql") {
|
} else if (data.type === "mysql") {
|
||||||
promise = mysqlMutation.mutateAsync({
|
promise = mysqlMutation.mutateAsync({
|
||||||
@@ -224,6 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
|
serverId: data.serverId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +371,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serverId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Select a Server</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>
|
||||||
|
Servers ({servers?.length})
|
||||||
|
</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="appName"
|
name="appName"
|
||||||
|
|||||||
@@ -29,11 +29,27 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ScrollArea } from "@radix-ui/react-scroll-area";
|
import { ScrollArea } from "@radix-ui/react-scroll-area";
|
||||||
@@ -43,6 +59,7 @@ import {
|
|||||||
Code,
|
Code,
|
||||||
Github,
|
Github,
|
||||||
Globe,
|
Globe,
|
||||||
|
HelpCircle,
|
||||||
PuzzleIcon,
|
PuzzleIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -58,9 +75,12 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { data } = api.compose.templates.useQuery();
|
const { data } = api.compose.templates.useQuery();
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { data: tags, isLoading: isLoadingTags } =
|
const { data: tags, isLoading: isLoadingTags } =
|
||||||
api.compose.getTags.useQuery();
|
api.compose.getTags.useQuery();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const [serverId, setServerId] = useState<string | undefined>(undefined);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.deployTemplate.useMutation();
|
api.compose.deployTemplate.useMutation();
|
||||||
|
|
||||||
@@ -109,7 +129,6 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:max-w-[15rem] w-full justify-between !bg-input",
|
"md:max-w-[15rem] w-full justify-between !bg-input",
|
||||||
// !field.value && "text-muted-foreground",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoadingTags
|
{isLoadingTags
|
||||||
@@ -268,30 +287,79 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
{template.name} template and add it to your
|
{template.name} template and add it to your
|
||||||
project.
|
project.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
|
||||||
|
Select a Server (Optional)
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
If not server is selected, the
|
||||||
|
application will be deployed on the
|
||||||
|
server where the user is logged in.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setServerId(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>
|
||||||
|
Servers ({servers?.length})
|
||||||
|
</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
|
disabled={isLoading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
const promise = mutateAsync({
|
||||||
projectId,
|
projectId,
|
||||||
|
serverId: serverId || undefined,
|
||||||
id: template.id,
|
id: template.id,
|
||||||
})
|
});
|
||||||
.then(async () => {
|
toast.promise(promise, {
|
||||||
toast.success(
|
loading: "Setting up...",
|
||||||
`Succesfully created ${template.name} application from template`,
|
success: (data) => {
|
||||||
);
|
|
||||||
|
|
||||||
utils.project.one.invalidate({
|
utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
})
|
return `${template.name} template created succesfully`;
|
||||||
.catch(() => {
|
},
|
||||||
toast.error(
|
error: (err) => {
|
||||||
`Error creating ${template.name} application from template`,
|
return `Ocurred an error deploying ${template.name} template`;
|
||||||
);
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -109,6 +110,7 @@ export const AddProject = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
<StopRedis redisId={redisId} />
|
<StopRedis redisId={redisId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal appName={data?.appName || ""}>
|
<DockerTerminalModal
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Terminal />
|
<Terminal />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Edit } from "lucide-react";
|
import { Edit } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
|
||||||
|
export const ShowDokployActions = () => {
|
||||||
|
const { mutateAsync: reloadServer, isLoading } =
|
||||||
|
api.settings.reloadServer.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||||
|
<Button isLoading={isLoading} variant="outline">
|
||||||
|
Server
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-56" align="start">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await reloadServer()
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Server Reloaded");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.success("Server Reloaded");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Reload</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<ShowModalLogs appName="dokploy">
|
||||||
|
<span>Watch logs</span>
|
||||||
|
</ShowModalLogs>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { CardDescription, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowStorageActions } from "./show-storage-actions";
|
||||||
|
import { ShowTraefikActions } from "./show-traefik-actions";
|
||||||
|
import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowServerActions = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
View Actions
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen ">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
||||||
|
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 w-full gap-4">
|
||||||
|
<ShowTraefikActions serverId={serverId} />
|
||||||
|
<ShowStorageActions serverId={serverId} />
|
||||||
|
<ToggleDockerCleanup serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
export const ShowStorageActions = ({ serverId }: Props) => {
|
||||||
|
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||||
|
api.settings.cleanAll.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanDockerBuilder,
|
||||||
|
isLoading: cleanDockerBuilderIsLoading,
|
||||||
|
} = api.settings.cleanDockerBuilder.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
|
||||||
|
api.settings.cleanMonitoring.useMutation();
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanUnusedImages,
|
||||||
|
isLoading: cleanUnusedImagesIsLoading,
|
||||||
|
} = api.settings.cleanUnusedImages.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanUnusedVolumes,
|
||||||
|
isLoading: cleanUnusedVolumesIsLoading,
|
||||||
|
} = api.settings.cleanUnusedVolumes.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanStoppedContainers,
|
||||||
|
isLoading: cleanStoppedContainersIsLoading,
|
||||||
|
} = api.settings.cleanStoppedContainers.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
disabled={
|
||||||
|
cleanAllIsLoading ||
|
||||||
|
cleanDockerBuilderIsLoading ||
|
||||||
|
cleanUnusedImagesIsLoading ||
|
||||||
|
cleanUnusedVolumesIsLoading ||
|
||||||
|
cleanStoppedContainersIsLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isLoading={
|
||||||
|
cleanAllIsLoading ||
|
||||||
|
cleanDockerBuilderIsLoading ||
|
||||||
|
cleanUnusedImagesIsLoading ||
|
||||||
|
cleanUnusedVolumesIsLoading ||
|
||||||
|
cleanStoppedContainersIsLoading
|
||||||
|
}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Space
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-64" align="start">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanUnusedImages({
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned images");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean images");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean unused images</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanUnusedVolumes({
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned volumes");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean volumes");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean unused volumes</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanStoppedContainers({
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Stopped containers cleaned");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean stopped containers");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean stopped containers</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanDockerBuilder({
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned Docker Builder");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean Docker Builder");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean Docker Builder & System</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{!serverId && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanMonitoring()
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned Monitoring");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean Monitoring");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean Monitoring </span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanAll({
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned all");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean all");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean all</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||||
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||||
|
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||||
|
api.settings.reloadTraefik.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
||||||
|
api.settings.toggleDashboard.useMutation();
|
||||||
|
|
||||||
|
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||||
|
api.settings.haveTraefikDashboardPortEnabled.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Traefik
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-56" align="start">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await reloadTraefik({
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Traefik Reloaded");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to reload the traefik");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Reload</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
|
||||||
|
<span>Watch logs</span>
|
||||||
|
</ShowModalLogs>
|
||||||
|
<EditTraefikEnv serverId={serverId}>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
>
|
||||||
|
<span>Modify Env</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</EditTraefikEnv>
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await toggleDashboard({
|
||||||
|
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(
|
||||||
|
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||||
|
);
|
||||||
|
refetchDashboard();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{/*
|
||||||
|
<DockerTerminalModal appName="dokploy-traefik">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<span>Enter the terminal</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DockerTerminalModal> */}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||||
|
const { data, refetch } = api.admin.one.useQuery(undefined, {
|
||||||
|
enabled: !serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
|
||||||
|
{
|
||||||
|
serverId: serverId || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
|
||||||
|
|
||||||
|
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={async (e) => {
|
||||||
|
await mutateAsync({
|
||||||
|
enableDockerCleanup: e,
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Docker Cleanup Enabled");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Docker Cleanup Error");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
refetchServer();
|
||||||
|
} else {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label className="text-primary">Daily Docker Cleanup</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
|
description: z.string().optional(),
|
||||||
|
ipAddress: z.string().min(1, {
|
||||||
|
message: "IP Address is required",
|
||||||
|
}),
|
||||||
|
port: z.number().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
sshKeyId: z.string().min(1, {
|
||||||
|
message: "SSH Key is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
|
export const AddServer = () => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
|
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
description: "",
|
||||||
|
name: "",
|
||||||
|
ipAddress: "",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
sshKeyId: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(Schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
description: "",
|
||||||
|
name: "",
|
||||||
|
ipAddress: "",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
sshKeyId: "",
|
||||||
|
});
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
await mutateAsync({
|
||||||
|
name: data.name,
|
||||||
|
description: data.description || "",
|
||||||
|
ipAddress: data.ipAddress || "",
|
||||||
|
port: data.port || 22,
|
||||||
|
username: data.username || "root",
|
||||||
|
sshKeyId: data.sshKeyId || "",
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
await utils.server.all.invalidate();
|
||||||
|
toast.success("Server Created");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to create a server");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Create Server
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-3xl ">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Server</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Add a server to deploy your applications remotely.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-add-server"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 ">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Hostinger Server" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="This server is for databases..."
|
||||||
|
className="resize-none"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshKeyId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Select a SSH Key</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a SSH Key" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{sshKeys?.map((sshKey) => (
|
||||||
|
<SelectItem
|
||||||
|
key={sshKey.sshKeyId}
|
||||||
|
value={sshKey.sshKeyId}
|
||||||
|
>
|
||||||
|
{sshKey.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>
|
||||||
|
Registries ({sshKeys?.length})
|
||||||
|
</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ipAddress"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>IP Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="192.168.1.100" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="22" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="root" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
form="hook-form-add-server"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
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 { Separator } from "@/components/ui/separator";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import {
|
||||||
|
CopyIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
RocketIcon,
|
||||||
|
ServerIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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(
|
||||||
|
`echo "${server?.sshKey?.publicKey}" >> ~/.ssh/authorized_keys`,
|
||||||
|
);
|
||||||
|
toast.success("Copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="mt-1">
|
||||||
|
3. You're done, you can test the connection by entering
|
||||||
|
to the terminal or by setting up the server tab.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="deployments">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<CardTitle className="text-xl">
|
||||||
|
Deployments
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
See all the 5 Server Setup
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<DialogAction
|
||||||
|
title={"Setup Server?"}
|
||||||
|
description="This will setup the server and all associated data"
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
serverId: server?.serverId || "",
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Server setup successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error configuring server");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button isLoading={isLoading}>Setup Server</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{server?.deployments?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No deployments found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{deployments?.map((deployment) => (
|
||||||
|
<div
|
||||||
|
key={deployment.deploymentId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
|
{deployment.status}
|
||||||
|
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment?.status}
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.title}
|
||||||
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="break-all text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment.logPath);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ShowDeployment
|
||||||
|
open={activeLog !== null}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { ContainerIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowContainers } from "../../docker/show/show-containers";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowDockerContainersModal = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Show Docker Containers
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<ContainerIcon className="size-5" /> Docker Containers
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
See all the containers of your remote server
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid w-full gap-1">
|
||||||
|
<ShowContainers serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const ShowServers = () => {
|
||||||
|
const { data, refetch } = api.server.all.useQuery();
|
||||||
|
const { mutateAsync } = api.server.remove.useMutation();
|
||||||
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<div className="space-y-2 flex flex-row justify-between items-end">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold">Servers</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Add servers to deploy your applications remotely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sshKeys && sshKeys?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<AddServer />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-1">
|
||||||
|
{sshKeys?.length === 0 && data?.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
|
<KeyIcon className="size-8" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No SSH Keys found. Add a SSH Key to start adding servers.{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/ssh-keys"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
Add SSH Key
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data &&
|
||||||
|
data.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
|
<ServerIcon className="size-8" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No Servers found. Add a server to deploy your applications
|
||||||
|
remotely.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{data && data?.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<Table>
|
||||||
|
<TableCaption>See all servers</TableCaption>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[100px]">Name</TableHead>
|
||||||
|
<TableHead className="text-center">IP Address</TableHead>
|
||||||
|
<TableHead className="text-center">Port</TableHead>
|
||||||
|
<TableHead className="text-center">Username</TableHead>
|
||||||
|
<TableHead className="text-center">SSH Key</TableHead>
|
||||||
|
<TableHead className="text-center">Created</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((server) => {
|
||||||
|
const canDelete = server.totalSum === 0;
|
||||||
|
return (
|
||||||
|
<TableRow key={server.serverId}>
|
||||||
|
<TableCell className="w-[100px]">{server.name}</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<Badge>{server.ipAddress}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{server.port}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{server.username}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{server.sshKeyId ? "Yes" : "No"}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{format(new Date(server.createdAt), "PPpp")}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="text-right flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
{server.sshKeyId && (
|
||||||
|
<TerminalModal serverId={server.serverId}>
|
||||||
|
<span>Enter the terminal</span>
|
||||||
|
</TerminalModal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SetupServer serverId={server.serverId} />
|
||||||
|
|
||||||
|
<UpdateServer serverId={server.serverId} />
|
||||||
|
{server.sshKeyId && (
|
||||||
|
<ShowServerActions serverId={server.serverId} />
|
||||||
|
)}
|
||||||
|
<DialogAction
|
||||||
|
disabled={!canDelete}
|
||||||
|
title={
|
||||||
|
canDelete
|
||||||
|
? "Delete Server"
|
||||||
|
: "Server has active services"
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
canDelete ? (
|
||||||
|
"This will delete the server and all associated data"
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
You can not delete this server because it
|
||||||
|
has active services.
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You have active services associated with
|
||||||
|
this server, please delete them first.
|
||||||
|
</AlertBlock>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
serverId: server.serverId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
`Server ${server.name} deleted succesfully`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Delete Server
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
|
{server.sshKeyId && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||||
|
|
||||||
|
<ShowTraefikFileSystemModal
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
|
<ShowDockerContainersModal
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { FileTextIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowTraefikFileSystemModal = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Show Traefik File System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<FileTextIcon className="size-5" /> Traefik File System
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
See all the files and directories of your traefik configuration
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||||
|
<ShowTraefikSystem serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
|
description: z.string().optional(),
|
||||||
|
ipAddress: z.string().min(1, {
|
||||||
|
message: "IP Address is required",
|
||||||
|
}),
|
||||||
|
port: z.number().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
sshKeyId: z.string().min(1, {
|
||||||
|
message: "SSH Key is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateServer = ({ serverId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data, isLoading } = api.server.one.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
|
const { mutateAsync, error, isError } = api.server.update.useMutation();
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
description: "",
|
||||||
|
name: "",
|
||||||
|
ipAddress: "",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
sshKeyId: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(Schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
description: data?.description || "",
|
||||||
|
name: data?.name || "",
|
||||||
|
ipAddress: data?.ipAddress || "",
|
||||||
|
port: data?.port || 22,
|
||||||
|
username: data?.username || "root",
|
||||||
|
sshKeyId: data?.sshKeyId || "",
|
||||||
|
});
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Schema) => {
|
||||||
|
await mutateAsync({
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description || "",
|
||||||
|
ipAddress: formData.ipAddress || "",
|
||||||
|
port: formData.port || 22,
|
||||||
|
username: formData.username || "root",
|
||||||
|
sshKeyId: formData.sshKeyId || "",
|
||||||
|
serverId: serverId,
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
await utils.server.all.invalidate();
|
||||||
|
toast.success("Server Updated");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update a server");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Edit Server
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-3xl ">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Server</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update a server to deploy your applications remotely.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-update-server"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 ">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Hostinger Server" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="This server is for databases..."
|
||||||
|
className="resize-none"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshKeyId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Select a SSH Key</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a SSH Key" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{sshKeys?.map((sshKey) => (
|
||||||
|
<SelectItem
|
||||||
|
key={sshKey.sshKeyId}
|
||||||
|
value={sshKey.sshKeyId}
|
||||||
|
>
|
||||||
|
{sshKey.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>
|
||||||
|
Registries ({sshKeys?.length})
|
||||||
|
</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ipAddress"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>IP Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="192.168.1.100" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="22" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="root" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
form="hook-form-update-server"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -41,8 +41,8 @@ export const ShowUsers = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full col-span-2">
|
<div className=" col-span-2">
|
||||||
<Card className="bg-transparent h-full ">
|
<Card className="bg-transparent ">
|
||||||
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
|
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl">Users</CardTitle>
|
<CardTitle className="text-xl">Users</CardTitle>
|
||||||
@@ -55,9 +55,9 @@ export const ShowUsers = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 h-full">
|
<CardContent className="space-y-2">
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3 h-full">
|
||||||
<Users className="size-8 self-center text-muted-foreground" />
|
<Users className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a user, you need to add:
|
To create a user, you need to add:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -6,334 +5,34 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { cn } from "@/lib/utils";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import React from "react";
|
||||||
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||||
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
|
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||||
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
|
||||||
import { ShowModalLogs } from "./web-server/show-modal-logs";
|
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
|
||||||
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
|
|
||||||
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
|
|
||||||
import { TerminalModal } from "./web-server/terminal-modal";
|
|
||||||
import { UpdateServer } from "./web-server/update-server";
|
import { UpdateServer } from "./web-server/update-server";
|
||||||
|
|
||||||
export const WebServer = () => {
|
interface Props {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
className?: string;
|
||||||
const { mutateAsync: reloadServer, isLoading } =
|
}
|
||||||
api.settings.reloadServer.useMutation();
|
export const WebServer = ({ className }: Props) => {
|
||||||
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
const { data } = api.admin.one.useQuery();
|
||||||
api.settings.reloadTraefik.useMutation();
|
|
||||||
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
|
||||||
api.settings.cleanAll.useMutation();
|
|
||||||
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
|
||||||
api.settings.toggleDashboard.useMutation();
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: cleanDockerBuilder,
|
|
||||||
isLoading: cleanDockerBuilderIsLoading,
|
|
||||||
} = api.settings.cleanDockerBuilder.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
|
|
||||||
api.settings.cleanMonitoring.useMutation();
|
|
||||||
const {
|
|
||||||
mutateAsync: cleanUnusedImages,
|
|
||||||
isLoading: cleanUnusedImagesIsLoading,
|
|
||||||
} = api.settings.cleanUnusedImages.useMutation();
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: cleanUnusedVolumes,
|
|
||||||
isLoading: cleanUnusedVolumesIsLoading,
|
|
||||||
} = api.settings.cleanUnusedVolumes.useMutation();
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: cleanStoppedContainers,
|
|
||||||
isLoading: cleanStoppedContainersIsLoading,
|
|
||||||
} = api.settings.cleanStoppedContainers.useMutation();
|
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: updateDockerCleanup } =
|
|
||||||
api.settings.updateDockerCleanup.useMutation();
|
|
||||||
|
|
||||||
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
|
||||||
api.settings.haveTraefikDashboardPortEnabled.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="rounded-lg w-full bg-transparent">
|
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Web server settings</CardTitle>
|
<CardTitle className="text-xl">Web server settings</CardTitle>
|
||||||
<CardDescription>Reload or clean the web server.</CardDescription>
|
<CardDescription>Reload or clean the web server.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4 ">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<DropdownMenu>
|
<ShowDokployActions />
|
||||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
<ShowTraefikActions />
|
||||||
<Button isLoading={isLoading} variant="outline">
|
<ShowStorageActions />
|
||||||
Server
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={async () => {
|
|
||||||
await reloadServer()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Server Reloaded");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.success("Server Reloaded");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Reload</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<ShowModalLogs appName="dokploy">
|
|
||||||
<span>Watch logs</span>
|
|
||||||
</ShowModalLogs>
|
|
||||||
|
|
||||||
<ShowServerTraefikConfig>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
>
|
|
||||||
<span>View Traefik config</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</ShowServerTraefikConfig>
|
|
||||||
|
|
||||||
<ShowServerMiddlewareConfig>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
>
|
|
||||||
<span>View middlewares config</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</ShowServerMiddlewareConfig>
|
|
||||||
|
|
||||||
<TerminalModal>
|
|
||||||
<span>Enter the terminal</span>
|
|
||||||
</TerminalModal>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
asChild
|
|
||||||
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Traefik
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={async () => {
|
|
||||||
await reloadTraefik()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Traefik Reloaded");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to reload the traefik");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Reload</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<ShowModalLogs appName="dokploy-traefik">
|
|
||||||
<span>Watch logs</span>
|
|
||||||
</ShowModalLogs>
|
|
||||||
<ShowMainTraefikConfig>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
>
|
|
||||||
<span>View Traefik config</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</ShowMainTraefikConfig>
|
|
||||||
<EditTraefikEnv>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
>
|
|
||||||
<span>Modify Env</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</EditTraefikEnv>
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={async () => {
|
|
||||||
await toggleDashboard({
|
|
||||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success(
|
|
||||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
|
||||||
);
|
|
||||||
refetchDashboard();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
|
|
||||||
Dashboard
|
|
||||||
</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DockerTerminalModal appName="dokploy-traefik">
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<span>Enter the terminal</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DockerTerminalModal>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
asChild
|
|
||||||
disabled={
|
|
||||||
cleanAllIsLoading ||
|
|
||||||
cleanDockerBuilderIsLoading ||
|
|
||||||
cleanUnusedImagesIsLoading ||
|
|
||||||
cleanUnusedVolumesIsLoading ||
|
|
||||||
cleanStoppedContainersIsLoading
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
isLoading={
|
|
||||||
cleanAllIsLoading ||
|
|
||||||
cleanDockerBuilderIsLoading ||
|
|
||||||
cleanUnusedImagesIsLoading ||
|
|
||||||
cleanUnusedVolumesIsLoading ||
|
|
||||||
cleanStoppedContainersIsLoading
|
|
||||||
}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Space
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-64" align="start">
|
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await cleanUnusedImages()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Cleaned images");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to clean images");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Clean unused images</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await cleanUnusedVolumes()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Cleaned volumes");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to clean volumes");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Clean unused volumes</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await cleanStoppedContainers()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Stopped containers cleaned");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to clean stopped containers");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Clean stopped containers</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await cleanDockerBuilder()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Cleaned Docker Builder");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to clean Docker Builder");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Clean Docker Builder & System</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await cleanMonitoring()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Cleaned Monitoring");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to clean Monitoring");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Clean Monitoring </span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await cleanAll()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Cleaned all");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to clean all");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Clean all</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<UpdateServer />
|
<UpdateServer />
|
||||||
</div>
|
</div>
|
||||||
@@ -345,25 +44,8 @@ export const WebServer = () => {
|
|||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version: {dokployVersion}
|
Version: {dokployVersion}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Switch
|
|
||||||
checked={data?.enableDockerCleanup}
|
|
||||||
onCheckedChange={async (e) => {
|
|
||||||
await updateDockerCleanup({
|
|
||||||
enableDockerCleanup: e,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Docker Cleanup Enabled");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Docker Cleanup Error");
|
|
||||||
});
|
|
||||||
|
|
||||||
refetch();
|
<ToggleDockerCleanup />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label className="text-primary">Daily Docker Cleanup</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -34,12 +35,14 @@ const Terminal = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerTerminalModal = ({ children, appName }: Props) => {
|
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -65,7 +68,14 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a container" />
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a container" />
|
||||||
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -82,6 +92,7 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Terminal
|
<Terminal
|
||||||
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
id="terminal"
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -33,12 +33,15 @@ type Schema = z.infer<typeof schema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditTraefikEnv = ({ children }: Props) => {
|
export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
||||||
const [canEdit, setCanEdit] = useState(true);
|
const [canEdit, setCanEdit] = useState(true);
|
||||||
|
|
||||||
const { data } = api.settings.readTraefikEnv.useQuery();
|
const { data } = api.settings.readTraefikEnv.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.settings.writeTraefikEnv.useMutation();
|
api.settings.writeTraefikEnv.useMutation();
|
||||||
@@ -62,6 +65,7 @@ export const EditTraefikEnv = ({ children }: Props) => {
|
|||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
env: data.env,
|
env: data.env,
|
||||||
|
serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Traefik Env Updated");
|
toast.success("Traefik Env Updated");
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
|
||||||
|
|
||||||
const UpdateMainTraefikConfigSchema = z.object({
|
|
||||||
traefikConfig: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdateTraefikConfig = z.infer<typeof UpdateMainTraefikConfigSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowMainTraefikConfig = ({ children }: Props) => {
|
|
||||||
const { data, refetch } = api.settings.readTraefikConfig.useQuery();
|
|
||||||
const [canEdit, setCanEdit] = useState(true);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.settings.updateTraefikConfig.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<UpdateTraefikConfig>({
|
|
||||||
defaultValues: {
|
|
||||||
traefikConfig: "",
|
|
||||||
},
|
|
||||||
disabled: canEdit,
|
|
||||||
resolver: zodResolver(UpdateMainTraefikConfigSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
traefikConfig: data || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateTraefikConfig) => {
|
|
||||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
|
||||||
if (!valid) {
|
|
||||||
form.setError("traefikConfig", {
|
|
||||||
type: "manual",
|
|
||||||
message: error || "Invalid YAML",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.clearErrors("traefikConfig");
|
|
||||||
await mutateAsync({
|
|
||||||
traefikConfig: data.traefikConfig,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Traefik config Updated");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to update the traefik config");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Update traefik config</DialogTitle>
|
|
||||||
<DialogDescription>Update the traefik config</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-update-main-traefik-config"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="w-full space-y-4 relative"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="traefikConfig"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="relative">
|
|
||||||
<FormLabel>Traefik config</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<CodeEditor
|
|
||||||
lineWrapping
|
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
|
||||||
placeholder={`providers:
|
|
||||||
docker:
|
|
||||||
defaultRule: 'Host('dokploy.com')'
|
|
||||||
file:
|
|
||||||
directory: /etc/dokploy/traefik
|
|
||||||
watch: true
|
|
||||||
entryPoints:
|
|
||||||
web:
|
|
||||||
address: ':80'
|
|
||||||
websecure:
|
|
||||||
address: ':443'
|
|
||||||
api:
|
|
||||||
insecure: true
|
|
||||||
|
|
||||||
`}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
<FormMessage />
|
|
||||||
</pre>
|
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
|
||||||
<Button
|
|
||||||
className="shadow-sm"
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
setCanEdit(!canEdit);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{canEdit ? "Unlock" : "Lock"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={canEdit}
|
|
||||||
form="hook-form-update-main-traefik-config"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -35,12 +36,14 @@ export const DockerLogsId = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowModalLogs = ({ appName, children }: Props) => {
|
export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
||||||
const { data } = api.docker.getContainersByAppLabel.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -72,7 +75,14 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
|||||||
<Label>Select a container to view logs</Label>
|
<Label>Select a container to view logs</Label>
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a container" />
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a container" />
|
||||||
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@@ -88,7 +98,11 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
<DockerLogsId
|
||||||
|
id="terminal"
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
|
||||||
|
|
||||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
|
||||||
traefikConfig: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdateServerMiddlewareConfig = z.infer<
|
|
||||||
typeof UpdateServerMiddlewareConfigSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
|
||||||
const { data, refetch } = api.settings.readMiddlewareTraefikConfig.useQuery();
|
|
||||||
const [canEdit, setCanEdit] = useState(true);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.settings.updateMiddlewareTraefikConfig.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<UpdateServerMiddlewareConfig>({
|
|
||||||
defaultValues: {
|
|
||||||
traefikConfig: "",
|
|
||||||
},
|
|
||||||
disabled: canEdit,
|
|
||||||
resolver: zodResolver(UpdateServerMiddlewareConfigSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
traefikConfig: data || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
|
||||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
|
||||||
console.log(error);
|
|
||||||
if (!valid) {
|
|
||||||
form.setError("traefikConfig", {
|
|
||||||
type: "manual",
|
|
||||||
message: error || "Invalid YAML",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.clearErrors("traefikConfig");
|
|
||||||
await mutateAsync({
|
|
||||||
traefikConfig: data.traefikConfig,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Middleware config Updated");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to update the middleware traefik config");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Update Middleware config</DialogTitle>
|
|
||||||
<DialogDescription>Update the middleware config</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-update-server-traefik-config"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="w-full space-y-4 relative overflow-auto"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="traefikConfig"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="relative">
|
|
||||||
<FormLabel>Traefik config</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<CodeEditor
|
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
|
||||||
placeholder={`http:
|
|
||||||
routers:
|
|
||||||
router-name:
|
|
||||||
rule: Host('domain.com')
|
|
||||||
service: container-name
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
tls: false
|
|
||||||
middlewares: []
|
|
||||||
`}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
<FormMessage />
|
|
||||||
</pre>
|
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
|
||||||
<Button
|
|
||||||
className="shadow-sm"
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
setCanEdit(!canEdit);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{canEdit ? "Unlock" : "Lock"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={canEdit}
|
|
||||||
form="hook-form-update-server-traefik-config"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
|
||||||
|
|
||||||
const UpdateServerTraefikConfigSchema = z.object({
|
|
||||||
traefikConfig: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdateServerTraefikConfig = z.infer<
|
|
||||||
typeof UpdateServerTraefikConfigSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowServerTraefikConfig = ({ children }: Props) => {
|
|
||||||
const { data, refetch } = api.settings.readWebServerTraefikConfig.useQuery();
|
|
||||||
const [canEdit, setCanEdit] = useState(true);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.settings.updateWebServerTraefikConfig.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<UpdateServerTraefikConfig>({
|
|
||||||
defaultValues: {
|
|
||||||
traefikConfig: "",
|
|
||||||
},
|
|
||||||
disabled: canEdit,
|
|
||||||
resolver: zodResolver(UpdateServerTraefikConfigSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
traefikConfig: data || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateServerTraefikConfig) => {
|
|
||||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
|
||||||
console.log(error);
|
|
||||||
if (!valid) {
|
|
||||||
form.setError("traefikConfig", {
|
|
||||||
type: "manual",
|
|
||||||
message: error || "Invalid YAML",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.clearErrors("traefikConfig");
|
|
||||||
await mutateAsync({
|
|
||||||
traefikConfig: data.traefikConfig,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Traefik config Updated");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to update the traefik config");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Update traefik config</DialogTitle>
|
|
||||||
<DialogDescription>Update the traefik config</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-update-server-traefik-config"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="w-full space-y-4 relative overflow-auto"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="traefikConfig"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="relative">
|
|
||||||
<FormLabel>Traefik config</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<CodeEditor
|
|
||||||
lineWrapping
|
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
|
||||||
placeholder={`http:
|
|
||||||
routers:
|
|
||||||
router-name:
|
|
||||||
rule: Host('domain.com')
|
|
||||||
service: container-name
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
tls: false
|
|
||||||
middlewares: []
|
|
||||||
`}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
<FormMessage />
|
|
||||||
</pre>
|
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-0">
|
|
||||||
<Button
|
|
||||||
className="shadow-sm"
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
setCanEdit(!canEdit);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{canEdit ? "Unlock" : "Lock"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={canEdit}
|
|
||||||
form="hook-form-update-server-traefik-config"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,79 +7,27 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { RemoveSSHPrivateKey } from "./remove-ssh-private-key";
|
|
||||||
|
|
||||||
const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), {
|
const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const addSSHPrivateKey = z.object({
|
|
||||||
sshPrivateKey: z
|
|
||||||
.string({
|
|
||||||
required_error: "SSH private key is required",
|
|
||||||
})
|
|
||||||
.min(1, "SSH private key is required"),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AddSSHPrivateKey = z.infer<typeof addSSHPrivateKey>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TerminalModal = ({ children }: Props) => {
|
export const TerminalModal = ({ children, serverId }: Props) => {
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
const { data } = api.server.one.useQuery(
|
||||||
const [user, setUser] = useState("root");
|
{
|
||||||
const [terminalUser, setTerminalUser] = useState("root");
|
serverId,
|
||||||
|
|
||||||
const { mutateAsync, isLoading } =
|
|
||||||
api.settings.saveSSHPrivateKey.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddSSHPrivateKey>({
|
|
||||||
defaultValues: {
|
|
||||||
sshPrivateKey: "",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addSSHPrivateKey),
|
{ enabled: !!serverId },
|
||||||
});
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({});
|
|
||||||
}
|
|
||||||
}, [data, form, form.reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: AddSSHPrivateKey) => {
|
|
||||||
await mutateAsync({
|
|
||||||
sshPrivateKey: formData.sshPrivateKey,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("SSH Key Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to Update the ssh key");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -92,75 +39,14 @@ export const TerminalModal = ({ children }: Props) => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||||
<DialogHeader className="flex flex-row justify-between pt-4">
|
<DialogHeader className="flex flex-col gap-1">
|
||||||
<div>
|
<DialogTitle>Terminal ({data?.name})</DialogTitle>
|
||||||
<DialogTitle>Terminal</DialogTitle>
|
<DialogDescription>Easy way to access the server</DialogDescription>
|
||||||
<DialogDescription>Easy way to access the server</DialogDescription>
|
|
||||||
</div>
|
|
||||||
{data?.haveSSH && (
|
|
||||||
<div>
|
|
||||||
<RemoveSSHPrivateKey />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{!data?.haveSSH ? (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-8 "
|
|
||||||
>
|
|
||||||
<div className="grid w-full">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sshPrivateKey"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>SSH Private Key</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
In order to access the server you need to add an
|
|
||||||
ssh private key
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder={
|
|
||||||
"-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----"
|
|
||||||
}
|
|
||||||
className="h-32"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-end">
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label>Log in as</Label>
|
|
||||||
<div className="flex flex-row gap-4">
|
|
||||||
<Input value={user} onChange={(e) => setUser(e.target.value)} />
|
|
||||||
<Button onClick={() => setTerminalUser(user)}>Login</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Terminal id="terminal" userSSH={terminalUser} />
|
<div className="flex flex-col gap-4">
|
||||||
</div>
|
<Terminal id="terminal" serverId={serverId} />
|
||||||
)}
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { AttachAddon } from "@xterm/addon-attach";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
userSSH?: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
export const Terminal: React.FC<Props> = ({ id, serverId }) => {
|
||||||
const termRef = useRef(null);
|
const termRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,7 +33,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
|||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/terminal?userSSH=${userSSH}`;
|
const wsUrl = `${protocol}//${window.location.host}/terminal?serverId=${serverId}`;
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
const addonAttach = new AttachAddon(ws);
|
const addonAttach = new AttachAddon(ws);
|
||||||
@@ -46,7 +46,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
ws.readyState === WebSocket.OPEN && ws.close();
|
ws.readyState === WebSocket.OPEN && ws.close();
|
||||||
};
|
};
|
||||||
}, [id, userSSH]);
|
}, [id, serverId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
{
|
{
|
||||||
title: "Cluster",
|
title: "Cluster",
|
||||||
label: "",
|
label: "",
|
||||||
icon: Server,
|
icon: BoxesIcon,
|
||||||
href: "/dashboard/settings/cluster",
|
href: "/dashboard/settings/cluster",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,6 +83,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: Bell,
|
icon: Bell,
|
||||||
href: "/dashboard/settings/notifications",
|
href: "/dashboard/settings/notifications",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Servers",
|
||||||
|
label: "",
|
||||||
|
icon: Server,
|
||||||
|
href: "/dashboard/settings/servers",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(user?.canAccessToSSHKeys
|
...(user?.canAccessToSSHKeys
|
||||||
@@ -117,6 +123,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Bell,
|
Bell,
|
||||||
|
BoxesIcon,
|
||||||
Database,
|
Database,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string | React.ReactNode;
|
||||||
description?: string;
|
description?: string | React.ReactNode;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DialogAction = ({
|
export const DialogAction = ({
|
||||||
@@ -22,6 +23,7 @@ export const DialogAction = ({
|
|||||||
children,
|
children,
|
||||||
description,
|
description,
|
||||||
title,
|
title,
|
||||||
|
disabled,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@@ -37,7 +39,9 @@ export const DialogAction = ({
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={onClick}>Confirm</AlertDialogAction>
|
<AlertDialogAction disabled={disabled} onClick={onClick}>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { HeartIcon } from "lucide-react";
|
|
||||||
|
|
||||||
export const ShowSupport = () => {
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline" className="rounded-full">
|
|
||||||
<span className="text-sm font-semibold">Support </span>
|
|
||||||
<HeartIcon className="size-4 text-red-500 fill-red-600 animate-heartbeat " />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-xl ">
|
|
||||||
<DialogHeader className="text-center flex justify-center items-center">
|
|
||||||
<DialogTitle>Dokploy Support</DialogTitle>
|
|
||||||
<DialogDescription>Consider supporting Dokploy</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid w-full gap-4">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<span className="text-sm font-semibold">Name</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
81
apps/dokploy/drizzle/0037_legal_namor.sql
Normal file
81
apps/dokploy/drizzle/0037_legal_namor.sql
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "server" (
|
||||||
|
"serverId" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"ipAddress" text NOT NULL,
|
||||||
|
"port" integer NOT NULL,
|
||||||
|
"username" text DEFAULT 'root' NOT NULL,
|
||||||
|
"appName" text NOT NULL,
|
||||||
|
"enableDockerCleanup" boolean DEFAULT false NOT NULL,
|
||||||
|
"createdAt" text NOT NULL,
|
||||||
|
"adminId" text NOT NULL,
|
||||||
|
"sshKeyId" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "postgres" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mariadb" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mongo" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mysql" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "redis" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "server" ADD CONSTRAINT "server_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "server" ADD CONSTRAINT "server_sshKeyId_ssh-key_sshKeyId_fk" FOREIGN KEY ("sshKeyId") REFERENCES "public"."ssh-key"("sshKeyId") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "application" ADD CONSTRAINT "application_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "postgres" ADD CONSTRAINT "postgres_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "mariadb" ADD CONSTRAINT "mariadb_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "mongo" ADD CONSTRAINT "mongo_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "mysql" ADD CONSTRAINT "mysql_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "redis" ADD CONSTRAINT "redis_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "compose" ADD CONSTRAINT "compose_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
3823
apps/dokploy/drizzle/meta/0037_snapshot.json
Normal file
3823
apps/dokploy/drizzle/meta/0037_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -260,6 +260,13 @@
|
|||||||
"when": 1725519351871,
|
"when": 1725519351871,
|
||||||
"tag": "0036_tired_ronan",
|
"tag": "0036_tired_ronan",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 37,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1726988289562,
|
||||||
|
"tag": "0037_legal_namor",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rotating-file-stream": "3.2.3",
|
"rotating-file-stream": "3.2.3",
|
||||||
"@aws-sdk/client-s3": "3.515.0",
|
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
@@ -130,7 +129,8 @@
|
|||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4",
|
||||||
"zod-form-data": "^2.0.2",
|
"zod-form-data": "^2.0.2",
|
||||||
"@radix-ui/react-primitive": "2.0.0",
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"ssh2": "1.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.8.3",
|
||||||
@@ -167,7 +167,8 @@
|
|||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"vite-tsconfig-paths": "4.3.2",
|
"vite-tsconfig-paths": "4.3.2",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"xterm-readline": "1.1.1"
|
"xterm-readline": "1.1.1",
|
||||||
|
"@types/ssh2": "1.15.1"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.25.2"
|
"initVersion": "7.25.2"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export default async function handler(
|
|||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!application.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default async function handler(
|
|||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
server: !!composeResult.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export default async function handler(
|
|||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
|
|||||||
18
apps/dokploy/pages/api/teapot.ts
Normal file
18
apps/dokploy/pages/api/teapot.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import { renderToString } from "react-dom/server";
|
||||||
|
import Page418 from "../hola"; // Importa la página 418
|
||||||
|
|
||||||
|
export const GET = async (req: NextRequest) => {
|
||||||
|
// Renderiza el componente de la página 418 como HTML
|
||||||
|
const htmlContent = renderToString(Page418());
|
||||||
|
|
||||||
|
// Devuelve la respuesta con el código de estado HTTP 418
|
||||||
|
return new Response(htmlContent, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html",
|
||||||
|
},
|
||||||
|
status: 418,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GET;
|
||||||
@@ -16,12 +16,14 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
|
|||||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -98,6 +100,9 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -125,10 +130,17 @@ const Service = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
@@ -152,14 +164,20 @@ const Service = (
|
|||||||
<ShowEnvironment applicationId={applicationId} />
|
<ShowEnvironment applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments" className="w-full">
|
<TabsContent value="deployments" className="w-full">
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring
|
|||||||
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -92,13 +94,16 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex flex-row gap-4">
|
<div className="relative flex flex-row gap-4">
|
||||||
<div className="absolute -right-1 -top-2">
|
<div className="absolute -right-1 -top-2">
|
||||||
<StatusTooltip status={data?.composeStatus} />
|
<StatusTooltip status={data?.composeStatus} />
|
||||||
@@ -119,10 +124,23 @@ const Service = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||||
|
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
||||||
|
data?.serverId && data?.composeType === "stack"
|
||||||
|
? "md:grid-cols-5"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{data?.composeType === "docker-compose" && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
@@ -147,19 +165,22 @@ const Service = (
|
|||||||
<ShowEnvironmentCompose composeId={composeId} />
|
<ShowEnvironmentCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
{!data?.serverId && (
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowMonitoringCompose
|
<ShowMonitoringCompose
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appName={data?.appName || ""}
|
||||||
/>
|
appType={data?.composeType || "docker-compose"}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogsCompose
|
<ShowDockerLogsCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appType={data?.composeType || "docker-compose"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/g
|
|||||||
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
|
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
|
||||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -81,7 +82,9 @@ const Mariadb = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -108,10 +111,17 @@ const Mariadb = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -136,14 +146,19 @@ const Mariadb = (
|
|||||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/gener
|
|||||||
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
|
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
|
||||||
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
|
||||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -82,7 +83,9 @@ const Mongo = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -109,10 +112,17 @@ const Mongo = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -138,14 +148,19 @@ const Mongo = (
|
|||||||
<ShowMongoEnvironment mongoId={mongoId} />
|
<ShowMongoEnvironment mongoId={mongoId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-gene
|
|||||||
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
|
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
|
||||||
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
||||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -80,7 +81,9 @@ const MySql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -108,10 +111,17 @@ const MySql = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -137,14 +147,19 @@ const MySql = (
|
|||||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/sho
|
|||||||
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
||||||
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
||||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -81,7 +82,9 @@ const Postgresql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -109,10 +112,17 @@ const Postgresql = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@@ -138,14 +148,19 @@ const Postgresql = (
|
|||||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ import { ShowGeneralRedis } from "@/components/dashboard/redis/general/show-gene
|
|||||||
import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
|
import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/general/show-internal-redis-credentials";
|
||||||
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
||||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -80,7 +81,9 @@ const Redis = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||||
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
{data?.description}
|
{data?.description}
|
||||||
@@ -108,10 +111,17 @@ const Redis = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-y-scroll justify-start">
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -136,14 +146,19 @@ const Redis = (
|
|||||||
<ShowRedisEnvironment redisId={redisId} />
|
<ShowRedisEnvironment redisId={redisId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
{!data?.serverId && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs appName={data?.appName || ""} />
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
49
apps/dokploy/pages/dashboard/settings/servers.tsx
Normal file
49
apps/dokploy/pages/dashboard/settings/servers.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import React, { type ReactElement } from "react";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<ShowServers />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
Page.getLayout = (page: ReactElement) => {
|
||||||
|
return (
|
||||||
|
<DashboardLayout tab={"settings"}>
|
||||||
|
<SettingsLayout>{page}</SettingsLayout>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(
|
||||||
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
) {
|
||||||
|
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (user.rol === "user") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/dashboard/settings/profile",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
3
apps/dokploy/pages/hola.tsx
Normal file
3
apps/dokploy/pages/hola.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function hola() {
|
||||||
|
return <div>hola</div>;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { isAdminPresent } from "@/server/api/services/admin";
|
import { isAdminPresent } from "@/server/api/services/admin";
|
||||||
|
// import { IS_CLOUD } from "@/server/constants";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
@@ -220,6 +221,11 @@ const Register = ({ hasAdmin }: Props) => {
|
|||||||
|
|
||||||
export default Register;
|
export default Register;
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
|
// if (IS_CLOUD) {
|
||||||
|
// return {
|
||||||
|
// props: {},
|
||||||
|
// };
|
||||||
|
// }
|
||||||
const hasAdmin = await isAdminPresent();
|
const hasAdmin = await isAdminPresent();
|
||||||
|
|
||||||
if (hasAdmin) {
|
if (hasAdmin) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { redirectsRouter } from "./routers/redirects";
|
|||||||
import { redisRouter } from "./routers/redis";
|
import { redisRouter } from "./routers/redis";
|
||||||
import { registryRouter } from "./routers/registry";
|
import { registryRouter } from "./routers/registry";
|
||||||
import { securityRouter } from "./routers/security";
|
import { securityRouter } from "./routers/security";
|
||||||
|
import { serverRouter } from "./routers/server";
|
||||||
import { settingsRouter } from "./routers/settings";
|
import { settingsRouter } from "./routers/settings";
|
||||||
import { sshRouter } from "./routers/ssh-key";
|
import { sshRouter } from "./routers/ssh-key";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
@@ -35,6 +36,7 @@ import { userRouter } from "./routers/user";
|
|||||||
*
|
*
|
||||||
* All routers added in /api/routers should be manually added here.
|
* All routers added in /api/routers should be manually added here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
admin: adminRouter,
|
admin: adminRouter,
|
||||||
docker: dockerRouter,
|
docker: dockerRouter,
|
||||||
@@ -66,6 +68,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
bitbucket: bitbucketRouter,
|
bitbucket: bitbucketRouter,
|
||||||
gitlab: gitlabRouter,
|
gitlab: gitlabRouter,
|
||||||
github: githubRouter,
|
github: githubRouter,
|
||||||
|
server: serverRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
|
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
export const adminRouter = createTRPCRouter({
|
export const adminRouter = createTRPCRouter({
|
||||||
one: adminProcedure.query(async () => {
|
one: adminProcedure.query(async ({ ctx }) => {
|
||||||
const { sshPrivateKey, ...rest } = await findAdmin();
|
const { sshPrivateKey, ...rest } = await findAdmin();
|
||||||
return {
|
return {
|
||||||
haveSSH: !!sshPrivateKey,
|
haveSSH: !!sshPrivateKey,
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ import {
|
|||||||
cleanQueuesByApplication,
|
cleanQueuesByApplication,
|
||||||
} from "@/server/queues/deployments-queue";
|
} from "@/server/queues/deployments-queue";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
import {
|
import {
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
stopService,
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import {
|
import {
|
||||||
removeDirectoryCode,
|
removeDirectoryCode,
|
||||||
@@ -35,10 +38,13 @@ import {
|
|||||||
} from "@/server/utils/filesystem/directory";
|
} from "@/server/utils/filesystem/directory";
|
||||||
import {
|
import {
|
||||||
readConfig,
|
readConfig,
|
||||||
|
readRemoteConfig,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
|
writeConfigRemote,
|
||||||
} from "@/server/utils/traefik/application";
|
} from "@/server/utils/traefik/application";
|
||||||
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware";
|
import { deleteAllMiddlewares } from "@/server/utils/traefik/middleware";
|
||||||
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@@ -53,9 +59,6 @@ import {
|
|||||||
import { removeDeployments } from "../services/deployment";
|
import { removeDeployments } from "../services/deployment";
|
||||||
import { addNewService, checkServiceAccess } from "../services/user";
|
import { addNewService, checkServiceAccess } from "../services/user";
|
||||||
|
|
||||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
|
||||||
import { uploadFileSchema } from "@/utils/schema";
|
|
||||||
|
|
||||||
export const applicationRouter = createTRPCRouter({
|
export const applicationRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
@@ -96,9 +99,19 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiReloadApplication)
|
.input(apiReloadApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await stopService(input.appName);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
if (application.serverId) {
|
||||||
|
await stopServiceRemote(application.serverId, input.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(input.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "idle");
|
await updateApplicationStatus(input.applicationId, "idle");
|
||||||
await startService(input.appName);
|
|
||||||
|
if (application.serverId) {
|
||||||
|
await startServiceRemote(application.serverId, input.appName);
|
||||||
|
} else {
|
||||||
|
await startService(input.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "done");
|
await updateApplicationStatus(input.applicationId, "done");
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
@@ -121,12 +134,19 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => deleteAllMiddlewares(application),
|
async () => await deleteAllMiddlewares(application),
|
||||||
async () => await removeDeployments(application),
|
async () => await removeDeployments(application),
|
||||||
async () => await removeDirectoryCode(application?.appName),
|
async () =>
|
||||||
async () => await removeMonitoringDirectory(application?.appName),
|
await removeDirectoryCode(application.appName, application.serverId),
|
||||||
async () => await removeTraefikConfig(application?.appName),
|
async () =>
|
||||||
async () => await removeService(application?.appName),
|
await removeMonitoringDirectory(
|
||||||
|
application.appName,
|
||||||
|
application.serverId,
|
||||||
|
),
|
||||||
|
async () =>
|
||||||
|
await removeTraefikConfig(application.appName, application.serverId),
|
||||||
|
async () =>
|
||||||
|
await removeService(application?.appName, application.serverId),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const operation of cleanupOperations) {
|
for (const operation of cleanupOperations) {
|
||||||
@@ -142,7 +162,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
await stopService(service.appName);
|
if (service.serverId) {
|
||||||
|
await stopServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(service.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "idle");
|
await updateApplicationStatus(input.applicationId, "idle");
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
@@ -152,8 +176,11 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findApplicationById(input.applicationId);
|
const service = await findApplicationById(input.applicationId);
|
||||||
|
if (service.serverId) {
|
||||||
await startService(service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await startService(service.appName);
|
||||||
|
}
|
||||||
await updateApplicationStatus(input.applicationId, "done");
|
await updateApplicationStatus(input.applicationId, "done");
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
@@ -162,12 +189,14 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!application.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -306,13 +335,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!application.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -334,8 +365,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
let traefikConfig = null;
|
||||||
const traefikConfig = readConfig(application.appName);
|
if (application.serverId) {
|
||||||
|
traefikConfig = await readRemoteConfig(
|
||||||
|
application.serverId,
|
||||||
|
application.appName,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
traefikConfig = readConfig(application.appName);
|
||||||
|
}
|
||||||
return traefikConfig;
|
return traefikConfig;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -359,7 +397,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const app = await findApplicationById(input.applicationId as string);
|
const app = await findApplicationById(input.applicationId as string);
|
||||||
await unzipDrop(zipFile, app.appName);
|
await unzipDrop(zipFile, app);
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: app.applicationId,
|
applicationId: app.applicationId,
|
||||||
@@ -367,6 +405,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -382,7 +421,16 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
writeConfig(application.appName, input.traefikConfig);
|
|
||||||
|
if (application.serverId) {
|
||||||
|
await writeConfigRemote(
|
||||||
|
application.serverId,
|
||||||
|
application.appName,
|
||||||
|
input.traefikConfig,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
writeConfig(application.appName, input.traefikConfig);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
readAppMonitoring: protectedProcedure
|
readAppMonitoring: protectedProcedure
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { lucia, validateRequest } from "@/server/auth/auth";
|
import { lucia, validateRequest } from "@/server/auth/auth";
|
||||||
import { luciaToken } from "@/server/auth/token";
|
import { luciaToken } from "@/server/auth/token";
|
||||||
|
// import { IS_CLOUD } from "@/server/constants";
|
||||||
import {
|
import {
|
||||||
apiCreateAdmin,
|
apiCreateAdmin,
|
||||||
apiCreateUser,
|
apiCreateUser,
|
||||||
@@ -35,14 +36,16 @@ export const authRouter = createTRPCRouter({
|
|||||||
.input(apiCreateAdmin)
|
.input(apiCreateAdmin)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
|
// if (!IS_CLOUD) {
|
||||||
const admin = await db.query.admins.findFirst({});
|
const admin = await db.query.admins.findFirst({});
|
||||||
|
|
||||||
if (admin) {
|
if (admin) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Admin already exists",
|
message: "Admin already exists",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
const newAdmin = await createAdmin(input);
|
const newAdmin = await createAdmin(input);
|
||||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
const session = await lucia.createSession(newAdmin.id || "", {});
|
||||||
ctx.res.appendHeader(
|
ctx.res.appendHeader(
|
||||||
@@ -51,6 +54,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to create the main admin",
|
message: "Error to create the main admin",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
|
||||||
if (backup.enabled) {
|
if (backup.enabled) {
|
||||||
|
removeScheduleBackup(input.backupId);
|
||||||
scheduleBackup(backup);
|
scheduleBackup(backup);
|
||||||
} else {
|
} else {
|
||||||
removeScheduleBackup(input.backupId);
|
removeScheduleBackup(input.backupId);
|
||||||
@@ -90,7 +91,6 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||||
await runPostgresBackup(postgres, backup);
|
await runPostgresBackup(postgres, backup);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import {
|
|||||||
} from "@/server/queues/deployments-queue";
|
} from "@/server/queues/deployments-queue";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { createCommand } from "@/server/utils/builders/compose";
|
import { createCommand } from "@/server/utils/builders/compose";
|
||||||
|
import { randomizeComposeFile } from "@/server/utils/docker/compose";
|
||||||
import {
|
import {
|
||||||
randomizeComposeFile,
|
addDomainToCompose,
|
||||||
randomizeSpecificationFile,
|
cloneCompose,
|
||||||
} from "@/server/utils/docker/compose";
|
cloneComposeRemote,
|
||||||
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
|
} from "@/server/utils/docker/domain";
|
||||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
@@ -33,7 +34,7 @@ import { eq } from "drizzle-orm";
|
|||||||
import { dump } from "js-yaml";
|
import { dump } from "js-yaml";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin, findAdminById } from "../services/admin";
|
||||||
import {
|
import {
|
||||||
createCompose,
|
createCompose,
|
||||||
createComposeByTemplate,
|
createComposeByTemplate,
|
||||||
@@ -47,6 +48,7 @@ import { removeDeploymentsByComposeId } from "../services/deployment";
|
|||||||
import { createDomain, findDomainsByComposeId } from "../services/domain";
|
import { createDomain, findDomainsByComposeId } from "../services/domain";
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
import { findProjectById } from "../services/project";
|
import { findProjectById } from "../services/project";
|
||||||
|
import { findServerById } from "../services/server";
|
||||||
import { addNewService, checkServiceAccess } from "../services/user";
|
import { addNewService, checkServiceAccess } from "../services/user";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
@@ -130,7 +132,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
await cloneCompose(compose);
|
if (compose.serverId) {
|
||||||
|
await cloneComposeRemote(compose);
|
||||||
|
} else {
|
||||||
|
await cloneCompose(compose);
|
||||||
|
}
|
||||||
return compose.sourceType;
|
return compose.sourceType;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -151,9 +157,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
const domains = await findDomainsByComposeId(input.composeId);
|
const domains = await findDomainsByComposeId(input.composeId);
|
||||||
|
|
||||||
const composeFile = await addDomainToCompose(compose, domains);
|
const composeFile = await addDomainToCompose(compose, domains);
|
||||||
|
|
||||||
return dump(composeFile, {
|
return dump(composeFile, {
|
||||||
lineWidth: 1000,
|
lineWidth: 1000,
|
||||||
});
|
});
|
||||||
@@ -162,12 +166,14 @@ export const composeRouter = createTRPCRouter({
|
|||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -181,12 +187,14 @@ export const composeRouter = createTRPCRouter({
|
|||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: input.composeId,
|
composeId: input.composeId,
|
||||||
titleLog: "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -227,7 +235,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||||
|
|
||||||
const admin = await findAdmin();
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
let serverIp = admin.serverIp;
|
||||||
|
|
||||||
if (!admin.serverIp) {
|
if (!admin.serverIp) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -239,9 +248,15 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
serverIp = server.ipAddress;
|
||||||
|
} else if (process.env.NODE_ENV === "development") {
|
||||||
|
serverIp = "127.0.0.1";
|
||||||
|
}
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const { envs, mounts, domains } = generate({
|
const { envs, mounts, domains } = generate({
|
||||||
serverIp: admin.serverIp,
|
serverIp: serverIp || "",
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,6 +264,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
...input,
|
...input,
|
||||||
composeFile: composeFile,
|
composeFile: composeFile,
|
||||||
env: envs?.join("\n"),
|
env: envs?.join("\n"),
|
||||||
|
serverId: input.serverId,
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: `${projectName}-${generatePassword(6)}`,
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
apiFindAllByApplication,
|
apiFindAllByApplication,
|
||||||
apiFindAllByCompose,
|
apiFindAllByCompose,
|
||||||
|
apiFindAllByServer,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
findAllDeploymentsByApplicationId,
|
findAllDeploymentsByApplicationId,
|
||||||
findAllDeploymentsByComposeId,
|
findAllDeploymentsByComposeId,
|
||||||
|
findAllDeploymentsByServerId,
|
||||||
} from "../services/deployment";
|
} from "../services/deployment";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
@@ -20,4 +22,9 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await findAllDeploymentsByComposeId(input.composeId);
|
return await findAllDeploymentsByComposeId(input.composeId);
|
||||||
}),
|
}),
|
||||||
|
allByServer: protectedProcedure
|
||||||
|
.input(apiFindAllByServer)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await findAllDeploymentsByServerId(input.serverId);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
apiRemoveDestination,
|
apiRemoveDestination,
|
||||||
apiUpdateDestination,
|
apiUpdateDestination,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
|
import { execAsync } from "@/server/utils/process/execAsync";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin } from "../services/admin";
|
||||||
import {
|
import {
|
||||||
@@ -39,22 +39,22 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateDestination)
|
.input(apiCreateDestination)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { secretAccessKey, bucket, region, endpoint, accessKey } = input;
|
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 {
|
try {
|
||||||
await s3Client.send(headBucketCommand);
|
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 rcloneDestination = `:s3:${bucket}`;
|
||||||
|
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
|
await execAsync(rcloneCommand);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to connect to bucket",
|
message: "Error to connect to bucket",
|
||||||
|
|||||||
@@ -9,9 +9,15 @@ import {
|
|||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
export const dockerRouter = createTRPCRouter({
|
export const dockerRouter = createTRPCRouter({
|
||||||
getContainers: protectedProcedure.query(async () => {
|
getContainers: protectedProcedure
|
||||||
return await getContainers();
|
.input(
|
||||||
}),
|
z.object({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getContainers(input.serverId);
|
||||||
|
}),
|
||||||
|
|
||||||
restartContainer: protectedProcedure
|
restartContainer: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -27,10 +33,11 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
containerId: z.string().min(1),
|
containerId: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getConfig(input.containerId);
|
return await getConfig(input.containerId, input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getContainersByAppNameMatch: protectedProcedure
|
getContainersByAppNameMatch: protectedProcedure
|
||||||
@@ -40,19 +47,25 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
.union([z.literal("stack"), z.literal("docker-compose")])
|
.union([z.literal("stack"), z.literal("docker-compose")])
|
||||||
.optional(),
|
.optional(),
|
||||||
appName: z.string().min(1),
|
appName: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getContainersByAppNameMatch(input.appName, input.appType);
|
return await getContainersByAppNameMatch(
|
||||||
|
input.appName,
|
||||||
|
input.appType,
|
||||||
|
input.serverId,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getContainersByAppLabel: protectedProcedure
|
getContainersByAppLabel: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appName: z.string().min(1),
|
appName: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getContainersByAppLabel(input.appName);
|
return await getContainersByAppLabel(input.appName, input.serverId);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
apiCreateDomain,
|
apiCreateDomain,
|
||||||
apiCreateTraefikMeDomain,
|
|
||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
apiFindDomain,
|
apiFindDomain,
|
||||||
apiFindDomainByApplication,
|
|
||||||
apiFindDomainByCompose,
|
|
||||||
apiFindOneApplication,
|
apiFindOneApplication,
|
||||||
apiUpdateDomain,
|
apiUpdateDomain,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
|
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
import { findApplicationById } from "../services/application";
|
import { findApplicationById } from "../services/application";
|
||||||
import {
|
import {
|
||||||
createDomain,
|
createDomain,
|
||||||
@@ -47,9 +45,13 @@ export const domainRouter = createTRPCRouter({
|
|||||||
return await findDomainsByComposeId(input.composeId);
|
return await findDomainsByComposeId(input.composeId);
|
||||||
}),
|
}),
|
||||||
generateDomain: protectedProcedure
|
generateDomain: protectedProcedure
|
||||||
.input(apiCreateTraefikMeDomain)
|
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return generateTraefikMeDomain(input.appName);
|
return generateTraefikMeDomain(
|
||||||
|
input.appName,
|
||||||
|
ctx.user.adminId,
|
||||||
|
input.serverId,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
@@ -71,8 +73,10 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
const result = await removeDomainById(input.domainId);
|
const result = await removeDomainById(input.domainId);
|
||||||
if (domain.application) {
|
|
||||||
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
|
if (domain.applicationId) {
|
||||||
|
const application = await findApplicationById(domain.applicationId);
|
||||||
|
await removeDomain(application, domain.uniqueConfigKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
stopService,
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import {
|
import {
|
||||||
@@ -72,8 +74,11 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findMariadbById(input.mariadbId);
|
const service = await findMariadbById(input.mariadbId);
|
||||||
|
if (service.serverId) {
|
||||||
await startService(service.appName);
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await startService(service.appName);
|
||||||
|
}
|
||||||
await updateMariadbById(input.mariadbId, {
|
await updateMariadbById(input.mariadbId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
@@ -83,13 +88,18 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const mongo = await findMariadbById(input.mariadbId);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
await stopService(mongo.appName);
|
|
||||||
|
if (mariadb.serverId) {
|
||||||
|
await stopServiceRemote(mariadb.serverId, mariadb.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(mariadb.appName);
|
||||||
|
}
|
||||||
await updateMariadbById(input.mariadbId, {
|
await updateMariadbById(input.mariadbId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
|
|
||||||
return mongo;
|
return mariadb;
|
||||||
}),
|
}),
|
||||||
saveExternalPort: protectedProcedure
|
saveExternalPort: protectedProcedure
|
||||||
.input(apiSaveExternalPortMariaDB)
|
.input(apiSaveExternalPortMariaDB)
|
||||||
@@ -125,7 +135,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
const mongo = await findMariadbById(input.mariadbId);
|
const mongo = await findMariadbById(input.mariadbId);
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => await removeService(mongo?.appName),
|
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||||
async () => await removeMariadbById(input.mariadbId),
|
async () => await removeMariadbById(input.mariadbId),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -156,11 +166,21 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetMariadb)
|
.input(apiResetMariadb)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await stopService(input.appName);
|
const mariadb = await findMariadbById(input.mariadbId);
|
||||||
|
if (mariadb.serverId) {
|
||||||
|
await stopServiceRemote(mariadb.serverId, mariadb.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(mariadb.appName);
|
||||||
|
}
|
||||||
await updateMariadbById(input.mariadbId, {
|
await updateMariadbById(input.mariadbId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
await startService(input.appName);
|
|
||||||
|
if (mariadb.serverId) {
|
||||||
|
await startServiceRemote(mariadb.serverId, mariadb.appName);
|
||||||
|
} else {
|
||||||
|
await startService(mariadb.appName);
|
||||||
|
}
|
||||||
await updateMariadbById(input.mariadbId, {
|
await updateMariadbById(input.mariadbId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
stopService,
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import {
|
import {
|
||||||
@@ -74,7 +76,11 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findMongoById(input.mongoId);
|
const service = await findMongoById(input.mongoId);
|
||||||
|
|
||||||
await startService(service.appName);
|
if (service.serverId) {
|
||||||
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await startService(service.appName);
|
||||||
|
}
|
||||||
await updateMongoById(input.mongoId, {
|
await updateMongoById(input.mongoId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
@@ -85,7 +91,12 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
await stopService(mongo.appName);
|
|
||||||
|
if (mongo.serverId) {
|
||||||
|
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(mongo.appName);
|
||||||
|
}
|
||||||
await updateMongoById(input.mongoId, {
|
await updateMongoById(input.mongoId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
@@ -119,11 +130,21 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetMongo)
|
.input(apiResetMongo)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await stopService(input.appName);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
|
if (mongo.serverId) {
|
||||||
|
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(mongo.appName);
|
||||||
|
}
|
||||||
await updateMongoById(input.mongoId, {
|
await updateMongoById(input.mongoId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
await startService(input.appName);
|
|
||||||
|
if (mongo.serverId) {
|
||||||
|
await startServiceRemote(mongo.serverId, mongo.appName);
|
||||||
|
} else {
|
||||||
|
await startService(mongo.appName);
|
||||||
|
}
|
||||||
await updateMongoById(input.mongoId, {
|
await updateMongoById(input.mongoId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
@@ -139,7 +160,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
const mongo = await findMongoById(input.mongoId);
|
const mongo = await findMongoById(input.mongoId);
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => await removeService(mongo?.appName),
|
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||||
async () => await removeMongoById(input.mongoId),
|
async () => await removeMongoById(input.mongoId),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
stopService,
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
import {
|
import {
|
||||||
createMysql,
|
createMysql,
|
||||||
@@ -74,7 +75,11 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findMySqlById(input.mysqlId);
|
const service = await findMySqlById(input.mysqlId);
|
||||||
|
|
||||||
await startService(service.appName);
|
if (service.serverId) {
|
||||||
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await startService(service.appName);
|
||||||
|
}
|
||||||
await updateMySqlById(input.mysqlId, {
|
await updateMySqlById(input.mysqlId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
@@ -85,7 +90,11 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const mongo = await findMySqlById(input.mysqlId);
|
const mongo = await findMySqlById(input.mysqlId);
|
||||||
await stopService(mongo.appName);
|
if (mongo.serverId) {
|
||||||
|
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(mongo.appName);
|
||||||
|
}
|
||||||
await updateMySqlById(input.mysqlId, {
|
await updateMySqlById(input.mysqlId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
@@ -119,11 +128,20 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetMysql)
|
.input(apiResetMysql)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await stopService(input.appName);
|
const mysql = await findMySqlById(input.mysqlId);
|
||||||
|
if (mysql.serverId) {
|
||||||
|
await stopServiceRemote(mysql.serverId, mysql.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(mysql.appName);
|
||||||
|
}
|
||||||
await updateMySqlById(input.mysqlId, {
|
await updateMySqlById(input.mysqlId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
await startService(input.appName);
|
if (mysql.serverId) {
|
||||||
|
await startServiceRemote(mysql.serverId, mysql.appName);
|
||||||
|
} else {
|
||||||
|
await startService(mysql.appName);
|
||||||
|
}
|
||||||
await updateMySqlById(input.mysqlId, {
|
await updateMySqlById(input.mysqlId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
@@ -138,7 +156,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
const mongo = await findMySqlById(input.mysqlId);
|
const mongo = await findMySqlById(input.mysqlId);
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => await removeService(mongo?.appName),
|
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||||
async () => await removeMySqlById(input.mysqlId),
|
async () => await removeMySqlById(input.mysqlId),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
stopService,
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
@@ -74,7 +76,11 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const service = await findPostgresById(input.postgresId);
|
const service = await findPostgresById(input.postgresId);
|
||||||
|
|
||||||
await startService(service.appName);
|
if (service.serverId) {
|
||||||
|
await startServiceRemote(service.serverId, service.appName);
|
||||||
|
} else {
|
||||||
|
await startService(service.appName);
|
||||||
|
}
|
||||||
await updatePostgresById(input.postgresId, {
|
await updatePostgresById(input.postgresId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
@@ -85,7 +91,11 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
await stopService(postgres.appName);
|
if (postgres.serverId) {
|
||||||
|
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(postgres.appName);
|
||||||
|
}
|
||||||
await updatePostgresById(input.postgresId, {
|
await updatePostgresById(input.postgresId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
@@ -125,7 +135,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
const postgres = await findPostgresById(input.postgresId);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
removeService(postgres.appName),
|
removeService(postgres.appName, postgres.serverId),
|
||||||
removePostgresById(input.postgresId),
|
removePostgresById(input.postgresId),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -152,11 +162,21 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
reload: protectedProcedure
|
reload: protectedProcedure
|
||||||
.input(apiResetPostgres)
|
.input(apiResetPostgres)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await stopService(input.appName);
|
const postgres = await findPostgresById(input.postgresId);
|
||||||
|
if (postgres.serverId) {
|
||||||
|
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||||
|
} else {
|
||||||
|
await stopService(postgres.appName);
|
||||||
|
}
|
||||||
await updatePostgresById(input.postgresId, {
|
await updatePostgresById(input.postgresId, {
|
||||||
applicationStatus: "idle",
|
applicationStatus: "idle",
|
||||||
});
|
});
|
||||||
await startService(input.appName);
|
|
||||||
|
if (postgres.serverId) {
|
||||||
|
await startServiceRemote(postgres.serverId, postgres.appName);
|
||||||
|
} else {
|
||||||
|
await startService(postgres.appName);
|
||||||
|
}
|
||||||
await updatePostgresById(input.postgresId, {
|
await updatePostgresById(input.postgresId, {
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
cliProcedure,
|
|
||||||
createTRPCRouter,
|
|
||||||
protectedProcedure,
|
|
||||||
} from "@/server/api/trpc";
|
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
applications,
|
applications,
|
||||||
@@ -43,7 +39,8 @@ export const projectRouter = createTRPCRouter({
|
|||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkProjectAccess(ctx.user.authId, "create");
|
await checkProjectAccess(ctx.user.authId, "create");
|
||||||
}
|
}
|
||||||
const project = await createProject(input);
|
|
||||||
|
const project = await createProject(input, ctx.user.adminId);
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await addNewProject(ctx.user.authId, project.projectId);
|
await addNewProject(ctx.user.authId, project.projectId);
|
||||||
}
|
}
|
||||||
@@ -153,6 +150,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await db.query.projects.findMany({
|
return await db.query.projects.findMany({
|
||||||
with: {
|
with: {
|
||||||
applications: true,
|
applications: true,
|
||||||
@@ -163,6 +161,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
redis: true,
|
redis: true,
|
||||||
compose: true,
|
compose: true,
|
||||||
},
|
},
|
||||||
|
where: eq(projects.adminId, ctx.user.adminId),
|
||||||
orderBy: desc(projects.createdAt),
|
orderBy: desc(projects.createdAt),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user