Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64ada7020a | ||
|
|
faf24dfa25 | ||
|
|
6f4bf428c7 | ||
|
|
a43627d869 | ||
|
|
addd102d39 | ||
|
|
0d85fd0e3c | ||
|
|
86fc59d850 | ||
|
|
3cd3db6828 | ||
|
|
2c6fd0f52b | ||
|
|
5fb682b58d | ||
|
|
b77f330222 | ||
|
|
f0b8f3eaa0 | ||
|
|
36af22630b | ||
|
|
2d53a700f6 | ||
|
|
00eeffee13 | ||
|
|
7a32698031 | ||
|
|
9d834e1a79 | ||
|
|
91819c2488 | ||
|
|
f64392469d | ||
|
|
889e72d21e | ||
|
|
86165d1104 | ||
|
|
54adab16cf | ||
|
|
2e3b0ddcde | ||
|
|
ed0f3cadd6 | ||
|
|
898880634a | ||
|
|
b4e154fb28 | ||
|
|
2e6489d315 | ||
|
|
210fed30a2 | ||
|
|
60521c1025 | ||
|
|
1a496e35c0 | ||
|
|
f37f98aade | ||
|
|
0eb7b3ecb1 | ||
|
|
1a7c602861 | ||
|
|
4706adc0c0 | ||
|
|
85f025c729 | ||
|
|
06005eb333 | ||
|
|
0c01efb249 | ||
|
|
7e9e9dc865 | ||
|
|
b28bf5f9ec | ||
|
|
c071be6ad9 | ||
|
|
059a98c575 | ||
|
|
d2a07195b0 | ||
|
|
4ff1b3c19f | ||
|
|
39abd7e374 | ||
|
|
899d7565f6 | ||
|
|
3cfc2d6cd8 | ||
|
|
817fa91173 | ||
|
|
4d8a4f713d | ||
|
|
4865f4f969 | ||
|
|
0b7feb5483 | ||
|
|
1c139b9503 | ||
|
|
d47b7e62e6 | ||
|
|
33940a345a | ||
|
|
f230bda1f7 | ||
|
|
687524d154 | ||
|
|
e01d92d1d9 | ||
|
|
8f0a4f0886 | ||
|
|
01794c7742 | ||
|
|
19bbe69ee3 | ||
|
|
92c4e769ab | ||
|
|
0801e91816 | ||
|
|
b360cc2af4 | ||
|
|
ba5d8feba2 | ||
|
|
43be9d0171 | ||
|
|
a6bbf5d96b | ||
|
|
7e6e43adfe | ||
|
|
e8a4611ab7 | ||
|
|
608db3d401 | ||
|
|
0add62f14d | ||
|
|
1754f63352 | ||
|
|
edcc7544fb | ||
|
|
1edf30546d | ||
|
|
ad806437af | ||
|
|
f0eecf354b | ||
|
|
c5ace67b3f | ||
|
|
fe22890311 | ||
|
|
3f2eeaf386 | ||
|
|
7826ba5bb5 | ||
|
|
bdc488e179 | ||
|
|
fc2abac989 | ||
|
|
101bbd44d8 | ||
|
|
68c2272e98 | ||
|
|
bc28464430 | ||
|
|
7df415a386 | ||
|
|
9fbd3039a6 | ||
|
|
3c00937b94 | ||
|
|
ae4226531e | ||
|
|
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 | ||
|
|
b9bff95c3d | ||
|
|
fae180f157 |
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
@@ -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
@@ -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
@@ -55,3 +55,4 @@ yarn-error.log*
|
|||||||
|
|
||||||
*.lockb
|
*.lockb
|
||||||
*.rdb
|
*.rdb
|
||||||
|
.idea
|
||||||
|
|||||||
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/API**: Manage your applications and databases using the command line or trought the API.
|
||||||
|
* **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>
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{deployment.title}
|
{deployment.title}
|
||||||
</span>
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
|||||||
79
components/dashboard/application/domains/generate-domain.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import { GenerateTraefikMe } from "./generate-traefikme";
|
||||||
|
import { GenerateWildCard } from "./generate-wildcard";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenerateDomain = ({ applicationId }: Props) => {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
<Button variant="secondary">
|
||||||
|
Generate Domain
|
||||||
|
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Generate Domain</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Generate Domains for your applications
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<ul className="flex flex-col gap-4">
|
||||||
|
<li className="flex flex-row items-center gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-base font-bold">
|
||||||
|
1. Generate TraefikMe Domain
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
This option generates a free domain provided by{" "}
|
||||||
|
<Link
|
||||||
|
href="https://traefik.me"
|
||||||
|
className="text-primary"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
TraefikMe
|
||||||
|
</Link>
|
||||||
|
. We recommend using this for quick domain testing or if you
|
||||||
|
don't have a domain yet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/* <li className="flex flex-row items-center gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-base font-bold">
|
||||||
|
2. Use Wildcard Domain
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
To use this option, you need to set up an 'A' record in your
|
||||||
|
domain provider. For example, create a record for
|
||||||
|
*.yourdomain.com.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li> */}
|
||||||
|
</ul>
|
||||||
|
<div className="flex flex-row gap-4 w-full">
|
||||||
|
<GenerateTraefikMe applicationId={applicationId} />
|
||||||
|
{/* <GenerateWildCard applicationId={applicationId} /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
export const GenerateTraefikMe = ({ applicationId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="secondary" isLoading={isLoading}>
|
||||||
|
Generate Domain
|
||||||
|
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure to generate a new domain?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will generate a new domain and will be used to access to the
|
||||||
|
application
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
utils.domain.byApplicationId.invalidate({
|
||||||
|
applicationId: applicationId,
|
||||||
|
});
|
||||||
|
utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId: applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Generated Domain succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to generate Domain");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { SquareAsterisk } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
export const GenerateWildCard = ({ applicationId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="secondary" isLoading={isLoading}>
|
||||||
|
Generate Wildcard Domain
|
||||||
|
<SquareAsterisk className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure to generate a new wildcard domain?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will generate a new domain and will be used to access to the
|
||||||
|
application
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
utils.domain.byApplicationId.invalidate({
|
||||||
|
applicationId: applicationId,
|
||||||
|
});
|
||||||
|
utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId: applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Generated Domain succesfully");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error(`Error to generate Domain: ${e.message}`);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { ExternalLink, GlobeIcon } from "lucide-react";
|
import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -14,6 +14,7 @@ import { DeleteDomain } from "./delete-domain";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AddDomain } from "./add-domain";
|
import { AddDomain } from "./add-domain";
|
||||||
import { UpdateDomain } from "./update-domain";
|
import { UpdateDomain } from "./update-domain";
|
||||||
|
import { GenerateDomain } from "./generate-domain";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -31,7 +32,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
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 className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<CardTitle className="text-xl">Domains</CardTitle>
|
<CardTitle className="text-xl">Domains</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -39,11 +40,16 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.length > 0 && (
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<AddDomain applicationId={applicationId}>
|
{data && data?.length > 0 && (
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<AddDomain applicationId={applicationId}>
|
||||||
</AddDomain>
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
)}
|
</AddDomain>
|
||||||
|
)}
|
||||||
|
{data && data?.length > 0 && (
|
||||||
|
<GenerateDomain applicationId={applicationId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
@@ -53,9 +59,13 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
To access to the application is required to set at least 1
|
To access to the application is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<AddDomain applicationId={applicationId}>
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<AddDomain applicationId={applicationId}>
|
||||||
</AddDomain>
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</AddDomain>
|
||||||
|
|
||||||
|
<GenerateDomain applicationId={applicationId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { RedbuildApplication } from "../rebuild-application";
|
|||||||
import { StartApplication } from "../start-application";
|
import { StartApplication } from "../start-application";
|
||||||
import { StopApplication } from "../stop-application";
|
import { StopApplication } from "../stop-application";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
import { Terminal } from "lucide-react";
|
import { CheckCircle2, Terminal } from "lucide-react";
|
||||||
import { DeployApplication } from "./deploy-application";
|
import { DeployApplication } from "./deploy-application";
|
||||||
import { ResetApplication } from "./reset-application";
|
import { ResetApplication } from "./reset-application";
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -55,8 +55,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
toast.error("Error to update Auto Deploy");
|
toast.error("Error to update Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center"
|
||||||
>
|
>
|
||||||
Autodeploy
|
Autodeploy
|
||||||
|
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
||||||
</Toggle>
|
</Toggle>
|
||||||
<RedbuildApplication applicationId={applicationId} />
|
<RedbuildApplication applicationId={applicationId} />
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ export const StopCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Compose rebuild succesfully");
|
toast.success("Compose rebuild succesfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to rebuild the compose");
|
toast.error("Error to stop the compose");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to rebuild the compose");
|
toast.error("Error to stop the compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -161,10 +161,6 @@ export const ShowContainers = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end space-x-2 py-4">
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
<div className="flex-1 text-sm text-muted-foreground">
|
|
||||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
|
||||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
||||||
</div>
|
|
||||||
<div className="space-x-2 flex flex-wrap">
|
<div className="space-x-2 flex flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -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`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -141,7 +141,13 @@ export const DockerMonitoring = ({
|
|||||||
network: data.network[data.network.length - 1] ?? currentData.network,
|
network: data.network[data.network.length - 1] ?? currentData.network,
|
||||||
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
||||||
});
|
});
|
||||||
setAcummulativeData(data);
|
setAcummulativeData({
|
||||||
|
block: data?.block || [],
|
||||||
|
cpu: data?.cpu || [],
|
||||||
|
disk: data?.disk || [],
|
||||||
|
memory: data?.memory || [],
|
||||||
|
network: data?.network || [],
|
||||||
|
});
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
||||||
<div className="sticky top-0 z-10 flex flex-col gap-4 bg-black p-6 border-b">
|
<div className="sticky top-0 z-10 flex flex-col gap-4 dark:bg-black p-6 border-b">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Template</DialogTitle>
|
<DialogTitle>Create Template</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ export const ShowProjects = () => {
|
|||||||
project?.compose.length;
|
project?.compose.length;
|
||||||
return (
|
return (
|
||||||
<div key={project.projectId} className="w-full lg:max-w-md">
|
<div key={project.projectId} className="w-full lg:max-w-md">
|
||||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||||
<Button
|
<Button
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -85,113 +85,122 @@ export const ShowProjects = () => {
|
|||||||
>
|
>
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
<CardHeader>
|
||||||
<CardHeader>
|
<CardTitle className="flex items-center justify-between gap-2">
|
||||||
<CardTitle className="flex items-center justify-between gap-2">
|
<span className="flex flex-col gap-1.5">
|
||||||
<span className="flex flex-col gap-1.5">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<BookIcon className="size-4 text-muted-foreground" />
|
||||||
<BookIcon className="size-4 text-muted-foreground" />
|
<span className="text-base font-medium leading-none">
|
||||||
<Link
|
{project.name}
|
||||||
className="text-base font-medium leading-none"
|
</span>
|
||||||
href={`/dashboard/project/${project.projectId}`}
|
</div>
|
||||||
>
|
|
||||||
{project.name}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
{project.description}
|
{project.description}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
<div className="flex self-start space-x-1">
|
||||||
<div className="flex self-start space-x-1">
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<Button
|
||||||
<Button variant="ghost" size="icon" className="px-2">
|
variant="ghost"
|
||||||
<MoreHorizontalIcon className="size-5" />
|
size="icon"
|
||||||
</Button>
|
className="px-2"
|
||||||
</DropdownMenuTrigger>
|
>
|
||||||
<DropdownMenuContent className="w-[200px] space-y-2">
|
<MoreHorizontalIcon className="size-5" />
|
||||||
<DropdownMenuLabel className="font-normal">
|
</Button>
|
||||||
Actions
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuContent className="w-[200px] space-y-2">
|
||||||
|
<DropdownMenuLabel className="font-normal">
|
||||||
|
Actions
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
<UpdateProject projectId={project.projectId} />
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<UpdateProject projectId={project.projectId} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{(auth?.rol === "admin" ||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
user?.canDeleteProjects) && (
|
{(auth?.rol === "admin" ||
|
||||||
<AlertDialog>
|
user?.canDeleteProjects) && (
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialog>
|
||||||
<DropdownMenuItem
|
<AlertDialogTrigger className="w-full">
|
||||||
className="w-full cursor-pointer space-x-3"
|
<DropdownMenuItem
|
||||||
onSelect={(e) => e.preventDefault()}
|
className="w-full cursor-pointer space-x-3"
|
||||||
>
|
onSelect={(e) => e.preventDefault()}
|
||||||
<TrashIcon className="size-4" />
|
>
|
||||||
<span>Delete</span>
|
<TrashIcon className="size-4" />
|
||||||
</DropdownMenuItem>
|
<span>Delete</span>
|
||||||
</AlertDialogTrigger>
|
</DropdownMenuItem>
|
||||||
<AlertDialogContent>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogHeader>
|
<AlertDialogContent>
|
||||||
<AlertDialogTitle>
|
<AlertDialogHeader>
|
||||||
Are you sure to delete this project?
|
<AlertDialogTitle>
|
||||||
</AlertDialogTitle>
|
Are you sure to delete this project?
|
||||||
{!emptyServices ? (
|
</AlertDialogTitle>
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
{!emptyServices ? (
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
You have active services, please delete
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
them first
|
You have active services, please
|
||||||
</span>
|
delete them first
|
||||||
</div>
|
</span>
|
||||||
) : (
|
</div>
|
||||||
<AlertDialogDescription>
|
) : (
|
||||||
This action cannot be undone
|
<AlertDialogDescription>
|
||||||
</AlertDialogDescription>
|
This action cannot be undone
|
||||||
)}
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
)}
|
||||||
<AlertDialogFooter>
|
</AlertDialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction
|
<AlertDialogCancel>
|
||||||
disabled={!emptyServices}
|
Cancel
|
||||||
onClick={async () => {
|
</AlertDialogCancel>
|
||||||
await mutateAsync({
|
<AlertDialogAction
|
||||||
projectId: project.projectId,
|
disabled={!emptyServices}
|
||||||
})
|
onClick={async () => {
|
||||||
.then(() => {
|
await mutateAsync({
|
||||||
toast.success(
|
projectId: project.projectId,
|
||||||
"Project delete succesfully",
|
})
|
||||||
);
|
.then(() => {
|
||||||
})
|
toast.success(
|
||||||
.catch(() => {
|
"Project delete succesfully",
|
||||||
toast.error(
|
);
|
||||||
"Error to delete this project",
|
})
|
||||||
);
|
.catch(() => {
|
||||||
})
|
toast.error(
|
||||||
.finally(() => {
|
"Error to delete this project",
|
||||||
utils.project.all.invalidate();
|
);
|
||||||
});
|
})
|
||||||
}}
|
.finally(() => {
|
||||||
>
|
utils.project.all.invalidate();
|
||||||
Delete
|
});
|
||||||
</AlertDialogAction>
|
}}
|
||||||
</AlertDialogFooter>
|
>
|
||||||
</AlertDialogContent>
|
Delete
|
||||||
</AlertDialog>
|
</AlertDialogAction>
|
||||||
)}
|
</AlertDialogFooter>
|
||||||
</DropdownMenuContent>
|
</AlertDialogContent>
|
||||||
</DropdownMenu>
|
</AlertDialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="pt-4">
|
||||||
|
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||||
|
<DateTooltip date={project.createdAt}>
|
||||||
|
Created
|
||||||
|
</DateTooltip>
|
||||||
|
<span>
|
||||||
|
{totalServices}{" "}
|
||||||
|
{totalServices === 1 ? "service" : "services"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardFooter>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardFooter className="pt-4">
|
</Link>
|
||||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
|
||||||
<DateTooltip date={project.createdAt}>Created</DateTooltip>
|
|
||||||
<span>
|
|
||||||
{totalServices}{" "}
|
|
||||||
{totalServices === 1 ? "service" : "services"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
73
components/dashboard/settings/profile/generate-token.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ExternalLinkIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export const GenerateToken = () => {
|
||||||
|
const { data, refetch } = api.auth.get.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||||
|
api.auth.generateToken.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl">API/CLI</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Generate a token to access the API/CLI
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 max-sm:flex-wrap items-end">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Swagger API:
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
href="/swagger"
|
||||||
|
target="_blank"
|
||||||
|
className="flex flex-row gap-2 items-center"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium">View</span>
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex flex-row gap-2 max-sm:flex-wrap justify-end items-end">
|
||||||
|
<div className="grid w-full gap-8">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Token</Label>
|
||||||
|
<ToggleVisibilityInput
|
||||||
|
placeholder="Token"
|
||||||
|
value={data?.token || ""}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingToken}
|
||||||
|
onClick={async () => {
|
||||||
|
await generateToken().then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Token generated");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -51,6 +51,9 @@ const randomImages = [
|
|||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.auth.get.useQuery();
|
||||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||||
|
api.auth.generateToken.useMutation();
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: data?.email || "",
|
email: data?.email || "",
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const ShowSettings = () => {
|
|||||||
<ShowCertificates />
|
<ShowCertificates />
|
||||||
<WebDomain />
|
<WebDomain />
|
||||||
<WebServer />
|
<WebServer />
|
||||||
|
|
||||||
<ShowUsers />
|
<ShowUsers />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const addPermissions = z.object({
|
|||||||
canDeleteServices: z.boolean().optional().default(false),
|
canDeleteServices: z.boolean().optional().default(false),
|
||||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||||
canAccessToDocker: z.boolean().optional().default(false),
|
canAccessToDocker: z.boolean().optional().default(false),
|
||||||
|
canAccessToAPI: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddPermissions = z.infer<typeof addPermissions>;
|
type AddPermissions = z.infer<typeof addPermissions>;
|
||||||
@@ -80,6 +81,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canDeleteServices: data.canDeleteServices,
|
canDeleteServices: data.canDeleteServices,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||||
@@ -95,6 +97,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
accesedProjects: data.accesedProjects || [],
|
accesedProjects: data.accesedProjects || [],
|
||||||
accesedServices: data.accesedServices || [],
|
accesedServices: data.accesedServices || [],
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Permissions updated");
|
toast.success("Permissions updated");
|
||||||
@@ -247,6 +250,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="canAccessToAPI"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Access to API/CLI</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allow the user to access to the API/CLI
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accesedProjects"
|
name="accesedProjects"
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
1
drizzle/0015_fearless_callisto.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "auth" ADD COLUMN "token" text;
|
||||||
1
drizzle/0016_chunky_leopardon.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "canAccessToAPI" boolean DEFAULT false NOT NULL;
|
||||||
1
drizzle/0017_minor_post.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "deployment" ADD COLUMN "description" text;
|
||||||
2613
drizzle/meta/0015_snapshot.json
Normal file
2620
drizzle/meta/0016_snapshot.json
Normal file
2626
drizzle/meta/0017_snapshot.json
Normal file
@@ -106,6 +106,27 @@
|
|||||||
"when": 1716715367982,
|
"when": 1716715367982,
|
||||||
"tag": "0014_same_hammerhead",
|
"tag": "0014_same_hammerhead",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1717564517104,
|
||||||
|
"tag": "0015_fearless_callisto",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1719109196484,
|
||||||
|
"tag": "0016_chunky_leopardon",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1719547174326,
|
||||||
|
"tag": "0017_minor_post",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
19
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.2.1",
|
"version": "v0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build-server && npm run build-next",
|
"build": "npm run build-server && npm run build-next",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
|
"migration:drop": "drizzle-kit drop --config ./server/db/drizzle.config.ts",
|
||||||
"db:push": "drizzle-kit --config ./server/db/drizzle.config.ts",
|
"db:push": "drizzle-kit --config ./server/db/drizzle.config.ts",
|
||||||
"db:truncate": "tsx -r dotenv/config ./server/db/reset.ts",
|
"db:truncate": "tsx -r dotenv/config ./server/db/reset.ts",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"lint": "biome lint",
|
"lint": "biome lint",
|
||||||
"db:seed": "tsx -r dotenv/config ./server/db/seed.ts",
|
"db:seed": "tsx -r dotenv/config ./server/db/seed.ts",
|
||||||
"db:clean": "tsx -r dotenv/config ./server/db/reset.ts",
|
"db:clean": "tsx -r dotenv/config ./server/db/reset.ts",
|
||||||
@@ -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,17 +106,18 @@
|
|||||||
"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",
|
||||||
|
"swagger-ui-react": "^5.17.14",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tar-fs": "3.0.5",
|
"tar-fs": "3.0.5",
|
||||||
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
"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",
|
||||||
@@ -129,6 +131,7 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"@types/tar-fs": "2.0.4",
|
"@types/tar-fs": "2.0.4",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
|||||||
29
pages/api/[...trpc].ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createOpenApiNextHandler } from "@dokploy/trpc-openapi";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { createTRPCContext } from "@/server/api/trpc";
|
||||||
|
import { validateBearerToken } from "@/server/auth/token";
|
||||||
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
let { session, user } = await validateBearerToken(req);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
const cookieResult = await validateRequest(req, res);
|
||||||
|
session = cookieResult.session;
|
||||||
|
user = cookieResult.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !session) {
|
||||||
|
res.status(401).json({ message: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return createOpenApiNextHandler({
|
||||||
|
router: appRouter,
|
||||||
|
createContext: createTRPCContext,
|
||||||
|
})(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
@@ -33,6 +33,7 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
|
|
||||||
const sourceType = application.sourceType;
|
const sourceType = application.sourceType;
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ export default async function handler(
|
|||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: application.applicationId as string,
|
applicationId: application.applicationId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
};
|
};
|
||||||
@@ -166,6 +168,37 @@ export const extractCommitMessage = (headers: any, body: any) => {
|
|||||||
return "NEW CHANGES";
|
return "NEW CHANGES";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const extractHash = (headers: any, body: any) => {
|
||||||
|
// GitHub
|
||||||
|
if (headers["x-github-event"]) {
|
||||||
|
return body.head_commit ? body.head_commit.id : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitLab
|
||||||
|
if (headers["x-gitlab-event"]) {
|
||||||
|
return (
|
||||||
|
body.checkout_sha ||
|
||||||
|
(body.commits && body.commits.length > 0
|
||||||
|
? body.commits[0].id
|
||||||
|
: "NEW COMMIT")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitbucket
|
||||||
|
if (headers["x-event-key"]?.includes("repo:push")) {
|
||||||
|
return body.push.changes && body.push.changes.length > 0
|
||||||
|
? body.push.changes[0].new.target.hash
|
||||||
|
: "NEW COMMIT";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gitea
|
||||||
|
if (headers["x-gitea-event"]) {
|
||||||
|
return body.after || "NEW COMMIT";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
export const extractBranchName = (headers: any, body: any) => {
|
export const extractBranchName = (headers: any, body: any) => {
|
||||||
if (headers["x-github-event"] || headers["x-gitea-event"]) {
|
if (headers["x-github-event"] || headers["x-gitea-event"]) {
|
||||||
return body?.ref?.replace("refs/heads/", "");
|
return body?.ref?.replace("refs/heads/", "");
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
|||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { extractBranchName, extractCommitMessage } from "../[refreshToken]";
|
import {
|
||||||
|
extractBranchName,
|
||||||
|
extractCommitMessage,
|
||||||
|
extractHash,
|
||||||
|
} from "../[refreshToken]";
|
||||||
import { updateCompose } from "@/server/api/services/compose";
|
import { updateCompose } from "@/server/api/services/compose";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
@@ -34,7 +38,7 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
const sourceType = composeResult.sourceType;
|
const sourceType = composeResult.sourceType;
|
||||||
|
|
||||||
if (sourceType === "github") {
|
if (sourceType === "github") {
|
||||||
@@ -61,6 +65,7 @@ export default async function handler(
|
|||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { createTRPCContext } from "@/server/api/trpc";
|
|||||||
|
|
||||||
// export API handler
|
// export API handler
|
||||||
export default createNextApiHandler({
|
export default createNextApiHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: createTRPCContext,
|
createContext: createTRPCContext,
|
||||||
onError:
|
onError:
|
||||||
process.env.NODE_ENV === "development"
|
process.env.NODE_ENV === "development"
|
||||||
? ({ path, error }) => {
|
? ({ path, error }) => {
|
||||||
console.error(
|
console.error(
|
||||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
|
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
|
||||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
|
const { data } = api.auth.get.useQuery();
|
||||||
|
const { data: user } = api.user.byAuthId.useQuery(
|
||||||
|
{
|
||||||
|
authId: data?.id || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!data?.id && data?.rol === "user",
|
||||||
|
},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
|
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
80
pages/swagger.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import type { GetServerSidePropsContext, NextPage } from "next";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import "swagger-ui-react/swagger-ui.css";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||||
|
|
||||||
|
const Home: NextPage = () => {
|
||||||
|
const { data } = api.settings.getOpenApiDocument.useQuery();
|
||||||
|
const [spec, setSpec] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Esto solo se ejecutará en el cliente
|
||||||
|
if (data) {
|
||||||
|
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
|
||||||
|
const newSpec = {
|
||||||
|
...data,
|
||||||
|
servers: [{ url: protocolAndHost }],
|
||||||
|
externalDocs: {
|
||||||
|
url: `${protocolAndHost}/settings.getOpenApiDocument`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setSpec(newSpec);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen bg-white">
|
||||||
|
<SwaggerUI spec={spec} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
const { req, res } = context;
|
||||||
|
const { user, session } = await validateRequest(context.req, context.res);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
if (user.rol === "user") {
|
||||||
|
const result = await helpers.user.byAuthId.fetch({
|
||||||
|
authId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canAccessToAPI) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
1690
pnpm-lock.yaml
generated
BIN
public/templates/appsmith.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/templates/baserow.webp
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/templates/directus.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/templates/documenso.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/templates/excalidraw.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/templates/ghost.jpeg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/templates/glitchtip.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
70
public/templates/grafana.svg
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 142.5 145.6" style="enable-background:new 0 0 142.5 145.6;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#565656;}
|
||||||
|
.st1{fill:url(#SVGID_1_);}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M28.7,131.5c-0.3,7.9-6.6,14.1-14.4,14.1C6.1,145.6,0,139,0,130.9s6.6-14.7,14.7-14.7c3.6,0,7.2,1.6,10.2,4.4
|
||||||
|
l-2.3,2.9c-2.3-2-5.1-3.4-7.9-3.4c-5.9,0-10.8,4.8-10.8,10.8c0,6.1,4.6,10.8,10.4,10.8c5.2,0,9.3-3.8,10.2-8.8H12.6v-3.5h16.1
|
||||||
|
V131.5z"/>
|
||||||
|
<path class="st0" d="M42.3,129.5h-2.2c-2.4,0-4.4,2-4.4,4.4v11.4h-3.9v-19.6H35v1.6c1.1-1.1,2.7-1.6,4.6-1.6h4.2L42.3,129.5z"/>
|
||||||
|
<path class="st0" d="M63.7,145.3h-3.4v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||||
|
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4V145.3z M59.7,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||||
|
C57.1,141.2,59.1,139.3,59.7,137z"/>
|
||||||
|
<path class="st0" d="M71.5,124.7v1.1h6.2v3.4h-6.2v16.1h-3.8v-20.5c0-4.3,3.1-6.8,7-6.8h4.7l-1.6,3.7h-3.1
|
||||||
|
C72.9,121.6,71.5,123,71.5,124.7z"/>
|
||||||
|
<path class="st0" d="M98.5,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||||
|
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H98.5z M94.5,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||||
|
C92,141.2,93.9,139.3,94.5,137z"/>
|
||||||
|
<path class="st0" d="M119.4,133.8v11.5h-3.9v-11.6c0-2.4-2-4.4-4.4-4.4c-2.5,0-4.4,2-4.4,4.4v11.6h-3.9v-19.6h3.2v1.7
|
||||||
|
c1.4-1.3,3.3-2,5.2-2C115.8,125.5,119.4,129.2,119.4,133.8z"/>
|
||||||
|
<path class="st0" d="M142.4,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
|
||||||
|
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H142.4z M138.4,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
|
||||||
|
C135.9,141.2,137.8,139.3,138.4,137z"/>
|
||||||
|
</g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="71.25" y1="10.4893" x2="71.25" y2="113.3415" gradientTransform="matrix(1 0 0 -1 0 148.6)">
|
||||||
|
<stop offset="0" style="stop-color:#FCEE1F"/>
|
||||||
|
<stop offset="1" style="stop-color:#F15B2A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path class="st1" d="M122.9,49.9c-0.2-1.9-0.5-4.1-1.1-6.5c-0.6-2.4-1.6-5-2.9-7.8c-1.4-2.7-3.1-5.6-5.4-8.3
|
||||||
|
c-0.9-1.1-1.9-2.1-2.9-3.2c1.6-6.3-1.9-11.8-1.9-11.8c-6.1-0.4-9.9,1.9-11.3,2.9c-0.2-0.1-0.5-0.2-0.7-0.3c-1-0.4-2.1-0.8-3.2-1.2
|
||||||
|
c-1.1-0.3-2.2-0.7-3.3-0.9c-1.1-0.3-2.3-0.5-3.5-0.7c-0.2,0-0.4-0.1-0.6-0.1C83.5,3.6,75.9,0,75.9,0c-8.7,5.6-10.4,13.1-10.4,13.1
|
||||||
|
s0,0.2-0.1,0.4c-0.5,0.1-0.9,0.3-1.4,0.4c-0.6,0.2-1.3,0.4-1.9,0.7c-0.6,0.3-1.3,0.5-1.9,0.8c-1.3,0.6-2.5,1.2-3.8,1.9
|
||||||
|
c-1.2,0.7-2.4,1.4-3.5,2.2c-0.2-0.1-0.3-0.2-0.3-0.2c-11.7-4.5-22.1,0.9-22.1,0.9c-0.9,12.5,4.7,20.3,5.8,21.7
|
||||||
|
c-0.3,0.8-0.5,1.5-0.8,2.3c-0.9,2.8-1.5,5.7-1.9,8.7c-0.1,0.4-0.1,0.9-0.2,1.3c-10.8,5.3-14,16.3-14,16.3c9,10.4,19.6,11,19.6,11
|
||||||
|
l0,0c1.3,2.4,2.9,4.7,4.6,6.8c0.7,0.9,1.5,1.7,2.3,2.6c-3.3,9.4,0.5,17.3,0.5,17.3c10.1,0.4,16.7-4.4,18.1-5.5c1,0.3,2,0.6,3,0.9
|
||||||
|
c3.1,0.8,6.3,1.3,9.4,1.4c0.8,0,1.6,0,2.4,0h0.4H80h0.5H81l0,0c4.7,6.8,13.1,7.7,13.1,7.7c5.9-6.3,6.3-12.4,6.3-13.8l0,0
|
||||||
|
c0,0,0,0,0-0.1s0-0.2,0-0.2l0,0c0-0.1,0-0.2,0-0.3c1.2-0.9,2.4-1.8,3.6-2.8c2.4-2.1,4.4-4.6,6.2-7.2c0.2-0.2,0.3-0.5,0.5-0.7
|
||||||
|
c6.7,0.4,11.4-4.2,11.4-4.2c-1.1-7-5.1-10.4-5.9-11l0,0c0,0,0,0-0.1-0.1l-0.1-0.1l0,0l-0.1-0.1c0-0.4,0.1-0.8,0.1-1.3
|
||||||
|
c0.1-0.8,0.1-1.5,0.1-2.3v-0.6v-0.3v-0.1c0-0.2,0-0.1,0-0.2v-0.5v-0.6c0-0.2,0-0.4,0-0.6s0-0.4-0.1-0.6l-0.1-0.6l-0.1-0.6
|
||||||
|
c-0.1-0.8-0.3-1.5-0.4-2.3c-0.7-3-1.9-5.9-3.4-8.4c-1.6-2.6-3.5-4.8-5.7-6.8c-2.2-1.9-4.6-3.5-7.2-4.6c-2.6-1.2-5.2-1.9-7.9-2.2
|
||||||
|
c-1.3-0.2-2.7-0.2-4-0.2h-0.5h-0.1h-0.2h-0.2h-0.5c-0.2,0-0.4,0-0.5,0c-0.7,0.1-1.4,0.2-2,0.3c-2.7,0.5-5.2,1.5-7.4,2.8
|
||||||
|
c-2.2,1.3-4.1,3-5.7,4.9s-2.8,3.9-3.6,6.1c-0.8,2.1-1.3,4.4-1.4,6.5c0,0.5,0,1.1,0,1.6c0,0.1,0,0.3,0,0.4v0.4c0,0.3,0,0.5,0.1,0.8
|
||||||
|
c0.1,1.1,0.3,2.1,0.6,3.1c0.6,2,1.5,3.8,2.7,5.4s2.5,2.8,4,3.8s3,1.7,4.6,2.2c1.6,0.5,3.1,0.7,4.5,0.6c0.2,0,0.4,0,0.5,0
|
||||||
|
c0.1,0,0.2,0,0.3,0s0.2,0,0.3,0c0.2,0,0.3,0,0.5,0h0.1h0.1c0.1,0,0.2,0,0.3,0c0.2,0,0.4-0.1,0.5-0.1c0.2,0,0.3-0.1,0.5-0.1
|
||||||
|
c0.3-0.1,0.7-0.2,1-0.3c0.6-0.2,1.2-0.5,1.8-0.7c0.6-0.3,1.1-0.6,1.5-0.9c0.1-0.1,0.3-0.2,0.4-0.3c0.5-0.4,0.6-1.1,0.2-1.6
|
||||||
|
c-0.4-0.4-1-0.5-1.5-0.3C88,74,87.9,74,87.7,74.1c-0.4,0.2-0.9,0.4-1.3,0.5c-0.5,0.1-1,0.3-1.5,0.4c-0.3,0-0.5,0.1-0.8,0.1
|
||||||
|
c-0.1,0-0.3,0-0.4,0c-0.1,0-0.3,0-0.4,0s-0.3,0-0.4,0c-0.2,0-0.3,0-0.5,0c0,0-0.1,0,0,0h-0.1h-0.1c-0.1,0-0.1,0-0.2,0
|
||||||
|
s-0.3,0-0.4-0.1c-1.1-0.2-2.3-0.5-3.4-1c-1.1-0.5-2.2-1.2-3.1-2.1c-1-0.9-1.8-1.9-2.5-3.1c-0.7-1.2-1.1-2.5-1.3-3.8
|
||||||
|
c-0.1-0.7-0.2-1.4-0.1-2.1c0-0.2,0-0.4,0-0.6c0,0.1,0,0,0,0v-0.1v-0.1c0-0.1,0-0.2,0-0.3c0-0.4,0.1-0.7,0.2-1.1c0.5-3,2-5.9,4.3-8.1
|
||||||
|
c0.6-0.6,1.2-1.1,1.9-1.5c0.7-0.5,1.4-0.9,2.1-1.2c0.7-0.3,1.5-0.6,2.3-0.8s1.6-0.4,2.4-0.4c0.4,0,0.8-0.1,1.2-0.1
|
||||||
|
c0.1,0,0.2,0,0.3,0h0.3h0.2c0.1,0,0,0,0,0h0.1h0.3c0.9,0.1,1.8,0.2,2.6,0.4c1.7,0.4,3.4,1,5,1.9c3.2,1.8,5.9,4.5,7.5,7.8
|
||||||
|
c0.8,1.6,1.4,3.4,1.7,5.3c0.1,0.5,0.1,0.9,0.2,1.4v0.3V66c0,0.1,0,0.2,0,0.3c0,0.1,0,0.2,0,0.3v0.3v0.3c0,0.2,0,0.6,0,0.8
|
||||||
|
c0,0.5-0.1,1-0.1,1.5c-0.1,0.5-0.1,1-0.2,1.5s-0.2,1-0.3,1.5c-0.2,1-0.6,1.9-0.9,2.9c-0.7,1.9-1.7,3.7-2.9,5.3
|
||||||
|
c-2.4,3.3-5.7,6-9.4,7.7c-1.9,0.8-3.8,1.5-5.8,1.8c-1,0.2-2,0.3-3,0.3H81h-0.2h-0.3H80h-0.3c0.1,0,0,0,0,0h-0.1
|
||||||
|
c-0.5,0-1.1,0-1.6-0.1c-2.2-0.2-4.3-0.6-6.4-1.2c-2.1-0.6-4.1-1.4-6-2.4c-3.8-2-7.2-4.9-9.9-8.2c-1.3-1.7-2.5-3.5-3.5-5.4
|
||||||
|
s-1.7-3.9-2.3-5.9c-0.6-2-0.9-4.1-1-6.2v-0.4v-0.1v-0.1v-0.2V60v-0.1v-0.1v-0.2v-0.5V59l0,0v-0.2c0-0.3,0-0.5,0-0.8
|
||||||
|
c0-1,0.1-2.1,0.3-3.2c0.1-1.1,0.3-2.1,0.5-3.2c0.2-1.1,0.5-2.1,0.8-3.2c0.6-2.1,1.3-4.1,2.2-6c1.8-3.8,4.1-7.2,6.8-9.9
|
||||||
|
c0.7-0.7,1.4-1.3,2.2-1.9c0.3-0.3,1-0.9,1.8-1.4c0.8-0.5,1.6-1,2.5-1.4c0.4-0.2,0.8-0.4,1.3-0.6c0.2-0.1,0.4-0.2,0.7-0.3
|
||||||
|
c0.2-0.1,0.4-0.2,0.7-0.3c0.9-0.4,1.8-0.7,2.7-1c0.2-0.1,0.5-0.1,0.7-0.2c0.2-0.1,0.5-0.1,0.7-0.2c0.5-0.1,0.9-0.2,1.4-0.4
|
||||||
|
c0.2-0.1,0.5-0.1,0.7-0.2c0.2,0,0.5-0.1,0.7-0.1c0.2,0,0.5-0.1,0.7-0.1l0.4-0.1l0.4-0.1c0.2,0,0.5-0.1,0.7-0.1
|
||||||
|
c0.3,0,0.5-0.1,0.8-0.1c0.2,0,0.6-0.1,0.8-0.1c0.2,0,0.3,0,0.5-0.1h0.3h0.2h0.2c0.3,0,0.5,0,0.8-0.1h0.4c0,0,0.1,0,0,0h0.1h0.2
|
||||||
|
c0.2,0,0.5,0,0.7,0c0.9,0,1.8,0,2.7,0c1.8,0.1,3.6,0.3,5.3,0.6c3.4,0.6,6.7,1.7,9.6,3.2c2.9,1.4,5.6,3.2,7.8,5.1
|
||||||
|
c0.1,0.1,0.3,0.2,0.4,0.4c0.1,0.1,0.3,0.2,0.4,0.4c0.3,0.2,0.5,0.5,0.8,0.7c0.3,0.2,0.5,0.5,0.8,0.7c0.2,0.3,0.5,0.5,0.7,0.8
|
||||||
|
c1,1,1.9,2.1,2.7,3.1c1.6,2.1,2.9,4.2,3.9,6.2c0.1,0.1,0.1,0.2,0.2,0.4c0.1,0.1,0.1,0.2,0.2,0.4s0.2,0.5,0.4,0.7
|
||||||
|
c0.1,0.2,0.2,0.5,0.3,0.7c0.1,0.2,0.2,0.5,0.3,0.7c0.4,0.9,0.7,1.8,1,2.7c0.5,1.4,0.8,2.6,1.1,3.6c0.1,0.4,0.5,0.7,0.9,0.7
|
||||||
|
c0.5,0,0.8-0.4,0.8-0.9C123,52.7,123,51.4,122.9,49.9z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/templates/meilisearch.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/templates/metabase.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/templates/minio.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/templates/n8n.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/templates/nocodb.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
public/templates/odoo.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
public/templates/phpmyadmin.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
public/templates/rocketchat.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/templates/uptime-kuma.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
public/templates/wordpress.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
@@ -23,6 +23,7 @@ import { dockerRouter } from "./routers/docker";
|
|||||||
import { composeRouter } from "./routers/compose";
|
import { composeRouter } from "./routers/compose";
|
||||||
import { registryRouter } from "./routers/registry";
|
import { registryRouter } from "./routers/registry";
|
||||||
import { clusterRouter } from "./routers/cluster";
|
import { clusterRouter } from "./routers/cluster";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
*
|
*
|
||||||
@@ -39,6 +40,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
redis: redisRouter,
|
redis: redisRouter,
|
||||||
mongo: mongoRouter,
|
mongo: mongoRouter,
|
||||||
mariadb: mariadbRouter,
|
mariadb: mariadbRouter,
|
||||||
|
compose: composeRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
domain: domainRouter,
|
domain: domainRouter,
|
||||||
destination: destinationRouter,
|
destination: destinationRouter,
|
||||||
@@ -50,7 +52,6 @@ export const appRouter = createTRPCRouter({
|
|||||||
security: securityRouter,
|
security: securityRouter,
|
||||||
redirects: redirectsRouter,
|
redirects: redirectsRouter,
|
||||||
port: portRouter,
|
port: portRouter,
|
||||||
compose: composeRouter,
|
|
||||||
registry: registryRouter,
|
registry: registryRouter,
|
||||||
cluster: clusterRouter,
|
cluster: clusterRouter,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -159,6 +162,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
|
descriptionLog: "",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
};
|
};
|
||||||
@@ -291,6 +295,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: input.applicationId,
|
applicationId: input.applicationId,
|
||||||
titleLog: "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
|
descriptionLog: "",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
updateAuthById,
|
updateAuthById,
|
||||||
verify2FA,
|
verify2FA,
|
||||||
} from "../services/auth";
|
} from "../services/auth";
|
||||||
|
import { luciaToken } from "@/server/auth/token";
|
||||||
|
|
||||||
export const authRouter = createTRPCRouter({
|
export const authRouter = createTRPCRouter({
|
||||||
createAdmin: publicProcedure
|
createAdmin: publicProcedure
|
||||||
@@ -138,6 +139,23 @@ export const authRouter = createTRPCRouter({
|
|||||||
return auth;
|
return auth;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||||
|
const auth = await findAuthById(ctx.user.authId);
|
||||||
|
|
||||||
|
if (auth.token) {
|
||||||
|
await luciaToken.invalidateSession(auth.token);
|
||||||
|
}
|
||||||
|
const session = await luciaToken.createSession(auth?.id || "", {
|
||||||
|
expiresIn: 60 * 60 * 24 * 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateAuthById(auth.id, {
|
||||||
|
token: session.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return auth;
|
||||||
|
}),
|
||||||
|
|
||||||
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
|
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
|
||||||
const auth = await findAuthById(input.id);
|
const auth = await findAuthById(input.id);
|
||||||
return auth;
|
return auth;
|
||||||
@@ -196,4 +214,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
return auth;
|
return auth;
|
||||||
}),
|
}),
|
||||||
|
verifyToken: protectedProcedure.mutation(async () => {
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -133,6 +138,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
titleLog: "Manual deployment",
|
titleLog: "Manual deployment",
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
|
descriptionLog: "",
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -151,6 +157,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
titleLog: "Rebuild deployment",
|
titleLog: "Rebuild deployment",
|
||||||
type: "redeploy",
|
type: "redeploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
|
descriptionLog: "",
|
||||||
};
|
};
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -229,7 +236,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 +248,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") {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
createDomain,
|
createDomain,
|
||||||
findDomainById,
|
findDomainById,
|
||||||
findDomainsByApplicationId,
|
findDomainsByApplicationId,
|
||||||
|
generateDomain,
|
||||||
|
generateWildcard,
|
||||||
removeDomainById,
|
removeDomainById,
|
||||||
updateDomainById,
|
updateDomainById,
|
||||||
} from "../services/domain";
|
} from "../services/domain";
|
||||||
@@ -35,6 +37,16 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await findDomainsByApplicationId(input.applicationId);
|
return await findDomainsByApplicationId(input.applicationId);
|
||||||
}),
|
}),
|
||||||
|
generateDomain: protectedProcedure
|
||||||
|
.input(apiFindDomainByApplication)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return generateDomain(input);
|
||||||
|
}),
|
||||||
|
generateWildcard: protectedProcedure
|
||||||
|
.input(apiFindDomainByApplication)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return generateWildcard(input);
|
||||||
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateDomain)
|
.input(apiUpdateDomain)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import {
|
||||||
|
cliProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiCreateProject,
|
apiCreateProject,
|
||||||
@@ -54,6 +58,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneProject)
|
.input(apiFindOneProject)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import {
|
|||||||
} from "../services/settings";
|
} from "../services/settings";
|
||||||
import { canAccessToTraefikFiles } from "../services/user";
|
import { canAccessToTraefikFiles } from "../services/user";
|
||||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||||
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
|
import { appRouter } from "../root";
|
||||||
|
|
||||||
export const settingsRouter = createTRPCRouter({
|
export const settingsRouter = createTRPCRouter({
|
||||||
reloadServer: adminProcedure.mutation(async () => {
|
reloadServer: adminProcedure.mutation(async () => {
|
||||||
@@ -242,5 +244,50 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return readConfigInPath(input.path);
|
return readConfigInPath(input.path);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getOpenApiDocument: protectedProcedure.query(
|
||||||
|
async ({ ctx }): Promise<unknown> => {
|
||||||
|
const protocol = ctx.req.headers["x-forwarded-proto"];
|
||||||
|
const url = `${protocol}://${ctx.req.headers.host}/api`;
|
||||||
|
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||||
|
title: "tRPC OpenAPI",
|
||||||
|
version: "1.0.0",
|
||||||
|
baseUrl: url,
|
||||||
|
docsUrl: `${url}/settings.getOpenApiDocument`,
|
||||||
|
tags: [
|
||||||
|
"admin",
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"registry",
|
||||||
|
"cluster",
|
||||||
|
"user",
|
||||||
|
"domain",
|
||||||
|
"destination",
|
||||||
|
"backup",
|
||||||
|
"deployment",
|
||||||
|
"mounts",
|
||||||
|
"certificates",
|
||||||
|
"settings",
|
||||||
|
"security",
|
||||||
|
"redirects",
|
||||||
|
"port",
|
||||||
|
"project",
|
||||||
|
"application",
|
||||||
|
"mysql",
|
||||||
|
"postgres",
|
||||||
|
"redis",
|
||||||
|
"mongo",
|
||||||
|
"mariadb",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
openApiDocument.info = {
|
||||||
|
title: "Dokploy API",
|
||||||
|
description: "Endpoints for dokploy",
|
||||||
|
version: getDokployVersion(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return openApiDocument;
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
// apt-get install apache2-utils
|
|
||||||
|
|||||||
@@ -15,11 +15,27 @@ 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";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
import { generateAppName } from "@/server/db/schema/utils";
|
||||||
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,
|
||||||
) => {
|
) => {
|
||||||
|
input.appName =
|
||||||
|
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
|
||||||
|
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)
|
||||||
@@ -114,15 +130,18 @@ export const updateApplicationStatus = async (
|
|||||||
export const deployApplication = async ({
|
export const deployApplication = async ({
|
||||||
applicationId,
|
applicationId,
|
||||||
titleLog = "Manual deployment",
|
titleLog = "Manual deployment",
|
||||||
|
descriptionLog = "",
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
const deployment = await createDeployment({
|
const deployment = await createDeployment({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
title: titleLog,
|
title: titleLog,
|
||||||
|
description: descriptionLog,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -157,14 +176,17 @@ export const deployApplication = async ({
|
|||||||
export const rebuildApplication = async ({
|
export const rebuildApplication = async ({
|
||||||
applicationId,
|
applicationId,
|
||||||
titleLog = "Rebuild deployment",
|
titleLog = "Rebuild deployment",
|
||||||
|
descriptionLog = "",
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
const deployment = await createDeployment({
|
const deployment = await createDeployment({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
title: titleLog,
|
title: titleLog,
|
||||||
|
description: descriptionLog,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,10 +13,25 @@ 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";
|
||||||
|
import { generateAppName } from "@/server/db/schema/utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
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) => {
|
||||||
|
input.appName =
|
||||||
|
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
|
||||||
|
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 +54,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({
|
||||||
@@ -109,15 +134,18 @@ export const updateCompose = async (
|
|||||||
export const deployCompose = async ({
|
export const deployCompose = async ({
|
||||||
composeId,
|
composeId,
|
||||||
titleLog = "Manual deployment",
|
titleLog = "Manual deployment",
|
||||||
|
descriptionLog = "",
|
||||||
}: {
|
}: {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
const deployment = await createDeploymentCompose({
|
const deployment = await createDeploymentCompose({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
title: titleLog,
|
title: titleLog,
|
||||||
|
description: descriptionLog,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -145,14 +173,17 @@ export const deployCompose = async ({
|
|||||||
export const rebuildCompose = async ({
|
export const rebuildCompose = async ({
|
||||||
composeId,
|
composeId,
|
||||||
titleLog = "Rebuild deployment",
|
titleLog = "Rebuild deployment",
|
||||||
|
descriptionLog = "",
|
||||||
}: {
|
}: {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
const deployment = await createDeploymentCompose({
|
const deployment = await createDeploymentCompose({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
title: titleLog,
|
title: titleLog,
|
||||||
|
description: descriptionLog,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const createDeployment = async (
|
|||||||
title: deployment.title || "Deployment",
|
title: deployment.title || "Deployment",
|
||||||
status: "running",
|
status: "running",
|
||||||
logPath: logFilePath,
|
logPath: logFilePath,
|
||||||
|
description: deployment.description || "",
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||||
@@ -100,6 +101,7 @@ export const createDeploymentCompose = async (
|
|||||||
.values({
|
.values({
|
||||||
composeId: deployment.composeId,
|
composeId: deployment.composeId,
|
||||||
title: deployment.title || "Deployment",
|
title: deployment.title || "Deployment",
|
||||||
|
description: deployment.description || "",
|
||||||
status: "running",
|
status: "running",
|
||||||
logPath: logFilePath,
|
logPath: logFilePath,
|
||||||
})
|
})
|
||||||
|
|||||||