mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b7c7632f4 | ||
|
|
7db662e332 | ||
|
|
cd1a686b59 | ||
|
|
78d573c4f3 | ||
|
|
0ef9b1427b | ||
|
|
993d6b52f2 | ||
|
|
b9ab4a4d1a | ||
|
|
83153471b8 | ||
|
|
909e536f45 | ||
|
|
295cf50060 | ||
|
|
dd16baf234 | ||
|
|
72c366aa10 | ||
|
|
9a4f79f9e6 | ||
|
|
30b81834fc | ||
|
|
4e3aaa2a69 | ||
|
|
3dcd89cc32 | ||
|
|
41dc388bb0 | ||
|
|
44a592f7a7 | ||
|
|
1a4f5607dc | ||
|
|
d54c6e4ac9 | ||
|
|
936cf76a4c | ||
|
|
65254f1686 | ||
|
|
b312b1d7e0 | ||
|
|
1b7244e841 | ||
|
|
7eb1716d71 | ||
|
|
b8a8e32659 | ||
|
|
eea00d28cd | ||
|
|
449a61e208 | ||
|
|
037f3ed118 | ||
|
|
471802c4da | ||
|
|
502ff638d6 | ||
|
|
750c12ded5 | ||
|
|
7fb34ade00 | ||
|
|
0c197c095b | ||
|
|
4cbff731d1 | ||
|
|
113df9ae12 | ||
|
|
7f13fd24ec | ||
|
|
290228116a | ||
|
|
fae180f157 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ yarn-error.log*
|
|||||||
|
|
||||||
*.lockb
|
*.lockb
|
||||||
*.rdb
|
*.rdb
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -39,10 +39,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
|||||||
|
|
||||||
Getestete Systems:
|
Getestete Systems:
|
||||||
|
|
||||||
- Ubuntu 20.04
|
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||||
|
- Ubuntu 23.10 (Mantic Minotaur)
|
||||||
|
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||||
|
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||||
|
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||||
|
- Debian 12
|
||||||
- Debian 11
|
- Debian 11
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Centos 9
|
- Centos 9
|
||||||
|
- Centos 8
|
||||||
|
|
||||||
|
|
||||||
## 📄 Dokumentation
|
## 📄 Dokumentation
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
|||||||
|
|
||||||
Проверенные системы:
|
Проверенные системы:
|
||||||
|
|
||||||
- Ubuntu 20.04
|
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||||
|
- Ubuntu 23.10 (Mantic Minotaur)
|
||||||
|
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||||
|
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||||
|
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||||
|
- Debian 12
|
||||||
- Debian 11
|
- Debian 11
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Centos 9
|
- Centos 9
|
||||||
|
- Centos 8
|
||||||
|
|
||||||
|
|
||||||
## 📄 Документация
|
## 📄 Документация
|
||||||
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).
|
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||||
|
|||||||
@@ -43,10 +43,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
|||||||
|
|
||||||
经过测试的系统:
|
经过测试的系统:
|
||||||
|
|
||||||
- Ubuntu 20.04
|
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||||
|
- Ubuntu 23.10 (Mantic Minotaur)
|
||||||
|
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||||
|
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||||
|
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||||
|
- Debian 12
|
||||||
- Debian 11
|
- Debian 11
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Centos 9
|
- Centos 9
|
||||||
|
- Centos 8
|
||||||
|
|
||||||
|
|
||||||
## 📄 文档
|
## 📄 文档
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,16 @@ curl -sSL https://dokploy.com/install.sh | sh
|
|||||||
|
|
||||||
Tested Systems:
|
Tested Systems:
|
||||||
|
|
||||||
- Ubuntu 20.04
|
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||||
|
- Ubuntu 23.10 (Mantic Minotaur)
|
||||||
|
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||||
|
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||||
|
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||||
|
- Debian 12
|
||||||
- Debian 11
|
- Debian 11
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Centos 9
|
- Centos 9
|
||||||
|
- Centos 8
|
||||||
|
|
||||||
## 📄 Documentation
|
## 📄 Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -183,8 +183,7 @@ export const AddPort = ({
|
|||||||
<SelectValue placeholder="Select a protocol" />
|
<SelectValue placeholder="Select a protocol" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent defaultValue={"none"}>
|
<SelectContent>
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
<SelectItem value={"tcp"}>TCP</SelectItem>
|
<SelectItem value={"tcp"}>TCP</SelectItem>
|
||||||
<SelectItem value={"udp"}>UDP</SelectItem>
|
<SelectItem value={"udp"}>UDP</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -19,8 +19,9 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@@ -33,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.application.saveEnvironment.useMutation();
|
api.application.saveEnvironment.useMutation();
|
||||||
|
|
||||||
@@ -72,15 +74,50 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
toast.error("Error to add environment");
|
toast.error("Error to add environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEnvVisible) {
|
||||||
|
if (data?.env) {
|
||||||
|
const maskedLines = data.env
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => "*".repeat(line.length))
|
||||||
|
.join("\n");
|
||||||
|
form.reset({
|
||||||
|
environment: maskedLines,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
environment: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
environment: data?.env || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form, isEnvVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your resource.
|
<CardDescription>
|
||||||
</CardDescription>
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -97,6 +134,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
@@ -111,7 +149,12 @@ PORT=3000
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
<Button
|
||||||
|
disabled={isEnvVisible}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -51,18 +52,14 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
|||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Application Deploying....");
|
toast.success("Deploying Application....");
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Application");
|
||||||
toast.success("Application Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Application");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@@ -32,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||||
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
@@ -71,14 +74,50 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEnvVisible) {
|
||||||
|
if (data?.env) {
|
||||||
|
const maskedLines = data.env
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => "*".repeat(line.length))
|
||||||
|
.join("\n");
|
||||||
|
form.reset({
|
||||||
|
environment: maskedLines,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
environment: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
environment: data?.env || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form, isEnvVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your resource.
|
<CardDescription>
|
||||||
</CardDescription>
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -95,6 +134,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
@@ -109,7 +149,12 @@ PORT=3000
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
<Button
|
||||||
|
disabled={isEnvVisible}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,18 +49,14 @@ export const DeployCompose = ({ composeId }: Props) => {
|
|||||||
composeStatus: "running",
|
composeStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Deploying....");
|
toast.success("Deploying Compose....");
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
composeId,
|
composeId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Compose");
|
||||||
toast.success("Compose Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Compose");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
mariadbId,
|
mariadbId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
@@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Root Password</Label>
|
<Label>Root Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databaseRootPassword}
|
value={data?.databaseRootPassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployMongo = ({ mongoId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
mongoId,
|
mongoId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
@@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployMysql = ({ mysqlId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
mysqlId,
|
mysqlId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
@@ -29,20 +30,18 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Root Password</Label>
|
<Label>Root Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databaseRootPassword}
|
value={data?.databaseRootPassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +57,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployPostgres = ({ postgresId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
postgresId,
|
postgresId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -137,7 +138,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
@@ -29,10 +30,9 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +48,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,16 +22,26 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Folder } from "lucide-react";
|
import { Folder } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
const AddTemplateSchema = z.object({
|
const AddTemplateSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
|
appName: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "App name is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||||
|
message:
|
||||||
|
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,10 +49,13 @@ type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddApplication = ({ projectId }: Props) => {
|
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const slug = slugify(projectName);
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.application.create.useMutation();
|
api.application.create.useMutation();
|
||||||
@@ -50,34 +63,34 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
const form = useForm<AddTemplate>({
|
const form = useForm<AddTemplate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
appName: `${slug}-`,
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddTemplateSchema),
|
resolver: zodResolver(AddTemplateSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset();
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddTemplate) => {
|
const onSubmit = async (data: AddTemplate) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
appName: data.appName,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Created");
|
toast.success("Service Created");
|
||||||
|
form.reset();
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
toast.error("Error to create the service");
|
toast.error("Error to create the service");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
@@ -95,29 +108,46 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="name"
|
||||||
name="name"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input placeholder="Frontend" {...field} />
|
placeholder="Frontend"
|
||||||
</FormControl>
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
<FormMessage />
|
const val = e.target.value?.trim() || "";
|
||||||
</FormItem>
|
form.setValue("appName", `${slug}-${val}`);
|
||||||
)}
|
field.onChange(val);
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
@@ -136,47 +166,6 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <FormField
|
|
||||||
control={form.control}
|
|
||||||
name="buildType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="space-y-3">
|
|
||||||
<FormLabel>Build Type</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
className="flex flex-col space-y-1"
|
|
||||||
>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="dockerfile" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Dockerfile
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="nixpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">Nixpacks</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="heroku_buildpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Heroku Buildpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/> */}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -34,12 +34,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
const AddComposeSchema = z.object({
|
const AddComposeSchema = z.object({
|
||||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
|
appName: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "App name is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||||
|
message:
|
||||||
|
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,11 +57,12 @@ type AddCompose = z.infer<typeof AddComposeSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddCompose = ({ projectId }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const slug = slugify(projectName);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
@@ -60,6 +71,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
composeType: "docker-compose",
|
composeType: "docker-compose",
|
||||||
|
appName: `${slug}-`,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddComposeSchema),
|
resolver: zodResolver(AddComposeSchema),
|
||||||
});
|
});
|
||||||
@@ -74,6 +86,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
composeType: data.composeType,
|
composeType: data.composeType,
|
||||||
|
appName: data.appName,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
@@ -120,14 +133,34 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Frontend" {...field} />
|
<Input
|
||||||
|
placeholder="Frontend"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value?.trim() || "";
|
||||||
|
form.setValue("appName", `${slug}-${val}`);
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="composeType"
|
name="composeType"
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Database } from "lucide-react";
|
import { Database, AlertTriangle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -58,6 +59,15 @@ const databasesUserDefaultPlaceholder: Record<
|
|||||||
|
|
||||||
const baseDatabaseSchema = z.object({
|
const baseDatabaseSchema = z.object({
|
||||||
name: z.string().min(1, "Name required"),
|
name: z.string().min(1, "Name required"),
|
||||||
|
appName: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "App name is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||||
|
message:
|
||||||
|
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
|
}),
|
||||||
databasePassword: z.string(),
|
databasePassword: z.string(),
|
||||||
dockerImage: z.string(),
|
dockerImage: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
@@ -101,30 +111,52 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
.merge(baseDatabaseSchema),
|
.merge(baseDatabaseSchema),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const databasesMap = {
|
||||||
|
postgres: {
|
||||||
|
icon: <PostgresqlIcon />,
|
||||||
|
label: "PostgreSQL",
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
icon: <MongodbIcon />,
|
||||||
|
label: "MongoDB",
|
||||||
|
},
|
||||||
|
mariadb: {
|
||||||
|
icon: <MariadbIcon />,
|
||||||
|
label: "MariaDB",
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
icon: <MysqlIcon />,
|
||||||
|
label: "MySQL",
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
icon: <RedisIcon />,
|
||||||
|
label: "Redis",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
type AddDatabase = z.infer<typeof mySchema>;
|
type AddDatabase = z.infer<typeof mySchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDatabase = ({ projectId }: Props) => {
|
export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
const { mutateAsync: createPostgresql } = api.postgres.create.useMutation();
|
const slug = slugify(projectName);
|
||||||
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
const { mutateAsync: createMongo } = api.mongo.create.useMutation();
|
const mongoMutation = api.mongo.create.useMutation();
|
||||||
|
const redisMutation = api.redis.create.useMutation();
|
||||||
const { mutateAsync: createRedis } = api.redis.create.useMutation();
|
const mariadbMutation = api.mariadb.create.useMutation();
|
||||||
|
const mysqlMutation = api.mysql.create.useMutation();
|
||||||
const { mutateAsync: createMariadb } = api.mariadb.create.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: createMysql } = api.mysql.create.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddDatabase>({
|
const form = useForm<AddDatabase>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
dockerImage: "",
|
dockerImage: "",
|
||||||
name: "",
|
name: "",
|
||||||
|
appName: `${slug}-`,
|
||||||
databasePassword: "",
|
databasePassword: "",
|
||||||
description: "",
|
description: "",
|
||||||
databaseName: "",
|
databaseName: "",
|
||||||
@@ -133,76 +165,65 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
resolver: zodResolver(mySchema),
|
resolver: zodResolver(mySchema),
|
||||||
});
|
});
|
||||||
const type = form.watch("type");
|
const type = form.watch("type");
|
||||||
|
const activeMutation = {
|
||||||
useEffect(() => {
|
postgres: postgresMutation,
|
||||||
form.reset({
|
mongo: mongoMutation,
|
||||||
type: "postgres",
|
redis: redisMutation,
|
||||||
dockerImage: "",
|
mariadb: mariadbMutation,
|
||||||
name: "",
|
mysql: mysqlMutation,
|
||||||
databasePassword: "",
|
};
|
||||||
description: "",
|
|
||||||
databaseName: "",
|
|
||||||
databaseUser: "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddDatabase) => {
|
const onSubmit = async (data: AddDatabase) => {
|
||||||
const defaultDockerImage =
|
const defaultDockerImage =
|
||||||
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
||||||
|
|
||||||
let promise: Promise<unknown> | null = null;
|
let promise: Promise<unknown> | null = null;
|
||||||
|
const commonParams = {
|
||||||
|
name: data.name,
|
||||||
|
appName: data.appName,
|
||||||
|
dockerImage: defaultDockerImage,
|
||||||
|
projectId,
|
||||||
|
description: data.description,
|
||||||
|
};
|
||||||
|
|
||||||
if (data.type === "postgres") {
|
if (data.type === "postgres") {
|
||||||
promise = createPostgresql({
|
promise = postgresMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mongo") {
|
} else if (data.type === "mongo") {
|
||||||
promise = createMongo({
|
promise = mongoMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "redis") {
|
} else if (data.type === "redis") {
|
||||||
promise = createRedis({
|
promise = redisMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
projectId,
|
projectId,
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mariadb") {
|
} else if (data.type === "mariadb") {
|
||||||
promise = createMariadb({
|
promise = mariadbMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
projectId,
|
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mysql") {
|
} else if (data.type === "mysql") {
|
||||||
promise = createMysql({
|
promise = mysqlMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +231,17 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
await promise
|
await promise
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Created");
|
toast.success("Database Created");
|
||||||
|
form.reset({
|
||||||
|
type: "postgres",
|
||||||
|
dockerImage: "",
|
||||||
|
name: "",
|
||||||
|
appName: `${projectName}-`,
|
||||||
|
databasePassword: "",
|
||||||
|
description: "",
|
||||||
|
databaseName: "",
|
||||||
|
databaseUser: "",
|
||||||
|
});
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
@@ -220,7 +252,7 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
@@ -230,18 +262,10 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
<span>Database</span>
|
<span>Database</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Databases</DialogTitle>
|
<DialogTitle>Databases</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* {isError && (
|
|
||||||
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -264,99 +288,40 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||||
>
|
>
|
||||||
<FormItem className="flex w-full items-center space-x-3 space-y-0">
|
{Object.entries(databasesMap).map(([key, value]) => (
|
||||||
<FormControl className="w-full">
|
<FormItem
|
||||||
<div>
|
key={key}
|
||||||
<RadioGroupItem
|
className="flex w-full items-center space-x-3 space-y-0"
|
||||||
value="postgres"
|
>
|
||||||
id="postgres"
|
<FormControl className="w-full">
|
||||||
className="peer sr-only"
|
<div>
|
||||||
/>
|
<RadioGroupItem
|
||||||
<Label
|
value={key}
|
||||||
htmlFor="postgres"
|
id={key}
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
className="peer sr-only"
|
||||||
>
|
/>
|
||||||
<PostgresqlIcon />
|
<Label
|
||||||
Postgresql
|
htmlFor={key}
|
||||||
</Label>
|
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||||
</div>
|
>
|
||||||
</FormControl>
|
{value.icon}
|
||||||
</FormItem>
|
{value.label}
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
</Label>
|
||||||
<FormControl className="w-full">
|
</div>
|
||||||
<div>
|
</FormControl>
|
||||||
<RadioGroupItem
|
</FormItem>
|
||||||
value="mysql"
|
))}
|
||||||
id="mysql"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="mysql"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<MysqlIcon />
|
|
||||||
Mysql
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl className="w-full">
|
|
||||||
<div>
|
|
||||||
<RadioGroupItem
|
|
||||||
value="mariadb"
|
|
||||||
id="mariadb"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="mariadb"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<MariadbIcon />
|
|
||||||
Mariadb
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl className="w-full">
|
|
||||||
<div>
|
|
||||||
<RadioGroupItem
|
|
||||||
value="mongo"
|
|
||||||
id="mongo"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="mongo"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<MongodbIcon />
|
|
||||||
Mongo
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl className="w-full">
|
|
||||||
<div>
|
|
||||||
<RadioGroupItem
|
|
||||||
value="redis"
|
|
||||||
id="redis"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="redis"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<RedisIcon />
|
|
||||||
Redis
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
{activeMutation[field.value].isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{activeMutation[field.value].error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -372,13 +337,34 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Name" {...field} />
|
<Input
|
||||||
|
placeholder="Name"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value?.trim() || "";
|
||||||
|
form.setValue("appName", `${slug}-${val}`);
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployRedis = ({ redisId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database...");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
redisId,
|
redisId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -129,7 +130,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
@@ -25,10 +26,9 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +44,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
|
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
26
components/shared/toggle-visibility-input.tsx
Normal file
26
components/shared/toggle-visibility-input.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import { Input, type InputProps } from "../ui/input";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||||
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
|
|
||||||
|
const togglePasswordVisibility = () => {
|
||||||
|
setIsPasswordVisible((prevVisibility) => !prevVisibility);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputType = isPasswordVisible ? "text" : "password";
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center space-x-2">
|
||||||
|
<Input type={inputType} {...props} />
|
||||||
|
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
||||||
|
{inputType === "password" ? (
|
||||||
|
<EyeIcon className="size-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeOffIcon className="size-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
15
lib/slug.ts
Normal file
15
lib/slug.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import slug from "slugify";
|
||||||
|
|
||||||
|
export const slugify = (text: string | undefined) => {
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
|
||||||
|
|
||||||
|
return slug(cleanedText, {
|
||||||
|
lower: true,
|
||||||
|
trim: true,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.2.0",
|
"version": "v0.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -31,11 +31,11 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language":"^6.10.1",
|
|
||||||
"@aws-sdk/client-s3": "3.515.0",
|
"@aws-sdk/client-s3": "3.515.0",
|
||||||
"@codemirror/legacy-modes":"6.4.0",
|
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
|
"@codemirror/language": "^6.10.1",
|
||||||
|
"@codemirror/legacy-modes": "6.4.0",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
"dockerode": "4.0.2",
|
"dockerode": "4.0.2",
|
||||||
"dockerode-compose": "^1.4.0",
|
"dockerode-compose": "^1.4.0",
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.49.3",
|
||||||
"recharts": "^2.12.3",
|
"recharts": "^2.12.3",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
@@ -113,9 +115,7 @@
|
|||||||
"use-resize-observer": "9.1.0",
|
"use-resize-observer": "9.1.0",
|
||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4"
|
||||||
"copy-webpack-plugin": "^12.0.2"
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.7.1",
|
"@biomejs/biome": "1.7.1",
|
||||||
|
|||||||
@@ -210,9 +210,12 @@ const Project = (
|
|||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<AddApplication projectId={projectId} />
|
<AddApplication
|
||||||
<AddDatabase projectId={projectId} />
|
projectId={projectId}
|
||||||
<AddCompose projectId={projectId} />
|
projectName={data?.name}
|
||||||
|
/>
|
||||||
|
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||||
|
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||||
<AddTemplate projectId={projectId} />
|
<AddTemplate projectId={projectId} />
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -230,6 +230,9 @@ dependencies:
|
|||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.12.3
|
specifier: ^2.12.3
|
||||||
version: 2.12.3(react-dom@18.2.0)(react@18.2.0)
|
version: 2.12.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
slugify:
|
||||||
|
specifier: ^1.6.6
|
||||||
|
version: 1.6.6
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
|
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -8015,6 +8018,11 @@ packages:
|
|||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/slugify@1.6.6:
|
||||||
|
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sonner@1.4.3(react-dom@18.2.0)(react@18.2.0):
|
/sonner@1.4.3(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==}
|
resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
await addNewService(ctx.user.authId, newApplication.applicationId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to create the application",
|
message: "Error to create the application",
|
||||||
|
|||||||
@@ -34,13 +34,18 @@ import { nanoid } from "nanoid";
|
|||||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { createCommand } from "@/server/utils/builders/compose";
|
import { createCommand } from "@/server/utils/builders/compose";
|
||||||
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
|
import {
|
||||||
|
generatePassword,
|
||||||
|
loadTemplateModule,
|
||||||
|
readComposeFile,
|
||||||
|
} from "@/templates/utils";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin } from "../services/admin";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { findProjectById, slugifyProjectName } from "../services/project";
|
import { findProjectById } from "../services/project";
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
export const composeRouter = createTRPCRouter({
|
export const composeRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const { envs, mounts } = generate({
|
const { envs, mounts } = generate({
|
||||||
serverIp: admin.serverIp,
|
serverIp: admin.serverIp,
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
@@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
env: envs.join("\n"),
|
env: envs.join("\n"),
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mariadb database",
|
message: "Error input: Inserting mariadb database",
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mongo database",
|
message: "Error input: Inserting mongo database",
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mysql database",
|
message: "Error input: Inserting mysql database",
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting postgresql database",
|
message: "Error input: Inserting postgresql database",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { manageRegistry } from "@/server/utils/traefik/registry";
|
import { manageRegistry } from "@/server/utils/traefik/registry";
|
||||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||||
import { docker } from "@/server/constants";
|
import { execAsync } from "@/server/utils/process/execAsync";
|
||||||
|
|
||||||
export const registryRouter = createTRPCRouter({
|
export const registryRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
@@ -57,15 +57,11 @@ export const registryRouter = createTRPCRouter({
|
|||||||
.input(apiTestRegistry)
|
.input(apiTestRegistry)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const result = await docker.checkAuth({
|
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||||
username: input.username,
|
await execAsync(loginCommand);
|
||||||
password: input.password,
|
|
||||||
serveraddress: input.registryUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log("Error Registry:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ import { findAdmin } from "./admin";
|
|||||||
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
||||||
import { docker } from "@/server/constants";
|
import { docker } from "@/server/constants";
|
||||||
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
export type Application = typeof applications.$inferSelect;
|
export type Application = typeof applications.$inferSelect;
|
||||||
|
|
||||||
export const createApplication = async (
|
export const createApplication = async (
|
||||||
input: typeof apiCreateApplication._type,
|
input: typeof apiCreateApplication._type,
|
||||||
) => {
|
) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Application with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const newApplication = await tx
|
const newApplication = await tx
|
||||||
.insert(applications)
|
.insert(applications)
|
||||||
|
|||||||
@@ -13,10 +13,21 @@ import { join } from "node:path";
|
|||||||
import { COMPOSE_PATH } from "@/server/constants";
|
import { COMPOSE_PATH } from "@/server/constants";
|
||||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
import { cloneGitRepository } from "@/server/utils/providers/git";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Compose = typeof compose.$inferSelect;
|
export type Compose = typeof compose.$inferSelect;
|
||||||
|
|
||||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const newDestination = await db
|
const newDestination = await db
|
||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
@@ -39,6 +50,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
|||||||
export const createComposeByTemplate = async (
|
export const createComposeByTemplate = async (
|
||||||
input: typeof compose.$inferInsert,
|
input: typeof compose.$inferInsert,
|
||||||
) => {
|
) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const newDestination = await db
|
const newDestination = await db
|
||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ export const getContainers = async () => {
|
|||||||
.filter((container) => !container.name.includes("dokploy"));
|
.filter((container) => !container.name.includes("dokploy"));
|
||||||
|
|
||||||
return containers;
|
return containers;
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConfig = async (containerId: string) => {
|
export const getConfig = async (containerId: string) => {
|
||||||
@@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => {
|
|||||||
const config = JSON.parse(stdout);
|
const config = JSON.parse(stdout);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContainersByAppNameMatch = async (appName: string) => {
|
export const getContainersByAppNameMatch = async (appName: string) => {
|
||||||
@@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
@@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Mariadb = typeof mariadb.$inferSelect;
|
export type Mariadb = typeof mariadb.$inferSelect;
|
||||||
|
|
||||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMariadb = await db
|
const newMariadb = await db
|
||||||
.insert(mariadb)
|
.insert(mariadb)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ import { buildMongo } from "@/server/utils/databases/mongo";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Mongo = typeof mongo.$inferSelect;
|
export type Mongo = typeof mongo.$inferSelect;
|
||||||
|
|
||||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMongo = await db
|
const newMongo = await db
|
||||||
.insert(mongo)
|
.insert(mongo)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,11 +5,22 @@ import { buildMysql } from "@/server/utils/databases/mysql";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type MySql = typeof mysql.$inferSelect;
|
export type MySql = typeof mysql.$inferSelect;
|
||||||
|
|
||||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMysql = await db
|
const newMysql = await db
|
||||||
.insert(mysql)
|
.insert(mysql)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Postgres = typeof postgres.$inferSelect;
|
export type Postgres = typeof postgres.$inferSelect;
|
||||||
|
|
||||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newPostgres = await db
|
const newPostgres = await db
|
||||||
.insert(postgres)
|
.insert(postgres)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { type apiCreateProject, projects } from "@/server/db/schema";
|
import {
|
||||||
|
type apiCreateProject,
|
||||||
|
applications,
|
||||||
|
mariadb,
|
||||||
|
mongo,
|
||||||
|
mysql,
|
||||||
|
postgres,
|
||||||
|
projects,
|
||||||
|
redis,
|
||||||
|
} from "@/server/db/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { findAdmin } from "./admin";
|
import { findAdmin } from "./admin";
|
||||||
@@ -75,12 +84,40 @@ export const updateProjectById = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const slugifyProjectName = (projectName: string): string => {
|
export const validUniqueServerAppName = async (appName: string) => {
|
||||||
return projectName
|
const query = await db.query.projects.findMany({
|
||||||
.toLowerCase()
|
with: {
|
||||||
.replace(/[0-9]/g, "")
|
applications: {
|
||||||
.replace(/[^a-z\s-]/g, "")
|
where: eq(applications.appName, appName),
|
||||||
.replace(/\s+/g, "-")
|
},
|
||||||
.replace(/-+/g, "-")
|
mariadb: {
|
||||||
.replace(/^-+|-+$/g, "");
|
where: eq(mariadb.appName, appName),
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
where: eq(mongo.appName, appName),
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
where: eq(mysql.appName, appName),
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
where: eq(postgres.appName, appName),
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
where: eq(redis.appName, appName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter out items with non-empty fields
|
||||||
|
const nonEmptyProjects = query.filter(
|
||||||
|
(project) =>
|
||||||
|
project.applications.length > 0 ||
|
||||||
|
project.mariadb.length > 0 ||
|
||||||
|
project.mongo.length > 0 ||
|
||||||
|
project.mysql.length > 0 ||
|
||||||
|
project.postgres.length > 0 ||
|
||||||
|
project.redis.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return nonEmptyProjects.length === 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,23 @@ import { buildRedis } from "@/server/utils/databases/redis";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Redis = typeof redis.$inferSelect;
|
export type Redis = typeof redis.$inferSelect;
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newRedis = await db
|
const newRedis = await db
|
||||||
.insert(redis)
|
.insert(redis)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -9,28 +9,35 @@ import {
|
|||||||
} from "@/server/utils/traefik/registry";
|
} from "@/server/utils/traefik/registry";
|
||||||
import { removeService } from "@/server/utils/docker/utils";
|
import { removeService } from "@/server/utils/docker/utils";
|
||||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||||
|
import { execAsync } from "@/server/utils/process/execAsync";
|
||||||
|
|
||||||
export type Registry = typeof registry.$inferSelect;
|
export type Registry = typeof registry.$inferSelect;
|
||||||
|
|
||||||
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
|
|
||||||
const newRegistry = await db
|
return await db.transaction(async (tx) => {
|
||||||
.insert(registry)
|
const newRegistry = await tx
|
||||||
.values({
|
.insert(registry)
|
||||||
...input,
|
.values({
|
||||||
adminId: admin.adminId,
|
...input,
|
||||||
})
|
adminId: admin.adminId,
|
||||||
.returning()
|
})
|
||||||
.then((value) => value[0]);
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
if (!newRegistry) {
|
if (!newRegistry) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting registry",
|
message: "Error input: Inserting registry",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return newRegistry;
|
|
||||||
|
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||||
|
await execAsync(loginCommand);
|
||||||
|
|
||||||
|
return newRegistry;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeRegistry = async (registryId: string) => {
|
export const removeRegistry = async (registryId: string) => {
|
||||||
@@ -53,6 +60,8 @@ export const removeRegistry = async (registryId: string) => {
|
|||||||
await removeService("dokploy-registry");
|
await removeService("dokploy-registry");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await execAsync(`docker logout ${response.registryUrl}`);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { registry } from "./registry";
|
import { registry } from "./registry";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
||||||
|
|
||||||
@@ -307,11 +308,17 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateApplication = createSchema.pick({
|
export const apiCreateApplication = createSchema
|
||||||
name: true,
|
.pick({
|
||||||
description: true,
|
name: true,
|
||||||
projectId: true,
|
appName: true,
|
||||||
});
|
description: true,
|
||||||
|
projectId: true,
|
||||||
|
})
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName: `${data.appName}-${generatePassword(6)}` || generateAppName("app"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneApplication = createSchema
|
export const apiFindOneApplication = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { deployments } from "./deployment";
|
|||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||||
"git",
|
"git",
|
||||||
@@ -74,12 +75,19 @@ const createSchema = createInsertSchema(compose, {
|
|||||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateCompose = createSchema.pick({
|
export const apiCreateCompose = createSchema
|
||||||
name: true,
|
.pick({
|
||||||
description: true,
|
name: true,
|
||||||
projectId: true,
|
description: true,
|
||||||
composeType: true,
|
projectId: true,
|
||||||
});
|
composeType: true,
|
||||||
|
appName: true,
|
||||||
|
})
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("compose"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiCreateComposeByTemplate = createSchema
|
export const apiCreateComposeByTemplate = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mariadb = pgTable("mariadb", {
|
export const mariadb = pgTable("mariadb", {
|
||||||
mariadbId: text("mariadbId")
|
mariadbId: text("mariadbId")
|
||||||
@@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, {
|
|||||||
export const apiCreateMariaDB = createSchema
|
export const apiCreateMariaDB = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
databaseRootPassword: true,
|
databaseRootPassword: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
@@ -87,7 +89,12 @@ export const apiCreateMariaDB = createSchema
|
|||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("mariadb"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneMariaDB = createSchema
|
export const apiFindOneMariaDB = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mongo = pgTable("mongo", {
|
export const mongo = pgTable("mongo", {
|
||||||
mongoId: text("mongoId")
|
mongoId: text("mongoId")
|
||||||
@@ -73,13 +74,19 @@ const createSchema = createInsertSchema(mongo, {
|
|||||||
export const apiCreateMongo = createSchema
|
export const apiCreateMongo = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneMongo = createSchema
|
export const apiFindOneMongo = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mysql = pgTable("mysql", {
|
export const mysql = pgTable("mysql", {
|
||||||
mysqlId: text("mysqlId")
|
mysqlId: text("mysqlId")
|
||||||
@@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, {
|
|||||||
export const apiCreateMySql = createSchema
|
export const apiCreateMySql = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
@@ -85,7 +87,12 @@ export const apiCreateMySql = createSchema
|
|||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
databaseRootPassword: true,
|
databaseRootPassword: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("mysql"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneMySql = createSchema
|
export const apiFindOneMySql = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const postgres = pgTable("postgres", {
|
export const postgres = pgTable("postgres", {
|
||||||
postgresId: text("postgresId")
|
postgresId: text("postgresId")
|
||||||
@@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, {
|
|||||||
export const apiCreatePostgres = createSchema
|
export const apiCreatePostgres = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
databaseName: true,
|
databaseName: true,
|
||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
@@ -81,7 +83,12 @@ export const apiCreatePostgres = createSchema
|
|||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOnePostgres = createSchema
|
export const apiFindOnePostgres = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
|||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const redis = pgTable("redis", {
|
export const redis = pgTable("redis", {
|
||||||
redisId: text("redisId")
|
redisId: text("redisId")
|
||||||
@@ -69,13 +70,18 @@ const createSchema = createInsertSchema(redis, {
|
|||||||
export const apiCreateRedis = createSchema
|
export const apiCreateRedis = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
})
|
})
|
||||||
|
.required()
|
||||||
.required();
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("redis"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneRedis = createSchema
|
export const apiFindOneRedis = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import type { MainTraefikConfig } from "../utils/traefik/types";
|
|||||||
import type { FileConfig } from "../utils/traefik/file-types";
|
import type { FileConfig } from "../utils/traefik/file-types";
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
import type { CreateServiceOptions } from "dockerode";
|
||||||
|
|
||||||
|
const TRAEFIK_SSL_PORT =
|
||||||
|
Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443;
|
||||||
|
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80;
|
||||||
|
|
||||||
export const initializeTraefik = async () => {
|
export const initializeTraefik = async () => {
|
||||||
const imageName = "traefik:v2.5";
|
const imageName = "traefik:v2.5";
|
||||||
const containerName = "dokploy-traefik";
|
const containerName = "dokploy-traefik";
|
||||||
@@ -47,12 +51,12 @@ export const initializeTraefik = async () => {
|
|||||||
Ports: [
|
Ports: [
|
||||||
{
|
{
|
||||||
TargetPort: 443,
|
TargetPort: 443,
|
||||||
PublishedPort: 443,
|
PublishedPort: TRAEFIK_SSL_PORT,
|
||||||
PublishMode: "host",
|
PublishMode: "host",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: 80,
|
TargetPort: 80,
|
||||||
PublishedPort: 80,
|
PublishedPort: TRAEFIK_PORT,
|
||||||
PublishMode: "host",
|
PublishMode: "host",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,10 +150,10 @@ export const createDefaultTraefikConfig = () => {
|
|||||||
},
|
},
|
||||||
entryPoints: {
|
entryPoints: {
|
||||||
web: {
|
web: {
|
||||||
address: ":80",
|
address: `:${TRAEFIK_PORT}`,
|
||||||
},
|
},
|
||||||
websecure: {
|
websecure: {
|
||||||
address: ":443",
|
address: `:${TRAEFIK_SSL_PORT}`,
|
||||||
...(process.env.NODE_ENV === "production" && {
|
...(process.env.NODE_ENV === "production" && {
|
||||||
http: {
|
http: {
|
||||||
tls: {
|
tls: {
|
||||||
|
|||||||
@@ -108,7 +108,12 @@ const createEnvFile = (compose: ComposeNested) => {
|
|||||||
join(COMPOSE_PATH, appName, "docker-compose.yml");
|
join(COMPOSE_PATH, appName, "docker-compose.yml");
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
let envContent = env || "";
|
||||||
|
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||||
|
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||||
|
|
||||||
if (!existsSync(dirname(envFilePath))) {
|
if (!existsSync(dirname(envFilePath))) {
|
||||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ export const mechanizeDockerContainer = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
await docker.createService(settings);
|
await docker.createService(settings);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,47 +3,47 @@ import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
|
|||||||
import { pullImage } from "../docker/utils";
|
import { pullImage } from "../docker/utils";
|
||||||
|
|
||||||
interface RegistryAuth {
|
interface RegistryAuth {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
serveraddress: string;
|
serveraddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildDocker = async (
|
export const buildDocker = async (
|
||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const { buildType, dockerImage, username, password } = application;
|
const { buildType, dockerImage, username, password } = application;
|
||||||
const authConfig: Partial<RegistryAuth> = {
|
const authConfig: Partial<RegistryAuth> = {
|
||||||
username: username || "",
|
username: username || "",
|
||||||
password: password || "",
|
password: password || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||||
|
|
||||||
writeStream.write(`\nBuild ${buildType}\n`);
|
writeStream.write(`\nBuild ${buildType}\n`);
|
||||||
|
|
||||||
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!dockerImage) {
|
if (!dockerImage) {
|
||||||
throw new Error("Docker image not found");
|
throw new Error("Docker image not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await pullImage(
|
await pullImage(
|
||||||
dockerImage,
|
dockerImage,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (writeStream.writable) {
|
if (writeStream.writable) {
|
||||||
writeStream.write(`${data.status}\n`);
|
writeStream.write(`${data.status}\n`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
authConfig,
|
authConfig,
|
||||||
);
|
);
|
||||||
await mechanizeDockerContainer(application);
|
await mechanizeDockerContainer(application);
|
||||||
writeStream.write("\nDocker Deployed: ✅\n");
|
writeStream.write("\nDocker Deployed: ✅\n");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeStream.write(`ERROR: ${error}: ❌`);
|
writeStream.write(`ERROR: ${error}: ❌`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
writeStream.end();
|
writeStream.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
67
utils/api.ts
67
utils/api.ts
@@ -11,50 +11,39 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
const getBaseUrl = () => {
|
const getBaseUrl = () => {
|
||||||
if (typeof window !== "undefined") return ""; // browser should use relative url
|
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||||
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A set of type-safe react-query hooks for your tRPC API. */
|
/** A set of type-safe react-query hooks for your tRPC API. */
|
||||||
export const api = createTRPCNext<AppRouter>({
|
export const api = createTRPCNext<AppRouter>({
|
||||||
config() {
|
config() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* Transformer used for data de-serialization from the server.
|
* Transformer used for data de-serialization from the server.
|
||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/data-transformers
|
* @see https://trpc.io/docs/data-transformers
|
||||||
*/
|
*/
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links used to determine request flow from client to server.
|
* Links used to determine request flow from client to server.
|
||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/links
|
* @see https://trpc.io/docs/links
|
||||||
*/
|
*/
|
||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
}),
|
}),
|
||||||
// createWSClient({
|
],
|
||||||
// url: `ws://localhost:3000`,
|
};
|
||||||
// }),
|
},
|
||||||
// loggerLink({
|
/**
|
||||||
// enabled: (opts) =>
|
* Whether tRPC should await queries when server rendering pages.
|
||||||
// process.env.NODE_ENV === "development" ||
|
*
|
||||||
// (opts.direction === "down" && opts.result instanceof Error),
|
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
|
||||||
// }),
|
*/
|
||||||
// httpBatchLink({
|
ssr: false,
|
||||||
// url: `${getBaseUrl()}/api/trpc`,
|
|
||||||
// }),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Whether tRPC should await queries when server rendering pages.
|
|
||||||
*
|
|
||||||
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
|
|
||||||
*/
|
|
||||||
ssr: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user