mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-27 10:05:32 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe22890311 | ||
|
|
3f2eeaf386 | ||
|
|
7826ba5bb5 | ||
|
|
bdc488e179 | ||
|
|
fc2abac989 | ||
|
|
101bbd44d8 | ||
|
|
68c2272e98 | ||
|
|
bc28464430 | ||
|
|
7df415a386 | ||
|
|
9fbd3039a6 | ||
|
|
3c00937b94 | ||
|
|
a57a776500 | ||
|
|
323e2f54ba | ||
|
|
2b7c7632f4 | ||
|
|
7db662e332 | ||
|
|
cd1a686b59 | ||
|
|
78d573c4f3 | ||
|
|
0ef9b1427b | ||
|
|
993d6b52f2 | ||
|
|
b9ab4a4d1a | ||
|
|
83153471b8 | ||
|
|
909e536f45 | ||
|
|
295cf50060 | ||
|
|
dd16baf234 | ||
|
|
72c366aa10 | ||
|
|
9a4f79f9e6 | ||
|
|
30b81834fc | ||
|
|
4e3aaa2a69 | ||
|
|
3dcd89cc32 | ||
|
|
41dc388bb0 | ||
|
|
44a592f7a7 | ||
|
|
1a4f5607dc | ||
|
|
d54c6e4ac9 | ||
|
|
936cf76a4c | ||
|
|
65254f1686 | ||
|
|
b312b1d7e0 | ||
|
|
fae180f157 |
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||||
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal 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
1
.gitignore
vendored
@@ -55,3 +55,4 @@ yarn-error.log*
|
|||||||
|
|
||||||
*.lockb
|
*.lockb
|
||||||
*.rdb
|
*.rdb
|
||||||
|
.idea
|
||||||
|
|||||||
87
README.md
87
README.md
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
@@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Root Password</Label>
|
<Label>Root Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databaseRootPassword}
|
value={data?.databaseRootPassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
@@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
@@ -29,20 +30,18 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Root Password</Label>
|
<Label>Root Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databaseRootPassword}
|
value={data?.databaseRootPassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +57,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -137,7 +138,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
@@ -29,10 +30,9 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +48,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,16 +22,26 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Folder } from "lucide-react";
|
import { Folder } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
const AddTemplateSchema = z.object({
|
const AddTemplateSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
|
appName: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "App name is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||||
|
message:
|
||||||
|
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,10 +49,13 @@ type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddApplication = ({ projectId }: Props) => {
|
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const slug = slugify(projectName);
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.application.create.useMutation();
|
api.application.create.useMutation();
|
||||||
@@ -50,34 +63,34 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
const form = useForm<AddTemplate>({
|
const form = useForm<AddTemplate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
appName: `${slug}-`,
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddTemplateSchema),
|
resolver: zodResolver(AddTemplateSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset();
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddTemplate) => {
|
const onSubmit = async (data: AddTemplate) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
appName: data.appName,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Created");
|
toast.success("Service Created");
|
||||||
|
form.reset();
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
toast.error("Error to create the service");
|
toast.error("Error to create the service");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
@@ -95,29 +108,46 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="name"
|
||||||
name="name"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input placeholder="Frontend" {...field} />
|
placeholder="Frontend"
|
||||||
</FormControl>
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
<FormMessage />
|
const val = e.target.value?.trim() || "";
|
||||||
</FormItem>
|
form.setValue("appName", `${slug}-${val}`);
|
||||||
)}
|
field.onChange(val);
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
@@ -136,47 +166,6 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <FormField
|
|
||||||
control={form.control}
|
|
||||||
name="buildType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="space-y-3">
|
|
||||||
<FormLabel>Build Type</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
className="flex flex-col space-y-1"
|
|
||||||
>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="dockerfile" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Dockerfile
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="nixpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">Nixpacks</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="heroku_buildpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Heroku Buildpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/> */}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -34,12 +34,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
const AddComposeSchema = z.object({
|
const AddComposeSchema = z.object({
|
||||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
|
appName: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "App name is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||||
|
message:
|
||||||
|
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,11 +57,12 @@ type AddCompose = z.infer<typeof AddComposeSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddCompose = ({ projectId }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const slug = slugify(projectName);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
@@ -60,6 +71,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
composeType: "docker-compose",
|
composeType: "docker-compose",
|
||||||
|
appName: `${slug}-`,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddComposeSchema),
|
resolver: zodResolver(AddComposeSchema),
|
||||||
});
|
});
|
||||||
@@ -74,6 +86,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
composeType: data.composeType,
|
composeType: data.composeType,
|
||||||
|
appName: data.appName,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
@@ -120,14 +133,34 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Frontend" {...field} />
|
<Input
|
||||||
|
placeholder="Frontend"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value?.trim() || "";
|
||||||
|
form.setValue("appName", `${slug}-${val}`);
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="composeType"
|
name="composeType"
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Database } from "lucide-react";
|
import { Database, AlertTriangle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -58,6 +59,15 @@ const databasesUserDefaultPlaceholder: Record<
|
|||||||
|
|
||||||
const baseDatabaseSchema = z.object({
|
const baseDatabaseSchema = z.object({
|
||||||
name: z.string().min(1, "Name required"),
|
name: z.string().min(1, "Name required"),
|
||||||
|
appName: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "App name is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||||
|
message:
|
||||||
|
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||||
|
}),
|
||||||
databasePassword: z.string(),
|
databasePassword: z.string(),
|
||||||
dockerImage: z.string(),
|
dockerImage: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
@@ -101,30 +111,52 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
.merge(baseDatabaseSchema),
|
.merge(baseDatabaseSchema),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const databasesMap = {
|
||||||
|
postgres: {
|
||||||
|
icon: <PostgresqlIcon />,
|
||||||
|
label: "PostgreSQL",
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
icon: <MongodbIcon />,
|
||||||
|
label: "MongoDB",
|
||||||
|
},
|
||||||
|
mariadb: {
|
||||||
|
icon: <MariadbIcon />,
|
||||||
|
label: "MariaDB",
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
icon: <MysqlIcon />,
|
||||||
|
label: "MySQL",
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
icon: <RedisIcon />,
|
||||||
|
label: "Redis",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
type AddDatabase = z.infer<typeof mySchema>;
|
type AddDatabase = z.infer<typeof mySchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDatabase = ({ projectId }: Props) => {
|
export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
const { mutateAsync: createPostgresql } = api.postgres.create.useMutation();
|
const slug = slugify(projectName);
|
||||||
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
const { mutateAsync: createMongo } = api.mongo.create.useMutation();
|
const mongoMutation = api.mongo.create.useMutation();
|
||||||
|
const redisMutation = api.redis.create.useMutation();
|
||||||
const { mutateAsync: createRedis } = api.redis.create.useMutation();
|
const mariadbMutation = api.mariadb.create.useMutation();
|
||||||
|
const mysqlMutation = api.mysql.create.useMutation();
|
||||||
const { mutateAsync: createMariadb } = api.mariadb.create.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: createMysql } = api.mysql.create.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddDatabase>({
|
const form = useForm<AddDatabase>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
dockerImage: "",
|
dockerImage: "",
|
||||||
name: "",
|
name: "",
|
||||||
|
appName: `${slug}-`,
|
||||||
databasePassword: "",
|
databasePassword: "",
|
||||||
description: "",
|
description: "",
|
||||||
databaseName: "",
|
databaseName: "",
|
||||||
@@ -133,76 +165,65 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
resolver: zodResolver(mySchema),
|
resolver: zodResolver(mySchema),
|
||||||
});
|
});
|
||||||
const type = form.watch("type");
|
const type = form.watch("type");
|
||||||
|
const activeMutation = {
|
||||||
useEffect(() => {
|
postgres: postgresMutation,
|
||||||
form.reset({
|
mongo: mongoMutation,
|
||||||
type: "postgres",
|
redis: redisMutation,
|
||||||
dockerImage: "",
|
mariadb: mariadbMutation,
|
||||||
name: "",
|
mysql: mysqlMutation,
|
||||||
databasePassword: "",
|
};
|
||||||
description: "",
|
|
||||||
databaseName: "",
|
|
||||||
databaseUser: "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddDatabase) => {
|
const onSubmit = async (data: AddDatabase) => {
|
||||||
const defaultDockerImage =
|
const defaultDockerImage =
|
||||||
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
||||||
|
|
||||||
let promise: Promise<unknown> | null = null;
|
let promise: Promise<unknown> | null = null;
|
||||||
|
const commonParams = {
|
||||||
|
name: data.name,
|
||||||
|
appName: data.appName,
|
||||||
|
dockerImage: defaultDockerImage,
|
||||||
|
projectId,
|
||||||
|
description: data.description,
|
||||||
|
};
|
||||||
|
|
||||||
if (data.type === "postgres") {
|
if (data.type === "postgres") {
|
||||||
promise = createPostgresql({
|
promise = postgresMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mongo") {
|
} else if (data.type === "mongo") {
|
||||||
promise = createMongo({
|
promise = mongoMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "redis") {
|
} else if (data.type === "redis") {
|
||||||
promise = createRedis({
|
promise = redisMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
projectId,
|
projectId,
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mariadb") {
|
} else if (data.type === "mariadb") {
|
||||||
promise = createMariadb({
|
promise = mariadbMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
projectId,
|
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mysql") {
|
} else if (data.type === "mysql") {
|
||||||
promise = createMysql({
|
promise = mysqlMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +231,17 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
await promise
|
await promise
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Created");
|
toast.success("Database Created");
|
||||||
|
form.reset({
|
||||||
|
type: "postgres",
|
||||||
|
dockerImage: "",
|
||||||
|
name: "",
|
||||||
|
appName: `${projectName}-`,
|
||||||
|
databasePassword: "",
|
||||||
|
description: "",
|
||||||
|
databaseName: "",
|
||||||
|
databaseUser: "",
|
||||||
|
});
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
@@ -220,7 +252,7 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
@@ -230,18 +262,10 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
<span>Database</span>
|
<span>Database</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Databases</DialogTitle>
|
<DialogTitle>Databases</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* {isError && (
|
|
||||||
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -264,99 +288,40 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||||
>
|
>
|
||||||
<FormItem className="flex w-full items-center space-x-3 space-y-0">
|
{Object.entries(databasesMap).map(([key, value]) => (
|
||||||
<FormControl className="w-full">
|
<FormItem
|
||||||
<div>
|
key={key}
|
||||||
<RadioGroupItem
|
className="flex w-full items-center space-x-3 space-y-0"
|
||||||
value="postgres"
|
>
|
||||||
id="postgres"
|
<FormControl className="w-full">
|
||||||
className="peer sr-only"
|
<div>
|
||||||
/>
|
<RadioGroupItem
|
||||||
<Label
|
value={key}
|
||||||
htmlFor="postgres"
|
id={key}
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
className="peer sr-only"
|
||||||
>
|
/>
|
||||||
<PostgresqlIcon />
|
<Label
|
||||||
Postgresql
|
htmlFor={key}
|
||||||
</Label>
|
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||||
</div>
|
>
|
||||||
</FormControl>
|
{value.icon}
|
||||||
</FormItem>
|
{value.label}
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
</Label>
|
||||||
<FormControl className="w-full">
|
</div>
|
||||||
<div>
|
</FormControl>
|
||||||
<RadioGroupItem
|
</FormItem>
|
||||||
value="mysql"
|
))}
|
||||||
id="mysql"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="mysql"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<MysqlIcon />
|
|
||||||
Mysql
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl className="w-full">
|
|
||||||
<div>
|
|
||||||
<RadioGroupItem
|
|
||||||
value="mariadb"
|
|
||||||
id="mariadb"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="mariadb"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<MariadbIcon />
|
|
||||||
Mariadb
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl className="w-full">
|
|
||||||
<div>
|
|
||||||
<RadioGroupItem
|
|
||||||
value="mongo"
|
|
||||||
id="mongo"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="mongo"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<MongodbIcon />
|
|
||||||
Mongo
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl className="w-full">
|
|
||||||
<div>
|
|
||||||
<RadioGroupItem
|
|
||||||
value="redis"
|
|
||||||
id="redis"
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="redis"
|
|
||||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
|
||||||
>
|
|
||||||
<RedisIcon />
|
|
||||||
Redis
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
{activeMutation[field.value].isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{activeMutation[field.value].error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -372,13 +337,34 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Name" {...field} />
|
<Input
|
||||||
|
placeholder="Name"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value?.trim() || "";
|
||||||
|
form.setValue("appName", `${slug}-${val}`);
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -129,7 +130,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
@@ -25,10 +26,9 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +44,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
|
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
26
components/shared/toggle-visibility-input.tsx
Normal file
26
components/shared/toggle-visibility-input.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import { Input, type InputProps } from "../ui/input";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||||
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
|
|
||||||
|
const togglePasswordVisibility = () => {
|
||||||
|
setIsPasswordVisible((prevVisibility) => !prevVisibility);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputType = isPasswordVisible ? "text" : "password";
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center space-x-2">
|
||||||
|
<Input type={inputType} {...props} />
|
||||||
|
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
||||||
|
{inputType === "password" ? (
|
||||||
|
<EyeIcon className="size-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeOffIcon className="size-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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
15
lib/slug.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import slug from "slugify";
|
||||||
|
|
||||||
|
export const slugify = (text: string | undefined) => {
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
|
||||||
|
|
||||||
|
return slug(cleanedText, {
|
||||||
|
lower: true,
|
||||||
|
trim: true,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.2.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",
|
||||||
|
|||||||
@@ -210,9 +210,12 @@ const Project = (
|
|||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<AddApplication projectId={projectId} />
|
<AddApplication
|
||||||
<AddDatabase projectId={projectId} />
|
projectId={projectId}
|
||||||
<AddCompose projectId={projectId} />
|
projectName={data?.name}
|
||||||
|
/>
|
||||||
|
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||||
|
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||||
<AddTemplate projectId={projectId} />
|
<AddTemplate projectId={projectId} />
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -230,6 +230,9 @@ dependencies:
|
|||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.12.3
|
specifier: ^2.12.3
|
||||||
version: 2.12.3(react-dom@18.2.0)(react@18.2.0)
|
version: 2.12.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
slugify:
|
||||||
|
specifier: ^1.6.6
|
||||||
|
version: 1.6.6
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
|
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -8015,6 +8018,11 @@ packages:
|
|||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/slugify@1.6.6:
|
||||||
|
resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sonner@1.4.3(react-dom@18.2.0)(react@18.2.0):
|
/sonner@1.4.3(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==}
|
resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
await addNewService(ctx.user.authId, newApplication.applicationId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to create the application",
|
message: "Error to create the application",
|
||||||
|
|||||||
@@ -34,13 +34,18 @@ import { nanoid } from "nanoid";
|
|||||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { createCommand } from "@/server/utils/builders/compose";
|
import { createCommand } from "@/server/utils/builders/compose";
|
||||||
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
|
import {
|
||||||
|
generatePassword,
|
||||||
|
loadTemplateModule,
|
||||||
|
readComposeFile,
|
||||||
|
} from "@/templates/utils";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin } from "../services/admin";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { findProjectById, slugifyProjectName } from "../services/project";
|
import { findProjectById } from "../services/project";
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
export const composeRouter = createTRPCRouter({
|
export const composeRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const { envs, mounts } = generate({
|
const { envs, mounts } = generate({
|
||||||
serverIp: admin.serverIp,
|
serverIp: admin.serverIp,
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
@@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
env: envs.join("\n"),
|
env: envs.join("\n"),
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mariadb database",
|
message: "Error input: Inserting mariadb database",
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mongo database",
|
message: "Error input: Inserting mongo database",
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mysql database",
|
message: "Error input: Inserting mysql database",
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting postgresql database",
|
message: "Error input: Inserting postgresql database",
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ import { findAdmin } from "./admin";
|
|||||||
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
||||||
import { docker } from "@/server/constants";
|
import { docker } from "@/server/constants";
|
||||||
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
export type Application = typeof applications.$inferSelect;
|
export type Application = typeof applications.$inferSelect;
|
||||||
|
|
||||||
export const createApplication = async (
|
export const createApplication = async (
|
||||||
input: typeof apiCreateApplication._type,
|
input: typeof apiCreateApplication._type,
|
||||||
) => {
|
) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Application with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const newApplication = await tx
|
const newApplication = await tx
|
||||||
.insert(applications)
|
.insert(applications)
|
||||||
|
|||||||
@@ -13,10 +13,21 @@ import { join } from "node:path";
|
|||||||
import { COMPOSE_PATH } from "@/server/constants";
|
import { COMPOSE_PATH } from "@/server/constants";
|
||||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
import { cloneGitRepository } from "@/server/utils/providers/git";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Compose = typeof compose.$inferSelect;
|
export type Compose = typeof compose.$inferSelect;
|
||||||
|
|
||||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const newDestination = await db
|
const newDestination = await db
|
||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
@@ -39,6 +50,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
|||||||
export const createComposeByTemplate = async (
|
export const createComposeByTemplate = async (
|
||||||
input: typeof compose.$inferInsert,
|
input: typeof compose.$inferInsert,
|
||||||
) => {
|
) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const newDestination = await db
|
const newDestination = await db
|
||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ export const getContainers = async () => {
|
|||||||
.filter((container) => !container.name.includes("dokploy"));
|
.filter((container) => !container.name.includes("dokploy"));
|
||||||
|
|
||||||
return containers;
|
return containers;
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConfig = async (containerId: string) => {
|
export const getConfig = async (containerId: string) => {
|
||||||
@@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => {
|
|||||||
const config = JSON.parse(stdout);
|
const config = JSON.parse(stdout);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContainersByAppNameMatch = async (appName: string) => {
|
export const getContainersByAppNameMatch = async (appName: string) => {
|
||||||
@@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
@@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Mariadb = typeof mariadb.$inferSelect;
|
export type Mariadb = typeof mariadb.$inferSelect;
|
||||||
|
|
||||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMariadb = await db
|
const newMariadb = await db
|
||||||
.insert(mariadb)
|
.insert(mariadb)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ import { buildMongo } from "@/server/utils/databases/mongo";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Mongo = typeof mongo.$inferSelect;
|
export type Mongo = typeof mongo.$inferSelect;
|
||||||
|
|
||||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMongo = await db
|
const newMongo = await db
|
||||||
.insert(mongo)
|
.insert(mongo)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,11 +5,22 @@ import { buildMysql } from "@/server/utils/databases/mysql";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type MySql = typeof mysql.$inferSelect;
|
export type MySql = typeof mysql.$inferSelect;
|
||||||
|
|
||||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMysql = await db
|
const newMysql = await db
|
||||||
.insert(mysql)
|
.insert(mysql)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Postgres = typeof postgres.$inferSelect;
|
export type Postgres = typeof postgres.$inferSelect;
|
||||||
|
|
||||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newPostgres = await db
|
const newPostgres = await db
|
||||||
.insert(postgres)
|
.insert(postgres)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { type apiCreateProject, projects } from "@/server/db/schema";
|
import {
|
||||||
|
type apiCreateProject,
|
||||||
|
applications,
|
||||||
|
mariadb,
|
||||||
|
mongo,
|
||||||
|
mysql,
|
||||||
|
postgres,
|
||||||
|
projects,
|
||||||
|
redis,
|
||||||
|
} from "@/server/db/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { findAdmin } from "./admin";
|
import { findAdmin } from "./admin";
|
||||||
@@ -75,12 +84,40 @@ export const updateProjectById = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const slugifyProjectName = (projectName: string): string => {
|
export const validUniqueServerAppName = async (appName: string) => {
|
||||||
return projectName
|
const query = await db.query.projects.findMany({
|
||||||
.toLowerCase()
|
with: {
|
||||||
.replace(/[0-9]/g, "")
|
applications: {
|
||||||
.replace(/[^a-z\s-]/g, "")
|
where: eq(applications.appName, appName),
|
||||||
.replace(/\s+/g, "-")
|
},
|
||||||
.replace(/-+/g, "-")
|
mariadb: {
|
||||||
.replace(/^-+|-+$/g, "");
|
where: eq(mariadb.appName, appName),
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
where: eq(mongo.appName, appName),
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
where: eq(mysql.appName, appName),
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
where: eq(postgres.appName, appName),
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
where: eq(redis.appName, appName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter out items with non-empty fields
|
||||||
|
const nonEmptyProjects = query.filter(
|
||||||
|
(project) =>
|
||||||
|
project.applications.length > 0 ||
|
||||||
|
project.mariadb.length > 0 ||
|
||||||
|
project.mongo.length > 0 ||
|
||||||
|
project.mysql.length > 0 ||
|
||||||
|
project.postgres.length > 0 ||
|
||||||
|
project.redis.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return nonEmptyProjects.length === 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,23 @@ import { buildRedis } from "@/server/utils/databases/redis";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Redis = typeof redis.$inferSelect;
|
export type Redis = typeof redis.$inferSelect;
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||||
|
if (input.appName) {
|
||||||
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: "Service with this 'AppName' already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newRedis = await db
|
const newRedis = await db
|
||||||
.insert(redis)
|
.insert(redis)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { registry } from "./registry";
|
import { registry } from "./registry";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
||||||
|
|
||||||
@@ -307,11 +308,17 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateApplication = createSchema.pick({
|
export const apiCreateApplication = createSchema
|
||||||
name: true,
|
.pick({
|
||||||
description: true,
|
name: true,
|
||||||
projectId: true,
|
appName: true,
|
||||||
});
|
description: true,
|
||||||
|
projectId: true,
|
||||||
|
})
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName: `${data.appName}-${generatePassword(6)}` || generateAppName("app"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneApplication = createSchema
|
export const apiFindOneApplication = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { deployments } from "./deployment";
|
|||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||||
"git",
|
"git",
|
||||||
@@ -74,12 +75,19 @@ const createSchema = createInsertSchema(compose, {
|
|||||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateCompose = createSchema.pick({
|
export const apiCreateCompose = createSchema
|
||||||
name: true,
|
.pick({
|
||||||
description: true,
|
name: true,
|
||||||
projectId: true,
|
description: true,
|
||||||
composeType: true,
|
projectId: true,
|
||||||
});
|
composeType: true,
|
||||||
|
appName: true,
|
||||||
|
})
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("compose"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiCreateComposeByTemplate = createSchema
|
export const apiCreateComposeByTemplate = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mariadb = pgTable("mariadb", {
|
export const mariadb = pgTable("mariadb", {
|
||||||
mariadbId: text("mariadbId")
|
mariadbId: text("mariadbId")
|
||||||
@@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, {
|
|||||||
export const apiCreateMariaDB = createSchema
|
export const apiCreateMariaDB = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
databaseRootPassword: true,
|
databaseRootPassword: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
@@ -87,7 +89,12 @@ export const apiCreateMariaDB = createSchema
|
|||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("mariadb"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneMariaDB = createSchema
|
export const apiFindOneMariaDB = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mongo = pgTable("mongo", {
|
export const mongo = pgTable("mongo", {
|
||||||
mongoId: text("mongoId")
|
mongoId: text("mongoId")
|
||||||
@@ -73,13 +74,19 @@ const createSchema = createInsertSchema(mongo, {
|
|||||||
export const apiCreateMongo = createSchema
|
export const apiCreateMongo = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneMongo = createSchema
|
export const apiFindOneMongo = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mysql = pgTable("mysql", {
|
export const mysql = pgTable("mysql", {
|
||||||
mysqlId: text("mysqlId")
|
mysqlId: text("mysqlId")
|
||||||
@@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, {
|
|||||||
export const apiCreateMySql = createSchema
|
export const apiCreateMySql = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
@@ -85,7 +87,12 @@ export const apiCreateMySql = createSchema
|
|||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
databaseRootPassword: true,
|
databaseRootPassword: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("mysql"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneMySql = createSchema
|
export const apiFindOneMySql = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const postgres = pgTable("postgres", {
|
export const postgres = pgTable("postgres", {
|
||||||
postgresId: text("postgresId")
|
postgresId: text("postgresId")
|
||||||
@@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, {
|
|||||||
export const apiCreatePostgres = createSchema
|
export const apiCreatePostgres = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
databaseName: true,
|
databaseName: true,
|
||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
@@ -81,7 +83,12 @@ export const apiCreatePostgres = createSchema
|
|||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOnePostgres = createSchema
|
export const apiFindOnePostgres = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
|||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const redis = pgTable("redis", {
|
export const redis = pgTable("redis", {
|
||||||
redisId: text("redisId")
|
redisId: text("redisId")
|
||||||
@@ -69,13 +70,18 @@ const createSchema = createInsertSchema(redis, {
|
|||||||
export const apiCreateRedis = createSchema
|
export const apiCreateRedis = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
})
|
})
|
||||||
|
.required()
|
||||||
.required();
|
.transform((data) => ({
|
||||||
|
...data,
|
||||||
|
appName:
|
||||||
|
`${data.appName}-${generatePassword(6)}` || generateAppName("redis"),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apiFindOneRedis = createSchema
|
export const apiFindOneRedis = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import type { MainTraefikConfig } from "../utils/traefik/types";
|
|||||||
import type { FileConfig } from "../utils/traefik/file-types";
|
import type { FileConfig } from "../utils/traefik/file-types";
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
import type { CreateServiceOptions } from "dockerode";
|
||||||
|
|
||||||
|
const TRAEFIK_SSL_PORT =
|
||||||
|
Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443;
|
||||||
|
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80;
|
||||||
|
|
||||||
export const initializeTraefik = async () => {
|
export const initializeTraefik = async () => {
|
||||||
const imageName = "traefik:v2.5";
|
const imageName = "traefik:v2.5";
|
||||||
const containerName = "dokploy-traefik";
|
const containerName = "dokploy-traefik";
|
||||||
@@ -47,12 +51,12 @@ export const initializeTraefik = async () => {
|
|||||||
Ports: [
|
Ports: [
|
||||||
{
|
{
|
||||||
TargetPort: 443,
|
TargetPort: 443,
|
||||||
PublishedPort: 443,
|
PublishedPort: TRAEFIK_SSL_PORT,
|
||||||
PublishMode: "host",
|
PublishMode: "host",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: 80,
|
TargetPort: 80,
|
||||||
PublishedPort: 80,
|
PublishedPort: TRAEFIK_PORT,
|
||||||
PublishMode: "host",
|
PublishMode: "host",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -146,10 +150,10 @@ export const createDefaultTraefikConfig = () => {
|
|||||||
},
|
},
|
||||||
entryPoints: {
|
entryPoints: {
|
||||||
web: {
|
web: {
|
||||||
address: ":80",
|
address: `:${TRAEFIK_PORT}`,
|
||||||
},
|
},
|
||||||
websecure: {
|
websecure: {
|
||||||
address: ":443",
|
address: `:${TRAEFIK_SSL_PORT}`,
|
||||||
...(process.env.NODE_ENV === "production" && {
|
...(process.env.NODE_ENV === "production" && {
|
||||||
http: {
|
http: {
|
||||||
tls: {
|
tls: {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ export const mechanizeDockerContainer = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
await docker.createService(settings);
|
await docker.createService(settings);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,47 +3,47 @@ import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
|
|||||||
import { pullImage } from "../docker/utils";
|
import { pullImage } from "../docker/utils";
|
||||||
|
|
||||||
interface RegistryAuth {
|
interface RegistryAuth {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
serveraddress: string;
|
serveraddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildDocker = async (
|
export const buildDocker = async (
|
||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const { buildType, dockerImage, username, password } = application;
|
const { buildType, dockerImage, username, password } = application;
|
||||||
const authConfig: Partial<RegistryAuth> = {
|
const authConfig: Partial<RegistryAuth> = {
|
||||||
username: username || "",
|
username: username || "",
|
||||||
password: password || "",
|
password: password || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||||
|
|
||||||
writeStream.write(`\nBuild ${buildType}\n`);
|
writeStream.write(`\nBuild ${buildType}\n`);
|
||||||
|
|
||||||
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!dockerImage) {
|
if (!dockerImage) {
|
||||||
throw new Error("Docker image not found");
|
throw new Error("Docker image not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await pullImage(
|
await pullImage(
|
||||||
dockerImage,
|
dockerImage,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (writeStream.writable) {
|
if (writeStream.writable) {
|
||||||
writeStream.write(`${data.status}\n`);
|
writeStream.write(`${data.status}\n`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
authConfig,
|
authConfig,
|
||||||
);
|
);
|
||||||
await mechanizeDockerContainer(application);
|
await mechanizeDockerContainer(application);
|
||||||
writeStream.write("\nDocker Deployed: ✅\n");
|
writeStream.write("\nDocker Deployed: ✅\n");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeStream.write(`ERROR: ${error}: ❌`);
|
writeStream.write(`ERROR: ${error}: ❌`);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
writeStream.end();
|
writeStream.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
67
utils/api.ts
67
utils/api.ts
@@ -11,50 +11,39 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
const getBaseUrl = () => {
|
const getBaseUrl = () => {
|
||||||
if (typeof window !== "undefined") return ""; // browser should use relative url
|
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||||
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A set of type-safe react-query hooks for your tRPC API. */
|
/** A set of type-safe react-query hooks for your tRPC API. */
|
||||||
export const api = createTRPCNext<AppRouter>({
|
export const api = createTRPCNext<AppRouter>({
|
||||||
config() {
|
config() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* Transformer used for data de-serialization from the server.
|
* Transformer used for data de-serialization from the server.
|
||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/data-transformers
|
* @see https://trpc.io/docs/data-transformers
|
||||||
*/
|
*/
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links used to determine request flow from client to server.
|
* Links used to determine request flow from client to server.
|
||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/links
|
* @see https://trpc.io/docs/links
|
||||||
*/
|
*/
|
||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
}),
|
}),
|
||||||
// createWSClient({
|
],
|
||||||
// url: `ws://localhost:3000`,
|
};
|
||||||
// }),
|
},
|
||||||
// loggerLink({
|
/**
|
||||||
// enabled: (opts) =>
|
* Whether tRPC should await queries when server rendering pages.
|
||||||
// process.env.NODE_ENV === "development" ||
|
*
|
||||||
// (opts.direction === "down" && opts.result instanceof Error),
|
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
|
||||||
// }),
|
*/
|
||||||
// httpBatchLink({
|
ssr: false,
|
||||||
// url: `${getBaseUrl()}/api/trpc`,
|
|
||||||
// }),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Whether tRPC should await queries when server rendering pages.
|
|
||||||
*
|
|
||||||
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
|
|
||||||
*/
|
|
||||||
ssr: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user