Compare commits

...

37 Commits

Author SHA1 Message Date
Mauricio Siu
fe22890311 Merge pull request #156 from Dokploy/canary
v0.2.3
2024-06-21 11:50:40 -06:00
Mauricio Siu
3f2eeaf386 chore: bump version 2024-06-21 10:37:26 -06:00
Mauricio Siu
7826ba5bb5 Merge pull request #150 from Dokploy/149-backup-db-mongodb-to-s3-errors
fix(#149): use database user in mongodump
2024-06-19 00:49:32 -06:00
Mauricio Siu
bdc488e179 fix(#149): use database user in mongodump 2024-06-19 00:45:33 -06:00
Mauricio Siu
fc2abac989 chore: update banner image 2024-06-15 19:27:31 -06:00
Mauricio Siu
101bbd44d8 Update README.md 2024-06-15 04:08:32 -06:00
Mauricio Siu
68c2272e98 chore: update readme 2024-06-15 04:02:24 -06:00
Mauricio Siu
bc28464430 chore: update readme 2024-06-15 04:01:35 -06:00
Mauricio Siu
7df415a386 chore: add issue template 2024-06-15 03:15:02 -06:00
Mauricio Siu
9fbd3039a6 Merge pull request #141 from hehehai/hehehai/fix-env-editor-width-over
fix: env editor width overflow
2024-06-15 03:03:13 -06:00
hehehai
3c00937b94 fix: env editor width overflow 2024-06-13 20:42:46 +08:00
Mauricio Siu
a57a776500 Merge pull request #137 from dharsanb/canary
Fix link to docs in README
2024-06-09 14:46:24 -06:00
dharsanb
323e2f54ba fix(readme): fixed readme link to docs 2024-06-10 01:29:19 +05:30
Mauricio Siu
2b7c7632f4 Merge pull request #136 from Dokploy/canary
v0.2.2
2024-06-08 22:06:39 -06:00
Mauricio Siu
7db662e332 chore: bump version 2024-06-08 21:17:55 -06:00
Mauricio Siu
cd1a686b59 refactor: update slugify compose 2024-06-08 18:40:34 -06:00
Mauricio Siu
78d573c4f3 Merge pull request #87 from hehehai/hehehai/feat-server-custom-name
feat: server support custom name
2024-06-08 16:49:15 -06:00
Mauricio Siu
0ef9b1427b chore: add slugify 2024-06-08 16:43:06 -06:00
Mauricio Siu
993d6b52f2 refactor: remove comments 2024-06-08 16:43:01 -06:00
Mauricio Siu
b9ab4a4d1a refactor: remove console log 2024-06-08 16:42:50 -06:00
Mauricio Siu
83153471b8 feat: add docker compose appName validation 2024-06-08 16:42:38 -06:00
Mauricio Siu
909e536f45 refactor: hide enviroment variables when viewing enviroment variables 2024-06-08 16:42:07 -06:00
Mauricio Siu
295cf50060 feat: add slug function 2024-06-08 16:41:46 -06:00
Mauricio Siu
dd16baf234 refactor: use slugify function 2024-06-08 16:41:38 -06:00
Mauricio Siu
72c366aa10 refactor: slugify and add hash to appName 2024-06-08 16:41:05 -06:00
Mauricio Siu
9a4f79f9e6 Merge branch 'canary' into hehehai/feat-server-custom-name 2024-06-08 14:37:51 -06:00
Mauricio Siu
30b81834fc Merge pull request #135 from Dokploy/130-how-to-make-dokploy-to-do-not-require-port-80-and-443-to-be-avilable
feat(#130): allow to pass enviroment variables to assign custom port …
2024-06-08 14:19:14 -06:00
Mauricio Siu
4e3aaa2a69 feat(#130): allow to pass enviroment variables to assign custom port on traefik 2024-06-08 14:13:43 -06:00
Mauricio Siu
3dcd89cc32 Merge pull request #128 from mariusihring/canary
Updated the database view to hide the password in the connection string
2024-06-08 14:05:16 -06:00
Mauricio Siu
41dc388bb0 refactor: update import 2024-06-08 13:40:15 -06:00
Mauricio Siu
44a592f7a7 refactor: simplify props 2024-06-08 13:36:02 -06:00
Mauricio Siu
1a4f5607dc refactor: add props to toggle and change colors of button toggle 2024-06-08 13:28:43 -06:00
Marius
d54c6e4ac9 feat: create input for toggleable inputs like passwords/connectinstrings 2024-06-08 11:55:05 +02:00
Marius
936cf76a4c fix: fix undefined error for database length 2024-06-07 13:04:30 +02:00
Marius
65254f1686 feat: Update the external database view to hide the password 2024-06-07 12:57:46 +02:00
Marius
b312b1d7e0 Updated the database view to hide the password in the connection string 2024-06-07 12:45:33 +02:00
hehehai
fae180f157 feat: server support custom name 2024-05-24 15:43:05 +08:00
68 changed files with 924 additions and 446 deletions

62
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Bug Report
description: Create a bug report
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Before opening a new issue, please do a search of existing issues.
If you need help with your own project, you can start a discussion in the [Q&A Section](https://github.com/Dokploy/dokploy/discussions).
- type: textarea
attributes:
label: To Reproduce
description: A step-by-step description of how to reproduce the issue, or a link to the reproducible repository.
placeholder: |
1. Create a application
2. Click X
3. Y will happen
validations:
required: true
- type: textarea
attributes:
label: Current vs. Expected behavior
description: A clear and concise description of what the bug is, and what you expected to happen.
placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead'
validations:
required: true
- type: textarea
attributes:
label: Provide environment information
description: Please provide the following information about your environment.
render: bash
placeholder: |
Operating System:
OS: Ubuntu 20.04
Arch: arm64
Dokploy version: 0.2.2'
VPS Provider: DigitalOcean, Hetzner, Linode, etc.
What applications/services are you tying to deploy?
eg - Database, Nextjs App, laravel, etc.
validations:
required: true
- type: dropdown
attributes:
label: Which area(s) are affected? (Select all that apply)
multiple: true
options:
- 'Installation'
- 'Application'
- 'Databases'
- 'Docker Compose'
- 'Traefik'
- 'Docker'
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: |
Any extra information that might help us investigate.
placeholder: |
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions?
url: https://github.com/Dokploy/dokploy/discussions
about: Ask your questions here.

View File

@@ -0,0 +1,33 @@
name: Feature Request
description: Suggest a new feature or improvement to the project
labels: ['enhancement']
body:
- type: textarea
attributes:
label: What problem will this feature address?
description: A clear and concise description of what the problem is.
placeholder: |
I'm always frustrated when I can't do X
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
placeholder: Add X to the core
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
placeholder: |
Maybe use Y as a workaround?
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

1
.gitignore vendored
View File

@@ -55,3 +55,4 @@ yarn-error.log*
*.lockb *.lockb
*.rdb *.rdb
.idea

View File

@@ -1,36 +1,37 @@
<div align="center"> <div align="center">
<h1 align="center">Dokploy</h1> <h1 align="center">Dokploy</h1>
<div>
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
</div> </div>
<div align="center" style="width:100%;">
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Reflex Logo" style="width:60%;">
</div> </div>
<hr>
<br />
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases using Docker and Traefik. Designed to enhance efficiency and security, Dokploy allows you to deploy your applications on any VPS. Dokploy include multiples features to make your life easier.
* **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
## Explanation * **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md) * **Backups**: Automate backups for databases to a external storage destination.
* **Docker Compose**: Native support for Docker Compose to manage complex applications.
* **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
* **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
* **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
* **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
* **Docker Management**: Easily deploy and manage Docker containers.
* **CLI (Soon⌛)**: Manage your applications and databases using the command line.
* **Self-Hosted**: Self-host Dokploy on your VPS.
## 🌟 Features
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease.
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis, and more.
- **Docker Management**: Easily deploy and manage Docker containers.
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage.
- **Database Backups**: Automate backups with support for multiple storage destinations.
## 🚀 Getting Started ## 🚀 Getting Started
To get started run the following command in a VPS: To get started run the following command in a VPS:
@@ -40,20 +41,54 @@ To get started run the following command in a VPS:
curl -sSL https://dokploy.com/install.sh | sh curl -sSL https://dokploy.com/install.sh | sh
``` ```
Tested Systems:
- Ubuntu 24.04 LTS (Noble Numbat) ## 📄 Documentation
- Ubuntu 23.10 (Mantic Minotaur)
- Ubuntu 22.04 LTS (Jammy Jellyfish) For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
- Ubuntu 20.04 LTS (Focal Fossa)
- Ubuntu 18.04 LTS (Bionic Beaver)
## Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
</a>
## Donations
If you like dokploy, and want to support the project to cover the costs of hosting, testing and development new features, you can donate to the project using the following link:
Thanks to all the supporters!
https://opencollective.com/dokploy
<a href="https://opencollective.com/dokploy"><img src="https://opencollective.com/dokploy/individuals.svg?width=890"></a>
## Contributors
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
</a>
## Support OS
- Ubuntu 24.04 LTS
- Ubuntu 23.10
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Ubuntu 18.04 LTS
- Debian 12 - Debian 12
- Debian 11 - Debian 11
- Fedora 40 - Fedora 40
- Centos 9 - Centos 9
- Centos 8 - Centos 8
## 📄 Documentation
For detailed documentation, visit [docs.dokploy.com/docs](https://docs.dokploy.com).
## Explanation
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)

View File

@@ -19,7 +19,7 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -58,6 +58,7 @@ export const validateAndFormatYAML = (yamlText: string) => {
}; };
export const UpdateTraefikConfig = ({ applicationId }: Props) => { export const UpdateTraefikConfig = ({ applicationId }: Props) => {
const [open, setOpen] = useState(false);
const { data, refetch } = api.application.readTraefikConfig.useQuery( const { data, refetch } = api.application.readTraefikConfig.useQuery(
{ {
applicationId, applicationId,
@@ -81,7 +82,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
traefikConfig: data || "", traefikConfig: data || "",
}); });
} }
}, [form, form.reset, data]); }, [data]);
const onSubmit = async (data: UpdateTraefikConfig) => { const onSubmit = async (data: UpdateTraefikConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig); const { valid, error } = validateAndFormatYAML(data.traefikConfig);
@@ -100,6 +101,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
.then(async () => { .then(async () => {
toast.success("Traefik config Updated"); toast.success("Traefik config Updated");
refetch(); refetch();
setOpen(false);
form.reset();
}) })
.catch(() => { .catch(() => {
toast.error("Error to update the traefik config"); toast.error("Error to update the traefik config");
@@ -107,7 +110,12 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={open} onOpenChange={(open) => {
setOpen(open)
if (!open) {
form.reset();
}
}}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button isLoading={isLoading}>Modify</Button> <Button isLoading={isLoading}>Modify</Button>
</DialogTrigger> </DialogTrigger>
@@ -122,7 +130,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
<form <form
id="hook-form-update-traefik-config" id="hook-form-update-traefik-config"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 overflow-auto" className="w-full space-y-4 overflow-auto"
> >
<div className="flex flex-col"> <div className="flex flex-col">
<FormField <FormField

View File

@@ -189,7 +189,7 @@ export const AddVolumes = ({
/> />
<Label <Label
htmlFor="bind" htmlFor="bind"
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="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"
> >
Bind Mount Bind Mount
</Label> </Label>
@@ -209,7 +209,7 @@ export const AddVolumes = ({
/> />
<Label <Label
htmlFor="volume" htmlFor="volume"
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="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"
> >
Volume Mount Volume Mount
</Label> </Label>
@@ -233,7 +233,7 @@ export const AddVolumes = ({
/> />
<Label <Label
htmlFor="file" htmlFor="file"
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="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"
> >
File Mount File Mount
</Label> </Label>

View File

@@ -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,22 +74,57 @@ 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}>
<form <form
id="hook-form" id="hook-form"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="w-full space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}
@@ -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>

View File

@@ -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,21 +74,57 @@ 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}>
<form <form
id="hook-form" id="hook-form"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="w-full space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}
@@ -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>

View File

@@ -87,7 +87,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full relative gap-4" className="w-full relative space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}
@@ -99,8 +99,8 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
<CodeEditor <CodeEditor
// disabled // disabled
value={field.value} value={field.value}
className="font-mono min-h-[20rem] compose-file-editor" className="font-mono"
wrapperClassName="min-h-[20rem]" wrapperClassName="compose-file-editor"
placeholder={`version: '3' placeholder={`version: '3'
services: services:
web: web:

View File

@@ -90,7 +90,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full relative z-[5]" className="w-full relative z-[5]"
> >
<div className="flex flex-col overflow-auto"> <div className="flex flex-col overflow-auto">
<FormField <FormField

View File

@@ -85,7 +85,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
<form <form
id="hook-form" id="hook-form"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="w-full space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -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>
)} )}

View File

@@ -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}`}
/> />

View File

@@ -85,7 +85,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
<form <form
id="hook-form" id="hook-form"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="w-full space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -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>
)} )}

View File

@@ -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`}
/> />

View File

@@ -85,7 +85,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
<form <form
id="hook-form" id="hook-form"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="w-full space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -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>
)} )}

