Merge branch 'canary' into feat/multi-node-support

This commit is contained in:
Mauricio Siu
2024-05-17 21:43:54 -06:00
62 changed files with 609 additions and 545 deletions

1
.gitignore vendored
View File

@@ -54,3 +54,4 @@ yarn-error.log*
/.main /.main
*.lockb *.lockb
*.rdb

View File

@@ -1,3 +1,5 @@
# License
Copyright 2024 Mauricio Siu. Copyright 2024 Mauricio Siu.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@@ -9,16 +11,13 @@ You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and limitations under the License.
limitations under the License.
Appendix: ## Appendix
In cases of conflict, the provisions in this appendix supersede those in the general Apache License. In the event of a conflict, the provisions in this appendix shall take precedence over those in the Apache License.
- **Modification of Paid Features:** Written consent or a formal agreement is required for modifying any paid features of Dokploy. - **Modification Distribution:** Any modifications to the software must be distributed freely.
- **Prohibition of Unauthorized Resale:** No party is permitted to sell, resell, or otherwise distribute for commercial gain, the software or any of its components, including both original and modified versions, without prior written consent from the copyright holder. - **Future Paid Features:** Any future paid features of Dokploy cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Commercial Distribution:** Any distribution of Dokploy, whether for direct profit or indirect financial benefit, through commercial channels is strictly prohibited without a separate commercial agreement negotiated with the copyright holder.
- **Open Source Distribution of Free Features:** Any modifications to the free features of Dokploy must be distributed freely and must not be included in any paid or commercial package unless they comply with the license and appendix terms specified in this agreement.
For further inquiries or permissions, please contact us directly. For further inquiries or permissions, please contact us directly.

48
README-de.md Normal file
View File

@@ -0,0 +1,48 @@
<div align="center">
<h1 align="center">Dokploy</h1>
</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>
<hr>
Dokploy ist eine kostenlose und self-hostable Platform as a Service (PaaS), welche das hosten und managen von deinen Projekten und Datenbanken vereinfacht, das geschieht mithilfe von Docker und Treafik. Es ist designt, um deine Leistung und die Sicherheit deiner Projekte zu verbessern. Dokploy erlaubt dir schnell und einfach auf jeder VPS deine Projekte zu verwirklichen.
## Erklärung
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
## 🌟 Vorteile
- **Projekte**: - **Projekte**: Hoste jegliche Art von Projekt (Node.js, PHP, Python, Go, Ruby, etc.) mit Einfachheit.
- **Datenbanken**: Erstelle und manage Datenbanken, wie MySQL, PostgreSQL, MongoDB, MariaDB, Redis, und mehr.
- **Docker Management**: Einfach Docker container hosten und managen.
- **Traefik Integration**: Automatische Integration mit Traefik für routing und load balancing
- **Real-time Monitoring**: Monitor von CPU, RAM, Speicher, und network Nutzung.
- **Database Backups**: Automatische Backups mit Support für mehrere Speicher Systeme.
## 🚀 Loslegen
Um anzufangen führe einfach den folgende command in einer VPS aus:
```bash
curl -sSL https://dokploy.com/install.sh | sh
```
Getestete Systems:
- Ubuntu 20.04
- Debian 11
## 📄 Dokumentation
Für eine detaillierte Dokumentation, siehe [docs.dokploy.com/docs](https://docs.dokploy.com)

47
README-ru.md Normal file
View File

@@ -0,0 +1,47 @@
<div align="center">
<h1 align="center">Dokploy</h1>
</div>
<div align="center" style="width:100%;">
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Логотип Dokploy" style="width:60%;">
</div>
<hr>
Dokploy - это бесплатная самоустанавливаемая Платформа как Сервис (PaaS), которая упрощает развертывание и управление приложениями и базами данных с использованием Docker и Traefik. Разработанный для повышения эффективности и безопасности, Dokploy позволяет развертывать ваши приложения на любом VPS.
## Объяснение
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md)
## 🌟 Особенности
- **Приложения**: Легко развертывать любой тип приложения (Node.js, PHP, Python, Go, Ruby и др.).
- **Базы данных**: Создавайте и управляйте базами данных с поддержкой MySQL, PostgreSQL, MongoDB, MariaDB, Redis и других.
- **Управление Docker**: Легко развертывать и управляйте контейнерами Docker.
- **Интеграция с Traefik**: Автоматически интегрируется с Traefik для маршрутизации и балансировки нагрузки.
- **Мониторинг в реальном времени**: Отслеживайте использование CPU, памяти, хранилища и сети.
- **Резервное копирование баз данных**: Автоматизируйте резервное копирование с поддержкой нескольких мест хранения.
## 🚀 Начало работы
Чтобы установить, выполните следующую команду на VPS:
```bash
curl -sSL https://dokploy.com/install.sh | sh
```
Проверенные системы:
- Ubuntu 20.04
- Debian 11
## 📄 Документация
Для подробной документации посетите docs.dokploy.com/docs.

View File

@@ -18,6 +18,10 @@ Dokploy 是一个免费的自托管平台即服务 (PaaS),它使用 Docker 和
[中文](README-zh.md) [中文](README-zh.md)
[Deutsch](README-de.md)
[Русский Язык](README-ru.md)

