mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
290228116a | ||
|
|
7cb299a4bb | ||
|
|
56a94ad14a | ||
|
|
0c194585ae | ||
|
|
9a94cffe00 | ||
|
|
adce4a3ff9 | ||
|
|
642deac709 | ||
|
|
6b2be464f5 | ||
|
|
12b56252dc | ||
|
|
270bdc9da8 | ||
|
|
92afac8044 | ||
|
|
7e29e9e0d6 | ||
|
|
654fe75c07 | ||
|
|
a673f07430 | ||
|
|
d539b80ef7 | ||
|
|
b4c07ce6d1 | ||
|
|
e0f1691731 | ||
|
|
9d02fd5207 | ||
|
|
5f52345b2e | ||
|
|
fe788d7209 | ||
|
|
e583089897 | ||
|
|
d6685e1f4a | ||
|
|
a109b39616 | ||
|
|
30cab150c9 | ||
|
|
61d7013a59 | ||
|
|
1279722704 | ||
|
|
e0b96378e4 | ||
|
|
428156579d | ||
|
|
26e1d65c36 | ||
|
|
c7e3b10eb3 | ||
|
|
ddc3274f38 | ||
|
|
45de9a879d | ||
|
|
ff1cb7d6ae | ||
|
|
1ac69fb81c | ||
|
|
c1f48578f0 | ||
|
|
704d682e3d | ||
|
|
0d41e7d0ef | ||
|
|
e8d50f3c29 | ||
|
|
6989047cbe | ||
|
|
9dffef71e5 | ||
|
|
f30474d445 | ||
|
|
71f9e7bbd1 | ||
|
|
5682cc12bc | ||
|
|
8188345faf | ||
|
|
156cac288b | ||
|
|
6f05047d6c | ||
|
|
18a6e6ac8c | ||
|
|
0a764fbc55 | ||
|
|
824613cce9 | ||
|
|
17da90238f | ||
|
|
907dc0784f | ||
|
|
b6a088ac6b | ||
|
|
bd47b6f50a | ||
|
|
eb05568798 | ||
|
|
9554818552 | ||
|
|
a83834fdef | ||
|
|
312d66f0fa | ||
|
|
9ba09debf8 | ||
|
|
3193d5eabd | ||
|
|
d1966d43f7 | ||
|
|
2170c3f1e8 | ||
|
|
2541832c19 | ||
|
|
237a01d7ae | ||
|
|
505fc3efff | ||
|
|
b167e592d3 | ||
|
|
2755bf3a31 | ||
|
|
0055a8322f | ||
|
|
3e8e43508c | ||
|
|
3990c39471 | ||
|
|
0d3d700e4c | ||
|
|
e67ea869d5 | ||
|
|
1b6d4273cc | ||
|
|
2e62c7fd28 | ||
|
|
c5f7f8bad6 | ||
|
|
ca54084793 | ||
|
|
c6581f73ff | ||
|
|
c8150f4f32 | ||
|
|
59bd46908f | ||
|
|
eebe071a43 | ||
|
|
47146dfedf | ||
|
|
701580afc4 |
24
.github/workflows/pull-request.yml
vendored
24
.github/workflows/pull-request.yml
vendored
@@ -31,28 +31,6 @@ jobs:
|
||||
- name: Run Build
|
||||
run: pnpm build
|
||||
|
||||
|
||||
build-docker-on-pr:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: build-app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Prepare .env file
|
||||
run: |
|
||||
cp .env.production.example .env.production
|
||||
|
||||
- name: Run custom Docker build script
|
||||
run: |
|
||||
chmod +x ./docker/build.sh
|
||||
echo "Building Docker image for ${{ github.base_ref }}"
|
||||
./docker/build.sh ${{ github.base_ref == 'canary' && 'canary' || '' }}
|
||||
|
||||
build-and-push-docker-on-push:
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -75,7 +53,5 @@ jobs:
|
||||
|
||||
- name: Build and push Docker image using custom script
|
||||
run: |
|
||||
chmod +x ./docker/build.sh
|
||||
chmod +x ./docker/push.sh
|
||||
./docker/build.sh ${{ github.ref_name == 'canary' && 'canary' || '' }}
|
||||
./docker/push.sh ${{ github.ref_name == 'canary' && 'canary' || '' }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -53,4 +53,5 @@ yarn-error.log*
|
||||
/.data
|
||||
/.main
|
||||
|
||||
*.lockb
|
||||
*.lockb
|
||||
*.rdb
|
||||
|
||||
@@ -107,7 +107,7 @@ pnpm run docker:push
|
||||
In the case you lost your password, you can reset it using the following command
|
||||
|
||||
```bash
|
||||
pnpm run build-server
|
||||
pnpm run reset-password
|
||||
```
|
||||
|
||||
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
|
||||
@@ -150,4 +150,4 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.
|
||||
- If your pull request fixes an open issue, please reference the issue in the pull request description.
|
||||
- Once your pull request is merged, you will be automatically added as a contributor to the project.
|
||||
|
||||
Thank you for your contribution!
|
||||
Thank you for your contribution!
|
||||
|
||||
@@ -26,7 +26,7 @@ FROM node:18-slim AS production
|
||||
# Install dependencies only for production
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN corepack enable && apt-get update && apt-get install -y curl && apt-get install -y apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -47,7 +47,6 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-l
|
||||
# Install docker
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
||||
|
||||
|
||||
# Install Nixpacks and tsx
|
||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
|
||||
33
LICENSE.MD
33
LICENSE.MD
@@ -1,4 +1,8 @@
|
||||
Copyright 2024-2024 Mauricio Siu.
|
||||
# License
|
||||
|
||||
## Core License (Apache License 2.0)
|
||||
|
||||
Copyright 2024 Mauricio Siu.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -9,27 +13,14 @@ You may obtain a copy of the License at
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
Appendix:
|
||||
In case of a conflict, the terms of this appendix supersede the general apache license.
|
||||
## Additional Terms for Specific Features
|
||||
|
||||
- Unless provided with a written agreement or permission, the paid features of Dokploy (as a service) cannot be modified.
|
||||
- Unless provided with a written agreement or permission, no persons are permitted to redistribute another paid version of Dokploy.
|
||||
- Unless provided with a written agreement or permission, no persons are permitted to create the same paid version of Dokploy.
|
||||
- Furthermore, any modifications of free features of Dokploy should be distributed as free & opensource software.
|
||||
|
||||
Appendix B:
|
||||
|
||||
- **Prohibition of Resale Without Permission:** Notwithstanding any provisions in the main body of the Apache License, Version 2.0, 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, through any form of commercial distribution channels, including but not limited to online marketplaces, software as a service (SaaS) platforms, or physical media distribution, without prior written consent from the copyright holder.
|
||||
|
||||
- **Commercial Distribution:** Any form of 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. This includes but is not limited to, offerings on software marketplaces or through third-party distributors.
|
||||
|
||||
- **Modification of Paid Features:** The paid features of Dokploy (as a service) may not be modified, integrated into other software, or redistributed in any form without explicit written permission from 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 without complying with the open-source license terms stipulated in this agreement.
|
||||
|
||||
If you have any questions, please feel free to reach out to us.
|
||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
||||
|
||||
For further inquiries or permissions, please contact us directly.
|
||||
|
||||
50
README-de.md
Normal file
50
README-de.md
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
|
||||
<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
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
|
||||
## 📄 Dokumentation
|
||||
|
||||
Für eine detaillierte Dokumentation, siehe [docs.dokploy.com/docs](https://docs.dokploy.com)
|
||||
|
||||
49
README-ru.md
Normal file
49
README-ru.md
Normal file
@@ -0,0 +1,49 @@
|
||||
<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
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
|
||||
## 📄 Документация
|
||||
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||
53
README-zh.md
Normal file
53
README-zh.md
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
|
||||
<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 是一个免费的自托管平台即服务 (PaaS),它使用 Docker 和 Traefik 简化了应用程序和数据库的部署和管理。 Dokploy 旨在提高效率和安全性,允许您在任何 VPS 上部署应用程序。
|
||||
|
||||
## 语言
|
||||
[English](README.md)
|
||||
|
||||
[中文](README-zh.md)
|
||||
|
||||
[Deutsch](README-de.md)
|
||||
|
||||
[Русский Язык](README-ru.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
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
|
||||
## 📄 文档
|
||||
|
||||
如需查看详细的文档资料 请访问[docs.dokploy.com/docs](https://docs.dokploy.com)
|
||||
@@ -15,6 +15,12 @@ Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the
|
||||
|
||||
|
||||
|
||||
## Explanation
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
||||
|
||||
|
||||
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease.
|
||||
@@ -38,6 +44,8 @@ Tested Systems:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
# Terms & Conditions
|
||||
|
||||
**Dokploy core** is a free and open-source solution intended as an alternative to established cloud platforms like Vercel and Netlify.
|
||||
|
||||
Dokploy core is a free and open-source program alternative to Vercel, Netlify, and other cloud services.
|
||||
The Dokploy team endeavors to mitigate potential defects and issues through stringent testing and adherence to principles of clean coding. Dokploy is provided "AS IS" without any warranties, express or implied. Refer to the [License](https://github.com/Dokploy/Dokploy/blob/main/LICENSE) for details on permissions and restrictions.
|
||||
|
||||
Developers of Dokploy do their best to prevent bugs and issues through rigorous testing processes and clean code principles. Dokploy is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and limitations under the [License](https://github.com/Dokploy/Dokploy/blob/main/LICENSE).
|
||||
|
||||
By using Dokploy you agree to Terms and Conditions, and the license of Dokploy.
|
||||
|
||||
### Description of Service:
|
||||
|
||||
Dokploy core is an open-source program designed to streamline application deployment processes for personal and commercial use. Users are free to install, modify, and run Dokploy on their own machines or within their organizations to enhance their development and deployment workflows. While Dokploy encourages a wide range of uses to foster innovation and efficiency, it is crucial to note that selling Dokploy itself as a service or repackaging it as part of a commercial offering without explicit permission is strictly prohibited. This ensures that the open-source nature of Dokploy remains intact and benefits the community as a whole.
|
||||
**Dokploy core** is an open-source tool designed to simplify the deployment of applications for both personal and business use. Users are permitted to install, modify, and operate Dokploy independently or within their organizations to improve their development and deployment operations. It is important to note that any commercial resale or redistribution of Dokploy as a service is strictly forbidden without explicit consent. This prohibition ensures the preservation of Dokploy's open-source character for the benefit of the entire community.
|
||||
|
||||
### Our Responsibility
|
||||
|
||||
Dokploy developers will do their best to ensure that Dokploy remains functional and major bugs are resolved quickly. If you have a feature request, you are more than welcome to open a request for it, but the ultimate decision whether or not the feature will be added is taken by Dokploy's core developers.
|
||||
The Dokploy development team commits to maintaining the functionality of the software and addressing major issues promptly. While we welcome suggestions for new features, the decision to include them rests solely with the core developers of Dokploy.
|
||||
|
||||
### Usage Data
|
||||
|
||||
Dokploy doesn't collect any usage data. It is a free and open-source program, and it is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
**Dokploy** does not collect any user data. It is distributed as a free and open-source tool under the terms of "AS IS", without any implied warranties or conditions.
|
||||
|
||||
### Future changes
|
||||
### Future Changes
|
||||
|
||||
Terms of Service / Terms & Conditions may change at any point without a prior notice.
|
||||
The Terms of Service and Terms & Conditions are subject to change without prior notice.
|
||||
|
||||
@@ -61,7 +61,7 @@ export const Login2FA = ({ authId }: Props) => {
|
||||
id: authId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Signin succesfully", {
|
||||
toast.success("Signin successfully", {
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,756 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { HelpCircle, Settings } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const HealthCheckSwarmSchema = z
|
||||
.object({
|
||||
Test: z.array(z.string()).optional(),
|
||||
Interval: z.number().optional(),
|
||||
Timeout: z.number().optional(),
|
||||
StartPeriod: z.number().optional(),
|
||||
Retries: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const RestartPolicySwarmSchema = z
|
||||
.object({
|
||||
Condition: z.string().optional(),
|
||||
Delay: z.number().optional(),
|
||||
MaxAttempts: z.number().optional(),
|
||||
Window: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PreferenceSchema = z
|
||||
.object({
|
||||
Spread: z.object({
|
||||
SpreadDescriptor: z.string(),
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PlatformSchema = z
|
||||
.object({
|
||||
Architecture: z.string(),
|
||||
OS: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PlacementSwarmSchema = z
|
||||
.object({
|
||||
Constraints: z.array(z.string()).optional(),
|
||||
Preferences: z.array(PreferenceSchema).optional(),
|
||||
MaxReplicas: z.number().optional(),
|
||||
Platforms: z.array(PlatformSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const UpdateConfigSwarmSchema = z
|
||||
.object({
|
||||
Parallelism: z.number(),
|
||||
Delay: z.number().optional(),
|
||||
FailureAction: z.string().optional(),
|
||||
Monitor: z.number().optional(),
|
||||
MaxFailureRatio: z.number().optional(),
|
||||
Order: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ReplicatedSchema = z
|
||||
.object({
|
||||
Replicas: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ReplicatedJobSchema = z
|
||||
.object({
|
||||
MaxConcurrent: z.number().optional(),
|
||||
TotalCompletions: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ServiceModeSwarmSchema = z
|
||||
.object({
|
||||
Replicated: ReplicatedSchema.optional(),
|
||||
Global: z.object({}).optional(),
|
||||
ReplicatedJob: ReplicatedJobSchema.optional(),
|
||||
GlobalJob: z.object({}).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const NetworkSwarmSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
Target: z.string().optional(),
|
||||
Aliases: z.array(z.string()).optional(),
|
||||
DriverOpts: z.object({}).optional(),
|
||||
})
|
||||
.strict(),
|
||||
);
|
||||
|
||||
const LabelsSwarmSchema = z.record(z.string());
|
||||
|
||||
const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
||||
return z
|
||||
.string()
|
||||
.transform((str, ctx) => {
|
||||
if (str === null || str === "") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||
return z.NEVER;
|
||||
}
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Object cannot be empty",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parseResult = schema.safeParse(data);
|
||||
if (!parseResult.success) {
|
||||
for (const error of parseResult.error.issues) {
|
||||
const path = error.path.join(".");
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `${path} ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addSwarmSettings = z.object({
|
||||
healthCheckSwarm: createStringToJSONSchema(HealthCheckSwarmSchema).nullable(),
|
||||
restartPolicySwarm: createStringToJSONSchema(
|
||||
RestartPolicySwarmSchema,
|
||||
).nullable(),
|
||||
placementSwarm: createStringToJSONSchema(PlacementSwarmSchema).nullable(),
|
||||
updateConfigSwarm: createStringToJSONSchema(
|
||||
UpdateConfigSwarmSchema,
|
||||
).nullable(),
|
||||
rollbackConfigSwarm: createStringToJSONSchema(
|
||||
UpdateConfigSwarmSchema,
|
||||
).nullable(),
|
||||
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
|
||||
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
||||
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
||||
});
|
||||
|
||||
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.application.update.useMutation();
|
||||
|
||||
const form = useForm<AddSwarmSettings>({
|
||||
defaultValues: {
|
||||
healthCheckSwarm: null,
|
||||
restartPolicySwarm: null,
|
||||
placementSwarm: null,
|
||||
updateConfigSwarm: null,
|
||||
rollbackConfigSwarm: null,
|
||||
modeSwarm: null,
|
||||
labelsSwarm: null,
|
||||
networkSwarm: null,
|
||||
},
|
||||
resolver: zodResolver(addSwarmSettings),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
healthCheckSwarm: data.healthCheckSwarm
|
||||
? JSON.stringify(data.healthCheckSwarm, null, 2)
|
||||
: null,
|
||||
restartPolicySwarm: data.restartPolicySwarm
|
||||
? JSON.stringify(data.restartPolicySwarm, null, 2)
|
||||
: null,
|
||||
placementSwarm: data.placementSwarm
|
||||
? JSON.stringify(data.placementSwarm, null, 2)
|
||||
: null,
|
||||
updateConfigSwarm: data.updateConfigSwarm
|
||||
? JSON.stringify(data.updateConfigSwarm, null, 2)
|
||||
: null,
|
||||
rollbackConfigSwarm: data.rollbackConfigSwarm
|
||||
? JSON.stringify(data.rollbackConfigSwarm, null, 2)
|
||||
: null,
|
||||
modeSwarm: data.modeSwarm
|
||||
? JSON.stringify(data.modeSwarm, null, 2)
|
||||
: null,
|
||||
labelsSwarm: data.labelsSwarm
|
||||
? JSON.stringify(data.labelsSwarm, null, 2)
|
||||
: null,
|
||||
networkSwarm: data.networkSwarm
|
||||
? JSON.stringify(data.networkSwarm, null, 2)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddSwarmSettings) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
healthCheckSwarm: data.healthCheckSwarm,
|
||||
restartPolicySwarm: data.restartPolicySwarm,
|
||||
placementSwarm: data.placementSwarm,
|
||||
updateConfigSwarm: data.updateConfigSwarm,
|
||||
rollbackConfigSwarm: data.rollbackConfigSwarm,
|
||||
modeSwarm: data.modeSwarm,
|
||||
labelsSwarm: data.labelsSwarm,
|
||||
networkSwarm: data.networkSwarm,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Swarm settings updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the swarm settings");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" className="cursor-pointer w-fit">
|
||||
<Settings className="size-4 text-muted-foreground" />
|
||||
Swarm Settings
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-5xl p-0">
|
||||
<DialogHeader className="p-6">
|
||||
<DialogTitle>Swarm Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update certain settings using a json object.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-permissions"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="healthCheckSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Health Check</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Test?: string[] | undefined;
|
||||
Interval?: number | undefined;
|
||||
Timeout?: number | undefined;
|
||||
StartPeriod?: number | undefined;
|
||||
Retries?: number | undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[11.2rem]"
|
||||
placeholder={`{
|
||||
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
|
||||
"Interval" : 10000,
|
||||
"Timeout" : 10000,
|
||||
"StartPeriod" : 10000,
|
||||
"Retries" : 10
|
||||
}`}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="restartPolicySwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Restart Policy</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Condition?: string | undefined;
|
||||
Delay?: number | undefined;
|
||||
MaxAttempts?: number | undefined;
|
||||
Window?: number | undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[11.2rem]"
|
||||
placeholder={`{
|
||||
"Condition" : "on-failure",
|
||||
"Delay" : 10000,
|
||||
"MaxAttempts" : 10,
|
||||
"Window" : 10000
|
||||
} `}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="placementSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Placement</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Constraints?: string[] | undefined;
|
||||
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
|
||||
MaxReplicas?: number | undefined;
|
||||
Platforms?:
|
||||
| Array<{
|
||||
Architecture: string;
|
||||
OS: string;
|
||||
}>
|
||||
| undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[18.7rem]"
|
||||
placeholder={`{
|
||||
"Constraints" : ["node.role==manager"],
|
||||
"Preferences" : [{
|
||||
"Spread" : {
|
||||
"SpreadDescriptor" : "node.labels.region"
|
||||
}
|
||||
}],
|
||||
"MaxReplicas" : 10,
|
||||
"Platforms" : [{
|
||||
"Architecture" : "amd64",
|
||||
"OS" : "linux"
|
||||
}]
|
||||
} `}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="updateConfigSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Update Config</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Parallelism?: number;
|
||||
Delay?: number | undefined;
|
||||
FailureAction?: string | undefined;
|
||||
Monitor?: number | undefined;
|
||||
MaxFailureRatio?: number | undefined;
|
||||
Order: string;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[18.7rem]"
|
||||
placeholder={`{
|
||||
"Parallelism" : 1,
|
||||
"Delay" : 10000,
|
||||
"FailureAction" : "continue",
|
||||
"Monitor" : 10000,
|
||||
"MaxFailureRatio" : 10,
|
||||
"Order" : "start-first"
|
||||
}`}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rollbackConfigSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Rollback Config</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Parallelism?: number;
|
||||
Delay?: number | undefined;
|
||||
FailureAction?: string | undefined;
|
||||
Monitor?: number | undefined;
|
||||
MaxFailureRatio?: number | undefined;
|
||||
Order: string;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[14.8rem]"
|
||||
placeholder={`{
|
||||
"Parallelism" : 1,
|
||||
"Delay" : 10000,
|
||||
"FailureAction" : "continue",
|
||||
"Monitor" : 10000,
|
||||
"MaxFailureRatio" : 10,
|
||||
"Order" : "start-first"
|
||||
}`}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="modeSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Mode</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="center"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Replicated?: { Replicas?: number | undefined } | undefined;
|
||||
Global?: {} | undefined;
|
||||
ReplicatedJob?:
|
||||
| {
|
||||
MaxConcurrent?: number | undefined;
|
||||
TotalCompletions?: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
GlobalJob?: {} | undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[14.8rem]"
|
||||
placeholder={`{
|
||||
"Replicated" : {
|
||||
"Replicas" : 1
|
||||
},
|
||||
"Global" : {},
|
||||
"ReplicatedJob" : {
|
||||
"MaxConcurrent" : 1,
|
||||
"TotalCompletions" : 1
|
||||
},
|
||||
"GlobalJob" : {}
|
||||
}`}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="networkSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Network</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`[
|
||||
{
|
||||
"Target" : string | undefined;
|
||||
"Aliases" : string[] | undefined;
|
||||
"DriverOpts" : { [key: string]: string } | undefined;
|
||||
}
|
||||
]`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[18.5rem]"
|
||||
placeholder={`[
|
||||
{
|
||||
"Target" : "dokploy-network",
|
||||
"Aliases" : ["dokploy-network"],
|
||||
"DriverOpts" : {
|
||||
"com.docker.network.driver.mtu" : "1500",
|
||||
"com.docker.network.driver.host_binding" : "true",
|
||||
"com.docker.network.driver.mtu" : "1500",
|
||||
"com.docker.network.driver.host_binding" : "true"
|
||||
}
|
||||
}
|
||||
]`}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="labelsSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Labels</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
[name: string]: string;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="font-mono [field-sizing:content;] min-h-[18.5rem]"
|
||||
placeholder={`{
|
||||
"com.example.app.name" : "my-app",
|
||||
"com.example.app.version" : "1.0.0"
|
||||
}`}
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border p-2 ">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-permissions"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { toast } from "sonner";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import Link from "next/link";
|
||||
import { Server } from "lucide-react";
|
||||
import { AddSwarmSettings } from "./modify-swarm-settings";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
replicas: z.number(),
|
||||
registryId: z.string(),
|
||||
});
|
||||
|
||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
||||
|
||||
export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { data: registries } = api.registry.all.useQuery();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading } = api.application.update.useMutation();
|
||||
|
||||
const form = useForm<AddCommand>({
|
||||
defaultValues: {
|
||||
registryId: data?.registryId || "",
|
||||
replicas: data?.replicas || 1,
|
||||
},
|
||||
resolver: zodResolver(AddRedirectchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.command) {
|
||||
form.reset({
|
||||
registryId: data?.registryId || "",
|
||||
replicas: data?.replicas || 1,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]);
|
||||
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
registryId: data?.registryId === "none" ? null : data?.registryId,
|
||||
replicas: data?.replicas,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Command Updated");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the command");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Cluster Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Add the registry and the replicas of the application
|
||||
</CardDescription>
|
||||
</div>
|
||||
<AddSwarmSettings applicationId={applicationId} />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="replicas"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Replicas</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number(e.target.value));
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{registries && registries?.length === 0 ? (
|
||||
<div className="pt-10">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Server className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To use a cluster feature, you need to configure at least a
|
||||
registry first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/cluster"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a registry</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a registry" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{registries?.map((registry) => (
|
||||
<SelectItem
|
||||
key={registry.registryId}
|
||||
value={registry.registryId}
|
||||
>
|
||||
{registry.registryName}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
<SelectLabel>
|
||||
Registries ({registries?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button isLoading={isLoading} type="submit" className="w-fit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -26,11 +26,11 @@ interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
const AddRedirectSchema = z.object({
|
||||
command: z.string(),
|
||||
});
|
||||
|
||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
||||
type AddCommand = z.infer<typeof AddRedirectSchema>;
|
||||
|
||||
export const AddCommand = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
@@ -48,7 +48,7 @@ export const AddCommand = ({ applicationId }: Props) => {
|
||||
defaultValues: {
|
||||
command: "",
|
||||
},
|
||||
resolver: zodResolver(AddRedirectchema),
|
||||
resolver: zodResolver(AddRedirectSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { z } from "zod";
|
||||
|
||||
const AddPortchema = z.object({
|
||||
const AddPortSchema = z.object({
|
||||
publishedPort: z.number().int().min(1).max(65535),
|
||||
targetPort: z.number().int().min(1).max(65535),
|
||||
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 {
|
||||
applicationId: string;
|
||||
@@ -62,7 +62,7 @@ export const AddPort = ({
|
||||
publishedPort: 0,
|
||||
targetPort: 0,
|
||||
},
|
||||
resolver: zodResolver(AddPortchema),
|
||||
resolver: zodResolver(AddPortSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -100,14 +100,7 @@ export const AddPort = ({
|
||||
Ports are used to expose your application to the internet.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Pencil } from "lucide-react";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -98,7 +99,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -106,14 +107,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the port</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -98,14 +98,7 @@ export const AddRedirect = ({
|
||||
Redirects are used to redirect requests to another url.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Pencil } from "lucide-react";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -93,7 +94,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -101,14 +102,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the redirect</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -90,14 +90,7 @@ export const AddSecurity = ({
|
||||
Add security to your application
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Pencil } from "lucide-react";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -88,7 +89,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -96,14 +97,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the security</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { File } from "lucide-react";
|
||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
@@ -43,11 +44,13 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 relative">
|
||||
<div className="flex flex-col gap-6 bg-input p-4 rounded-md max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<div>
|
||||
<pre className="font-sans">{data || "Empty"}</pre>
|
||||
</div>
|
||||
<div className="flex justify-end absolute z-50 right-6">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<CodeEditor
|
||||
value={data || "Empty"}
|
||||
disabled
|
||||
className="font-mono"
|
||||
/>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-6">
|
||||
<UpdateTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,15 +16,15 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import jsyaml from "js-yaml";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const UpdateTraefikConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
@@ -116,20 +116,13 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4"
|
||||
className="grid w-full py-4 overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
@@ -139,8 +132,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-[35rem]"
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
|
||||
@@ -147,7 +147,7 @@ export const AddVolumes = ({
|
||||
<DialogTitle>Volumes / Mounts</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* {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" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
|
||||
@@ -25,8 +25,8 @@ export const DeleteApplication = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
|
||||
@@ -27,8 +27,9 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, PlusIcon } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -106,14 +107,7 @@ export const AddDomain = ({
|
||||
In this section you can add custom domains
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -24,8 +24,8 @@ export const DeleteDomain = ({ domainId }: Props) => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
|
||||
@@ -40,7 +40,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
{data && data?.length > 0 && (
|
||||
<AddDomain applicationId={applicationId} />
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-row gap-4">
|
||||
@@ -51,7 +53,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
To access to the application is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<AddDomain applicationId={applicationId}>Add Domain</AddDomain>
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
@@ -75,8 +79,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
<Button variant="outline" disabled>
|
||||
{item.https ? "HTTPS" : "HTTP"}
|
||||
</Button>
|
||||
<UpdateDomain domainId={item.domainId} />
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateDomain domainId={item.domainId} />
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -27,8 +27,9 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, PenBoxIcon } from "lucide-react";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -114,8 +115,8 @@ export const UpdateDomain = ({ domainId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>
|
||||
<PenBoxIcon className="size-4" />
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
@@ -125,14 +126,7 @@ export const UpdateDomain = ({ domainId }: Props) => {
|
||||
In this section you can add custom domains
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -96,7 +96,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="NODE_ENV=production"
|
||||
className="h-96"
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,7 +90,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -97,14 +98,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
<DialogTitle>Modify Application</DialogTitle>
|
||||
<DialogDescription>Update the application data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil, CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { Pencil, CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -115,7 +115,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -32,14 +32,14 @@ export const ShowContainerConfig = ({ containerId }: Props) => {
|
||||
View Config
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||
<DialogContent className={"w-full md:w-[70vw] max-w-max"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Container Config</DialogTitle>
|
||||
<DialogDescription>
|
||||
See in detail the config of this container
|
||||
</DialogDescription>
|
||||
</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>
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
|
||||
@@ -24,11 +24,12 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
lineHeight: 1.4,
|
||||
fontWeight: 400,
|
||||
|
||||
convertEol: true,
|
||||
theme: {
|
||||
cursor: "transparent",
|
||||
background: "#19191A",
|
||||
background: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -81,8 +82,8 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full bg-input rounded-lg p-2 ">
|
||||
<div id={id} className="rounded-xl" />
|
||||
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
||||
<div id={id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { AttachAddon } from "@xterm/addon-attach";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
|
||||
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||
const termRef = useRef(null);
|
||||
const [activeWay, setActiveWay] = React.useState<string | null>("bash");
|
||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||
useEffect(() => {
|
||||
const container = document.getElementById(id);
|
||||
if (container) {
|
||||
@@ -26,7 +26,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||
convertEol: true,
|
||||
theme: {
|
||||
cursor: "transparent",
|
||||
background: "#19191A",
|
||||
background: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
});
|
||||
const addonFit = new FitAddon();
|
||||
@@ -54,14 +54,15 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||
<span>
|
||||
Select way to connect to <b>{containerId}</b>
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 w-fit">
|
||||
<Button onClick={() => setActiveWay("sh")}>SH</Button>
|
||||
<Button onClick={() => setActiveWay("bash")}>Bash</Button>
|
||||
<Button onClick={() => setActiveWay("sh")}>/bin/sh</Button>
|
||||
</div>
|
||||
<Tabs value={activeWay} onValueChange={setActiveWay}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="bash">Bash</TabsTrigger>
|
||||
<TabsTrigger value="sh">/bin/sh</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="w-full h-full bg-input rounded-lg p-2 ">
|
||||
<div id={id} ref={termRef} className="rounded-xl" />
|
||||
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
||||
<div id={id} ref={termRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
@@ -86,31 +86,25 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4 relative"
|
||||
className="grid w-full relative z-[5]"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col overflow-auto">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormDescription>{path}</FormDescription>
|
||||
<FormDescription className="break-all">
|
||||
{path}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-[35rem]"
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
@@ -128,8 +122,9 @@ routers:
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</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
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
|
||||
@@ -13,8 +13,8 @@ export const ShowTraefikSystem = () => {
|
||||
const { data: directories } = api.settings.readDirectories.useQuery();
|
||||
|
||||
return (
|
||||
<div className={cn("mt-6 grid gap-4 pb-20")}>
|
||||
<div className="flex flex-col md:flex-row gap-4 md:gap-10 w-full">
|
||||
<div className={cn("mt-6 md:grid gap-4")}>
|
||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
@@ -27,7 +27,7 @@ export const ShowTraefikSystem = () => {
|
||||
<>
|
||||
<Tree
|
||||
data={directories}
|
||||
className="md:max-w-[19rem] w-full md:h-[660px] border rounded-lg"
|
||||
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
|
||||
onSelectChange={(item) => setFile(item?.id || null)}
|
||||
folderIcon={Folder}
|
||||
itemIcon={Workflow}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
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";
|
||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
mariadbId: string;
|
||||
}
|
||||
|
||||
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mariadbId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mariadbId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="MARIADB_PASSWORD=1234567678"
|
||||
className="h-96"
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,7 +90,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -97,14 +98,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
<DialogTitle>Modify MariaDB</DialogTitle>
|
||||
<DialogDescription>Update the MariaDB data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
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";
|
||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
mongoId: string;
|
||||
}
|
||||
|
||||
export const DeleteMongo = ({ mongoId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="MONGO_PASSWORD=1234567678"
|
||||
className="h-96"
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,7 +90,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -97,14 +98,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
<DialogTitle>Modify MongoDB</DialogTitle>
|
||||
<DialogDescription>Update the MongoDB data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
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";
|
||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mysqlId: string;
|
||||
mysqlId: string;
|
||||
}
|
||||
|
||||
export const DeleteMysql = ({ mysqlId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mysqlId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mysqlId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="MYSQL_PASSWORD=1234567678"
|
||||
className="h-96"
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,7 +90,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -97,14 +98,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
||||
<DialogTitle>Modify MySQL</DialogTitle>
|
||||
<DialogDescription>Update the MySQL data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
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";
|
||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
postgresId: string;
|
||||
postgresId: string;
|
||||
}
|
||||
|
||||
export const DeletePostgres = ({ postgresId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
postgresId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
postgresId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="POSTGRES_PASSWORD=1234567678"
|
||||
className="h-96"
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
const hostname = window.location.hostname;
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
|
||||
return `postgresql://${data?.databasePassword}:${data?.databasePassword}@${hostname}:${port}/${data?.databaseName}`;
|
||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${hostname}:${port}/${data?.databaseName}`;
|
||||
};
|
||||
|
||||
setConnectionUrl(buildConnectionUrl());
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,7 +90,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -97,14 +98,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
<DialogTitle>Modify Postgres</DialogTitle>
|
||||
<DialogDescription>Update the Postgres data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -18,9 +18,10 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Folder } from "lucide-react";
|
||||
import { Folder } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -93,14 +94,7 @@ export const AddApplication = ({ projectId }: Props) => {
|
||||
Assign a name and description to your application
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -235,7 +235,7 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
<DialogTitle>Databases</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* {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" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, PlusIcon } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -85,14 +86,7 @@ export const AddProject = () => {
|
||||
<DialogTitle>Add a project</DialogTitle>
|
||||
<DialogDescription>The home of something big!</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-project"
|
||||
|
||||
@@ -48,9 +48,9 @@ export const ShowProjects = () => {
|
||||
return (
|
||||
<>
|
||||
{data?.length === 0 && (
|
||||
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center ">
|
||||
<FolderInput className="size-10 md:size-28 text-muted" />
|
||||
<span className="text-center font-medium text-muted-foreground">
|
||||
<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-foreground" />
|
||||
<span className="text-center font-medium text-muted-foreground">
|
||||
No projects added yet. Click on Create project.
|
||||
</span>
|
||||
</div>
|
||||
@@ -89,9 +89,9 @@ export const ShowProjects = () => {
|
||||
<span className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-base font-medium leading-none">
|
||||
<Link className="text-base font-medium leading-none" href={`/dashboard/project/${project.projectId}`}>
|
||||
{project.name}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -101,14 +102,7 @@ export const UpdateProject = ({ projectId }: Props) => {
|
||||
Update the details of the project
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
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";
|
||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
redisId: string;
|
||||
redisId: string;
|
||||
}
|
||||
|
||||
export const DeleteRedis = ({ redisId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
redisId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
redisId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,7 +90,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -97,14 +98,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
||||
<DialogTitle>Modify Redis</DialogTitle>
|
||||
<DialogDescription>Update the redis data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
|
||||
const appearanceFormSchema = z.object({
|
||||
theme: z.enum(["light", "dark"], {
|
||||
theme: z.enum(["light", "dark", "system"], {
|
||||
required_error: "Please select a theme.",
|
||||
}),
|
||||
});
|
||||
@@ -34,7 +34,7 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
||||
|
||||
// This can come from your database or API.
|
||||
const defaultValues: Partial<AppearanceFormValues> = {
|
||||
theme: "light",
|
||||
theme: "system",
|
||||
};
|
||||
|
||||
export function AppearanceForm() {
|
||||
@@ -46,7 +46,7 @@ export function AppearanceForm() {
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
theme: theme === "light" ? "light" : "dark",
|
||||
theme: (theme ?? "system") as AppearanceFormValues["theme"],
|
||||
});
|
||||
}, [form, theme]);
|
||||
function onSubmit(data: AppearanceFormValues) {
|
||||
@@ -81,28 +81,15 @@ export function AppearanceForm() {
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
className="grid max-w-md grid-cols-1 sm:grid-cols-2 gap-8 pt-2"
|
||||
className="grid max-w-md md:max-w-lg grid-cols-1 sm:grid-cols-3 gap-8 pt-2"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="light" className="sr-only" />
|
||||
</FormControl>
|
||||
<div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
||||
<div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
|
||||
<div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
|
||||
<div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="items-center rounded-md border-2 border-muted p-1 hover:bg-accent transition-colors cursor-pointer">
|
||||
<img src="/images/theme-light.svg" alt="light" />
|
||||
</div>
|
||||
<span className="block w-full p-2 text-center font-normal">
|
||||
Light
|
||||
@@ -114,27 +101,30 @@ export function AppearanceForm() {
|
||||
<FormControl>
|
||||
<RadioGroupItem value="dark" className="sr-only" />
|
||||
</FormControl>
|
||||
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
|
||||
<div className="space-y-2 rounded-sm bg-slate-950 p-2">
|
||||
<div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||
<div className="h-2 w-[80px] rounded-lg bg-slate-400" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-slate-400" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-slate-400" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer">
|
||||
<img src="/images/theme-dark.svg" alt="dark" />
|
||||
</div>
|
||||
<span className="block w-full p-2 text-center font-normal">
|
||||
Dark
|
||||
</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value="system"
|
||||
className="sr-only"
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer">
|
||||
<img src="/images/theme-system.svg" alt="system" />
|
||||
</div>
|
||||
<span className="block w-full p-2 text-center font-normal">
|
||||
System
|
||||
</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
@@ -26,140 +27,135 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const certificateDataHolder = `-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`;
|
||||
|
||||
const addCertificate = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
certificateData: z.string().min(1, "Certificate data is required"),
|
||||
privateKey: z.string().min(1, "Private key is required"),
|
||||
autoRenew: z.boolean().optional(),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
certificateData: z.string().min(1, "Certificate data is required"),
|
||||
privateKey: z.string().min(1, "Private key is required"),
|
||||
autoRenew: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type AddCertificate = z.infer<typeof addCertificate>;
|
||||
|
||||
export const AddCertificate = () => {
|
||||
const utils = api.useUtils();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.certificates.create.useMutation();
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.certificates.create.useMutation();
|
||||
|
||||
const form = useForm<AddCertificate>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
certificateData: "",
|
||||
privateKey: "",
|
||||
autoRenew: false,
|
||||
},
|
||||
resolver: zodResolver(addCertificate),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
const form = useForm<AddCertificate>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
certificateData: "",
|
||||
privateKey: "",
|
||||
autoRenew: false,
|
||||
},
|
||||
resolver: zodResolver(addCertificate),
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (data: AddCertificate) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
certificateData: data.certificateData,
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Certificate Created");
|
||||
await utils.certificates.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Certificate");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>Add Certificate</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Certificate</DialogTitle>
|
||||
<DialogDescription>Add a new certificate</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
const onSubmit = async (data: AddCertificate) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
certificateData: data.certificateData,
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Certificate Created");
|
||||
await utils.certificates.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the Certificate");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>Add Certificate</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Certificate</DialogTitle>
|
||||
<DialogDescription>Add a new certificate</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-certificate"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Certificate Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"My Certificate"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certificateData"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Certificate Data</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-32"
|
||||
placeholder={`-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="privateKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Private Key</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-32"
|
||||
placeholder={`-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-certificate"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Certificate Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"My Certificate"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certificateData"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Certificate Data</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-32"
|
||||
placeholder={certificateDataHolder}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="privateKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Private Key</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-32"
|
||||
placeholder={certificateDataHolder}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-certificate"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-certificate"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,21 +33,23 @@ export const ShowCertificates = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.map((destination, index) => (
|
||||
<div
|
||||
key={destination.certificateId}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{index + 1}. {destination.name}
|
||||
</span>
|
||||
<div className="flex flex-row gap-3">
|
||||
<DeleteCertificate
|
||||
certificateId={destination.certificateId}
|
||||
/>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{data?.map((destination, index) => (
|
||||
<div
|
||||
key={destination.certificateId}
|
||||
className="flex items-center justify-between border p-4 rounded-lg"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{index + 1}. {destination.name}
|
||||
</span>
|
||||
<div className="flex flex-row gap-3">
|
||||
<DeleteCertificate
|
||||
certificateId={destination.certificateId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<AddCertificate />
|
||||
</div>
|
||||
|
||||
66
components/dashboard/settings/cluster/nodes/add-node.tsx
Normal file
66
components/dashboard/settings/cluster/nodes/add-node.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLink, PlusIcon } from "lucide-react";
|
||||
import { AddWorker } from "./workers/add-worker";
|
||||
import { AddManager } from "./manager/add-manager";
|
||||
import Link from "next/link";
|
||||
|
||||
export const AddNode = () => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full cursor-pointer space-x-3">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Node
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Node</DialogTitle>
|
||||
<DialogDescription className="flex flex-col gap-2">
|
||||
Follow the steps to add a new node to your cluster, before you start
|
||||
using this feature, you need to understand how docker swarm works.{" "}
|
||||
<Link
|
||||
href="https://docs.docker.com/engine/swarm/"
|
||||
target="_blank"
|
||||
className="text-primary flex flex-row gap-2 items-center"
|
||||
>
|
||||
Docker Swarm
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="https://docs.docker.com/engine/swarm/how-swarm-mode-works/nodes/"
|
||||
target="_blank"
|
||||
className="text-primary flex flex-row gap-2 items-center"
|
||||
>
|
||||
Architecture
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Link>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Tabs defaultValue="worker">
|
||||
<TabsList>
|
||||
<TabsTrigger value="worker">Worker</TabsTrigger>
|
||||
<TabsTrigger value="manager">Manager</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="worker" className="pt-4">
|
||||
<AddWorker />
|
||||
</TabsContent>
|
||||
<TabsContent value="manager" className="pt-4">
|
||||
<AddManager />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const AddManager = () => {
|
||||
const { data } = api.cluster.addManager.useQuery();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a new manager</DialogTitle>
|
||||
<DialogDescription>Add a new manager</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>1. Go to your new server and run the following command</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||
curl https://get.docker.com | sh -s -- --version 24.0
|
||||
<button
|
||||
type="button"
|
||||
className="self-center"
|
||||
onClick={() => {
|
||||
copy("curl https://get.docker.com | sh -s -- --version 24.0");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>
|
||||
2. Run the following command to add the node(manager) to your
|
||||
cluster
|
||||
</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex">
|
||||
{data}
|
||||
<button
|
||||
type="button"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
copy(data || "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface Props {
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export const ShowNodeData = ({ data }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
View Config
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Node Config</DialogTitle>
|
||||
<DialogDescription>
|
||||
See in detail the metadata of this node
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem] bg-card">
|
||||
<code>
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
</code>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
162
components/dashboard/settings/cluster/nodes/show-nodes.tsx
Normal file
162
components/dashboard/settings/cluster/nodes/show-nodes.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DeleteWorker } from "./workers/delete-worker";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { HelpCircle, LockIcon, MoreHorizontal } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ShowNodeData } from "./show-node-data";
|
||||
import { AddNode } from "./add-node";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export const ShowNodes = () => {
|
||||
const { data, isLoading } = api.cluster.getNodes.useQuery();
|
||||
const { data: registry } = api.registry.all.useQuery();
|
||||
|
||||
const haveAtLeastOneRegistry = !!(registry && registry?.length > 0);
|
||||
return (
|
||||
<Card className="bg-transparent h-full">
|
||||
<CardHeader className="flex flex-row gap-2 justify-between w-full items-center flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Cluster</CardTitle>
|
||||
<CardDescription>Add nodes to your cluster</CardDescription>
|
||||
</div>
|
||||
{haveAtLeastOneRegistry && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<AddNode />
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{haveAtLeastOneRegistry ? (
|
||||
<div className="grid md:grid-cols-1 gap-4">
|
||||
{isLoading && <div>Loading...</div>}
|
||||
<Table>
|
||||
<TableCaption>A list of your managers / workers.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Hostname</TableHead>
|
||||
<TableHead className="text-right">Status</TableHead>
|
||||
<TableHead className="text-right">Role</TableHead>
|
||||
<TableHead className="text-right">Availability</TableHead>
|
||||
<TableHead className="text-right">Engine Version</TableHead>
|
||||
<TableHead className="text-right">Created</TableHead>
|
||||
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((node) => {
|
||||
const isManager = node.Spec.Role === "manager";
|
||||
return (
|
||||
<TableRow key={node.ID}>
|
||||
<TableCell className="w-[100px]">
|
||||
{node.Description.Hostname}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{node.Status.State}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Badge variant={isManager ? "default" : "secondary"}>
|
||||
{node?.Spec?.Role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{node.Spec.Availability}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
{node?.Description.Engine.EngineVersion}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right">
|
||||
<DateTooltip date={node.CreatedAt} className="text-sm">
|
||||
Created{" "}
|
||||
</DateTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="text-right flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<ShowNodeData data={node} />
|
||||
{!node?.ManagerStatus?.Leader && (
|
||||
<DeleteWorker nodeId={node.ID} />
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<LockIcon className="size-8 text-muted-foreground" />
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className="text-base text-muted-foreground ">
|
||||
To add nodes to your cluster, you need to configure at least one
|
||||
registry.
|
||||
</span>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="self-center">
|
||||
<HelpCircle className="size-5 text-muted-foreground " />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Nodes need a registry to pull images from.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<ul className="list-disc list-inside text-sm text-muted-foreground border p-4 rounded-lg flex flex-col gap-1.5 mt-2.5">
|
||||
<li>
|
||||
<strong>Docker Registry:</strong> Use custom registries like
|
||||
Docker Hub, DigitalOcean Registry, etc.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Self-Hosted Docker Registry:</strong> Automatically set
|
||||
up a local registry to store all images.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const AddWorker = () => {
|
||||
const { data } = api.cluster.addWorker.useQuery();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a new worker</DialogTitle>
|
||||
<DialogDescription>Add a new worker</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>1. Go to your new server and run the following command</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||
curl https://get.docker.com | sh -s -- --version 24.0
|
||||
<button
|
||||
type="button"
|
||||
className="self-center"
|
||||
onClick={() => {
|
||||
copy("curl https://get.docker.com | sh -s -- --version 24.0");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>
|
||||
2. Run the following command to add the node(worker) to your cluster
|
||||
</span>
|
||||
|
||||
<span className="bg-muted rounded-lg p-2 flex">
|
||||
{data}
|
||||
<button
|
||||
type="button"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
copy(data || "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
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 { TrashIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
}
|
||||
export const DeleteWorker = ({ nodeId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.cluster.removeWorker.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
worker.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
nodeId,
|
||||
})
|
||||
.then(async () => {
|
||||
utils.cluster.getNodes.invalidate();
|
||||
toast.success("Worker deleted succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the worker");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Container } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const AddRegistrySchema = z.object({
|
||||
registryName: z.string().min(1, {
|
||||
message: "Registry name is required",
|
||||
}),
|
||||
username: z.string().min(1, {
|
||||
message: "Username is required",
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
imagePrefix: z.string(),
|
||||
});
|
||||
|
||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||
|
||||
export const AddRegistry = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
||||
const { mutateAsync: testRegistry, isLoading } =
|
||||
api.registry.testRegistry.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<AddRegistry>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
imagePrefix: "",
|
||||
registryName: "",
|
||||
},
|
||||
resolver: zodResolver(AddRegistrySchema),
|
||||
});
|
||||
|
||||
const password = form.watch("password");
|
||||
const username = form.watch("username");
|
||||
const registryUrl = form.watch("registryUrl");
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
username: "",
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
imagePrefix: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (data: AddRegistry) => {
|
||||
await mutateAsync({
|
||||
password: data.password,
|
||||
registryName: data.registryName,
|
||||
username: data.username,
|
||||
registryUrl: data.registryUrl,
|
||||
registryType: "cloud",
|
||||
imagePrefix: data.imagePrefix,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.registry.all.invalidate();
|
||||
toast.success("Registry added");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add a registry");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Container className="h-4 w-4" />
|
||||
Create Registry
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:m:max-w-lg ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a external registry</DialogTitle>
|
||||
<DialogDescription>
|
||||
Fill the next fields to add a external registry.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Registry Name" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Username" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imagePrefix"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image Prefix</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Image Prefix" />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="aws_account_id.dkr.ecr.us-west-2.amazonaws.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testRegistry({
|
||||
username: username,
|
||||
password: password,
|
||||
registryUrl: registryUrl,
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
toast.success("Registry Tested Successfully");
|
||||
} else {
|
||||
toast.error("Registry Test Failed");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to test the registry");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
<Button isLoading={form.formState.isSubmitting} type="submit">
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Container } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const AddRegistrySchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "Username is required",
|
||||
})
|
||||
.regex(/^[a-zA-Z0-9]+$/, {
|
||||
message: "Username can only contain letters and numbers",
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||
|
||||
export const AddSelfHostedRegistry = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.registry.enableSelfHostedRegistry.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<AddRegistry>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
},
|
||||
resolver: zodResolver(AddRegistrySchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
registryUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (data: AddRegistry) => {
|
||||
await mutateAsync({
|
||||
registryUrl: data.registryUrl,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.registry.all.invalidate();
|
||||
toast.success("Self Hosted Registry Created");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create a self hosted registry");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Container className="h-4 w-4" />
|
||||
Enable Self Hosted Registry
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:m:max-w-lg ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a self hosted registry</DialogTitle>
|
||||
<DialogDescription>
|
||||
Fill the next fields to add a self hosted registry.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Username" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="registry.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Point a DNS record to the VPS IP address.
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
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 { TrashIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
registryId: string;
|
||||
}
|
||||
export const DeleteRegistry = ({ registryId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.registry.remove.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
registry.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
registryId,
|
||||
})
|
||||
.then(async () => {
|
||||
utils.registry.all.invalidate();
|
||||
toast.success("Registry deleted");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the registry");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Server } from "lucide-react";
|
||||
import { AddRegistry } from "./add-docker-registry";
|
||||
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
|
||||
import { DeleteRegistry } from "./delete-registry";
|
||||
import { UpdateDockerRegistry } from "./update-docker-registry";
|
||||
|
||||
export const ShowRegistry = () => {
|
||||
const { data } = api.registry.all.useQuery();
|
||||
|
||||
const haveSelfHostedRegistry = data?.some(
|
||||
(registry) => registry.registryType === "selfHosted",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<Card className="bg-transparent h-full">
|
||||
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between w-full items-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Registry</CardTitle>
|
||||
<CardDescription>Add registry to your application.</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
{data && data?.length > 0 && (
|
||||
<>
|
||||
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
|
||||
|
||||
<AddRegistry />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 pt-4 h-full">
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Server className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a cluster is required to set a registry.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<AddSelfHostedRegistry />
|
||||
<AddRegistry />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-1 gap-6">
|
||||
{data?.map((registry, index) => (
|
||||
<div
|
||||
key={registry.registryId}
|
||||
className="flex items-center justify-between border p-4 rounded-lg"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{index + 1}. {registry.registryName}
|
||||
</span>
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateDockerRegistry registryId={registry.registryId} />
|
||||
<DeleteRegistry registryId={registry.registryId} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const updateRegistry = z.object({
|
||||
registryName: z.string().min(1, {
|
||||
message: "Registry name is required",
|
||||
}),
|
||||
username: z.string().min(1, {
|
||||
message: "Username is required",
|
||||
}),
|
||||
password: z.string(),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
imagePrefix: z.string(),
|
||||
});
|
||||
|
||||
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
||||
|
||||
interface Props {
|
||||
registryId: string;
|
||||
}
|
||||
|
||||
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync: testRegistry, isLoading } =
|
||||
api.registry.testRegistry.useMutation();
|
||||
const { data, refetch } = api.registry.one.useQuery(
|
||||
{
|
||||
registryId,
|
||||
},
|
||||
{
|
||||
enabled: !!registryId,
|
||||
},
|
||||
);
|
||||
|
||||
const isCloud = data?.registryType === "cloud";
|
||||
const { mutateAsync, isError, error } = api.registry.update.useMutation();
|
||||
|
||||
const form = useForm<UpdateRegistry>({
|
||||
defaultValues: {
|
||||
imagePrefix: "",
|
||||
registryName: "",
|
||||
username: "",
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
},
|
||||
resolver: zodResolver(updateRegistry),
|
||||
});
|
||||
|
||||
const password = form.watch("password");
|
||||
const username = form.watch("username");
|
||||
const registryUrl = form.watch("registryUrl");
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
imagePrefix: data.imagePrefix || "",
|
||||
registryName: data.registryName || "",
|
||||
username: data.username || "",
|
||||
password: "",
|
||||
registryUrl: data.registryUrl || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateRegistry) => {
|
||||
await mutateAsync({
|
||||
registryId,
|
||||
...(data.password ? { password: data.password } : {}),
|
||||
registryName: data.registryName,
|
||||
username: data.username,
|
||||
registryUrl: data.registryUrl,
|
||||
imagePrefix: data.imagePrefix,
|
||||
})
|
||||
.then(async (data) => {
|
||||
toast.success("Registry Updated");
|
||||
await refetch();
|
||||
await utils.registry.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the registry");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Registry</DialogTitle>
|
||||
<DialogDescription>Update the registry information</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Registry Name" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Username" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imagePrefix"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Image Prefix</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Image Prefix" />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://aws_account_id.dkr.ecr.us-west-2.amazonaws.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
isCloud ? "sm:justify-between " : "",
|
||||
"flex flex-row w-full gap-4 flex-wrap",
|
||||
)}
|
||||
>
|
||||
{isCloud && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testRegistry({
|
||||
username: username,
|
||||
password: password,
|
||||
registryUrl: registryUrl,
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
toast.success("Registry Tested Successfully");
|
||||
} else {
|
||||
toast.error("Registry Test Failed");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to test the registry");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -87,14 +87,7 @@ export const AddDestination = () => {
|
||||
In this section you can add destinations for your backups.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -34,16 +34,16 @@ export const ShowDestinations = () => {
|
||||
<AddDestination />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
{data?.map((destination, index) => (
|
||||
<div
|
||||
key={destination.destinationId}
|
||||
className="flex items-center justify-between"
|
||||
className="flex items-center justify-between border p-3.5 rounded-lg"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{index + 1}. {destination.name}
|
||||
</span>
|
||||
<div className="flex flex-row gap-3">
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateDestination
|
||||
destinationId={destination.destinationId}
|
||||
/>
|
||||
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, PenBoxIcon } from "lucide-react";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -111,14 +112,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
Update the current destination config
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -87,10 +87,10 @@ export const GithubSetup = () => {
|
||||
{haveGithubConfigured ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-muted-foreground">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Github account configured succesfully.
|
||||
</span>
|
||||
<BadgeCheck className="size-5 text-green-700" />
|
||||
<BadgeCheck className="size-4 text-green-700" />
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<RemoveGithubApp />
|
||||
@@ -101,9 +101,9 @@ export const GithubSetup = () => {
|
||||
{data?.githubAppName ? (
|
||||
<div className="flex w-fit flex-col gap-4">
|
||||
<span className="text-muted-foreground">
|
||||
Youve successfully created a GitHub app named
|
||||
{data.githubAppName}! The next step is to install this app in
|
||||
your GitHub account.
|
||||
You've successfully created a github app named{" "}
|
||||
<strong>{data.githubAppName}</strong>! The next step is to
|
||||
install this app in your GitHub account.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
@@ -121,12 +121,12 @@ export const GithubSetup = () => {
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">
|
||||
To integrate your GitHub account with our services, youll
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your GitHub account with our services, you'll
|
||||
need to create and install a GitHub app. This process is
|
||||
straightforward and only takes a few minutes. Click the
|
||||
button below to get started.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
|
||||
@@ -147,11 +147,11 @@ export const ProfileForm = () => {
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
className="flex flex-row flex-wrap gap-2 max-xl:justify-cente"
|
||||
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
|
||||
>
|
||||
{randomImages.map((image) => (
|
||||
<FormItem key={image}>
|
||||
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px">
|
||||
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value={image}
|
||||
@@ -163,7 +163,7 @@ export const ProfileForm = () => {
|
||||
key={image}
|
||||
src={image}
|
||||
alt="avatar"
|
||||
className="h-12 w-12 rounded-full border transition-transform"
|
||||
className="h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-transform"
|
||||
/>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
|
||||
@@ -21,12 +21,13 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, ListTodo } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
|
||||
const addPermissions = z.object({
|
||||
accesedProjects: z.array(z.string()).optional(),
|
||||
@@ -106,23 +107,19 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="ghost">
|
||||
<ListTodo className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Add Permissions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Permissions</DialogTitle>
|
||||
<DialogDescription>Add or remove permissions</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -19,12 +19,13 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
const addUser = z.object({
|
||||
email: z
|
||||
@@ -66,21 +67,16 @@ export const AddUser = () => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>Add User</Button>
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" /> Add User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add User</DialogTitle>
|
||||
<DialogDescription>Invite a new user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface Props {
|
||||
authId: string;
|
||||
@@ -24,9 +25,12 @@ export const DeleteUser = ({ authId }: Props) => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { CopyIcon, Users } from "lucide-react";
|
||||
import { MoreHorizontal, Users } from "lucide-react";
|
||||
import { AddUser } from "./add-user";
|
||||
import { DeleteUser } from "./delete-user";
|
||||
import { format } from "date-fns";
|
||||
@@ -14,7 +14,24 @@ import { useEffect, useState } from "react";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { toast } from "sonner";
|
||||
import { UpdateUser } from "./update-user";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export const ShowUsers = () => {
|
||||
const { data } = api.user.all.useQuery();
|
||||
@@ -25,82 +42,109 @@ export const ShowUsers = () => {
|
||||
|
||||
return (
|
||||
<div className="h-full col-span-2">
|
||||
<Card className="bg-transparent h-full border-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Users</CardTitle>
|
||||
<CardDescription>Add, manage and delete users.</CardDescription>
|
||||
<Card className="bg-transparent h-full ">
|
||||
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Users</CardTitle>
|
||||
<CardDescription>Add, manage and delete users.</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 && (
|
||||
<div className="flex flex-col gap-3 items-end">
|
||||
<AddUser />
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 h-full">
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Users className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a user is required to add
|
||||
To create a user, you need to add:
|
||||
</span>
|
||||
<AddUser />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.map((user) => {
|
||||
return (
|
||||
<div
|
||||
key={user.userId}
|
||||
className="flex gap-2 flex-col justify-start border p-4 rounded-lg"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{user.auth.email}
|
||||
</span>
|
||||
{!user.isRegistered && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Expire In{" "}
|
||||
{format(new Date(user.expirationDate), "PPpp")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{user.isRegistered ? "Registered" : "Not Registered"}
|
||||
</span>
|
||||
{user.auth.is2FAEnabled && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{user.auth.is2FAEnabled
|
||||
? "2FA Enabled"
|
||||
: "2FA Not Enabled"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap flex-row gap-3">
|
||||
{!user.isRegistered && (
|
||||
<div className="overflow-x-auto flex flex-row gap-4 items-center">
|
||||
<div className="overflow-x-auto">
|
||||
<span className="text-sm text-muted-foreground ">
|
||||
{`${url}/invitation?token=${user.token}`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
// className="absolute right-2 top-2"
|
||||
onClick={() => {
|
||||
copy(`${url}/invitation?token=${user.token}`);
|
||||
toast.success("Invitation Copied to clipboard");
|
||||
}}
|
||||
<Table>
|
||||
<TableCaption>See all users</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Email</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
<TableHead className="text-center">2FA</TableHead>
|
||||
<TableHead className="text-center">Expiration</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((user) => {
|
||||
return (
|
||||
<TableRow key={user.userId}>
|
||||
<TableCell className="w-[100px]">
|
||||
{user.auth.email}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
user.isRegistered ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
<CopyIcon className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{user.isRegistered
|
||||
? "Registered"
|
||||
: "Not Registered"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{user.auth.is2FAEnabled
|
||||
? "2FA Enabled"
|
||||
: "2FA Not Enabled"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(new Date(user.expirationDate), "PPpp")}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{user.isRegistered && (
|
||||
<AddUserPermissions userId={user.userId} />
|
||||
)}
|
||||
{user.isRegistered && <UpdateUser authId={user.authId} />}
|
||||
<DeleteUser authId={user.authId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col justify-end gap-3 w-full items-end">
|
||||
<AddUser />
|
||||
</div>
|
||||
<TableCell className="text-right flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
{!user.isRegistered && (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
copy(
|
||||
`${origin}/invitation?token=${user.token}`,
|
||||
);
|
||||
toast.success(
|
||||
"Invitation Copied to clipboard",
|
||||
);
|
||||
}}
|
||||
>
|
||||
Copy Invitation
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{user.isRegistered && (
|
||||
<AddUserPermissions userId={user.userId} />
|
||||
)}
|
||||
|
||||
<DeleteUser authId={user.authId} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -18,11 +18,12 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { SquarePen } from "lucide-react";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const updateUserSchema = z.object({
|
||||
@@ -100,14 +101,7 @@ export const UpdateUser = ({ authId }: Props) => {
|
||||
<DialogTitle>Update User</DialogTitle>
|
||||
<DialogDescription>Update the user</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
|
||||
@@ -27,10 +27,9 @@ import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
||||
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
||||
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
|
||||
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
|
||||
import { UpdateWebServer } from "./web-server/update-webserver";
|
||||
import { UpdateServer } from "./web-server/update-server";
|
||||
|
||||
export const WebServer = () => {
|
||||
const [fetchAfterFirstRender, setFetchAfterFirstRender] = useState(false);
|
||||
const { data, refetch } = api.admin.one.useQuery();
|
||||
const { mutateAsync: reloadServer, isLoading } =
|
||||
api.settings.reloadServer.useMutation();
|
||||
@@ -61,13 +60,6 @@ export const WebServer = () => {
|
||||
|
||||
const { mutateAsync: updateDockerCleanup } =
|
||||
api.settings.updateDockerCleanup.useMutation();
|
||||
const { data: query } = api.settings.checkAndUpdateImage.useQuery(void 0, {
|
||||
enabled: fetchAfterFirstRender,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setFetchAfterFirstRender(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="rounded-lg w-full bg-transparent">
|
||||
@@ -279,7 +271,7 @@ export const WebServer = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{query ? <UpdateWebServer /> : null}
|
||||
<UpdateServer />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||
|
||||
@@ -16,14 +16,14 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateMainTraefikConfigSchema = z.object({
|
||||
@@ -89,14 +89,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -112,8 +105,8 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-[35rem]"
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`providers:
|
||||
docker:
|
||||
defaultRule: 'Host('dokploy.com')'
|
||||
@@ -136,8 +129,10 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</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
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
|
||||
@@ -16,14 +16,14 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||
@@ -92,20 +92,13 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
||||
<DialogTitle>Update Middleware config</DialogTitle>
|
||||
<DialogDescription>Update the middleware config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4 relative"
|
||||
className="grid w-full py-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
@@ -115,8 +108,8 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-[35rem]"
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
@@ -134,8 +127,10 @@ routers:
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</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
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
|
||||
@@ -16,14 +16,14 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
|
||||
const UpdateServerTraefikConfigSchema = z.object({
|
||||
@@ -92,20 +92,13 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
||||
<DialogTitle>Update traefik config</DialogTitle>
|
||||
<DialogDescription>Update the traefik config</DialogDescription>
|
||||
</DialogHeader>
|
||||
{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">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4 relative"
|
||||
className="grid w-full py-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
@@ -115,8 +108,8 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-[35rem]"
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
@@ -134,8 +127,10 @@ routers:
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</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
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import type React from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
98
components/dashboard/settings/web-server/update-server.tsx
Normal file
98
components/dashboard/settings/web-server/update-server.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
import { UpdateWebServer } from "./update-webserver";
|
||||
|
||||
export const UpdateServer = () => {
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState<null | boolean>(
|
||||
null,
|
||||
);
|
||||
const { mutateAsync: checkAndUpdateImage, isLoading } =
|
||||
api.settings.checkAndUpdateImage.useMutation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
Updates
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:m:max-w-lg ">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Web Server Update</DialogTitle>
|
||||
<DialogDescription>
|
||||
Check new releases and update your dokploy
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
We suggest to update your dokploy to the latest version only if you:
|
||||
</span>
|
||||
<ul className="list-disc list-inside text-sm text-muted-foreground">
|
||||
<li>Want to try the latest features</li>
|
||||
<li>Some bug that is blocking to use some features</li>
|
||||
</ul>
|
||||
<AlertBlock type="info">
|
||||
Please we recommend to see the latest version to see if there are
|
||||
any breaking changes before updating. Go to{" "}
|
||||
<Link
|
||||
href="https://github.com/Dokploy/dokploy/releases"
|
||||
target="_blank"
|
||||
className="text-foreground"
|
||||
>
|
||||
Dokploy Releases
|
||||
</Link>{" "}
|
||||
to check the latest version.
|
||||
</AlertBlock>
|
||||
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
{isUpdateAvailable === false && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<RefreshCcw className="size-6 self-center text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
You are using the latest version
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isUpdateAvailable ? (
|
||||
<UpdateWebServer />
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
await checkAndUpdateImage()
|
||||
.then(async (e) => {
|
||||
setIsUpdateAvailable(e);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsUpdateAvailable(false);
|
||||
toast.error("Error to check updates");
|
||||
});
|
||||
toast.success("Check updates");
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Check updates
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -19,7 +19,11 @@ export const UpdateWebServer = () => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button className="relative" variant="secondary" isLoading={isLoading}>
|
||||
<Button
|
||||
className="relative w-full"
|
||||
variant="secondary"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<span className="absolute -right-1 -top-2 flex h-3 w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
|
||||
|
||||
@@ -10,19 +10,15 @@ export const DashboardLayout = ({ children, tab }: Props) => {
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="w-full">
|
||||
<Navbar />
|
||||
<main className="mt-6 flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-8xl px-4 lg:px-8">
|
||||
<NavigationTabs tab={tab}>{children}</NavigationTabs>
|
||||
</div>
|
||||
</main>
|
||||
<Navbar />
|
||||
<main className="pt-6 flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-8xl px-4 lg:px-8">
|
||||
<NavigationTabs tab={tab}>{children}</NavigationTabs>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ export const Navbar = () => {
|
||||
const { mutateAsync } = api.auth.logout.useMutation();
|
||||
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">
|
||||
<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">
|
||||
<Link
|
||||
href="/dashboard/projects"
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { AddProject } from "@/components/dashboard/projects/add";
|
||||
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 { 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 =
|
||||
| "projects"
|
||||
@@ -11,6 +21,41 @@ export type TabState =
|
||||
| "traefik"
|
||||
| "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 {
|
||||
tab: TabState;
|
||||
children: React.ReactNode;
|
||||
@@ -34,23 +79,19 @@ export const NavigationTabs = ({ tab, children }: Props) => {
|
||||
setActiveTab(tab);
|
||||
}, [tab]);
|
||||
|
||||
const activeTabInfo = useMemo(() => {
|
||||
return tabMap[activeTab];
|
||||
}, [activeTab]);
|
||||
|
||||
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">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-xl font-bold lg:text-3xl">
|
||||
{tab === "projects" && "Projects"}
|
||||
{tab === "monitoring" && "Monitoring"}
|
||||
{tab === "settings" && "Settings"}
|
||||
{tab === "traefik" && "Traefik"}
|
||||
{tab === "docker" && "Docker"}
|
||||
{activeTabInfo.label}
|
||||
</h1>
|
||||
<p className="lg:text-medium text-muted-foreground">
|
||||
{tab === "projects" && "Manage your deployments"}
|
||||
{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"}
|
||||
{activeTabInfo.description}
|
||||
</p>
|
||||
</div>
|
||||
{tab === "projects" &&
|
||||
@@ -60,54 +101,36 @@ export const NavigationTabs = ({ tab, children }: Props) => {
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
if (e === "settings") {
|
||||
router.push("/dashboard/settings/server");
|
||||
} else {
|
||||
router.push(`/dashboard/${e}`);
|
||||
}
|
||||
onValueChange={async (e) => {
|
||||
setActiveTab(e as TabState);
|
||||
router.push(tabMap[e as TabState].index);
|
||||
}}
|
||||
>
|
||||
{/* 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">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 justify-start bg-transparent">
|
||||
<TabsTrigger
|
||||
value="projects"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Projects
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="monitoring"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
|
||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
||||
<TabsTrigger
|
||||
value="traefik"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Traefik File System
|
||||
</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>
|
||||
<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="bg-transparent relative px-0">
|
||||
{Object.keys(tabMap).map((key) => {
|
||||
const tab = tabMap[key as TabState];
|
||||
if (tab.isShow && !tab.isShow?.({ rol: data?.rol, user })) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={key}
|
||||
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"
|
||||
>
|
||||
<span className="relative z-[1] w-full">
|
||||
{tab.tabLabel || tab.label}
|
||||
</span>
|
||||
{key === activeTab && (
|
||||
<div className="absolute -bottom-[5.5px] w-full">
|
||||
<div className="h-0.5 bg-foreground rounded-t-md" />
|
||||
</div>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export const ProjectLayout = ({ children }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="bg-radial relative flex flex-col bg-background pt-6"
|
||||
className="bg-radial relative flex flex-col bg-background"
|
||||
id="app-container"
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
|
||||
@@ -59,6 +59,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: Users,
|
||||
href: "/dashboard/settings/users",
|
||||
},
|
||||
{
|
||||
title: "Cluster",
|
||||
label: "",
|
||||
icon: Server,
|
||||
href: "/dashboard/settings/cluster",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
@@ -75,6 +81,7 @@ import {
|
||||
Activity,
|
||||
Database,
|
||||
Route,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
User2,
|
||||
Users,
|
||||
@@ -102,7 +109,6 @@ export const Nav = ({ links }: NavProps) => {
|
||||
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
||||
{links.map((link, index) => {
|
||||
const isActive = router.pathname === link.href;
|
||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
@@ -110,7 +116,7 @@ export const Nav = ({ links }: NavProps) => {
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "sm" }),
|
||||
isActive &&
|
||||
"dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white",
|
||||
"dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white bg-muted",
|
||||
"justify-start",
|
||||
)}
|
||||
>
|
||||
|
||||
51
components/shared/alert-block.tsx
Normal file
51
components/shared/alert-block.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
components/shared/code-editor.tsx
Normal file
45
components/shared/code-editor.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
interface Props extends ReactCodeMirrorProps {
|
||||
wrapperClassName?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const CodeEditor = ({
|
||||
className,
|
||||
wrapperClassName,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={cn("relative overflow-auto", wrapperClassName)}>
|
||||
<CodeMirror
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
highlightSelectionMatches: true,
|
||||
highlightActiveLine: !props.disabled,
|
||||
allowMultipleSelections: true,
|
||||
}}
|
||||
theme={resolvedTheme === "dark" ? githubDark : githubLight}
|
||||
extensions={[yaml(), json()]}
|
||||
{...props}
|
||||
editable={!props.disabled}
|
||||
className={cn(
|
||||
"w-full h-full text-sm leading-relaxed",
|
||||
`cm-theme-${resolvedTheme}`,
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
{props.disabled && (
|
||||
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,19 +4,26 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
|
||||
interface Props {
|
||||
date: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DateTooltip = ({ date, children }: Props) => {
|
||||
export const DateTooltip = ({ date, children, className }: Props) => {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="flex items-center text-muted-foreground text-left">
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center text-muted-foreground text-left",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}{" "}
|
||||
{formatDistanceToNow(new Date(date), {
|
||||
addSuffix: true,
|
||||
|
||||
@@ -17,21 +17,21 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
{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" && (
|
||||
<div
|
||||
className={cn(" size-3.5 rounded-full bg-destructive", className)}
|
||||
className={cn("size-3.5 rounded-full bg-destructive", className)}
|
||||
/>
|
||||
)}
|
||||
{status === "done" && (
|
||||
<div
|
||||
className={cn(" size-3.5 rounded-full bg-primary", className)}
|
||||
className={cn("size-3.5 rounded-full bg-primary", className)}
|
||||
/>
|
||||
)}
|
||||
{status === "running" && (
|
||||
<div
|
||||
className={cn(" size-3.5 rounded-full bg-yellow-500", className)}
|
||||
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
|
||||
/>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
|
||||
59
components/ui/alert.tsx
Normal file
59
components/ui/alert.tsx
Normal 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 }
|
||||
@@ -88,8 +88,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
|
||||
const { ref: refRoot, width, height } = useResizeObserver();
|
||||
|
||||
return (
|
||||
<div ref={refRoot} className={cn("overflow-hidden", className)}>
|
||||
{/* style={{ width, height }} */}
|
||||
<div ref={refRoot} className={cn("overflow-y-auto", className)}>
|
||||
<ScrollArea>
|
||||
<div className="relative p-2">
|
||||
<TreeItem
|
||||
@@ -165,7 +164,7 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm truncate">{item.name}</span>
|
||||
<span className="text-sm truncate font-mono">{item.name}</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pl-6">
|
||||
{item.children.length === 0 && (
|
||||
@@ -244,7 +243,7 @@ const Leaf = React.forwardRef<
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<p className=" text-sm whitespace-normal">{item.name}</p>
|
||||
<p className=" text-sm whitespace-normal font-mono">{item.name}</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -147,7 +147,6 @@ const FormMessage = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user