View File

@@ -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}`}
/> />

View File

@@ -85,7 +85,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
<form <form
id="hook-form" id="hook-form"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="w-full space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -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>
)} )}

View File

@@ -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}`}
/> />

View File

@@ -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>

View File

@@ -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"

View File

@@ -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}

View File

@@ -85,7 +85,7 @@ export const ShowRedisEnvironment = ({ redisId }: Props) => {
<form <form
id="hook-form" id="hook-form"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="w-full space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -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>
)} )}

View File

@@ -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`}
/> />

View File

@@ -194,7 +194,7 @@ export const WebServer = () => {
Space Space
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start"> <DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>

View File

@@ -95,7 +95,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
<form <form
id="hook-form-update-main-traefik-config" id="hook-form-update-main-traefik-config"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 relative" className="w-full space-y-4 relative"
> >
<div className="flex flex-col"> <div className="flex flex-col">
<FormField <FormField

View File

@@ -98,7 +98,7 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
<form <form
id="hook-form-update-server-traefik-config" id="hook-form-update-server-traefik-config"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 relative overflow-auto" className="w-full space-y-4 relative overflow-auto"
> >
<div className="flex flex-col"> <div className="flex flex-col">
<FormField <FormField

View File

@@ -98,7 +98,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
<form <form
id="hook-form-update-server-traefik-config" id="hook-form-update-server-traefik-config"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 relative overflow-auto" className="w-full space-y-4 relative overflow-auto"
> >
<div className="flex flex-col"> <div className="flex flex-col">
<FormField <FormField

View File

@@ -46,7 +46,7 @@ export const CodeEditor = ({
)} )}
/> />
{props.disabled && ( {props.disabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" /> <div className="absolute top-0 rounded-md left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" />
)} )}
</div> </div>
); );

View 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>
);
};

View File

@@ -36,12 +36,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className, className,
)} )}
{...props} {...props}
> >
{children} <div className="space-y-4 w-full">
{children}
</div>
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>

15
lib/slug.ts Normal file
View 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,
});
};

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.2.1", "version": "v0.2.3",
"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",

View File

@@ -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
View File

@@ -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:

View File

@@ -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",

View File

@@ -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") {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)

View File

@@ -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({

View File

@@ -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 [];
}; };

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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;
}; };

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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: {

View File

@@ -8,7 +8,7 @@ import { getServiceContainer } from "../docker/utils";
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true // mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { appName, databasePassword } = mongo; const { appName, databasePassword, databaseUser } = mongo;
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`; const backupFileName = `${new Date().toISOString()}.dump.gz`;
@@ -23,7 +23,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
); );
await execAsync( await execAsync(
`docker exec ${containerId} sh -c "mongodump -d '${database}' -u 'mongo' -p '${databasePassword}' --authenticationDatabase=admin --archive=${containerPath} --gzip"`, `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --authenticationDatabase=admin --archive=${containerPath} --gzip"`,
); );
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`); await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
await uploadToS3(destination, bucketDestination, hostPath); await uploadToS3(destination, bucketDestination, hostPath);

View File

@@ -148,7 +148,6 @@ export const mechanizeDockerContainer = async (
}, },
}); });
} catch (error) { } catch (error) {
console.log(error);
await docker.createService(settings); await docker.createService(settings);
} }
}; };

View File

@@ -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();
} }
}; };

View File

@@ -127,10 +127,8 @@
background-color: transparent; background-color: transparent;
} }
.compose-file-editor .cm-editor { .compose-file-editor .cm-editor {
@apply min-h-[25rem]; @apply min-h-[25rem];
} }

View File

@@ -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,
}); });
/** /**