View File

@@ -16,7 +16,7 @@ Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the
## Explanation ## Explanation
[English](README.md) | [中文](README-zh.md) [English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)

View File

@@ -61,7 +61,7 @@ export const Login2FA = ({ authId }: Props) => {
id: authId, id: authId,
}) })
.then(() => { .then(() => {
toast.success("Signin succesfully", { toast.success("Signin successfully", {
duration: 2000, duration: 2000,
}); });

View File

@@ -26,11 +26,11 @@ interface Props {
applicationId: string; applicationId: string;
} }
const AddRedirectchema = z.object({ const AddRedirectSchema = z.object({
command: z.string(), command: z.string(),
}); });
type AddCommand = z.infer<typeof AddRedirectchema>; type AddCommand = z.infer<typeof AddRedirectSchema>;
export const AddCommand = ({ applicationId }: Props) => { export const AddCommand = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery( const { data } = api.application.one.useQuery(
@@ -48,7 +48,7 @@ export const AddCommand = ({ applicationId }: Props) => {
defaultValues: { defaultValues: {
command: "", command: "",
}, },
resolver: zodResolver(AddRedirectchema), resolver: zodResolver(AddRedirectSchema),
}); });
useEffect(() => { useEffect(() => {

View File

@@ -18,8 +18,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -33,7 +33,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { z } from "zod"; import { z } from "zod";
const AddPortchema = z.object({ const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535), publishedPort: z.number().int().min(1).max(65535),
targetPort: z.number().int().min(1).max(65535), targetPort: z.number().int().min(1).max(65535),
protocol: z.enum(["tcp", "udp"], { protocol: z.enum(["tcp", "udp"], {
@@ -41,7 +41,7 @@ const AddPortchema = z.object({
}), }),
}); });
type AddPort = z.infer<typeof AddPortchema>; type AddPort = z.infer<typeof AddPortSchema>;
interface Props { interface Props {
applicationId: string; applicationId: string;
@@ -62,7 +62,7 @@ export const AddPort = ({
publishedPort: 0, publishedPort: 0,
targetPort: 0, targetPort: 0,
}, },
resolver: zodResolver(AddPortchema), resolver: zodResolver(AddPortSchema),
}); });
useEffect(() => { useEffect(() => {
@@ -100,14 +100,7 @@ export const AddPort = ({
Ports are used to expose your application to the internet. Ports are used to expose your application to the internet.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -18,8 +18,9 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Pencil } from "lucide-react"; import { Pencil } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -106,14 +107,7 @@ export const UpdatePort = ({ portId }: Props) => {
<DialogTitle>Update</DialogTitle> <DialogTitle>Update</DialogTitle>
<DialogDescription>Update the port</DialogDescription> <DialogDescription>Update the port</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -19,8 +19,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -98,14 +98,7 @@ export const AddRedirect = ({
Redirects are used to redirect requests to another url. Redirects are used to redirect requests to another url.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -19,8 +19,9 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Pencil } from "lucide-react"; import { Pencil } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -101,14 +102,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
<DialogTitle>Update</DialogTitle> <DialogTitle>Update</DialogTitle>
<DialogDescription>Update the redirect</DialogDescription> <DialogDescription>Update the redirect</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -18,8 +18,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -90,14 +90,7 @@ export const AddSecurity = ({
Add security to your application Add security to your application
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -18,8 +18,9 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Pencil } from "lucide-react"; import { Pencil } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -96,14 +97,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
<DialogTitle>Update</DialogTitle> <DialogTitle>Update</DialogTitle>
<DialogDescription>Update the security</DialogDescription> <DialogDescription>Update the security</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -18,8 +18,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -116,14 +116,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
<DialogTitle>Update traefik config</DialogTitle> <DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</DialogDescription> <DialogDescription>Update the traefik config</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form
@@ -140,7 +133,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
<FormLabel>Traefik config</FormLabel> <FormLabel>Traefik config</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-[35rem]" className="h-[35rem] font-mono"
placeholder={`http: placeholder={`http:
routers: routers:
router-name: router-name:

View File

@@ -147,7 +147,7 @@ export const AddVolumes = ({
<DialogTitle>Volumes / Mounts</DialogTitle> <DialogTitle>Volumes / Mounts</DialogTitle>
</DialogHeader> </DialogHeader>
{/* {isError && ( {/* {isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950"> <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" /> <AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400"> <span className="text-sm text-red-600 dark:text-red-400">
{error?.message} {error?.message}

View File

@@ -27,8 +27,9 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -106,14 +107,7 @@ export const AddDomain = ({
In this section you can add custom domains In this section you can add custom domains
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -27,8 +27,9 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, PenBoxIcon } from "lucide-react"; import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -125,14 +126,7 @@ export const UpdateDomain = ({ domainId }: Props) => {
In this section you can add custom domains In this section you can add custom domains
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -96,7 +96,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
<FormControl> <FormControl>
<Textarea <Textarea
placeholder="NODE_ENV=production" placeholder="NODE_ENV=production"
className="h-96" className="h-96 font-mono"
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -97,14 +98,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
<DialogTitle>Modify Application</DialogTitle> <DialogTitle>Modify Application</DialogTitle>
<DialogDescription>Update the application data</DialogDescription> <DialogDescription>Update the application data</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -32,14 +32,14 @@ export const ShowContainerConfig = ({ containerId }: Props) => {
View Config View Config
</DropdownMenuItem> </DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}> <DialogContent className={"w-full md:w-[70vw] max-w-max"}>
<DialogHeader> <DialogHeader>
<DialogTitle>Container Config</DialogTitle> <DialogTitle>Container Config</DialogTitle>
<DialogDescription> <DialogDescription>
See in detail the config of this container See in detail the config of this container
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem] bg-card"> <div className="text-wrap rounded-lg border p-4 text-sm bg-card overflow-y-auto max-h-[80vh]">
<code> <code>
<pre className="whitespace-pre-wrap break-words"> <pre className="whitespace-pre-wrap break-words">
{JSON.stringify(data, null, 2)} {JSON.stringify(data, null, 2)}

View File

@@ -24,11 +24,12 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
cols: 80, cols: 80,
rows: 30, rows: 30,
lineHeight: 1.4, lineHeight: 1.4,
fontWeight: 400,
convertEol: true, convertEol: true,
theme: { theme: {
cursor: "transparent", cursor: "transparent",
background: "#19191A", background: "rgba(0, 0, 0, 0)",
}, },
}); });
@@ -81,8 +82,8 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
/> />
</div> </div>
<div className="w-full h-full bg-input rounded-lg p-2 "> <div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
<div id={id} className="rounded-xl" /> <div id={id} />
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,3 @@
import React from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,

View File

@@ -3,7 +3,7 @@ import { Terminal } from "@xterm/xterm";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css";
import { AttachAddon } from "@xterm/addon-attach"; import { AttachAddon } from "@xterm/addon-attach";
import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface Props { interface Props {
id: string; id: string;
@@ -12,7 +12,7 @@ interface Props {
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => { export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
const termRef = useRef(null); const termRef = useRef(null);
const [activeWay, setActiveWay] = React.useState<string | null>("bash"); const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
useEffect(() => { useEffect(() => {
const container = document.getElementById(id); const container = document.getElementById(id);
if (container) { if (container) {
@@ -26,7 +26,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
convertEol: true, convertEol: true,
theme: { theme: {
cursor: "transparent", cursor: "transparent",
background: "#19191A", background: "rgba(0, 0, 0, 0)",
}, },
}); });
const addonFit = new FitAddon(); const addonFit = new FitAddon();
@@ -54,14 +54,15 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
<span> <span>
Select way to connect to <b>{containerId}</b> Select way to connect to <b>{containerId}</b>
</span> </span>
<div className="flex flex-row gap-4 w-fit"> <Tabs value={activeWay} onValueChange={setActiveWay}>
<Button onClick={() => setActiveWay("sh")}>SH</Button> <TabsList>
<Button onClick={() => setActiveWay("bash")}>Bash</Button> <TabsTrigger value="bash">Bash</TabsTrigger>
<Button onClick={() => setActiveWay("sh")}>/bin/sh</Button> <TabsTrigger value="sh">/bin/sh</TabsTrigger>
</div> </TabsList>
</Tabs>
</div> </div>
<div className="w-full h-full bg-input rounded-lg p-2 "> <div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
<div id={id} ref={termRef} className="rounded-xl" /> <div id={id} ref={termRef} />
</div> </div>
</div> </div>
); );

View File

@@ -11,6 +11,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -86,19 +87,12 @@ export const ShowTraefikFile = ({ path }: Props) => {
return ( return (
<div> <div>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 relative" className="grid w-full relative"
> >
<div className="flex flex-col"> <div className="flex flex-col">
<FormField <FormField
@@ -107,10 +101,12 @@ export const ShowTraefikFile = ({ path }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative"> <FormItem className="relative">
<FormLabel>Traefik config</FormLabel> <FormLabel>Traefik config</FormLabel>
<FormDescription>{path}</FormDescription> <FormDescription className="break-all">
{path}
</FormDescription>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-[35rem]" className="h-[35rem] font-mono"
placeholder={`http: placeholder={`http:
routers: routers:
router-name: router-name:
@@ -128,8 +124,9 @@ routers:
<pre> <pre>
<FormMessage /> <FormMessage />
</pre> </pre>
<div className="flex justify-end absolute z-50 right-6 top-10"> <div className="flex justify-end absolute z-50 right-6 top-8">
<Button <Button
className="shadow-sm"
variant="secondary" variant="secondary"
type="button" type="button"
onClick={async () => { onClick={async () => {

View File

@@ -13,7 +13,7 @@ export const ShowTraefikSystem = () => {
const { data: directories } = api.settings.readDirectories.useQuery(); const { data: directories } = api.settings.readDirectories.useQuery();
return ( return (
<div className={cn("mt-6 grid gap-4 pb-20")}> <div className={cn("mt-6 md:grid gap-4")}>
<div className="flex flex-col md:flex-row gap-4 md:gap-10 w-full"> <div className="flex flex-col md:flex-row gap-4 md:gap-10 w-full">
{directories?.length === 0 && ( {directories?.length === 0 && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]"> <div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">

View File

@@ -95,7 +95,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
<FormControl> <FormControl>
<Textarea <Textarea
placeholder="MARIADB_PASSWORD=1234567678" placeholder="MARIADB_PASSWORD=1234567678"
className="h-96" className="h-96 font-mono"
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -97,14 +98,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
<DialogTitle>Modify MariaDB</DialogTitle> <DialogTitle>Modify MariaDB</DialogTitle>
<DialogDescription>Update the MariaDB data</DialogDescription> <DialogDescription>Update the MariaDB data</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -95,7 +95,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
<FormControl> <FormControl>
<Textarea <Textarea
placeholder="MONGO_PASSWORD=1234567678" placeholder="MONGO_PASSWORD=1234567678"
className="h-96" className="h-96 font-mono"
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -97,14 +98,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
<DialogTitle>Modify MongoDB</DialogTitle> <DialogTitle>Modify MongoDB</DialogTitle>
<DialogDescription>Update the MongoDB data</DialogDescription> <DialogDescription>Update the MongoDB data</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -95,7 +95,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
<FormControl> <FormControl>
<Textarea <Textarea
placeholder="MYSQL_PASSWORD=1234567678" placeholder="MYSQL_PASSWORD=1234567678"
className="h-96" className="h-96 font-mono"
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -97,14 +98,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
<DialogTitle>Modify MySQL</DialogTitle> <DialogTitle>Modify MySQL</DialogTitle>
<DialogDescription>Update the MySQL data</DialogDescription> <DialogDescription>Update the MySQL data</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -95,7 +95,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
<FormControl> <FormControl>
<Textarea <Textarea
placeholder="POSTGRES_PASSWORD=1234567678" placeholder="POSTGRES_PASSWORD=1234567678"
className="h-96" className="h-96 font-mono"
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -97,14 +98,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
<DialogTitle>Modify Postgres</DialogTitle> <DialogTitle>Modify Postgres</DialogTitle>
<DialogDescription>Update the Postgres data</DialogDescription> <DialogDescription>Update the Postgres data</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -18,9 +18,10 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
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 { AlertTriangle, Folder } from "lucide-react"; import { Folder } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -93,14 +94,7 @@ export const AddApplication = ({ projectId }: Props) => {
Assign a name and description to your application Assign a name and description to your application
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -235,7 +235,7 @@ export const AddDatabase = ({ projectId }: Props) => {
<DialogTitle>Databases</DialogTitle> <DialogTitle>Databases</DialogTitle>
</DialogHeader> </DialogHeader>
{/* {isError && ( {/* {isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950"> <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" /> <AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400"> <span className="text-sm text-red-600 dark:text-red-400">
{error?.message} {error?.message}

View File

@@ -19,8 +19,9 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -85,14 +86,7 @@ export const AddProject = () => {
<DialogTitle>Add a project</DialogTitle> <DialogTitle>Add a project</DialogTitle>
<DialogDescription>The home of something big!</DialogDescription> <DialogDescription>The home of something big!</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form
id="hook-form-add-project" id="hook-form-add-project"

View File

@@ -48,9 +48,9 @@ export const ShowProjects = () => {
return ( return (
<> <>
{data?.length === 0 && ( {data?.length === 0 && (
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center "> <div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
<FolderInput className="size-10 md:size-28 text-muted" /> <FolderInput className="size-10 md:size-28 text-muted-foreground" />
<span className="text-center font-medium text-muted-foreground"> <span className="text-center font-medium text-muted-foreground">
No projects added yet. Click on Create project. No projects added yet. Click on Create project.
</span> </span>
</div> </div>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -101,14 +102,7 @@ export const UpdateProject = ({ projectId }: Props) => {
Update the details of the project Update the details of the project
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -97,14 +98,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
<DialogTitle>Modify Redis</DialogTitle> <DialogTitle>Modify Redis</DialogTitle>
<DialogDescription>Update the redis data</DialogDescription> <DialogDescription>Update the redis data</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -1,24 +1,25 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -26,140 +27,135 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const certificateDataHolder = `-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`;
const addCertificate = z.object({ const addCertificate = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
certificateData: z.string().min(1, "Certificate data is required"), certificateData: z.string().min(1, "Certificate data is required"),
privateKey: z.string().min(1, "Private key is required"), privateKey: z.string().min(1, "Private key is required"),
autoRenew: z.boolean().optional(), autoRenew: z.boolean().optional(),
}); });
type AddCertificate = z.infer<typeof addCertificate>; type AddCertificate = z.infer<typeof addCertificate>;
export const AddCertificate = () => { export const AddCertificate = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync, isError, error, isLoading } = const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation(); api.certificates.create.useMutation();
const form = useForm<AddCertificate>({ const form = useForm<AddCertificate>({
defaultValues: { defaultValues: {
name: "", name: "",
certificateData: "", certificateData: "",
privateKey: "", privateKey: "",
autoRenew: false, autoRenew: false,
}, },
resolver: zodResolver(addCertificate), resolver: zodResolver(addCertificate),
}); });
useEffect(() => { useEffect(() => {
form.reset(); form.reset();
}, [form, form.formState.isSubmitSuccessful, form.reset]); }, [form, form.formState.isSubmitSuccessful, form.reset]);
const onSubmit = async (data: AddCertificate) => { const onSubmit = async (data: AddCertificate) => {
await mutateAsync({ await mutateAsync({
name: data.name, name: data.name,
certificateData: data.certificateData, certificateData: data.certificateData,
privateKey: data.privateKey, privateKey: data.privateKey,
autoRenew: data.autoRenew, autoRenew: data.autoRenew,
}) })
.then(async () => { .then(async () => {
toast.success("Certificate Created"); toast.success("Certificate Created");
await utils.certificates.all.invalidate(); await utils.certificates.all.invalidate();
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the Certificate"); toast.error("Error to create the Certificate");
}); });
}; };
return ( return (
<Dialog> <Dialog>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
<Button>Add Certificate</Button> <Button>Add Certificate</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl"> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Add Certificate</DialogTitle> <DialogTitle>Add Certificate</DialogTitle>
<DialogDescription>Add a new certificate</DialogDescription> <DialogDescription>Add a new certificate</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form
id="hook-form-add-certificate" id="hook-form-add-certificate"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 " className="grid w-full gap-4 "
> >
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<FormLabel>Certificate Name</FormLabel> <FormLabel>Certificate Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder={"My Certificate"} {...field} /> <Input placeholder={"My Certificate"} {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
); );
}} }}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="certificateData" name="certificateData"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Certificate Data</FormLabel> <FormLabel>Certificate Data</FormLabel>
</div> </div>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-32" className="h-32"
placeholder={`-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`} placeholder={certificateDataHolder}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="privateKey" name="privateKey"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Private Key</FormLabel> <FormLabel>Private Key</FormLabel>
</div> </div>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-32" className="h-32"
placeholder={`-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`} placeholder={certificateDataHolder}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</form> </form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3"> <DialogFooter className="flex w-full flex-row !justify-between pt-3">
<Button <Button
isLoading={isLoading} isLoading={isLoading}
form="hook-form-add-certificate" form="hook-form-add-certificate"
type="submit" type="submit"
> >
Create Create
</Button> </Button>
</DialogFooter> </DialogFooter>
</Form> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };

View File

@@ -18,8 +18,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -87,14 +87,7 @@ export const AddDestination = () => {
In this section you can add destinations for your backups. In this section you can add destinations for your backups.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -18,8 +18,9 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, PenBoxIcon } from "lucide-react"; import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -111,14 +112,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
Update the current destination config Update the current destination config
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -21,8 +21,9 @@ import {
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { extractServices } from "@/pages/dashboard/project/[projectId]"; import { extractServices } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, ListTodo } from "lucide-react"; import { ListTodo } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -115,14 +116,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
<DialogTitle>Permissions</DialogTitle> <DialogTitle>Permissions</DialogTitle>
<DialogDescription>Add or remove permissions</DialogDescription> <DialogDescription>Add or remove permissions</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -19,8 +19,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -73,14 +73,7 @@ export const AddUser = () => {
<DialogTitle>Add User</DialogTitle> <DialogTitle>Add User</DialogTitle>
<DialogDescription>Invite a new user</DialogDescription> <DialogDescription>Invite a new user</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -47,7 +47,7 @@ export const ShowUsers = () => {
key={user.userId} key={user.userId}
className="flex gap-2 flex-col justify-start border p-4 rounded-lg" className="flex gap-2 flex-col justify-start border p-4 rounded-lg"
> >
<span className="text-sm text-muted-foreground"> <span className="text-sm text-foreground">
{user.auth.email} {user.auth.email}
</span> </span>
{!user.isRegistered && ( {!user.isRegistered && (

View File

@@ -18,11 +18,12 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
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 } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { AlertTriangle, SquarePen } from "lucide-react"; import { SquarePen } from "lucide-react";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
const updateUserSchema = z.object({ const updateUserSchema = z.object({
@@ -100,14 +101,7 @@ export const UpdateUser = ({ authId }: Props) => {
<DialogTitle>Update User</DialogTitle> <DialogTitle>Update User</DialogTitle>
<DialogDescription>Update the user</DialogDescription> <DialogDescription>Update the user</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error.message}
</span>
</div>
)}
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid items-center gap-4"> <div className="grid items-center gap-4">

View File

@@ -18,8 +18,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } 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";
@@ -89,14 +89,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
<DialogTitle>Update traefik config</DialogTitle> <DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</DialogDescription> <DialogDescription>Update the traefik config</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form
@@ -113,7 +106,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
<FormLabel>Traefik config</FormLabel> <FormLabel>Traefik config</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-[35rem]" className="h-[35rem] font-mono"
placeholder={`providers: placeholder={`providers:
docker: docker:
defaultRule: 'Host('dokploy.com')' defaultRule: 'Host('dokploy.com')'
@@ -136,8 +129,10 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
<pre> <pre>
<FormMessage /> <FormMessage />
</pre> </pre>
<div className="flex justify-end absolute z-50 right-6 top-10"> <div className="flex justify-end absolute z-50 right-6 top-0">
<Button <Button
className="shadow-sm"
variant="secondary"
type="button" type="button"
onClick={async () => { onClick={async () => {
setCanEdit(!canEdit); setCanEdit(!canEdit);

View File

@@ -18,8 +18,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } 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";
@@ -92,14 +92,7 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
<DialogTitle>Update Middleware config</DialogTitle> <DialogTitle>Update Middleware config</DialogTitle>
<DialogDescription>Update the middleware config</DialogDescription> <DialogDescription>Update the middleware config</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form
@@ -116,7 +109,7 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
<FormLabel>Traefik config</FormLabel> <FormLabel>Traefik config</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-[35rem]" className="h-[35rem] font-mono"
placeholder={`http: placeholder={`http:
routers: routers:
router-name: router-name:
@@ -134,8 +127,10 @@ routers:
<pre> <pre>
<FormMessage /> <FormMessage />
</pre> </pre>
<div className="flex justify-end absolute z-50 right-6 top-10"> <div className="flex justify-end absolute z-50 right-6 top-0">
<Button <Button
className="shadow-sm"
variant="secondary"
type="button" type="button"
onClick={async () => { onClick={async () => {
setCanEdit(!canEdit); setCanEdit(!canEdit);

View File

@@ -18,8 +18,8 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } 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";
@@ -92,14 +92,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
<DialogTitle>Update traefik config</DialogTitle> <DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</DialogDescription> <DialogDescription>Update the traefik config</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && ( {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<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">
{error?.message}
</span>
</div>
)}
<Form {...form}> <Form {...form}>
<form <form
@@ -116,7 +109,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
<FormLabel>Traefik config</FormLabel> <FormLabel>Traefik config</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
className="h-[35rem]" className="h-[35rem] font-mono"
placeholder={`http: placeholder={`http:
routers: routers:
router-name: router-name:
@@ -134,8 +127,10 @@ routers:
<pre> <pre>
<FormMessage /> <FormMessage />
</pre> </pre>
<div className="flex justify-end absolute z-50 right-6 top-10"> <div className="flex justify-end absolute z-50 right-6 top-0">
<Button <Button
className="shadow-sm"
variant="secondary"
type="button" type="button"
onClick={async () => { onClick={async () => {
setCanEdit(!canEdit); setCanEdit(!canEdit);

View File

@@ -10,19 +10,15 @@ export const DashboardLayout = ({ children, tab }: Props) => {
return ( return (
<div> <div>
<div <div
className="bg-radial relative flex flex-col bg-background pt-6" className="bg-radial relative flex flex-col bg-background min-h-screen w-full"
id="app-container" id="app-container"
> >
<div className="flex items-center justify-center"> <Navbar />
<div className="w-full"> <main className="pt-6 flex w-full flex-col items-center">
<Navbar /> <div className="w-full max-w-8xl px-4 lg:px-8">
<main className="mt-6 flex w-full flex-col items-center"> <NavigationTabs tab={tab}>{children}</NavigationTabs>
<div className="w-full max-w-8xl px-4 lg:px-8">
<NavigationTabs tab={tab}>{children}</NavigationTabs>
</div>
</main>
</div> </div>
</div> </main>
</div> </div>
</div> </div>
); );

View File

@@ -29,7 +29,7 @@ export const Navbar = () => {
const { mutateAsync } = api.auth.logout.useMutation(); const { mutateAsync } = api.auth.logout.useMutation();
return ( return (
<nav className="border-divider sticky inset-x-0 top-0 z-40 flex h-auto w-full items-center justify-center border-b bg-background/70 backdrop-blur-lg backdrop-saturate-150 data-[menu-open=true]:border-none data-[menu-open=true]:backdrop-blur-xl"> <nav className="border-divider sticky inset-x-0 top-0 z-40 flex h-auto w-full items-center justify-center border-b bg-background/70 backdrop-blur-lg backdrop-saturate-150 data-[menu-open=true]:border-none data-[menu-open=true]:backdrop-blur-xl">
<header className="relative z-40 flex h-[var(--navbar-height)] w-full max-w-8xl flex-row flex-nowrap items-center justify-between gap-4 px-4 sm:px-6"> <header className="relative z-40 flex w-full max-w-8xl flex-row flex-nowrap items-center justify-between gap-4 px-4 sm:px-6 h-16">
<div className="text-medium box-border flex flex-grow basis-0 flex-row flex-nowrap items-center justify-start whitespace-nowrap bg-transparent no-underline"> <div className="text-medium box-border flex flex-grow basis-0 flex-row flex-nowrap items-center justify-start whitespace-nowrap bg-transparent no-underline">
<Link <Link
href="/dashboard/projects" href="/dashboard/projects"

View File

@@ -1,8 +1,18 @@
import { AddProject } from "@/components/dashboard/projects/add"; import { AddProject } from "@/components/dashboard/projects/add";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { Auth } from "@/server/api/services/auth";
import type { User } from "@/server/api/services/user";
interface TabInfo {
label: string;
tabLabel?: string;
description: string;
index: string;
isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean;
}
export type TabState = export type TabState =
| "projects" | "projects"
@@ -11,6 +21,41 @@ export type TabState =
| "traefik" | "traefik"
| "docker"; | "docker";
const tabMap: Record<TabState, TabInfo> = {
projects: {
label: "Projects",
description: "Manage your projects",
index: "/dashboard/projects",
},
monitoring: {
label: "Monitoring",
description: "Monitor your projects",
index: "/dashboard/monitoring",
},
traefik: {
label: "Traefik",
tabLabel: "Traefik File System",
description: "Manage your traefik",
index: "/dashboard/traefik",
isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToTraefikFiles);
},
},
docker: {
label: "Docker",
description: "Manage your docker",
index: "/dashboard/docker",
isShow: ({ rol, user }) => {
return Boolean(rol === "admin" || user?.canAccessToDocker);
},
},
settings: {
label: "Settings",
description: "Manage your settings",
index: "/dashboard/settings/server",
},
};
interface Props { interface Props {
tab: TabState; tab: TabState;
children: React.ReactNode; children: React.ReactNode;
@@ -34,23 +79,19 @@ export const NavigationTabs = ({ tab, children }: Props) => {
setActiveTab(tab); setActiveTab(tab);
}, [tab]); }, [tab]);
const activeTabInfo = useMemo(() => {
return tabMap[activeTab];
}, [activeTab]);
return ( return (
<div className="gap-12 min-h-screen"> <div className="gap-12">
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap"> <header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-xl font-bold lg:text-3xl"> <h1 className="text-xl font-bold lg:text-3xl">
{tab === "projects" && "Projects"} {activeTabInfo.label}
{tab === "monitoring" && "Monitoring"}
{tab === "settings" && "Settings"}
{tab === "traefik" && "Traefik"}
{tab === "docker" && "Docker"}
</h1> </h1>
<p className="lg:text-medium text-muted-foreground"> <p className="lg:text-medium text-muted-foreground">
{tab === "projects" && "Manage your deployments"} {activeTabInfo.description}
{tab === "monitoring" && "Watch the usage of your server"}
{tab === "settings" && "Check the configuration"}
{tab === "traefik" && "Read the traefik config and update it"}
{tab === "docker" && "Manage the docker containers"}
</p> </p>
</div> </div>
{tab === "projects" && {tab === "projects" &&
@@ -60,54 +101,36 @@ export const NavigationTabs = ({ tab, children }: Props) => {
<Tabs <Tabs
value={activeTab} value={activeTab}
className="w-full" className="w-full"
onValueChange={(e) => { onValueChange={async (e) => {
if (e === "settings") {
router.push("/dashboard/settings/server");
} else {
router.push(`/dashboard/${e}`);
}
setActiveTab(e as TabState); setActiveTab(e as TabState);
router.push(tabMap[e as TabState].index);
}} }}
> >
{/* className="grid w-fit grid-cols-4 bg-transparent" */} {/* className="grid w-fit grid-cols-4 bg-transparent" */}
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto"> <div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto border-b border-b-divider pb-1">
<TabsList className="md:grid md:w-fit md:grid-cols-5 justify-start bg-transparent"> <TabsList className="bg-transparent relative px-0">
<TabsTrigger {Object.keys(tabMap).map((key) => {
value="projects" const tab = tabMap[key as TabState];
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border" if (tab.isShow && !tab.isShow?.({ rol: data?.rol, user })) {
> return null;
Projects }
</TabsTrigger> return (
<TabsTrigger <TabsTrigger
value="monitoring" key={key}
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border" value={key}
> className="relative py-2.5 md:px-5 data-[state=active]:shadow-none data-[state=active]:bg-transparent rounded-md hover:bg-zinc-100 hover:dark:bg-zinc-800 data-[state=active]:hover:bg-zinc-100 data-[state=active]:hover:dark:bg-zinc-800"
Monitoring >
</TabsTrigger> <span className="relative z-[1] w-full">
{tab.tabLabel || tab.label}
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && ( </span>
<TabsTrigger {key === activeTab && (
value="traefik" <div className="absolute -bottom-[5.5px] w-full">
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border" <div className="h-0.5 bg-foreground rounded-t-md" />
> </div>
Traefik File System )}
</TabsTrigger> </TabsTrigger>
)} );
{(data?.rol === "admin" || user?.canAccessToDocker) && ( })}
<TabsTrigger
value="docker"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Docker
</TabsTrigger>
)}
<TabsTrigger
value="settings"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Settings
</TabsTrigger>
</TabsList> </TabsList>
</div> </div>

View File

@@ -0,0 +1,51 @@
interface Props extends React.ComponentPropsWithoutRef<"div"> {
icon?: React.ReactNode;
type?: "info" | "success" | "warning" | "error";
}
import { cn } from "@/lib/utils";
import { AlertTriangle, AlertCircle, CheckCircle2, Info } from "lucide-react";
const iconMap = {
info: {
className: "bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400",
icon: Info,
},
success: {
className:
"bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400",
icon: CheckCircle2,
},
warning: {
className:
"bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400",
icon: AlertCircle,
},
error: {
className: "bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400",
icon: AlertTriangle,
},
};
export function AlertBlock({
type = "info",
icon,
children,
className,
...props
}: Props) {
const { className: iconClassName, icon: Icon } = iconMap[type];
return (
<div
{...props}
className={cn(
"flex items-center flex-row gap-4 rounded-lg p-2",
iconClassName,
className,
)}
>
{icon || <Icon className="text-current" />}
<span className="text-sm text-current">{children}</span>
</div>
);
}

View File

@@ -17,21 +17,21 @@ export const StatusTooltip = ({ status, className }: Props) => {
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
{status === "idle" && ( {status === "idle" && (
<div className={cn(" size-3.5 rounded-full bg-card", className)} /> <div className={cn("size-3.5 rounded-full bg-muted-foreground dark:bg-card", className)} />
)} )}
{status === "error" && ( {status === "error" && (
<div <div
className={cn(" size-3.5 rounded-full bg-destructive", className)} className={cn("size-3.5 rounded-full bg-destructive", className)}
/> />
)} )}
{status === "done" && ( {status === "done" && (
<div <div
className={cn(" size-3.5 rounded-full bg-primary", className)} className={cn("size-3.5 rounded-full bg-primary", className)}
/> />
)} )}
{status === "running" && ( {status === "running" && (
<div <div
className={cn(" size-3.5 rounded-full bg-yellow-500", className)} className={cn("size-3.5 rounded-full bg-yellow-500", className)}
/> />
)} )}
</TooltipTrigger> </TooltipTrigger>

59
components/ui/alert.tsx Normal file
View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@@ -165,7 +165,7 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
aria-hidden="true" aria-hidden="true"
/> />
)} )}
<span className="text-sm truncate">{item.name}</span> <span className="text-sm truncate font-mono">{item.name}</span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="pl-6"> <AccordionContent className="pl-6">
{item.children.length === 0 && ( {item.children.length === 0 && (
@@ -244,7 +244,7 @@ const Leaf = React.forwardRef<
aria-hidden="true" aria-hidden="true"
/> />
)} )}
<p className=" text-sm whitespace-normal">{item.name}</p> <p className=" text-sm whitespace-normal font-mono">{item.name}</p>
</div> </div>
); );
}); });

View File

@@ -105,7 +105,6 @@
"zod": "^3.23.4" "zod": "^3.23.4"
}, },
"devDependencies": { "devDependencies": {
"xterm-readline": "1.1.1",
"@biomejs/biome": "1.7.1", "@biomejs/biome": "1.7.1",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23", "@types/dockerode": "3.3.23",
@@ -127,7 +126,8 @@
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typescript": "^5.4.2" "typescript": "^5.4.2",
"xterm-readline": "1.1.1"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.25.2" "initVersion": "7.25.2"

View File

@@ -1,44 +1,53 @@
import { Toaster } from "@/components/ui/sonner";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head"; import Head from "next/head";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
const inter = Inter({ subsets: ["latin"] });
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & { export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode; getLayout?: (page: ReactElement) => ReactNode;
// session: Session | null; // session: Session | null;
theme?: string; theme?: string;
}; };
type AppPropsWithLayout = AppProps & { type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout; Component: NextPageWithLayout;
}; };
const MyApp = ({ const MyApp = ({
Component, Component,
pageProps: { ...pageProps }, pageProps: { ...pageProps },
}: AppPropsWithLayout) => { }: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page); const getLayout = Component.getLayout ?? ((page) => page);
return ( return (
<> <>
<Head> <style jsx global>{`
<title>Dokploy</title> :root {
</Head> --font-inter: ${inter.style.fontFamily};
<ThemeProvider }
attribute="class" `}</style>
defaultTheme="system" <Head>
enableSystem <title>Dokploy</title>
disableTransitionOnChange </Head>
forcedTheme={Component.theme} <ThemeProvider
> attribute="class"
<Toaster richColors /> defaultTheme="system"
{getLayout(<Component {...pageProps} />)} enableSystem
</ThemeProvider> disableTransitionOnChange
</> forcedTheme={Component.theme}
); >
<Toaster richColors />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</>
);
}; };
export default api.withTRPC(MyApp); export default api.withTRPC(MyApp);

View File

@@ -1,18 +1,11 @@
import { Head, Html, Main, NextScript } from "next/document"; import { Head, Html, Main, NextScript } from "next/document";
export default function Document() { export default function Document() {
return ( return (
<Html lang="en"> <Html lang="en" className="font-sans">
<Head> <Head />
<link rel="shortcut icon" href="/icon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;400;500;600;700&display=swap"
rel="stylesheet"
/>
</Head>
<body className="flex h-full flex-col "> <body className="flex h-full flex-col font-sans">
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>

View File

@@ -2,11 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
*,
html {
font-family: "Inter", sans-serif;
}
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;

View File

@@ -1,4 +1,5 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import defaultTheme from "tailwindcss/defaultTheme";
const config = { const config = {
darkMode: ["class"], darkMode: ["class"],
@@ -18,6 +19,9 @@ const config = {
}, },
}, },
extend: { extend: {
fontFamily: {
sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
},
maxWidth: { maxWidth: {
"2xl": "40rem", "2xl": "40rem",
"8xl": "85rem", "8xl": "85rem",