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
|
- name: Run Build
|
||||||
run: pnpm 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:
|
build-and-push-docker-on-push:
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -75,7 +53,5 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Docker image using custom script
|
- name: Build and push Docker image using custom script
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./docker/build.sh
|
|
||||||
chmod +x ./docker/push.sh
|
chmod +x ./docker/push.sh
|
||||||
./docker/build.sh ${{ github.ref_name == 'canary' && 'canary' || '' }}
|
|
||||||
./docker/push.sh ${{ github.ref_name == 'canary' && 'canary' || '' }}
|
./docker/push.sh ${{ github.ref_name == 'canary' && 'canary' || '' }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,3 +54,4 @@ yarn-error.log*
|
|||||||
/.main
|
/.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
|
In the case you lost your password, you can reset it using the following command
|
||||||
|
|
||||||
```bash
|
```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`
|
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ FROM node:18-slim AS production
|
|||||||
# Install dependencies only for production
|
# Install dependencies only for production
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-l
|
|||||||
# Install docker
|
# Install docker
|
||||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
||||||
|
|
||||||
|
|
||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and limitations under the License.
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
Appendix:
|
## Additional Terms for Specific Features
|
||||||
In case of a conflict, the terms of this appendix supersede the general apache license.
|
|
||||||
|
|
||||||
- Unless provided with a written agreement or permission, the paid features of Dokploy (as a service) cannot be modified.
|
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:
|
||||||
- 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.
|
|
||||||
|
|
||||||
|
- **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
|
## 🌟 Features
|
||||||
|
|
||||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease.
|
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease.
|
||||||
@@ -38,6 +44,8 @@ Tested Systems:
|
|||||||
|
|
||||||
- Ubuntu 20.04
|
- Ubuntu 20.04
|
||||||
- Debian 11
|
- Debian 11
|
||||||
|
- Fedora 40
|
||||||
|
- Centos 9
|
||||||
|
|
||||||
## 📄 Documentation
|
## 📄 Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
# Terms & Conditions
|
# 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:
|
### 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
|
### 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
|
### 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,
|
id: authId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Signin succesfully", {
|
toast.success("Signin successfully", {
|
||||||
duration: 2000,
|
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;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectSchema = z.object({
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
type AddCommand = z.infer<typeof AddRedirectSchema>;
|
||||||
|
|
||||||
export const AddCommand = ({ applicationId }: Props) => {
|
export const AddCommand = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const { data } = api.application.one.useQuery(
|
||||||
@@ -48,7 +48,7 @@ export const AddCommand = ({ applicationId }: Props) => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
command: "",
|
command: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddRedirectchema),
|
resolver: zodResolver(AddRedirectSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const AddPortchema = z.object({
|
const AddPortSchema = z.object({
|
||||||
publishedPort: z.number().int().min(1).max(65535),
|
publishedPort: z.number().int().min(1).max(65535),
|
||||||
targetPort: z.number().int().min(1).max(65535),
|
targetPort: z.number().int().min(1).max(65535),
|
||||||
protocol: z.enum(["tcp", "udp"], {
|
protocol: z.enum(["tcp", "udp"], {
|
||||||
@@ -41,7 +41,7 @@ const AddPortchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddPort = z.infer<typeof AddPortchema>;
|
type AddPort = z.infer<typeof AddPortSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -62,7 +62,7 @@ export const AddPort = ({
|
|||||||
publishedPort: 0,
|
publishedPort: 0,
|
||||||
targetPort: 0,
|
targetPort: 0,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddPortchema),
|
resolver: zodResolver(AddPortSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -100,14 +100,7 @@ export const AddPort = ({
|
|||||||
Ports are used to expose your application to the internet.
|
Ports are used to expose your application to the internet.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -98,7 +99,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
@@ -106,14 +107,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<DialogTitle>Update</DialogTitle>
|
<DialogTitle>Update</DialogTitle>
|
||||||
<DialogDescription>Update the port</DialogDescription>
|
<DialogDescription>Update the port</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -98,14 +98,7 @@ export const AddRedirect = ({
|
|||||||
Redirects are used to redirect requests to another url.
|
Redirects are used to redirect requests to another url.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -93,7 +94,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
@@ -101,14 +102,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
|||||||
<DialogTitle>Update</DialogTitle>
|
<DialogTitle>Update</DialogTitle>
|
||||||
<DialogDescription>Update the redirect</DialogDescription>
|
<DialogDescription>Update the redirect</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -90,14 +90,7 @@ export const AddSecurity = ({
|
|||||||
Add security to your application
|
Add security to your application
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -88,7 +89,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
@@ -96,14 +97,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
|||||||
<DialogTitle>Update</DialogTitle>
|
<DialogTitle>Update</DialogTitle>
|
||||||
<DialogDescription>Update the security</DialogDescription>
|
<DialogDescription>Update the security</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { File } from "lucide-react";
|
import { File } from "lucide-react";
|
||||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
@@ -43,11 +44,13 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 relative">
|
<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 className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||||
<div>
|
<CodeEditor
|
||||||
<pre className="font-sans">{data || "Empty"}</pre>
|
value={data || "Empty"}
|
||||||
</div>
|
disabled
|
||||||
<div className="flex justify-end absolute z-50 right-6">
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end absolute z-50 right-6 top-6">
|
||||||
<UpdateTraefikConfig applicationId={applicationId} />
|
<UpdateTraefikConfig applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import jsyaml from "js-yaml";
|
import jsyaml from "js-yaml";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
|
||||||
const UpdateTraefikConfigSchema = z.object({
|
const UpdateTraefikConfigSchema = z.object({
|
||||||
traefikConfig: z.string(),
|
traefikConfig: z.string(),
|
||||||
@@ -116,20 +116,13 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
<DialogDescription>Update the traefik config</DialogDescription>
|
<DialogDescription>Update the traefik config</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-update-traefik-config"
|
id="hook-form-update-traefik-config"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full py-4"
|
className="grid w-full py-4 overflow-auto"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -139,8 +132,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
className="h-[35rem]"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`http:
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
router-name:
|
router-name:
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export const AddVolumes = ({
|
|||||||
<DialogTitle>Volumes / Mounts</DialogTitle>
|
<DialogTitle>Volumes / Mounts</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* {isError && (
|
{/* {isError && (
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
{error?.message}
|
{error?.message}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export const DeleteApplication = ({ applicationId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 " />
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -106,14 +107,7 @@ export const AddDomain = ({
|
|||||||
In this section you can add custom domains
|
In this section you can add custom domains
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export const DeleteDomain = ({ domainId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 " />
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<AddDomain applicationId={applicationId} />
|
<AddDomain applicationId={applicationId}>
|
||||||
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</AddDomain>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<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
|
To access to the application is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<AddDomain applicationId={applicationId}>Add Domain</AddDomain>
|
<AddDomain applicationId={applicationId}>
|
||||||
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</AddDomain>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
@@ -75,8 +79,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
<Button variant="outline" disabled>
|
<Button variant="outline" disabled>
|
||||||
{item.https ? "HTTPS" : "HTTP"}
|
{item.https ? "HTTPS" : "HTTP"}
|
||||||
</Button>
|
</Button>
|
||||||
<UpdateDomain domainId={item.domainId} />
|
<div className="flex flex-row gap-1">
|
||||||
<DeleteDomain domainId={item.domainId} />
|
<UpdateDomain domainId={item.domainId} />
|
||||||
|
<DeleteDomain domainId={item.domainId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -114,8 +115,8 @@ export const UpdateDomain = ({ domainId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>
|
<Button variant="ghost">
|
||||||
<PenBoxIcon className="size-4" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
@@ -125,14 +126,7 @@ export const UpdateDomain = ({ domainId }: Props) => {
|
|||||||
In this section you can add custom domains
|
In this section you can add custom domains
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="NODE_ENV=production"
|
placeholder="NODE_ENV=production"
|
||||||
className="h-96"
|
className="h-96 font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,7 +90,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<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>
|
<DialogTitle>Modify Application</DialogTitle>
|
||||||
<DialogDescription>Update the application data</DialogDescription>
|
<DialogDescription>Update the application data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -115,7 +115,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ export const ShowContainerConfig = ({ containerId }: Props) => {
|
|||||||
View Config
|
View Config
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"w-full md:w-[70vw] max-w-max"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Container Config</DialogTitle>
|
<DialogTitle>Container Config</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
See in detail the config of this container
|
See in detail the config of this container
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem] bg-card">
|
<div className="text-wrap rounded-lg border p-4 text-sm bg-card overflow-y-auto max-h-[80vh]">
|
||||||
<code>
|
<code>
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
{JSON.stringify(data, null, 2)}
|
{JSON.stringify(data, null, 2)}
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
|||||||
cols: 80,
|
cols: 80,
|
||||||
rows: 30,
|
rows: 30,
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
|
fontWeight: 400,
|
||||||
|
|
||||||
convertEol: true,
|
convertEol: true,
|
||||||
theme: {
|
theme: {
|
||||||
cursor: "transparent",
|
cursor: "transparent",
|
||||||
background: "#19191A",
|
background: "rgba(0, 0, 0, 0)",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,8 +82,8 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full h-full bg-input rounded-lg p-2 ">
|
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
||||||
<div id={id} className="rounded-xl" />
|
<div id={id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Terminal } from "@xterm/xterm";
|
|||||||
import { FitAddon } from "xterm-addon-fit";
|
import { FitAddon } from "xterm-addon-fit";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import { AttachAddon } from "@xterm/addon-attach";
|
import { AttachAddon } from "@xterm/addon-attach";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,7 +12,7 @@ interface Props {
|
|||||||
|
|
||||||
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||||
const termRef = useRef(null);
|
const termRef = useRef(null);
|
||||||
const [activeWay, setActiveWay] = React.useState<string | null>("bash");
|
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = document.getElementById(id);
|
const container = document.getElementById(id);
|
||||||
if (container) {
|
if (container) {
|
||||||
@@ -26,7 +26,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
|||||||
convertEol: true,
|
convertEol: true,
|
||||||
theme: {
|
theme: {
|
||||||
cursor: "transparent",
|
cursor: "transparent",
|
||||||
background: "#19191A",
|
background: "rgba(0, 0, 0, 0)",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const addonFit = new FitAddon();
|
const addonFit = new FitAddon();
|
||||||
@@ -54,14 +54,15 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
|||||||
<span>
|
<span>
|
||||||
Select way to connect to <b>{containerId}</b>
|
Select way to connect to <b>{containerId}</b>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 w-fit">
|
<Tabs value={activeWay} onValueChange={setActiveWay}>
|
||||||
<Button onClick={() => setActiveWay("sh")}>SH</Button>
|
<TabsList>
|
||||||
<Button onClick={() => setActiveWay("bash")}>Bash</Button>
|
<TabsTrigger value="bash">Bash</TabsTrigger>
|
||||||
<Button onClick={() => setActiveWay("sh")}>/bin/sh</Button>
|
<TabsTrigger value="sh">/bin/sh</TabsTrigger>
|
||||||
</div>
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full bg-input rounded-lg p-2 ">
|
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
||||||
<div id={id} ref={termRef} className="rounded-xl" />
|
<div id={id} ref={termRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
|
||||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||||
traefikConfig: z.string(),
|
traefikConfig: z.string(),
|
||||||
@@ -86,31 +86,25 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full py-4 relative"
|
className="grid w-full relative z-[5]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col overflow-auto">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="traefikConfig"
|
name="traefikConfig"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="relative">
|
<FormItem className="relative">
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormDescription>{path}</FormDescription>
|
<FormDescription className="break-all">
|
||||||
|
{path}
|
||||||
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
className="h-[35rem]"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`http:
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
router-name:
|
router-name:
|
||||||
@@ -128,8 +122,9 @@ routers:
|
|||||||
<pre>
|
<pre>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-10">
|
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||||
<Button
|
<Button
|
||||||
|
className="shadow-sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export const ShowTraefikSystem = () => {
|
|||||||
const { data: directories } = api.settings.readDirectories.useQuery();
|
const { data: directories } = api.settings.readDirectories.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("mt-6 grid gap-4 pb-20")}>
|
<div className={cn("mt-6 md:grid gap-4")}>
|
||||||
<div className="flex flex-col md:flex-row gap-4 md:gap-10 w-full">
|
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||||
{directories?.length === 0 && (
|
{directories?.length === 0 && (
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
@@ -27,7 +27,7 @@ export const ShowTraefikSystem = () => {
|
|||||||
<>
|
<>
|
||||||
<Tree
|
<Tree
|
||||||
data={directories}
|
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)}
|
onSelectChange={(item) => setFile(item?.id || null)}
|
||||||
folderIcon={Folder}
|
folderIcon={Folder}
|
||||||
itemIcon={Workflow}
|
itemIcon={Workflow}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 " />
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
mariadbId,
|
mariadbId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
toast.success("Database delete succesfully");
|
toast.success("Database delete succesfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the database");
|
toast.error("Error to delete the database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="MARIADB_PASSWORD=1234567678"
|
placeholder="MARIADB_PASSWORD=1234567678"
|
||||||
className="h-96"
|
className="h-96 font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,7 +90,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<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>
|
<DialogTitle>Modify MariaDB</DialogTitle>
|
||||||
<DialogDescription>Update the MariaDB data</DialogDescription>
|
<DialogDescription>Update the MariaDB data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteMongo = ({ mongoId }: Props) => {
|
export const DeleteMongo = ({ mongoId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 " />
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
mongoId,
|
mongoId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
toast.success("Database delete succesfully");
|
toast.success("Database delete succesfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the database");
|
toast.error("Error to delete the database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="MONGO_PASSWORD=1234567678"
|
placeholder="MONGO_PASSWORD=1234567678"
|
||||||
className="h-96"
|
className="h-96 font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,7 +90,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<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>
|
<DialogTitle>Modify MongoDB</DialogTitle>
|
||||||
<DialogDescription>Update the MongoDB data</DialogDescription>
|
<DialogDescription>Update the MongoDB data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteMysql = ({ mysqlId }: Props) => {
|
export const DeleteMysql = ({ mysqlId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 " />
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
mysqlId,
|
mysqlId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
toast.success("Database delete succesfully");
|
toast.success("Database delete succesfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the database");
|
toast.error("Error to delete the database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="MYSQL_PASSWORD=1234567678"
|
placeholder="MYSQL_PASSWORD=1234567678"
|
||||||
className="h-96"
|
className="h-96 font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,7 +90,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<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>
|
<DialogTitle>Modify MySQL</DialogTitle>
|
||||||
<DialogDescription>Update the MySQL data</DialogDescription>
|
<DialogDescription>Update the MySQL data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeletePostgres = ({ postgresId }: Props) => {
|
export const DeletePostgres = ({ postgresId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 " />
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
postgresId,
|
postgresId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
toast.success("Database delete succesfully");
|
toast.success("Database delete succesfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the database");
|
toast.error("Error to delete the database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="POSTGRES_PASSWORD=1234567678"
|
placeholder="POSTGRES_PASSWORD=1234567678"
|
||||||
className="h-96"
|
className="h-96 font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
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());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,7 +90,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<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>
|
<DialogTitle>Modify Postgres</DialogTitle>
|
||||||
<DialogDescription>Update the Postgres data</DialogDescription>
|
<DialogDescription>Update the Postgres data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Folder } from "lucide-react";
|
import { Folder } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -93,14 +94,7 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
Assign a name and description to your application
|
Assign a name and description to your application
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
<DialogTitle>Databases</DialogTitle>
|
<DialogTitle>Databases</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* {isError && (
|
{/* {isError && (
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
{error?.message}
|
{error?.message}
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -85,14 +86,7 @@ export const AddProject = () => {
|
|||||||
<DialogTitle>Add a project</DialogTitle>
|
<DialogTitle>Add a project</DialogTitle>
|
||||||
<DialogDescription>The home of something big!</DialogDescription>
|
<DialogDescription>The home of something big!</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-add-project"
|
id="hook-form-add-project"
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ export const ShowProjects = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data?.length === 0 && (
|
{data?.length === 0 && (
|
||||||
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center ">
|
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
||||||
<FolderInput className="size-10 md:size-28 text-muted" />
|
<FolderInput className="size-10 md:size-28 text-muted-foreground" />
|
||||||
<span className="text-center font-medium text-muted-foreground">
|
<span className="text-center font-medium text-muted-foreground">
|
||||||
No projects added yet. Click on Create project.
|
No projects added yet. Click on Create project.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,9 +89,9 @@ export const ShowProjects = () => {
|
|||||||
<span className="flex flex-col gap-1.5">
|
<span className="flex flex-col gap-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BookIcon className="size-4 text-muted-foreground" />
|
<BookIcon className="size-4 text-muted-foreground" />
|
||||||
<span className="text-base font-medium leading-none">
|
<Link className="text-base font-medium leading-none" href={`/dashboard/project/${project.projectId}`}>
|
||||||
{project.name}
|
{project.name}
|
||||||
</span>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -101,14 +102,7 @@ export const UpdateProject = ({ projectId }: Props) => {
|
|||||||
Update the details of the project
|
Update the details of the project
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteRedis = ({ redisId }: Props) => {
|
export const DeleteRedis = ({ redisId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
|
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<TrashIcon className="size-4 " />
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
database
|
database
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
redisId,
|
redisId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
push(`/dashboard/project/${data?.projectId}`);
|
||||||
toast.success("Database delete succesfully");
|
toast.success("Database delete succesfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the database");
|
toast.error("Error to delete the database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,7 +90,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<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>
|
<DialogTitle>Modify Redis</DialogTitle>
|
||||||
<DialogDescription>Update the redis data</DialogDescription>
|
<DialogDescription>Update the redis data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
const appearanceFormSchema = z.object({
|
const appearanceFormSchema = z.object({
|
||||||
theme: z.enum(["light", "dark"], {
|
theme: z.enum(["light", "dark", "system"], {
|
||||||
required_error: "Please select a theme.",
|
required_error: "Please select a theme.",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -34,7 +34,7 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
|
|||||||
|
|
||||||
// This can come from your database or API.
|
// This can come from your database or API.
|
||||||
const defaultValues: Partial<AppearanceFormValues> = {
|
const defaultValues: Partial<AppearanceFormValues> = {
|
||||||
theme: "light",
|
theme: "system",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppearanceForm() {
|
export function AppearanceForm() {
|
||||||
@@ -46,7 +46,7 @@ export function AppearanceForm() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
theme: theme === "light" ? "light" : "dark",
|
theme: (theme ?? "system") as AppearanceFormValues["theme"],
|
||||||
});
|
});
|
||||||
}, [form, theme]);
|
}, [form, theme]);
|
||||||
function onSubmit(data: AppearanceFormValues) {
|
function onSubmit(data: AppearanceFormValues) {
|
||||||
@@ -81,28 +81,15 @@ export function AppearanceForm() {
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
value={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>
|
<FormItem>
|
||||||
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
|
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value="light" className="sr-only" />
|
<RadioGroupItem value="light" className="sr-only" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
<div className="items-center rounded-md border-2 border-muted p-1 hover:bg-accent transition-colors cursor-pointer">
|
||||||
<div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
|
<img src="/images/theme-light.svg" alt="light" />
|
||||||
<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>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
Light
|
Light
|
||||||
@@ -114,27 +101,30 @@ export function AppearanceForm() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value="dark" className="sr-only" />
|
<RadioGroupItem value="dark" className="sr-only" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
|
<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">
|
||||||
<div className="space-y-2 rounded-sm bg-slate-950 p-2">
|
<img src="/images/theme-dark.svg" alt="dark" />
|
||||||
<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>
|
</div>
|
||||||
<span className="block w-full p-2 text-center font-normal">
|
<span className="block w-full p-2 text-center font-normal">
|
||||||
Dark
|
Dark
|
||||||
</span>
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</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>
|
</RadioGroup>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -26,140 +27,135 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const certificateDataHolder = `-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`;
|
||||||
|
|
||||||
const addCertificate = z.object({
|
const addCertificate = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
certificateData: z.string().min(1, "Certificate data is required"),
|
certificateData: z.string().min(1, "Certificate data is required"),
|
||||||
privateKey: z.string().min(1, "Private key is required"),
|
privateKey: z.string().min(1, "Private key is required"),
|
||||||
autoRenew: z.boolean().optional(),
|
autoRenew: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCertificate = z.infer<typeof addCertificate>;
|
type AddCertificate = z.infer<typeof addCertificate>;
|
||||||
|
|
||||||
export const AddCertificate = () => {
|
export const AddCertificate = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.certificates.create.useMutation();
|
api.certificates.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddCertificate>({
|
const form = useForm<AddCertificate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
certificateData: "",
|
certificateData: "",
|
||||||
privateKey: "",
|
privateKey: "",
|
||||||
autoRenew: false,
|
autoRenew: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addCertificate),
|
resolver: zodResolver(addCertificate),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset();
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddCertificate) => {
|
const onSubmit = async (data: AddCertificate) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
certificateData: data.certificateData,
|
certificateData: data.certificateData,
|
||||||
privateKey: data.privateKey,
|
privateKey: data.privateKey,
|
||||||
autoRenew: data.autoRenew,
|
autoRenew: data.autoRenew,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Certificate Created");
|
toast.success("Certificate Created");
|
||||||
await utils.certificates.all.invalidate();
|
await utils.certificates.all.invalidate();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the Certificate");
|
toast.error("Error to create the Certificate");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>Add Certificate</Button>
|
<Button>Add Certificate</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add Certificate</DialogTitle>
|
<DialogTitle>Add Certificate</DialogTitle>
|
||||||
<DialogDescription>Add a new certificate</DialogDescription>
|
<DialogDescription>Add a new certificate</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-add-certificate"
|
id="hook-form-add-certificate"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-4 "
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Certificate Name</FormLabel>
|
<FormLabel>Certificate Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={"My Certificate"} {...field} />
|
<Input placeholder={"My Certificate"} {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="certificateData"
|
name="certificateData"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Certificate Data</FormLabel>
|
<FormLabel>Certificate Data</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="h-32"
|
className="h-32"
|
||||||
placeholder={`-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`}
|
placeholder={certificateDataHolder}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="privateKey"
|
name="privateKey"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Private Key</FormLabel>
|
<FormLabel>Private Key</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="h-32"
|
className="h-32"
|
||||||
placeholder={`-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----`}
|
placeholder={certificateDataHolder}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-add-certificate"
|
form="hook-form-add-certificate"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,21 +33,23 @@ export const ShowCertificates = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{data?.map((destination, index) => (
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
<div
|
{data?.map((destination, index) => (
|
||||||
key={destination.certificateId}
|
<div
|
||||||
className="flex items-center justify-between"
|
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 className="text-sm text-muted-foreground">
|
||||||
</span>
|
{index + 1}. {destination.name}
|
||||||
<div className="flex flex-row gap-3">
|
</span>
|
||||||
<DeleteCertificate
|
<div className="flex flex-row gap-3">
|
||||||
certificateId={destination.certificateId}
|
<DeleteCertificate
|
||||||
/>
|
certificateId={destination.certificateId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<AddCertificate />
|
<AddCertificate />
|
||||||
</div>
|
</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";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -87,14 +87,7 @@ export const AddDestination = () => {
|
|||||||
In this section you can add destinations for your backups.
|
In this section you can add destinations for your backups.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -34,16 +34,16 @@ export const ShowDestinations = () => {
|
|||||||
<AddDestination />
|
<AddDestination />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-4">
|
||||||
{data?.map((destination, index) => (
|
{data?.map((destination, index) => (
|
||||||
<div
|
<div
|
||||||
key={destination.destinationId}
|
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">
|
<span className="text-sm text-muted-foreground">
|
||||||
{index + 1}. {destination.name}
|
{index + 1}. {destination.name}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-3">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateDestination
|
<UpdateDestination
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -111,14 +112,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
Update the current destination config
|
Update the current destination config
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ export const GithubSetup = () => {
|
|||||||
{haveGithubConfigured ? (
|
{haveGithubConfigured ? (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground text-sm">
|
||||||
Github account configured succesfully.
|
Github account configured succesfully.
|
||||||
</span>
|
</span>
|
||||||
<BadgeCheck className="size-5 text-green-700" />
|
<BadgeCheck className="size-4 text-green-700" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<RemoveGithubApp />
|
<RemoveGithubApp />
|
||||||
@@ -101,9 +101,9 @@ export const GithubSetup = () => {
|
|||||||
{data?.githubAppName ? (
|
{data?.githubAppName ? (
|
||||||
<div className="flex w-fit flex-col gap-4">
|
<div className="flex w-fit flex-col gap-4">
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
Youve successfully created a GitHub app named
|
You've successfully created a github app named{" "}
|
||||||
{data.githubAppName}! The next step is to install this app in
|
<strong>{data.githubAppName}</strong>! The next step is to
|
||||||
your GitHub account.
|
install this app in your GitHub account.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
@@ -121,12 +121,12 @@ export const GithubSetup = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-muted-foreground">
|
<p className="text-muted-foreground text-sm">
|
||||||
To integrate your GitHub account with our services, youll
|
To integrate your GitHub account with our services, you'll
|
||||||
need to create and install a GitHub app. This process is
|
need to create and install a GitHub app. This process is
|
||||||
straightforward and only takes a few minutes. Click the
|
straightforward and only takes a few minutes. Click the
|
||||||
button below to get started.
|
button below to get started.
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col gap-4">
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
|
|||||||
@@ -147,11 +147,11 @@ export const ProfileForm = () => {
|
|||||||
}}
|
}}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
value={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) => (
|
{randomImages.map((image) => (
|
||||||
<FormItem key={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>
|
<FormControl>
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
value={image}
|
value={image}
|
||||||
@@ -163,7 +163,7 @@ export const ProfileForm = () => {
|
|||||||
key={image}
|
key={image}
|
||||||
src={image}
|
src={image}
|
||||||
alt="avatar"
|
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>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ import {
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, ListTodo } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
const addPermissions = z.object({
|
const addPermissions = z.object({
|
||||||
accesedProjects: z.array(z.string()).optional(),
|
accesedProjects: z.array(z.string()).optional(),
|
||||||
@@ -106,23 +107,19 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button variant="ghost">
|
<DropdownMenuItem
|
||||||
<ListTodo className="size-4 text-muted-foreground " />
|
className="w-full cursor-pointer"
|
||||||
</Button>
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Add Permissions
|
||||||
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Permissions</DialogTitle>
|
<DialogTitle>Permissions</DialogTitle>
|
||||||
<DialogDescription>Add or remove permissions</DialogDescription>
|
<DialogDescription>Add or remove permissions</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
const addUser = z.object({
|
const addUser = z.object({
|
||||||
email: z
|
email: z
|
||||||
@@ -66,21 +67,16 @@ export const AddUser = () => {
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>Add User</Button>
|
<Button>
|
||||||
|
<PlusIcon className="h-4 w-4" /> Add User
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add User</DialogTitle>
|
<DialogTitle>Add User</DialogTitle>
|
||||||
<DialogDescription>Invite a new user</DialogDescription>
|
<DialogDescription>Invite a new user</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
authId: string;
|
authId: string;
|
||||||
@@ -24,9 +25,12 @@ export const DeleteUser = ({ authId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<DropdownMenuItem
|
||||||
<TrashIcon className="size-4 text-muted-foreground " />
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
</Button>
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Delete User
|
||||||
|
</DropdownMenuItem>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { CopyIcon, Users } from "lucide-react";
|
import { MoreHorizontal, Users } from "lucide-react";
|
||||||
import { AddUser } from "./add-user";
|
import { AddUser } from "./add-user";
|
||||||
import { DeleteUser } from "./delete-user";
|
import { DeleteUser } from "./delete-user";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
@@ -14,7 +14,24 @@ import { useEffect, useState } from "react";
|
|||||||
import { AddUserPermissions } from "./add-permissions";
|
import { AddUserPermissions } from "./add-permissions";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { toast } from "sonner";
|
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 = () => {
|
export const ShowUsers = () => {
|
||||||
const { data } = api.user.all.useQuery();
|
const { data } = api.user.all.useQuery();
|
||||||
@@ -25,82 +42,109 @@ export const ShowUsers = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full col-span-2">
|
<div className="h-full col-span-2">
|
||||||
<Card className="bg-transparent h-full border-none">
|
<Card className="bg-transparent h-full ">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
|
||||||
<CardTitle className="text-xl">Users</CardTitle>
|
<div className="flex flex-col gap-2">
|
||||||
<CardDescription>Add, manage and delete users.</CardDescription>
|
<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>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 h-full">
|
<CardContent className="space-y-2 h-full">
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Users className="size-8 self-center text-muted-foreground" />
|
<Users className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base 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>
|
</span>
|
||||||
<AddUser />
|
<AddUser />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{data?.map((user) => {
|
<Table>
|
||||||
return (
|
<TableCaption>See all users</TableCaption>
|
||||||
<div
|
<TableHeader>
|
||||||
key={user.userId}
|
<TableRow>
|
||||||
className="flex gap-2 flex-col justify-start border p-4 rounded-lg"
|
<TableHead className="w-[100px]">Email</TableHead>
|
||||||
>
|
<TableHead className="text-center">Status</TableHead>
|
||||||
<span className="text-sm text-muted-foreground">
|
<TableHead className="text-center">2FA</TableHead>
|
||||||
{user.auth.email}
|
<TableHead className="text-center">Expiration</TableHead>
|
||||||
</span>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
{!user.isRegistered && (
|
</TableRow>
|
||||||
<span className="text-sm text-muted-foreground">
|
</TableHeader>
|
||||||
Expire In{" "}
|
<TableBody>
|
||||||
{format(new Date(user.expirationDate), "PPpp")}
|
{data?.map((user) => {
|
||||||
</span>
|
return (
|
||||||
)}
|
<TableRow key={user.userId}>
|
||||||
|
<TableCell className="w-[100px]">
|
||||||
<span className="text-sm text-muted-foreground">
|
{user.auth.email}
|
||||||
{user.isRegistered ? "Registered" : "Not Registered"}
|
</TableCell>
|
||||||
</span>
|
<TableCell className="text-center">
|
||||||
{user.auth.is2FAEnabled && (
|
<Badge
|
||||||
<span className="text-sm text-muted-foreground">
|
variant={
|
||||||
{user.auth.is2FAEnabled
|
user.isRegistered ? "default" : "secondary"
|
||||||
? "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");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CopyIcon className="size-4 text-muted-foreground" />
|
{user.isRegistered
|
||||||
</button>
|
? "Registered"
|
||||||
</div>
|
: "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 && (
|
<TableCell className="text-right flex justify-end">
|
||||||
<AddUserPermissions userId={user.userId} />
|
<DropdownMenu>
|
||||||
)}
|
<DropdownMenuTrigger asChild>
|
||||||
{user.isRegistered && <UpdateUser authId={user.authId} />}
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<DeleteUser authId={user.authId} />
|
<span className="sr-only">Open menu</span>
|
||||||
</div>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</div>
|
</Button>
|
||||||
);
|
</DropdownMenuTrigger>
|
||||||
})}
|
<DropdownMenuContent align="end">
|
||||||
<div className="flex flex-col justify-end gap-3 w-full items-end">
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<AddUser />
|
{!user.isRegistered && (
|
||||||
</div>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { SquarePen } from "lucide-react";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const updateUserSchema = z.object({
|
const updateUserSchema = z.object({
|
||||||
@@ -100,14 +101,7 @@ export const UpdateUser = ({ authId }: Props) => {
|
|||||||
<DialogTitle>Update User</DialogTitle>
|
<DialogTitle>Update User</DialogTitle>
|
||||||
<DialogDescription>Update the user</DialogDescription>
|
<DialogDescription>Update the user</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
|||||||
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
||||||
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
|
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
|
||||||
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-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 = () => {
|
export const WebServer = () => {
|
||||||
const [fetchAfterFirstRender, setFetchAfterFirstRender] = useState(false);
|
|
||||||
const { data, refetch } = api.admin.one.useQuery();
|
const { data, refetch } = api.admin.one.useQuery();
|
||||||
const { mutateAsync: reloadServer, isLoading } =
|
const { mutateAsync: reloadServer, isLoading } =
|
||||||
api.settings.reloadServer.useMutation();
|
api.settings.reloadServer.useMutation();
|
||||||
@@ -61,13 +60,6 @@ export const WebServer = () => {
|
|||||||
|
|
||||||
const { mutateAsync: updateDockerCleanup } =
|
const { mutateAsync: updateDockerCleanup } =
|
||||||
api.settings.updateDockerCleanup.useMutation();
|
api.settings.updateDockerCleanup.useMutation();
|
||||||
const { data: query } = api.settings.checkAndUpdateImage.useQuery(void 0, {
|
|
||||||
enabled: fetchAfterFirstRender,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFetchAfterFirstRender(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="rounded-lg w-full bg-transparent">
|
<Card className="rounded-lg w-full bg-transparent">
|
||||||
@@ -279,7 +271,7 @@ export const WebServer = () => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
{query ? <UpdateWebServer /> : null}
|
<UpdateServer />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||||
|
|
||||||
const UpdateMainTraefikConfigSchema = z.object({
|
const UpdateMainTraefikConfigSchema = z.object({
|
||||||
@@ -89,14 +89,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
|||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
<DialogDescription>Update the traefik config</DialogDescription>
|
<DialogDescription>Update the traefik config</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -112,8 +105,8 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
|||||||
<FormItem className="relative">
|
<FormItem className="relative">
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
className="h-[35rem]"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`providers:
|
placeholder={`providers:
|
||||||
docker:
|
docker:
|
||||||
defaultRule: 'Host('dokploy.com')'
|
defaultRule: 'Host('dokploy.com')'
|
||||||
@@ -136,8 +129,10 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
|||||||
<pre>
|
<pre>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-10">
|
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||||
<Button
|
<Button
|
||||||
|
className="shadow-sm"
|
||||||
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setCanEdit(!canEdit);
|
setCanEdit(!canEdit);
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||||
|
|
||||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||||
@@ -92,20 +92,13 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
|||||||
<DialogTitle>Update Middleware config</DialogTitle>
|
<DialogTitle>Update Middleware config</DialogTitle>
|
||||||
<DialogDescription>Update the middleware config</DialogDescription>
|
<DialogDescription>Update the middleware config</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-update-server-traefik-config"
|
id="hook-form-update-server-traefik-config"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full py-4 relative"
|
className="grid w-full py-4 relative overflow-auto"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -115,8 +108,8 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
|||||||
<FormItem className="relative">
|
<FormItem className="relative">
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
className="h-[35rem]"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`http:
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
router-name:
|
router-name:
|
||||||
@@ -134,8 +127,10 @@ routers:
|
|||||||
<pre>
|
<pre>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-10">
|
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||||
<Button
|
<Button
|
||||||
|
className="shadow-sm"
|
||||||
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setCanEdit(!canEdit);
|
setCanEdit(!canEdit);
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||||
|
|
||||||
const UpdateServerTraefikConfigSchema = z.object({
|
const UpdateServerTraefikConfigSchema = z.object({
|
||||||
@@ -92,20 +92,13 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
|||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
<DialogDescription>Update the traefik config</DialogDescription>
|
<DialogDescription>Update the traefik config</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && (
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-update-server-traefik-config"
|
id="hook-form-update-server-traefik-config"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full py-4 relative"
|
className="grid w-full py-4 relative overflow-auto"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -115,8 +108,8 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
|||||||
<FormItem className="relative">
|
<FormItem className="relative">
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
className="h-[35rem]"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`http:
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
router-name:
|
router-name:
|
||||||
@@ -134,8 +127,10 @@ routers:
|
|||||||
<pre>
|
<pre>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-10">
|
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||||
<Button
|
<Button
|
||||||
|
className="shadow-sm"
|
||||||
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setCanEdit(!canEdit);
|
setCanEdit(!canEdit);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
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 (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<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="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="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" />
|
<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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="bg-radial relative flex flex-col bg-background pt-6"
|
className="bg-radial relative flex flex-col bg-background min-h-screen w-full"
|
||||||
id="app-container"
|
id="app-container"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center">
|
<Navbar />
|
||||||
<div className="w-full">
|
<main className="pt-6 flex w-full flex-col items-center">
|
||||||
<Navbar />
|
<div className="w-full max-w-8xl px-4 lg:px-8">
|
||||||
<main className="mt-6 flex w-full flex-col items-center">
|
<NavigationTabs tab={tab}>{children}</NavigationTabs>
|
||||||
<div className="w-full max-w-8xl px-4 lg:px-8">
|
|
||||||
<NavigationTabs tab={tab}>{children}</NavigationTabs>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const Navbar = () => {
|
|||||||
const { mutateAsync } = api.auth.logout.useMutation();
|
const { mutateAsync } = api.auth.logout.useMutation();
|
||||||
return (
|
return (
|
||||||
<nav className="border-divider sticky inset-x-0 top-0 z-40 flex h-auto w-full items-center justify-center border-b bg-background/70 backdrop-blur-lg backdrop-saturate-150 data-[menu-open=true]:border-none data-[menu-open=true]:backdrop-blur-xl">
|
<nav className="border-divider sticky inset-x-0 top-0 z-40 flex h-auto w-full items-center justify-center border-b bg-background/70 backdrop-blur-lg backdrop-saturate-150 data-[menu-open=true]:border-none data-[menu-open=true]:backdrop-blur-xl">
|
||||||
<header className="relative z-40 flex h-[var(--navbar-height)] w-full max-w-8xl flex-row flex-nowrap items-center justify-between gap-4 px-4 sm:px-6">
|
<header className="relative z-40 flex w-full max-w-8xl flex-row flex-nowrap items-center justify-between gap-4 px-4 sm:px-6 h-16">
|
||||||
<div className="text-medium box-border flex flex-grow basis-0 flex-row flex-nowrap items-center justify-start whitespace-nowrap bg-transparent no-underline">
|
<div className="text-medium box-border flex flex-grow basis-0 flex-row flex-nowrap items-center justify-start whitespace-nowrap bg-transparent no-underline">
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/projects"
|
href="/dashboard/projects"
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import { AddProject } from "@/components/dashboard/projects/add";
|
import { AddProject } from "@/components/dashboard/projects/add";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import type { Auth } from "@/server/api/services/auth";
|
||||||
|
import type { User } from "@/server/api/services/user";
|
||||||
|
|
||||||
|
interface TabInfo {
|
||||||
|
label: string;
|
||||||
|
tabLabel?: string;
|
||||||
|
description: string;
|
||||||
|
index: string;
|
||||||
|
isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type TabState =
|
export type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -11,6 +21,41 @@ export type TabState =
|
|||||||
| "traefik"
|
| "traefik"
|
||||||
| "docker";
|
| "docker";
|
||||||
|
|
||||||
|
const tabMap: Record<TabState, TabInfo> = {
|
||||||
|
projects: {
|
||||||
|
label: "Projects",
|
||||||
|
description: "Manage your projects",
|
||||||
|
index: "/dashboard/projects",
|
||||||
|
},
|
||||||
|
monitoring: {
|
||||||
|
label: "Monitoring",
|
||||||
|
description: "Monitor your projects",
|
||||||
|
index: "/dashboard/monitoring",
|
||||||
|
},
|
||||||
|
traefik: {
|
||||||
|
label: "Traefik",
|
||||||
|
tabLabel: "Traefik File System",
|
||||||
|
description: "Manage your traefik",
|
||||||
|
index: "/dashboard/traefik",
|
||||||
|
isShow: ({ rol, user }) => {
|
||||||
|
return Boolean(rol === "admin" || user?.canAccessToTraefikFiles);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
docker: {
|
||||||
|
label: "Docker",
|
||||||
|
description: "Manage your docker",
|
||||||
|
index: "/dashboard/docker",
|
||||||
|
isShow: ({ rol, user }) => {
|
||||||
|
return Boolean(rol === "admin" || user?.canAccessToDocker);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
label: "Settings",
|
||||||
|
description: "Manage your settings",
|
||||||
|
index: "/dashboard/settings/server",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tab: TabState;
|
tab: TabState;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -34,23 +79,19 @@ export const NavigationTabs = ({ tab, children }: Props) => {
|
|||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
}, [tab]);
|
}, [tab]);
|
||||||
|
|
||||||
|
const activeTabInfo = useMemo(() => {
|
||||||
|
return tabMap[activeTab];
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gap-12 min-h-screen">
|
<div className="gap-12">
|
||||||
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
|
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-xl font-bold lg:text-3xl">
|
<h1 className="text-xl font-bold lg:text-3xl">
|
||||||
{tab === "projects" && "Projects"}
|
{activeTabInfo.label}
|
||||||
{tab === "monitoring" && "Monitoring"}
|
|
||||||
{tab === "settings" && "Settings"}
|
|
||||||
{tab === "traefik" && "Traefik"}
|
|
||||||
{tab === "docker" && "Docker"}
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="lg:text-medium text-muted-foreground">
|
<p className="lg:text-medium text-muted-foreground">
|
||||||
{tab === "projects" && "Manage your deployments"}
|
{activeTabInfo.description}
|
||||||
{tab === "monitoring" && "Watch the usage of your server"}
|
|
||||||
{tab === "settings" && "Check the configuration"}
|
|
||||||
{tab === "traefik" && "Read the traefik config and update it"}
|
|
||||||
{tab === "docker" && "Manage the docker containers"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{tab === "projects" &&
|
{tab === "projects" &&
|
||||||
@@ -60,54 +101,36 @@ export const NavigationTabs = ({ tab, children }: Props) => {
|
|||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(e) => {
|
onValueChange={async (e) => {
|
||||||
if (e === "settings") {
|
|
||||||
router.push("/dashboard/settings/server");
|
|
||||||
} else {
|
|
||||||
router.push(`/dashboard/${e}`);
|
|
||||||
}
|
|
||||||
setActiveTab(e as TabState);
|
setActiveTab(e as TabState);
|
||||||
|
router.push(tabMap[e as TabState].index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* className="grid w-fit grid-cols-4 bg-transparent" */}
|
{/* className="grid w-fit grid-cols-4 bg-transparent" */}
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto">
|
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto border-b border-b-divider pb-1">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 justify-start bg-transparent">
|
<TabsList className="bg-transparent relative px-0">
|
||||||
<TabsTrigger
|
{Object.keys(tabMap).map((key) => {
|
||||||
value="projects"
|
const tab = tabMap[key as TabState];
|
||||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
if (tab.isShow && !tab.isShow?.({ rol: data?.rol, user })) {
|
||||||
>
|
return null;
|
||||||
Projects
|
}
|
||||||
</TabsTrigger>
|
return (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="monitoring"
|
key={key}
|
||||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
value={key}
|
||||||
>
|
className="relative py-2.5 md:px-5 data-[state=active]:shadow-none data-[state=active]:bg-transparent rounded-md hover:bg-zinc-100 hover:dark:bg-zinc-800 data-[state=active]:hover:bg-zinc-100 data-[state=active]:hover:dark:bg-zinc-800"
|
||||||
Monitoring
|
>
|
||||||
</TabsTrigger>
|
<span className="relative z-[1] w-full">
|
||||||
|
{tab.tabLabel || tab.label}
|
||||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
</span>
|
||||||
<TabsTrigger
|
{key === activeTab && (
|
||||||
value="traefik"
|
<div className="absolute -bottom-[5.5px] w-full">
|
||||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
<div className="h-0.5 bg-foreground rounded-t-md" />
|
||||||
>
|
</div>
|
||||||
Traefik File System
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
);
|
||||||
{(data?.rol === "admin" || user?.canAccessToDocker) && (
|
})}
|
||||||
<TabsTrigger
|
|
||||||
value="docker"
|
|
||||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
|
||||||
>
|
|
||||||
Docker
|
|
||||||
</TabsTrigger>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TabsTrigger
|
|
||||||
value="settings"
|
|
||||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ProjectLayout = ({ children }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="bg-radial relative flex flex-col bg-background pt-6"
|
className="bg-radial relative flex flex-col bg-background"
|
||||||
id="app-container"
|
id="app-container"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
href: "/dashboard/settings/users",
|
href: "/dashboard/settings/users",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Cluster",
|
||||||
|
label: "",
|
||||||
|
icon: Server,
|
||||||
|
href: "/dashboard/settings/cluster",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
@@ -75,6 +81,7 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Database,
|
Database,
|
||||||
Route,
|
Route,
|
||||||
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
User2,
|
User2,
|
||||||
Users,
|
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">
|
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
||||||
{links.map((link, index) => {
|
{links.map((link, index) => {
|
||||||
const isActive = router.pathname === link.href;
|
const isActive = router.pathname === link.href;
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={index}
|
key={index}
|
||||||
@@ -110,7 +116,7 @@ export const Nav = ({ links }: NavProps) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "ghost", size: "sm" }),
|
buttonVariants({ variant: "ghost", size: "sm" }),
|
||||||
isActive &&
|
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",
|
"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,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
date: string;
|
date: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DateTooltip = ({ date, children }: Props) => {
|
export const DateTooltip = ({ date, children, className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<span className="flex items-center text-muted-foreground text-left">
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex items-center text-muted-foreground text-left",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}{" "}
|
{children}{" "}
|
||||||
{formatDistanceToNow(new Date(date), {
|
{formatDistanceToNow(new Date(date), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
{status === "idle" && (
|
{status === "idle" && (
|
||||||
<div className={cn(" size-3.5 rounded-full bg-card", className)} />
|
<div className={cn("size-3.5 rounded-full bg-muted-foreground dark:bg-card", className)} />
|
||||||
)}
|
)}
|
||||||
{status === "error" && (
|
{status === "error" && (
|
||||||
<div
|
<div
|
||||||
className={cn(" size-3.5 rounded-full bg-destructive", className)}
|
className={cn("size-3.5 rounded-full bg-destructive", className)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{status === "done" && (
|
{status === "done" && (
|
||||||
<div
|
<div
|
||||||
className={cn(" size-3.5 rounded-full bg-primary", className)}
|
className={cn("size-3.5 rounded-full bg-primary", className)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{status === "running" && (
|
{status === "running" && (
|
||||||
<div
|
<div
|
||||||
className={cn(" size-3.5 rounded-full bg-yellow-500", className)}
|
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|||||||
59
components/ui/alert.tsx
Normal file
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();
|
const { ref: refRoot, width, height } = useResizeObserver();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={refRoot} className={cn("overflow-hidden", className)}>
|
<div ref={refRoot} className={cn("overflow-y-auto", className)}>
|
||||||
{/* style={{ width, height }} */}
|
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="relative p-2">
|
<div className="relative p-2">
|
||||||
<TreeItem
|
<TreeItem
|
||||||
@@ -165,7 +164,7 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm truncate">{item.name}</span>
|
<span className="text-sm truncate font-mono">{item.name}</span>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="pl-6">
|
<AccordionContent className="pl-6">
|
||||||
{item.children.length === 0 && (
|
{item.children.length === 0 && (
|
||||||
@@ -244,7 +243,7 @@ const Leaf = React.forwardRef<
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className=" text-sm whitespace-normal">{item.name}</p>
|
<p className=" text-sm whitespace-normal font-mono">{item.name}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ const FormMessage = React.forwardRef<
|
|||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
const { error, formMessageId } = useFormField();
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message) : children;
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null;
|
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