mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
452793c8e5 | ||
|
|
38a75b07fb | ||
|
|
378b93f996 | ||
|
|
eb62d124bd | ||
|
|
bf6ed15ba7 | ||
|
|
31a66ce798 | ||
|
|
38c1d86e2f | ||
|
|
d08e232f50 | ||
|
|
3d49383c42 | ||
|
|
27706eaae4 | ||
|
|
c74b5a2677 | ||
|
|
0374165a7f | ||
|
|
b7dad5e1d9 | ||
|
|
65527bc39a | ||
|
|
096ef8cd93 | ||
|
|
d6eafcbb9b | ||
|
|
c0261384ca | ||
|
|
ca733addc2 | ||
|
|
7497671033 | ||
|
|
385fbf4af5 | ||
|
|
44e75ee7e1 | ||
|
|
6b4d6eac1d | ||
|
|
9379d4a31d | ||
|
|
dde799f510 | ||
|
|
ecb919e109 | ||
|
|
29ca894a97 | ||
|
|
84ba74a673 | ||
|
|
32b0d51e79 | ||
|
|
3e12e1b1b3 | ||
|
|
175e84f50e | ||
|
|
efb646c43d | ||
|
|
fa950dae39 | ||
|
|
712ad25e7a | ||
|
|
35a41e774e | ||
|
|
c2ac193fbe | ||
|
|
ce3c89a715 | ||
|
|
96f7206a1d | ||
|
|
b7ace886f3 | ||
|
|
5dc330eaa3 | ||
|
|
b7f5bee2f8 | ||
|
|
19ee5f073b | ||
|
|
1fd4a6ae80 | ||
|
|
3c8a412014 | ||
|
|
eee617719b | ||
|
|
fc611946a6 | ||
|
|
af13c84968 | ||
|
|
ddb78ef8dd | ||
|
|
3590f3bed2 | ||
|
|
c70089ee53 | ||
|
|
161e479a0b | ||
|
|
bd735bfb64 | ||
|
|
85c814620e | ||
|
|
fb013fe4ec | ||
|
|
90a1bd9027 | ||
|
|
610ef8f35c | ||
|
|
9b2fcaea31 | ||
|
|
2ffa95b9fa | ||
|
|
559872c4e4 | ||
|
|
85642ed5f2 | ||
|
|
8bf701d2f2 | ||
|
|
38809a2034 | ||
|
|
4bd6ec2232 | ||
|
|
95899b7208 | ||
|
|
ac26bb95e3 | ||
|
|
27256c609a | ||
|
|
c4d59177bf | ||
|
|
3c8ca2b012 |
@@ -52,7 +52,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& pnpm install -g tsx
|
||||
|
||||
# Install buildpacks
|
||||
RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
<div>
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://dokploy.com/og.png" >
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ title: "Overview"
|
||||
description: "Learn how to use Docker Compose with Dokploy"
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Dokploy integrates with Docker Compose and Docker Stack to provide flexible deployment solutions. Whether you are developing locally or deploying at scale, Dokploy facilitates application management through these powerful Docker tools.
|
||||
|
||||
### Configuration Methods
|
||||
|
||||
Dokploy provides two methods for creating Docker Compose configurations:
|
||||
|
||||
|
||||
- **Docker Compose**: Ideal for standard Docker Compose configurations.
|
||||
- **Stack**: Geared towards orchestrating applications using Docker Swarm. Note that some Docker Compose features, such as `build`, are not available in this mode.
|
||||
|
||||
@@ -21,7 +22,7 @@ Configure the source of your code, the way your application is built, and also m
|
||||
|
||||
A code editor within Dokploy allows you to specify environment variables for your Docker Compose file. By default, Dokploy creates a `.env` file in the specified Docker Compose file path.
|
||||
|
||||
### Monitoring
|
||||
### Monitoring
|
||||
|
||||
Monitor each service individually within Dokploy. If your application consists of multiple services, each can be monitored separately to ensure optimal performance.
|
||||
|
||||
@@ -29,7 +30,6 @@ Monitor each service individually within Dokploy. If your application consists o
|
||||
|
||||
Access detailed logs for each service through the Dokploy log viewer, which can help in troubleshooting and ensuring the stability of your services.
|
||||
|
||||
|
||||
### Deployments
|
||||
|
||||
You can view the last 10 deployments of your application. When you deploy your application in real time, a new deployment record will be created and it will gradually show you how your application is being built.
|
||||
@@ -38,7 +38,6 @@ We also offer a button to cancel deployments that are in queue. Note that those
|
||||
|
||||
We provide a webhook so that you can trigger your own deployments by pushing to your GitHub, Gitea, GitLab, Bitbucket repository.
|
||||
|
||||
|
||||
### Advanced
|
||||
|
||||
This section provides advanced configuration options for experienced users. It includes tools for custom commands within the container and volumes.
|
||||
@@ -46,4 +45,32 @@ This section provides advanced configuration options for experienced users. It i
|
||||
- **Command**: Dokploy has a defined command to run the Docker Compose file, ensuring complete control through the UI. However, you can append flags or options to the command.
|
||||
- **Volumes**: To ensure data persistence across deployments, configure storage volumes for your application.
|
||||
|
||||
<ImageZoom src="/assets/images/compose/overview.png" width={800} height={630} quality={100} priority alt='home og image' className="rounded-lg" />
|
||||
<ImageZoom
|
||||
src="/assets/images/compose/overview.png"
|
||||
width={800}
|
||||
height={630}
|
||||
quality={100}
|
||||
priority
|
||||
alt="home og image"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
<Callout title="Volumes">
|
||||
Docker volumes are a way to persist data generated and used by Docker containers. They are particularly useful for maintaining data between container restarts or for sharing data among different containers.
|
||||
|
||||
To bind a volume to the host machine, you can use the following syntax in your docker-compose.yml file, but this way will clean up the volumes when a new deployment is made:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "/folder:/path/in/container" ❌
|
||||
```
|
||||
|
||||
It's recommended to use the ../files folder to ensure your data persists between deployments. For example:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "../files/my-database:/var/lib/mysql" ✅
|
||||
- "../files/my-configs:/etc/my-app/config" ✅
|
||||
```
|
||||
|
||||
</Callout>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"fumadocs-core": "12.2.2",
|
||||
"fumadocs-mdx": "8.2.33",
|
||||
"fumadocs-openapi": "^3.1.3",
|
||||
"fumadocs-ui": "12.2.2",
|
||||
"fumadocs-core": "^12.5.6",
|
||||
"fumadocs-mdx": "^8.2.34",
|
||||
"fumadocs-openapi": "^3.3.0",
|
||||
"fumadocs-ui": "^12.5.6",
|
||||
"lucide-react": "^0.394.0",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.3.1",
|
||||
|
||||
89
apps/dokploy/__test__/compose/domain/labels.test.ts
Normal file
89
apps/dokploy/__test__/compose/domain/labels.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { Domain } from "@/server/api/services/domain";
|
||||
import { createDomainLabels } from "@/server/utils/docker/domain";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("createDomainLabels", () => {
|
||||
const appName = "test-app";
|
||||
const baseDomain: Domain = {
|
||||
host: "example.com",
|
||||
port: 8080,
|
||||
https: false,
|
||||
uniqueConfigKey: 1,
|
||||
certificateType: "none",
|
||||
applicationId: "",
|
||||
composeId: "",
|
||||
domainType: "compose",
|
||||
serviceName: "test-app",
|
||||
domainId: "",
|
||||
path: "/",
|
||||
createdAt: "",
|
||||
};
|
||||
|
||||
it("should create basic labels for web entrypoint", async () => {
|
||||
const labels = await createDomainLabels(appName, baseDomain, "web");
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-web.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-web.entrypoints=web",
|
||||
"traefik.http.services.test-app-1-web.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-web.service=test-app-1-web",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create labels for websecure entrypoint", async () => {
|
||||
const labels = await createDomainLabels(appName, baseDomain, "websecure");
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
|
||||
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add redirect middleware for https on web entrypoint", async () => {
|
||||
const httpsBaseDomain = { ...baseDomain, https: true };
|
||||
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file",
|
||||
);
|
||||
});
|
||||
|
||||
it("should add Let's Encrypt configuration for websecure with letsencrypt certificate", async () => {
|
||||
const letsencryptDomain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
certificateType: "letsencrypt" as const,
|
||||
};
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
letsencryptDomain,
|
||||
"websecure",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not add Let's Encrypt configuration for non-letsencrypt certificate", async () => {
|
||||
const nonLetsencryptDomain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
certificateType: "none" as const,
|
||||
};
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
nonLetsencryptDomain,
|
||||
"websecure",
|
||||
);
|
||||
expect(labels).not.toContain(
|
||||
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle different ports correctly", async () => {
|
||||
const customPortDomain = { ...baseDomain, port: 3000 };
|
||||
const labels = await createDomainLabels(appName, customPortDomain, "web");
|
||||
expect(labels).toContain(
|
||||
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
|
||||
);
|
||||
});
|
||||
});
|
||||
29
apps/dokploy/__test__/compose/domain/network-root.test.ts
Normal file
29
apps/dokploy/__test__/compose/domain/network-root.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToRoot", () => {
|
||||
it("should create network object if networks is undefined", () => {
|
||||
const result = addDokployNetworkToRoot(undefined);
|
||||
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||
});
|
||||
|
||||
it("should add network to an empty object", () => {
|
||||
const result = addDokployNetworkToRoot({});
|
||||
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||
});
|
||||
|
||||
it("should not modify existing network configuration", () => {
|
||||
const existing = { "dokploy-network": { external: false } };
|
||||
const result = addDokployNetworkToRoot(existing);
|
||||
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||
});
|
||||
|
||||
it("should add network alongside existing networks", () => {
|
||||
const existing = { "other-network": { external: true } };
|
||||
const result = addDokployNetworkToRoot(existing);
|
||||
expect(result).toEqual({
|
||||
"other-network": { external: true },
|
||||
"dokploy-network": { external: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
24
apps/dokploy/__test__/compose/domain/network-service.test.ts
Normal file
24
apps/dokploy/__test__/compose/domain/network-service.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToService", () => {
|
||||
it("should add network to an empty array", () => {
|
||||
const result = addDokployNetworkToService([]);
|
||||
expect(result).toEqual(["dokploy-network"]);
|
||||
});
|
||||
|
||||
it("should not add duplicate network to an array", () => {
|
||||
const result = addDokployNetworkToService(["dokploy-network"]);
|
||||
expect(result).toEqual(["dokploy-network"]);
|
||||
});
|
||||
|
||||
it("should add network to an existing array with other networks", () => {
|
||||
const result = addDokployNetworkToService(["other-network"]);
|
||||
expect(result).toEqual(["other-network", "dokploy-network"]);
|
||||
});
|
||||
|
||||
it("should add network to an object if networks is an object", () => {
|
||||
const result = addDokployNetworkToService({ "other-network": {} });
|
||||
expect(result).toEqual({ "other-network": {}, "dokploy-network": {} });
|
||||
});
|
||||
});
|
||||
@@ -67,6 +67,9 @@ const baseDomain: Domain = {
|
||||
https: false,
|
||||
path: null,
|
||||
port: null,
|
||||
serviceName: "",
|
||||
composeId: "",
|
||||
domainType: "application",
|
||||
uniqueConfigKey: 1,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -52,6 +52,7 @@ export const AddPort = ({
|
||||
applicationId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
@@ -82,6 +83,7 @@ export const AddPort = ({
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the port");
|
||||
@@ -89,7 +91,7 @@ export const AddPort = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -49,6 +49,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdatePort = ({ portId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data } = api.port.one.useQuery(
|
||||
{
|
||||
@@ -89,6 +90,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId: response?.applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the port");
|
||||
@@ -96,7 +98,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -45,6 +45,7 @@ export const AddRedirect = ({
|
||||
applicationId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
@@ -80,6 +81,7 @@ export const AddRedirect = ({
|
||||
await utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the redirect");
|
||||
@@ -87,7 +89,7 @@ export const AddRedirect = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
|
||||
export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data } = api.redirects.one.useQuery(
|
||||
{
|
||||
redirectId,
|
||||
@@ -84,6 +85,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId: response?.applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the redirect");
|
||||
@@ -91,7 +93,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -43,7 +43,7 @@ export const AddSecurity = ({
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.security.create.useMutation();
|
||||
|
||||
@@ -72,6 +72,7 @@ export const AddSecurity = ({
|
||||
await utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the security");
|
||||
@@ -79,7 +80,7 @@ export const AddSecurity = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -38,6 +38,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data } = api.security.one.useQuery(
|
||||
{
|
||||
@@ -79,6 +80,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId: response?.applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the security");
|
||||
@@ -86,7 +88,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -46,6 +46,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
<div className="flex flex-col pt-2 relative">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
value={data || "Empty"}
|
||||
disabled
|
||||
className="font-mono"
|
||||
|
||||
@@ -144,6 +144,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
|
||||
@@ -24,7 +24,7 @@ import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -77,6 +77,7 @@ export const AddVolumes = ({
|
||||
refetch,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync } = api.mounts.create.useMutation();
|
||||
const form = useForm<AddMount>({
|
||||
defaultValues: {
|
||||
@@ -103,6 +104,7 @@ export const AddVolumes = ({
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Created");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Bind mount");
|
||||
@@ -117,6 +119,7 @@ export const AddVolumes = ({
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Created");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Volume mount");
|
||||
@@ -132,6 +135,7 @@ export const AddVolumes = ({
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Created");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the File mount");
|
||||
@@ -142,7 +146,7 @@ export const AddVolumes = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>{children}</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
@@ -22,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -76,6 +77,7 @@ export const UpdateVolume = ({
|
||||
refetch,
|
||||
serviceType,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data } = api.mounts.one.useQuery(
|
||||
{
|
||||
@@ -135,6 +137,7 @@ export const UpdateVolume = ({
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Update");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Bind mount");
|
||||
@@ -148,6 +151,7 @@ export const UpdateVolume = ({
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Update");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Volume mount");
|
||||
@@ -162,6 +166,7 @@ export const UpdateVolume = ({
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Update");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the File mount");
|
||||
@@ -171,7 +176,7 @@ export const UpdateVolume = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
@@ -291,13 +296,15 @@ export const UpdateVolume = ({
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-volume"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
<DialogClose>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-volume"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||
<span>Webhook URL: </span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="text-muted-foreground">
|
||||
<span className="break-all text-muted-foreground">
|
||||
{`${url}/api/deploy/${data?.refreshToken}`}
|
||||
</span>
|
||||
<RefreshToken applicationId={applicationId} />
|
||||
@@ -72,7 +72,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
{deployments?.map((deployment) => (
|
||||
<div
|
||||
key={deployment.deploymentId}
|
||||
className="flex items-center justify-between rounded-lg border p-4"
|
||||
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">
|
||||
@@ -87,7 +87,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="break-all text-sm text-muted-foreground">
|
||||
{deployment.description}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -27,13 +27,20 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { domain } from "@/server/db/validations";
|
||||
import { domain } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Dices } from "lucide-react";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
@@ -60,10 +67,22 @@ export const AddDomain = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { data: application } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } = domainId
|
||||
? api.domain.update.useMutation()
|
||||
: api.domain.create.useMutation();
|
||||
|
||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||
api.domain.generateDomain.useMutation();
|
||||
|
||||
const form = useForm<Domain>({
|
||||
resolver: zodResolver(domain),
|
||||
});
|
||||
@@ -142,9 +161,42 @@ export const AddDomain = ({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="api.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
|
||||
<FormControl>
|
||||
<Input placeholder="api.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingGenerate}
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
appName: application?.appName || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
field.onChange(domain);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dices className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate traefik.me domain</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -44,12 +44,19 @@ export const DeleteDomain = ({ domainId }: Props) => {
|
||||
domainId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
if (data?.applicationId) {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: data?.applicationId,
|
||||
});
|
||||
} else if (data?.composeId) {
|
||||
utils.domain.byComposeId.invalidate({
|
||||
composeId: data?.composeId,
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Domain delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { GenerateTraefikMe } from "./generate-traefikme";
|
||||
import { GenerateWildCard } from "./generate-wildcard";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const GenerateDomain = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="secondary">
|
||||
Generate Domain
|
||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Generate Domain</DialogTitle>
|
||||
<DialogDescription>
|
||||
Generate Domains for your applications
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ul className="flex flex-col gap-4">
|
||||
<li className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-bold">
|
||||
1. Generate TraefikMe Domain
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
This option generates a free domain provided by{" "}
|
||||
<Link
|
||||
href="https://traefik.me"
|
||||
className="text-primary"
|
||||
target="_blank"
|
||||
>
|
||||
TraefikMe
|
||||
</Link>
|
||||
. We recommend using this for quick domain testing or if you
|
||||
don't have a domain yet.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/* <li className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-bold">
|
||||
2. Use Wildcard Domain
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
To use this option, you need to set up an 'A' record in your
|
||||
domain provider. For example, create a record for
|
||||
*.yourdomain.com.
|
||||
</div>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<div className="flex flex-row gap-4 w-full">
|
||||
<GenerateTraefikMe applicationId={applicationId} />
|
||||
{/* <GenerateWildCard applicationId={applicationId} /> */}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
export const GenerateTraefikMe = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Generate Domain
|
||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to generate a new domain?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will generate a new domain and will be used to access to the
|
||||
application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
toast.success("Generated Domain succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to generate Domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { SquareAsterisk } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
export const GenerateWildCard = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Generate Wildcard Domain
|
||||
<SquareAsterisk className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to generate a new wildcard domain?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will generate a new domain and will be used to access to the
|
||||
application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
toast.success("Generated Domain succesfully");
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(`Error to generate Domain: ${e.message}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,6 @@ import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { AddDomain } from "./add-domain";
|
||||
import { DeleteDomain } from "./delete-domain";
|
||||
import { GenerateDomain } from "./generate-domain";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
@@ -46,9 +45,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
</Button>
|
||||
</AddDomain>
|
||||
)}
|
||||
{data && data?.length > 0 && (
|
||||
<GenerateDomain applicationId={applicationId} />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-row gap-4">
|
||||
@@ -65,8 +61,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</Button>
|
||||
</AddDomain>
|
||||
|
||||
<GenerateDomain applicationId={applicationId} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.application.update.useMutation();
|
||||
@@ -79,6 +80,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
utils.application.one.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the application");
|
||||
@@ -87,7 +89,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
|
||||
441
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
441
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
@@ -0,0 +1,441 @@
|
||||
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,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { domainCompose } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domainCompose>;
|
||||
|
||||
export type CacheType = "fetch" | "cache";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
domainId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddDomainCompose = ({
|
||||
composeId,
|
||||
domainId = "",
|
||||
children,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||
const utils = api.useUtils();
|
||||
const { data, refetch } = api.domain.one.useQuery(
|
||||
{
|
||||
domainId,
|
||||
},
|
||||
{
|
||||
enabled: !!domainId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: services,
|
||||
isFetching: isLoadingServices,
|
||||
error: errorServices,
|
||||
refetch: refetchServices,
|
||||
} = api.compose.loadServices.useQuery(
|
||||
{
|
||||
composeId,
|
||||
type: cacheType,
|
||||
},
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||
api.domain.generateDomain.useMutation();
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } = domainId
|
||||
? api.domain.update.useMutation()
|
||||
: api.domain.create.useMutation();
|
||||
|
||||
const form = useForm<Domain>({
|
||||
resolver: zodResolver(domainCompose),
|
||||
});
|
||||
|
||||
const https = form.watch("https");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
...data,
|
||||
/* Convert null to undefined */
|
||||
path: data?.path || undefined,
|
||||
port: data?.port || undefined,
|
||||
serviceName: data?.serviceName || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (!domainId) {
|
||||
form.reset({});
|
||||
}
|
||||
}, [form, form.reset, data, isLoading]);
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error to update the domain"
|
||||
: "Error to create the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
: "In this section you can add domains",
|
||||
};
|
||||
|
||||
const onSubmit = async (data: Domain) => {
|
||||
await mutateAsync({
|
||||
domainId,
|
||||
composeId,
|
||||
domainType: "compose",
|
||||
...data,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.domain.byComposeId.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success(dictionary.success);
|
||||
if (domainId) {
|
||||
refetch();
|
||||
}
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(dictionary.error);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
<div className="flex flex-row gap-4 w-full items-end">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serviceName"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Service Name</FormLabel>
|
||||
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a service name" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
{services?.map((service, index) => (
|
||||
<SelectItem
|
||||
value={service}
|
||||
key={`${service}-${index}`}
|
||||
>
|
||||
{service}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none" disabled>
|
||||
Empty
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingServices}
|
||||
onClick={() => {
|
||||
if (cacheType === "fetch") {
|
||||
refetchServices();
|
||||
} else {
|
||||
setCacheType("fetch");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
services
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingServices}
|
||||
onClick={() => {
|
||||
if (cacheType === "cache") {
|
||||
refetchServices();
|
||||
} else {
|
||||
setCacheType("cache");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
compose, it will read the services from the
|
||||
last deployment/fetch from the repository
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
|
||||
<FormControl>
|
||||
<Input placeholder="api.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingGenerate}
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
appName: compose?.appName || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
field.onChange(domain);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dices className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate traefik.me domain</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"/"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"3000"}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{https && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
>
|
||||
{dictionary.submit}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { DeleteDomain } from "../../application/domains/delete-domain";
|
||||
import { AddDomainCompose } from "./add-domain";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.domain.byComposeId.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">Domains</CardTitle>
|
||||
<CardDescription>
|
||||
Domains are used to access to the application
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
{data && data?.length > 0 && (
|
||||
<AddDomainCompose composeId={composeId}>
|
||||
<Button>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-row gap-4">
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To access to the application is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<AddDomainCompose composeId={composeId}>
|
||||
<Button>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{data?.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.domainId}
|
||||
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||
>
|
||||
<Link target="_blank" href={`http://${item.host}`}>
|
||||
<ExternalLink className="size-5" />
|
||||
</Link>
|
||||
<Button variant="outline" disabled>
|
||||
{item.serviceName}
|
||||
</Button>
|
||||
<Input disabled value={item.host} />
|
||||
<Button variant="outline" disabled>
|
||||
{item.path}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.port}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.https ? "HTTPS" : "HTTP"}
|
||||
</Button>
|
||||
<div className="flex flex-row gap-1">
|
||||
<AddDomainCompose
|
||||
composeId={composeId}
|
||||
domainId={item.domainId}
|
||||
>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -72,7 +72,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
.then(async () => {
|
||||
toast.success("Compose config Updated");
|
||||
refetch();
|
||||
await utils.compose.allServices.invalidate({
|
||||
await utils.compose.getConvertedCompose.invalidate({
|
||||
composeId,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GitBranch, LockIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ComposeFileEditor } from "../compose-file-editor";
|
||||
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||
|
||||
@@ -29,7 +30,8 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
Select the source of your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2">
|
||||
<ShowConvertedCompose composeId={composeId} />
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { Puzzle, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const {
|
||||
data: compose,
|
||||
error,
|
||||
isError,
|
||||
refetch,
|
||||
} = api.compose.getConvertedCompose.useQuery(
|
||||
{ composeId },
|
||||
{
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="max-lg:w-full" variant="outline">
|
||||
<Puzzle className="h-4 w-4" />
|
||||
Preview Compose
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Converted Compose</DialogTitle>
|
||||
<DialogDescription>
|
||||
Preview your docker-compose file with added domains. Note: At least
|
||||
one domain must be specified for this conversion to take effect.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
mutateAsync({ composeId })
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Fetched source type");
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error("Error to fetch source type", {
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
Refresh <RefreshCw className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
/>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SquarePen } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateCompose = ({ composeId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.compose.update.useMutation();
|
||||
@@ -79,6 +80,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
||||
utils.compose.one.invalidate({
|
||||
composeId: composeId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Compose");
|
||||
@@ -87,7 +89,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -36,7 +36,7 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -57,6 +57,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data, isLoading } = api.destination.all.useQuery();
|
||||
const { data: backup } = api.backup.one.useQuery(
|
||||
{
|
||||
@@ -105,6 +106,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
.then(async () => {
|
||||
toast.success("Backup Updated");
|
||||
refetch();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the backup");
|
||||
@@ -112,7 +114,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -104,6 +104,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.mongo.update.useMutation();
|
||||
@@ -79,6 +80,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
utils.mongo.one.invalidate({
|
||||
mongoId: mongoId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update mongo database");
|
||||
@@ -87,7 +89,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.postgres.update.useMutation();
|
||||
@@ -79,6 +80,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
utils.postgres.one.invalidate({
|
||||
postgresId: postgresId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the postgres");
|
||||
@@ -87,7 +89,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -55,6 +55,7 @@ interface Props {
|
||||
|
||||
export const AddTemplate = ({ projectId }: Props) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data } = api.compose.templates.useQuery();
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const { data: tags, isLoading: isLoadingTags } =
|
||||
@@ -75,14 +76,14 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<PuzzleIcon className="size-4 text-muted-foreground" />
|
||||
<span>Templates</span>
|
||||
<span>Template</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
||||
@@ -283,6 +284,7 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -42,6 +42,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateProject = ({ projectId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError } = api.project.update.useMutation();
|
||||
const { data } = api.project.one.useQuery(
|
||||
@@ -77,6 +78,7 @@ export const UpdateProject = ({ projectId }: Props) => {
|
||||
.then(() => {
|
||||
toast.success("Project updated succesfully");
|
||||
utils.project.all.invalidate();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the project");
|
||||
@@ -85,7 +87,7 @@ export const UpdateProject = ({ projectId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -43,6 +43,7 @@ interface Props {
|
||||
|
||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data, refetch } = api.destination.one.useQuery(
|
||||
{
|
||||
destinationId,
|
||||
@@ -93,13 +94,14 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
toast.success("Destination Updated");
|
||||
await refetch();
|
||||
await utils.destination.all.invalidate();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Destination");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -9,36 +9,11 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import { BadgeCheck } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { RemoveGithubApp } from "./remove-github-app";
|
||||
export const generateName = () => {
|
||||
const n1 = ["Blue", "Green", "Red", "Orange", "Violet", "Indigo", "Yellow"];
|
||||
const n2 = [
|
||||
"One",
|
||||
"Two",
|
||||
"Three",
|
||||
"Four",
|
||||
"Five",
|
||||
"Six",
|
||||
"Seven",
|
||||
"Eight",
|
||||
"Nine",
|
||||
"Zero",
|
||||
];
|
||||
return `Dokploy-${n1[Math.round(Math.random() * (n1.length - 1))]}-${
|
||||
n2[Math.round(Math.random() * (n2.length - 1))]
|
||||
}`;
|
||||
};
|
||||
function slugify(text: string) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[\s\^&*()+=!]+/g, "-")
|
||||
.replace(/[\$.,*+~()'"!:@^&]+/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
export const GithubSetup = () => {
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
@@ -52,10 +27,9 @@ export const GithubSetup = () => {
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
||||
name: generateName(),
|
||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
// JUST FOR TESTING
|
||||
url: `${url}/api/deploy/github`,
|
||||
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
||||
},
|
||||
@@ -95,8 +69,8 @@ export const GithubSetup = () => {
|
||||
</div>
|
||||
<div className="flex items-end gap-4 flex-wrap">
|
||||
<RemoveGithubApp />
|
||||
{/* <Link
|
||||
href={`https://github.com/settings/apps/${data?.githubAppName}`}
|
||||
<Link
|
||||
href={`${data?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
@@ -104,7 +78,7 @@ export const GithubSetup = () => {
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Manage Github App</span>
|
||||
</Link> */}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -119,9 +93,9 @@ export const GithubSetup = () => {
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<Link
|
||||
href={`https://github.com/apps/${slugify(
|
||||
data.githubAppName,
|
||||
)}/installations/new?state=gh_setup:${data?.authId}`}
|
||||
href={`${
|
||||
data.githubAppName
|
||||
}/installations/new?state=gh_setup:${data?.authId}`}
|
||||
className={buttonVariants({ className: "w-fit" })}
|
||||
>
|
||||
Install Github App
|
||||
|
||||
@@ -27,7 +27,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Mail, PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
|
||||
export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data, refetch } = api.notification.one.useQuery(
|
||||
{
|
||||
notificationId,
|
||||
@@ -207,6 +208,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
toast.success("Notification Updated");
|
||||
await utils.notification.all.invalidate();
|
||||
refetch();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update a notification");
|
||||
@@ -214,7 +216,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ShowDestinations = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">SSH Keys</CardTitle>
|
||||
<CardDescription>
|
||||
Use SSH to beeing able cloning from private repositories.
|
||||
Use SSH to be able to clone from private repositories.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 pt-4">
|
||||
|
||||
@@ -39,6 +39,7 @@ const addPermissions = z.object({
|
||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||
canAccessToDocker: z.boolean().optional().default(false),
|
||||
canAccessToAPI: z.boolean().optional().default(false),
|
||||
canAccessToSSHKeys: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
type AddPermissions = z.infer<typeof addPermissions>;
|
||||
@@ -82,6 +83,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
});
|
||||
}
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||
@@ -98,6 +100,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
accesedServices: data.accesedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Permissions updated");
|
||||
@@ -270,6 +273,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToSSHKeys"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to SSH Keys</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the SSH Keys section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accesedProjects"
|
||||
|
||||
@@ -106,6 +106,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`providers:
|
||||
docker:
|
||||
|
||||
@@ -109,6 +109,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
|
||||
@@ -79,6 +79,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(user?.canAccessToSSHKeys
|
||||
? [
|
||||
{
|
||||
title: "SSH Keys",
|
||||
label: "",
|
||||
icon: KeyRound,
|
||||
href: "/dashboard/settings/ssh-keys",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { json } from "@codemirror/lang-json";
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { StreamLanguage } from "@codemirror/language";
|
||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { useTheme } from "next-themes";
|
||||
@@ -10,6 +11,7 @@ interface Props extends ReactCodeMirrorProps {
|
||||
wrapperClassName?: string;
|
||||
disabled?: boolean;
|
||||
language?: "yaml" | "json" | "properties";
|
||||
lineWrapping?: boolean;
|
||||
}
|
||||
|
||||
export const CodeEditor = ({
|
||||
@@ -36,6 +38,7 @@ export const CodeEditor = ({
|
||||
: language === "json"
|
||||
? json()
|
||||
: StreamLanguage.define(properties),
|
||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||
]}
|
||||
{...props}
|
||||
editable={!props.disabled}
|
||||
|
||||
1
apps/dokploy/drizzle/0030_little_kabuki.sql
Normal file
1
apps/dokploy/drizzle/0030_little_kabuki.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "canAccessToSSHKeys" boolean DEFAULT false NOT NULL;
|
||||
15
apps/dokploy/drizzle/0031_steep_vulture.sql
Normal file
15
apps/dokploy/drizzle/0031_steep_vulture.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."domainType" AS ENUM('compose', 'application');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "domain" ALTER COLUMN "applicationId" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "domain" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
||||
ALTER TABLE "domain" ADD COLUMN "domainType" "domainType" DEFAULT 'application';--> statement-breakpoint
|
||||
ALTER TABLE "domain" ADD COLUMN "composeId" text;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "domain" ADD CONSTRAINT "domain_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1
apps/dokploy/drizzle/0032_flashy_shadow_king.sql
Normal file
1
apps/dokploy/drizzle/0032_flashy_shadow_king.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "domain" ALTER COLUMN "port" SET DEFAULT 3000;
|
||||
3030
apps/dokploy/drizzle/meta/0030_snapshot.json
Normal file
3030
apps/dokploy/drizzle/meta/0030_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3071
apps/dokploy/drizzle/meta/0031_snapshot.json
Normal file
3071
apps/dokploy/drizzle/meta/0031_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3071
apps/dokploy/drizzle/meta/0032_snapshot.json
Normal file
3071
apps/dokploy/drizzle/meta/0032_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -211,6 +211,27 @@
|
||||
"when": 1722578386823,
|
||||
"tag": "0029_colossal_zodiak",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 30,
|
||||
"version": "6",
|
||||
"when": 1723608499147,
|
||||
"tag": "0030_little_kabuki",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 31,
|
||||
"version": "6",
|
||||
"when": 1723701656243,
|
||||
"tag": "0031_steep_vulture",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "6",
|
||||
"when": 1723705257806,
|
||||
"tag": "0032_flashy_shadow_king",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.6.0",
|
||||
"version": "v0.7.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -39,6 +39,7 @@
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/view": "6.29.0",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function handler(
|
||||
.update(admins)
|
||||
.set({
|
||||
githubAppId: data.id,
|
||||
githubAppName: data.name,
|
||||
githubAppName: data.html_url,
|
||||
githubClientId: data.client_id,
|
||||
githubClientSecret: data.client_secret,
|
||||
githubWebhookSecret: data.webhook_secret,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-c
|
||||
import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes";
|
||||
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
|
||||
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
|
||||
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
|
||||
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
|
||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||
@@ -34,6 +35,7 @@ type TabState =
|
||||
| "settings"
|
||||
| "advanced"
|
||||
| "deployments"
|
||||
| "domains"
|
||||
| "monitoring";
|
||||
|
||||
const Service = (
|
||||
@@ -117,12 +119,13 @@ const Service = (
|
||||
}}
|
||||
>
|
||||
<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="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
@@ -168,6 +171,12 @@ const Service = (
|
||||
<ShowDeploymentsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="domains">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomainsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommandCompose composeId={composeId} />
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -26,7 +29,7 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user || user.rol === "user") {
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@@ -34,8 +37,45 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req, res, resolvedUrl } = ctx;
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToSSHKeys) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateCompose,
|
||||
apiCreateComposeByTemplate,
|
||||
apiFetchServices,
|
||||
apiFindCompose,
|
||||
apiRandomizeCompose,
|
||||
apiUpdateCompose,
|
||||
@@ -15,16 +16,18 @@ import {
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { createCommand } from "@/server/utils/builders/compose";
|
||||
import { randomizeComposeFile } from "@/server/utils/docker/compose";
|
||||
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
|
||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { templates } from "@/templates/templates";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
import {
|
||||
generatePassword,
|
||||
loadTemplateModule,
|
||||
readComposeFile,
|
||||
readTemplateComposeFile,
|
||||
} from "@/templates/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { dump } from "js-yaml";
|
||||
import _ from "lodash";
|
||||
import { nanoid } from "nanoid";
|
||||
import { findAdmin } from "../services/admin";
|
||||
@@ -38,6 +41,7 @@ import {
|
||||
updateCompose,
|
||||
} from "../services/compose";
|
||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||
import { findDomainsByComposeId } from "../services/domain";
|
||||
import { createMount } from "../services/mount";
|
||||
import { findProjectById } from "../services/project";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
@@ -113,10 +117,25 @@ export const composeRouter = createTRPCRouter({
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
}),
|
||||
|
||||
allServices: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
loadServices: protectedProcedure
|
||||
.input(apiFetchServices)
|
||||
.query(async ({ input }) => {
|
||||
return await loadServices(input.composeId);
|
||||
return await loadServices(input.composeId, input.type);
|
||||
}),
|
||||
fetchSourceType: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
await cloneCompose(compose);
|
||||
return compose.sourceType;
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to fetch source type",
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
randomizeCompose: protectedProcedure
|
||||
@@ -124,6 +143,17 @@ export const composeRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
return await randomizeComposeFile(input.composeId, input.prefix);
|
||||
}),
|
||||
getConvertedCompose: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
const domains = await findDomainsByComposeId(input.composeId);
|
||||
|
||||
const composeFile = await addDomainToCompose(compose, domains);
|
||||
return dump(composeFile, {
|
||||
lineWidth: 1000,
|
||||
});
|
||||
}),
|
||||
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
@@ -189,7 +219,7 @@ export const composeRouter = createTRPCRouter({
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
}
|
||||
const composeFile = await readComposeFile(input.id);
|
||||
const composeFile = await readTemplateComposeFile(input.id);
|
||||
|
||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||
|
||||
@@ -227,7 +257,8 @@ export const composeRouter = createTRPCRouter({
|
||||
if (mounts && mounts?.length > 0) {
|
||||
for (const mount of mounts) {
|
||||
await createMount({
|
||||
mountPath: mount.mountPath,
|
||||
filePath: mount.filePath,
|
||||
mountPath: "",
|
||||
content: mount.content,
|
||||
serviceId: compose.composeId,
|
||||
serviceType: "compose",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateDomain,
|
||||
apiCreateTraefikMeDomain,
|
||||
apiFindCompose,
|
||||
apiFindDomain,
|
||||
apiFindDomainByApplication,
|
||||
apiFindDomainByCompose,
|
||||
apiFindOneApplication,
|
||||
apiUpdateDomain,
|
||||
} from "@/server/db/schema";
|
||||
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
|
||||
@@ -12,8 +16,8 @@ import {
|
||||
createDomain,
|
||||
findDomainById,
|
||||
findDomainsByApplicationId,
|
||||
generateDomain,
|
||||
generateWildcard,
|
||||
findDomainsByComposeId,
|
||||
generateTraefikMeDomain,
|
||||
removeDomainById,
|
||||
updateDomainById,
|
||||
} from "../services/domain";
|
||||
@@ -33,27 +37,30 @@ export const domainRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
byApplicationId: protectedProcedure
|
||||
.input(apiFindDomainByApplication)
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input }) => {
|
||||
return await findDomainsByApplicationId(input.applicationId);
|
||||
}),
|
||||
byComposeId: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input }) => {
|
||||
return await findDomainsByComposeId(input.composeId);
|
||||
}),
|
||||
generateDomain: protectedProcedure
|
||||
.input(apiFindDomainByApplication)
|
||||
.input(apiCreateTraefikMeDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
return generateDomain(input);
|
||||
}),
|
||||
generateWildcard: protectedProcedure
|
||||
.input(apiFindDomainByApplication)
|
||||
.mutation(async ({ input }) => {
|
||||
return generateWildcard(input);
|
||||
return generateTraefikMeDomain(input.appName);
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
const result = await updateDomainById(input.domainId, input);
|
||||
const domain = await findDomainById(input.domainId);
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
await manageDomain(application, domain);
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
await manageDomain(application, domain);
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => {
|
||||
@@ -64,7 +71,9 @@ export const domainRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
const domain = await findDomainById(input.domainId);
|
||||
const result = await removeDomainById(input.domainId);
|
||||
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
|
||||
if (domain.application) {
|
||||
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
|
||||
}
|
||||
|
||||
return result;
|
||||
}),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@/server/utils/docker/utils";
|
||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
import { spawnAsync } from "@/server/utils/process/spawnAsync";
|
||||
import {
|
||||
readConfig,
|
||||
@@ -49,14 +50,10 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
await spawnAsync("docker", [
|
||||
"service",
|
||||
"update",
|
||||
"--force",
|
||||
"--image",
|
||||
getDokployImage(),
|
||||
"dokploy",
|
||||
]);
|
||||
const { stdout } = await execAsync(
|
||||
"docker service inspect dokploy --format '{{.ID}}'",
|
||||
);
|
||||
await execAsync(`docker service update --force ${stdout.trim()}`);
|
||||
return true;
|
||||
}),
|
||||
reloadTraefik: adminProcedure.mutation(async () => {
|
||||
|
||||
@@ -34,21 +34,23 @@ export const sshRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
remove: adminProcedure.input(apiRemoveSshKey).mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removeSSHKeyById(input.sshKeyId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this ssh key",
|
||||
});
|
||||
}
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveSshKey)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removeSSHKeyById(input.sshKeyId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this ssh key",
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneSshKey).query(async ({ input }) => {
|
||||
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||
return sshKey;
|
||||
}),
|
||||
all: adminProcedure.query(async () => {
|
||||
all: protectedProcedure.query(async () => {
|
||||
return await db.query.sshKeys.findMany({});
|
||||
}),
|
||||
generate: protectedProcedure
|
||||
@@ -56,15 +58,17 @@ export const sshRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
return await generateSSHKey(input.type);
|
||||
}),
|
||||
update: adminProcedure.input(apiUpdateSshKey).mutation(async ({ input }) => {
|
||||
try {
|
||||
return await updateSSHKeyById(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this ssh key",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateSshKey)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await updateSSHKeyById(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this ssh key",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { db } from "@/server/db";
|
||||
import { type apiCreateCompose, compose } from "@/server/db/schema";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
import { buildCompose } from "@/server/utils/builders/compose";
|
||||
import { cloneCompose, loadDockerCompose } from "@/server/utils/docker/domain";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
|
||||
@@ -14,7 +15,6 @@ import { createComposeFile } from "@/server/utils/providers/raw";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { load } from "js-yaml";
|
||||
import { findAdmin, getDokployUrl } from "./admin";
|
||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
@@ -91,6 +91,7 @@ export const findComposeById = async (composeId: string) => {
|
||||
project: true,
|
||||
deployments: true,
|
||||
mounts: true,
|
||||
domains: true,
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
@@ -102,20 +103,27 @@ export const findComposeById = async (composeId: string) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export const loadServices = async (composeId: string) => {
|
||||
export const loadServices = async (
|
||||
composeId: string,
|
||||
type: "fetch" | "cache" = "fetch",
|
||||
) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
// use js-yaml to parse the docker compose file and then extact the services
|
||||
const composeFile = compose.composeFile;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
if (type === "fetch") {
|
||||
await cloneCompose(compose);
|
||||
}
|
||||
|
||||
const composeData = await loadDockerCompose(compose);
|
||||
if (!composeData?.services) {
|
||||
return ["All Services"];
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Services not found",
|
||||
});
|
||||
}
|
||||
|
||||
const services = Object.keys(composeData.services);
|
||||
|
||||
return [...services, "All Services"];
|
||||
return [...services];
|
||||
};
|
||||
|
||||
export const updateCompose = async (
|
||||
|
||||
@@ -15,8 +15,6 @@ export type Domain = typeof domains.$inferSelect;
|
||||
|
||||
export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
|
||||
const domain = await tx
|
||||
.insert(domains)
|
||||
.values({
|
||||
@@ -32,52 +30,19 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
});
|
||||
}
|
||||
|
||||
await manageDomain(application, domain);
|
||||
if (domain.applicationId) {
|
||||
const application = await findApplicationById(domain.applicationId);
|
||||
await manageDomain(application, domain);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const generateDomain = async (
|
||||
input: typeof apiFindDomainByApplication._type,
|
||||
) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
export const generateTraefikMeDomain = async (appName: string) => {
|
||||
const admin = await findAdmin();
|
||||
const domain = await createDomain({
|
||||
applicationId: application.applicationId,
|
||||
host: generateRandomDomain({
|
||||
serverIp: admin.serverIp || "",
|
||||
projectName: application.appName,
|
||||
}),
|
||||
port: 3000,
|
||||
certificateType: "none",
|
||||
https: false,
|
||||
path: "/",
|
||||
return generateRandomDomain({
|
||||
serverIp: admin.serverIp || "",
|
||||
projectName: appName,
|
||||
});
|
||||
|
||||
return domain;
|
||||
};
|
||||
|
||||
export const generateWildcard = async (
|
||||
input: typeof apiFindDomainByApplication._type,
|
||||
) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (!admin.host) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "We need a host to generate a wildcard domain",
|
||||
});
|
||||
}
|
||||
const domain = await createDomain({
|
||||
applicationId: application.applicationId,
|
||||
host: generateWildcardDomain(application.appName, admin.host || ""),
|
||||
port: 3000,
|
||||
certificateType: "none",
|
||||
https: false,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return domain;
|
||||
};
|
||||
|
||||
export const generateWildcardDomain = (
|
||||
@@ -114,6 +79,17 @@ export const findDomainsByApplicationId = async (applicationId: string) => {
|
||||
return domainsArray;
|
||||
};
|
||||
|
||||
export const findDomainsByComposeId = async (composeId: string) => {
|
||||
const domainsArray = await db.query.domains.findMany({
|
||||
where: eq(domains.composeId, composeId),
|
||||
with: {
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
|
||||
return domainsArray;
|
||||
};
|
||||
|
||||
export const updateDomainById = async (
|
||||
domainId: string,
|
||||
domainData: Partial<Domain>,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { sshKeys } from "@/server/db/schema/ssh-key";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { applicationStatus } from "./shared";
|
||||
@@ -72,6 +72,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
fields: [compose.customGitSSHKeyId],
|
||||
references: [sshKeys.sshKeyId],
|
||||
}),
|
||||
domains: many(domains),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(compose, {
|
||||
@@ -106,6 +107,11 @@ export const apiFindCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFetchServices = z.object({
|
||||
composeId: z.string().min(1),
|
||||
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
||||
});
|
||||
|
||||
export const apiUpdateCompose = createSchema.partial().extend({
|
||||
composeId: z.string(),
|
||||
composeFile: z.string().optional(),
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { domain } from "@/server/db/validations";
|
||||
import { domain } from "@/server/db/validations/domain";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
serial,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { certificateType } from "./shared";
|
||||
|
||||
export const domainType = pgEnum("domainType", ["compose", "application"]);
|
||||
|
||||
export const domains = pgTable("domain", {
|
||||
domainId: text("domainId")
|
||||
.notNull()
|
||||
@@ -13,15 +24,21 @@ export const domains = pgTable("domain", {
|
||||
.$defaultFn(() => nanoid()),
|
||||
host: text("host").notNull(),
|
||||
https: boolean("https").notNull().default(false),
|
||||
port: integer("port").default(80),
|
||||
port: integer("port").default(3000),
|
||||
path: text("path").default("/"),
|
||||
serviceName: text("serviceName"),
|
||||
domainType: domainType("domainType").default("application"),
|
||||
uniqueConfigKey: serial("uniqueConfigKey"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
});
|
||||
|
||||
@@ -30,6 +47,10 @@ export const domainsRelations = relations(domains, ({ one }) => ({
|
||||
fields: [domains.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [domains.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
||||
@@ -41,6 +62,9 @@ export const apiCreateDomain = createSchema.pick({
|
||||
https: true,
|
||||
applicationId: true,
|
||||
certificateType: true,
|
||||
composeId: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
});
|
||||
|
||||
export const apiFindDomain = createSchema
|
||||
@@ -53,6 +77,14 @@ export const apiFindDomainByApplication = createSchema.pick({
|
||||
applicationId: true,
|
||||
});
|
||||
|
||||
export const apiCreateTraefikMeDomain = createSchema.pick({}).extend({
|
||||
appName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindDomainByCompose = createSchema.pick({
|
||||
composeId: true,
|
||||
});
|
||||
|
||||
export const apiUpdateDomain = createSchema
|
||||
.pick({
|
||||
host: true,
|
||||
@@ -60,5 +92,7 @@ export const apiUpdateDomain = createSchema
|
||||
port: true,
|
||||
https: true,
|
||||
certificateType: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
})
|
||||
.merge(createSchema.pick({ domainId: true }).required());
|
||||
|
||||
@@ -28,6 +28,7 @@ export const users = pgTable("user", {
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
||||
canCreateServices: boolean("canCreateServices").notNull().default(false),
|
||||
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
||||
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
||||
@@ -107,6 +108,7 @@ export const apiAssignPermissions = createSchema
|
||||
canAccessToTraefikFiles: true,
|
||||
canAccessToDocker: true,
|
||||
canAccessToAPI: true,
|
||||
canAccessToSSHKeys: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
46
apps/dokploy/server/db/validations/domain.ts
Normal file
46
apps/dokploy/server/db/validations/domain.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const domain = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Add a hostname" }),
|
||||
path: z.string().min(1).optional(),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.max(65535, { message: "Port must be 65535 or below" })
|
||||
.optional(),
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (input.https && !input.certificateType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["certificateType"],
|
||||
message: "Required",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const domainCompose = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Host is required" }),
|
||||
path: z.string().min(1).optional(),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.max(65535, { message: "Port must be 65535 or below" })
|
||||
.optional(),
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||
serviceName: z.string().min(1, { message: "Service name is required" }),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (input.https && !input.certificateType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["certificateType"],
|
||||
message: "Required",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -35,27 +35,3 @@ export const sshKeyUpdate = sshKeyCreate.pick({
|
||||
export const sshKeyType = z.object({
|
||||
type: z.enum(["rsa", "ed25519"]).optional(),
|
||||
});
|
||||
|
||||
export const domain = z
|
||||
.object({
|
||||
host: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/, {
|
||||
message: "Invalid hostname",
|
||||
}),
|
||||
path: z.string().min(1).optional(),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.max(65535, { message: "Port must be 65535 or below" })
|
||||
.optional(),
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (input.https && !input.certificateType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["certificateType"],
|
||||
message: "Required",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,10 +36,9 @@ export const initializePostgres = async () => {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 5432,
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { PublishedPort: 5432 }
|
||||
: {}),
|
||||
PublishedPort: process.env.NODE_ENV === "development" ? 5432 : 0,
|
||||
Protocol: "tcp",
|
||||
PublishMode: "host",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -33,10 +33,9 @@ export const initializeRedis = async () => {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 6379,
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { PublishedPort: 6379 }
|
||||
: {}),
|
||||
PublishedPort: process.env.NODE_ENV === "development" ? 6379 : 0,
|
||||
Protocol: "tcp",
|
||||
PublishMode: "host",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,20 +8,28 @@ import { dirname, join } from "node:path";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import boxen from "boxen";
|
||||
import { writeDomainsToCompose } from "../docker/domain";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export type ComposeNested = InferResultType<
|
||||
"compose",
|
||||
{ project: true; mounts: true }
|
||||
{ project: true; mounts: true; domains: true }
|
||||
>;
|
||||
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { sourceType, appName, mounts, composeType, env, composePath } =
|
||||
compose;
|
||||
const {
|
||||
sourceType,
|
||||
appName,
|
||||
mounts,
|
||||
composeType,
|
||||
env,
|
||||
composePath,
|
||||
domains,
|
||||
} = compose;
|
||||
try {
|
||||
const command = createCommand(compose);
|
||||
|
||||
await writeDomainsToCompose(compose, domains);
|
||||
createEnvFile(compose);
|
||||
|
||||
const logContent = `
|
||||
|
||||
@@ -54,7 +54,11 @@ export const buildApplication = async (
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("Docker Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write("Error ❌");
|
||||
if (error instanceof Error) {
|
||||
writeStream.write(`Error ❌\n${error?.message}`);
|
||||
} else {
|
||||
writeStream.write("Error ❌");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
220
apps/dokploy/server/utils/docker/domain.ts
Normal file
220
apps/dokploy/server/utils/docker/domain.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import fs, { existsSync, readFileSync, writeSync } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Compose } from "@/server/api/services/compose";
|
||||
import type { Domain } from "@/server/api/services/domain";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { cloneGitRawRepository } from "../providers/git";
|
||||
import { cloneRawGithubRepository } from "../providers/github";
|
||||
import { createComposeFileRaw } from "../providers/raw";
|
||||
import type {
|
||||
ComposeSpecification,
|
||||
DefinitionsService,
|
||||
PropertiesNetworks,
|
||||
} from "./types";
|
||||
|
||||
export const cloneCompose = async (compose: Compose) => {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneRawGithubRepository(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRawRepository(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRaw(compose);
|
||||
}
|
||||
};
|
||||
|
||||
export const getComposePath = (compose: Compose) => {
|
||||
const { appName, sourceType, composePath } = compose;
|
||||
let path = "";
|
||||
|
||||
if (sourceType === "raw") {
|
||||
path = "docker-compose.yml";
|
||||
} else {
|
||||
path = composePath;
|
||||
}
|
||||
|
||||
return join(COMPOSE_PATH, appName, "code", path);
|
||||
};
|
||||
|
||||
export const loadDockerCompose = async (
|
||||
compose: Compose,
|
||||
): Promise<ComposeSpecification | null> => {
|
||||
const path = getComposePath(compose);
|
||||
|
||||
if (existsSync(path)) {
|
||||
const yamlStr = readFileSync(path, "utf8");
|
||||
const parsedConfig = load(yamlStr) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const readComposeFile = async (compose: Compose) => {
|
||||
const path = getComposePath(compose);
|
||||
if (existsSync(path)) {
|
||||
const yamlStr = readFileSync(path, "utf8");
|
||||
return yamlStr;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const writeDomainsToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
if (!domains.length) {
|
||||
return;
|
||||
}
|
||||
const composeConverted = await addDomainToCompose(compose, domains);
|
||||
|
||||
const path = getComposePath(compose);
|
||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
||||
try {
|
||||
await writeFile(path, composeString, "utf8");
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const addDomainToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
const { appName } = compose;
|
||||
const result = await loadDockerCompose(compose);
|
||||
|
||||
if (!result || domains.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const domain of domains) {
|
||||
const { serviceName, https } = domain;
|
||||
if (!serviceName) {
|
||||
throw new Error("Service name not found");
|
||||
}
|
||||
if (!result?.services?.[serviceName]) {
|
||||
throw new Error(`The service ${serviceName} not found in the compose`);
|
||||
}
|
||||
if (!result.services[serviceName].labels) {
|
||||
result.services[serviceName].labels = [];
|
||||
}
|
||||
|
||||
const httpLabels = await createDomainLabels(appName, domain, "web");
|
||||
if (https) {
|
||||
const httpsLabels = await createDomainLabels(
|
||||
appName,
|
||||
domain,
|
||||
"websecure",
|
||||
);
|
||||
httpLabels.push(...httpsLabels);
|
||||
}
|
||||
|
||||
const labels = result.services[serviceName].labels;
|
||||
|
||||
if (Array.isArray(labels)) {
|
||||
if (!labels.includes("traefik.enable=true")) {
|
||||
labels.push("traefik.enable=true");
|
||||
}
|
||||
labels.push(...httpLabels);
|
||||
}
|
||||
|
||||
// Add the dokploy-network to the service
|
||||
result.services[serviceName].networks = addDokployNetworkToService(
|
||||
result.services[serviceName].networks,
|
||||
);
|
||||
}
|
||||
|
||||
// Add dokploy-network to the root of the compose file
|
||||
result.networks = addDokployNetworkToRoot(result.networks);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const writeComposeFile = async (
|
||||
compose: Compose,
|
||||
composeSpec: ComposeSpecification,
|
||||
) => {
|
||||
const path = getComposePath(compose);
|
||||
|
||||
try {
|
||||
const composeFile = dump(composeSpec, {
|
||||
lineWidth: 1000,
|
||||
});
|
||||
fs.writeFileSync(path, composeFile, "utf8");
|
||||
} catch (e) {
|
||||
console.error("Error saving the YAML config file:", e);
|
||||
}
|
||||
};
|
||||
|
||||
export const createDomainLabels = async (
|
||||
appName: string,
|
||||
domain: Domain,
|
||||
entrypoint: "web" | "websecure",
|
||||
) => {
|
||||
const { host, port, https, uniqueConfigKey, certificateType } = domain;
|
||||
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
||||
const labels = [
|
||||
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)`,
|
||||
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
|
||||
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
|
||||
`traefik.http.routers.${routerName}.service=${routerName}`,
|
||||
];
|
||||
|
||||
if (entrypoint === "web" && https) {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
|
||||
);
|
||||
}
|
||||
|
||||
if (entrypoint === "websecure") {
|
||||
if (certificateType === "letsencrypt") {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
||||
|
||||
export const addDokployNetworkToService = (
|
||||
networkService: DefinitionsService["networks"],
|
||||
) => {
|
||||
let networks = networkService;
|
||||
const network = "dokploy-network";
|
||||
if (!networks) {
|
||||
networks = [];
|
||||
}
|
||||
|
||||
if (Array.isArray(networks)) {
|
||||
if (!networks.includes(network)) {
|
||||
networks.push(network);
|
||||
}
|
||||
} else if (networks && typeof networks === "object") {
|
||||
if (!(network in networks)) {
|
||||
networks[network] = {};
|
||||
}
|
||||
}
|
||||
|
||||
return networks;
|
||||
};
|
||||
|
||||
export const addDokployNetworkToRoot = (
|
||||
networkRoot: PropertiesNetworks | undefined,
|
||||
) => {
|
||||
let networks = networkRoot;
|
||||
const network = "dokploy-network";
|
||||
|
||||
if (!networks) {
|
||||
networks = {};
|
||||
}
|
||||
|
||||
if (networks[network] || !networks[network]) {
|
||||
networks[network] = {
|
||||
external: true,
|
||||
};
|
||||
}
|
||||
|
||||
return networks;
|
||||
};
|
||||
@@ -33,7 +33,9 @@ export const cloneGitRepository = async (
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
}
|
||||
await recreateDirectory(outputPath);
|
||||
// const command = `GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}" git clone --branch ${customGitBranch} --depth 1 ${customGitUrl} ${gitCopyPath} --progress`;
|
||||
// const { stdout, stderr } = await execAsync(command);
|
||||
@@ -56,6 +58,7 @@ export const cloneGitRepository = async (
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
@@ -84,6 +87,11 @@ export const cloneGitRepository = async (
|
||||
}
|
||||
};
|
||||
|
||||
const isHttpOrHttps = (url: string): boolean => {
|
||||
const regex = /^https?:\/\//;
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
@@ -121,7 +129,7 @@ const sanitizeRepoPathSSH = (input: string) => {
|
||||
return {
|
||||
user: found.groups?.user ?? "git",
|
||||
domain: found.groups?.domain,
|
||||
port: 22,
|
||||
port: Number(found.groups?.port ?? 22),
|
||||
owner: found.groups?.owner ?? "",
|
||||
repo: found.groups?.repo,
|
||||
get repoPath() {
|
||||
@@ -131,3 +139,61 @@ const sanitizeRepoPathSSH = (input: string) => {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const cloneGitRawRepository = async (entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
}) => {
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { Admin } from "@/server/api/services/admin";
|
||||
import { type Admin, findAdmin } from "@/server/api/services/admin";
|
||||
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -128,3 +128,34 @@ export const cloneGithubRepository = async (
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepository = async (entity: {
|
||||
appName: string;
|
||||
repository?: string | null;
|
||||
owner?: string | null;
|
||||
branch?: string | null;
|
||||
}) => {
|
||||
const { appName, repository, owner, branch } = entity;
|
||||
const admin = await findAdmin();
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(admin);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,3 +26,15 @@ export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const createComposeFileRaw = async (compose: Compose) => {
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
await writeFile(filePath, composeFile);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export function generate(schema: Schema): Template {
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./config.toml",
|
||||
filePath: "config.toml",
|
||||
content: `[app]
|
||||
address = "0.0.0.0:9000"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function generate(schema: Schema): Template {
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||
filePath: "/clickhouse/clickhouse-config.xml",
|
||||
content: `
|
||||
<clickhouse>
|
||||
<logger>
|
||||
@@ -45,7 +45,7 @@ export function generate(schema: Schema): Template {
|
||||
`,
|
||||
},
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-user-config.xml",
|
||||
filePath: "/clickhouse/clickhouse-user-config.xml",
|
||||
content: `
|
||||
<clickhouse>
|
||||
<profiles>
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface Schema {
|
||||
export interface Template {
|
||||
envs: string[];
|
||||
mounts?: {
|
||||
mountPath: string;
|
||||
filePath: string;
|
||||
content?: string;
|
||||
}[];
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export const loadTemplateModule = async (
|
||||
return generate;
|
||||
};
|
||||
|
||||
export const readComposeFile = async (id: string) => {
|
||||
export const readTemplateComposeFile = async (id: string) => {
|
||||
const cwd = process.cwd();
|
||||
const composeFile = await readFile(
|
||||
join(cwd, ".next", "templates", id, "docker-compose.yml"),
|
||||
|
||||
@@ -81,6 +81,7 @@ docker service create \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||
--publish published=3000,target=3000,mode=host \
|
||||
--update-parallelism 1 \
|
||||
--update-order stop-first \
|
||||
|
||||
@@ -65,6 +65,7 @@ docker service create \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||
--publish published=3000,target=3000,mode=host \
|
||||
--update-parallelism 1 \
|
||||
--update-order stop-first \
|
||||
|
||||
@@ -81,6 +81,7 @@ docker service create \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||
--publish published=3000,target=3000,mode=host \
|
||||
--update-parallelism 1 \
|
||||
--update-order stop-first \
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"format-and-lint": "biome check .",
|
||||
"check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true",
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"prepare": "node .husky/install.mjs"
|
||||
"prepare": "node ./.config/.husky/install.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "16.4.5",
|
||||
|
||||
150
pnpm-lock.yaml
generated
150
pnpm-lock.yaml
generated
@@ -39,17 +39,17 @@ importers:
|
||||
apps/docs:
|
||||
dependencies:
|
||||
fumadocs-core:
|
||||
specifier: 12.2.2
|
||||
version: 12.2.2(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: ^12.5.6
|
||||
version: 12.5.6(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
fumadocs-mdx:
|
||||
specifier: 8.2.33
|
||||
version: 8.2.33(fumadocs-core@12.2.2(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
specifier: ^8.2.34
|
||||
version: 8.2.34(fumadocs-core@12.5.6(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
fumadocs-openapi:
|
||||
specifier: ^3.1.3
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
fumadocs-ui:
|
||||
specifier: 12.2.2
|
||||
version: 12.2.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.7)
|
||||
specifier: ^12.5.6
|
||||
version: 12.5.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.7)
|
||||
lucide-react:
|
||||
specifier: ^0.394.0
|
||||
version: 0.394.0(react@18.3.1)
|
||||
@@ -114,6 +114,9 @@ importers:
|
||||
'@codemirror/legacy-modes':
|
||||
specifier: 6.4.0
|
||||
version: 6.4.0
|
||||
'@codemirror/view':
|
||||
specifier: 6.29.0
|
||||
version: 6.29.0
|
||||
'@dokploy/trpc-openapi':
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4(@trpc/server@10.45.2)(zod@3.23.8)
|
||||
@@ -2251,6 +2254,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-accordion@1.2.0':
|
||||
resolution: {integrity: sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.1':
|
||||
resolution: {integrity: sha512-wmCoJwj7byuVuiLKqDLlX7ClSUU0vd9sdCeM+2Ls+uf13+cpSJoMgwysHq1SGVVkJj5Xn0XWi1NoRCdkMpr6Mw==}
|
||||
peerDependencies:
|
||||
@@ -2316,6 +2332,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collapsible@1.1.0':
|
||||
resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collection@1.0.3':
|
||||
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
||||
peerDependencies:
|
||||
@@ -5409,15 +5438,15 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fumadocs-core@12.2.2:
|
||||
resolution: {integrity: sha512-e8WZ/+iNUuqsxMHWYr/gBXjB5tHVJpeNfNkINyzrwDpatVQ3Ds3sGyvPn/cWkNka4ZYvMbwkjw/7n/0ijsjfRA==}
|
||||
fumadocs-core@12.5.6:
|
||||
resolution: {integrity: sha512-sKbVq+Yrf7uLZD/pHojLGwClRxuOyjR1NaIlm/cO9dCaEO6n568NTom/2rEn+jyhII+LbGTJqfIJmktBW6rgNA==}
|
||||
peerDependencies:
|
||||
next: '>= 14.1.0'
|
||||
react: '>= 18'
|
||||
react-dom: '>= 18'
|
||||
|
||||
fumadocs-mdx@8.2.33:
|
||||
resolution: {integrity: sha512-bKT4CaFMWrPYUqsM2MD6uN4i2XIgZ2mCw9XoMJMpPS3Oq2Eq5MlNs9B5L6ItO7FfckCb9hoCA6LHdT1tVkgtyQ==}
|
||||
fumadocs-mdx@8.2.34:
|
||||
resolution: {integrity: sha512-vq7gd16z3fxMCjkjzFSTkqbQXbhZ3ucNvXAWdQxhkbhRbx3EAcmokkbcySsqVdqHPDIm0eGmCV33fQJsku1DgA==}
|
||||
peerDependencies:
|
||||
fumadocs-core: 12.x.x
|
||||
next: '>= 14.1.0'
|
||||
@@ -5425,8 +5454,8 @@ packages:
|
||||
fumadocs-openapi@3.3.0:
|
||||
resolution: {integrity: sha512-a6G1+FoBA4kH2HnjlgwmPpO9z4itrKPRENkVqxJPsOCwpCKdHZc6Tpwogo9CXvEifGqjAvyTbhNPy0N3+YmHLg==}
|
||||
|
||||
fumadocs-ui@12.2.2:
|
||||
resolution: {integrity: sha512-cOUiGk/Bp8Cqd/lBx5LqjbCHO4Ny8qtFItJog6aI5POFVJDJoYuCMv65MLoS/ar7QxIdaiFSw2XYYziZQaLmrQ==}
|
||||
fumadocs-ui@12.5.6:
|
||||
resolution: {integrity: sha512-8kYsrSCbRuo2rS09xTAb5HejtH8QDsSlqWGC8k+D9GI4WdAW7kAQCg6k9idnhhP/nq3QOsyMtvpoUGkje91bpQ==}
|
||||
peerDependencies:
|
||||
next: '>= 14.1.0'
|
||||
react: '>= 18'
|
||||
@@ -6212,10 +6241,10 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0
|
||||
|
||||
lucide-react@0.395.0:
|
||||
resolution: {integrity: sha512-6hzdNH5723A4FLaYZWpK50iyZH8iS2Jq5zuPRRotOFkhu6kxxJiebVdJ72tCR5XkiIeYFOU5NUawFZOac+VeYw==}
|
||||
lucide-react@0.414.0:
|
||||
resolution: {integrity: sha512-Krr/MHg9AWoJc52qx8hyJ64X9++JNfS1wjaJviLM1EP/68VNB7Tv0VMldLCB1aUe6Ka9QxURPhQm/eB6cqOM3A==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
luxon@3.4.4:
|
||||
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
|
||||
@@ -10203,18 +10232,17 @@ snapshots:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/react-accordion@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
'@radix-ui/react-accordion@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.0
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
'@radix-ui/react-collapsible': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
@@ -10298,17 +10326,16 @@ snapshots:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
'@radix-ui/react-collapsible@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.0
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
@@ -10328,19 +10355,6 @@ snapshots:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.0
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.2.0)
|
||||
@@ -10529,13 +10543,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@radix-ui/react-direction@1.0.1(@types/react@18.3.3)(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.0
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@radix-ui/react-direction@1.1.0(@types/react@18.3.3)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -12409,7 +12416,7 @@ snapshots:
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 2.0.10
|
||||
'@types/unist': 3.0.2
|
||||
|
||||
'@types/http-cache-semantics@4.0.4': {}
|
||||
|
||||
@@ -12429,7 +12436,7 @@ snapshots:
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 2.0.10
|
||||
'@types/unist': 3.0.2
|
||||
|
||||
'@types/mdx@2.0.13': {}
|
||||
|
||||
@@ -14033,7 +14040,7 @@ snapshots:
|
||||
eslint: 8.45.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.45.0)
|
||||
eslint-plugin-react: 7.35.0(eslint@8.45.0)
|
||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.45.0)
|
||||
@@ -14057,7 +14064,7 @@ snapshots:
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.45.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.5
|
||||
is-core-module: 2.15.0
|
||||
@@ -14079,7 +14086,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.8
|
||||
array.prototype.findlastindex: 1.2.5
|
||||
@@ -14434,7 +14441,7 @@ snapshots:
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
fumadocs-core@12.2.2(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
fumadocs-core@12.5.6(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.5.4
|
||||
'@shikijs/rehype': 1.12.0
|
||||
@@ -14458,13 +14465,13 @@ snapshots:
|
||||
- '@types/react'
|
||||
- supports-color
|
||||
|
||||
fumadocs-mdx@8.2.33(fumadocs-core@12.2.2(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
fumadocs-mdx@8.2.34(fumadocs-core@12.5.6(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
dependencies:
|
||||
'@mdx-js/mdx': 3.0.1
|
||||
cross-spawn: 7.0.3
|
||||
estree-util-value-to-estree: 3.1.2
|
||||
fast-glob: 3.3.2
|
||||
fumadocs-core: 12.2.2(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
fumadocs-core: 12.5.6(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
gray-matter: 4.0.3
|
||||
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
zod: 3.23.8
|
||||
@@ -14479,10 +14486,10 @@ snapshots:
|
||||
json-schema-to-typescript: 14.1.0
|
||||
openapi-sampler: 1.5.1
|
||||
|
||||
fumadocs-ui@12.2.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.7):
|
||||
fumadocs-ui@12.5.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.7):
|
||||
dependencies:
|
||||
'@radix-ui/react-accordion': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-accordion': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-collapsible': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-dialog': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-popover': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -14491,13 +14498,14 @@ snapshots:
|
||||
'@tailwindcss/typography': 0.5.13(tailwindcss@3.4.7)
|
||||
class-variance-authority: 0.7.0
|
||||
cmdk: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
fumadocs-core: 12.2.2(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
lucide-react: 0.395.0(react@18.3.1)
|
||||
fumadocs-core: 12.5.6(@types/react@18.3.3)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
lucide-react: 0.414.0(react@18.3.1)
|
||||
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-themes: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-medium-image-zoom: 5.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
swr: 2.2.5(react@18.3.1)
|
||||
tailwind-merge: 2.4.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
@@ -15357,7 +15365,7 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
lucide-react@0.395.0(react@18.3.1):
|
||||
lucide-react@0.414.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user