mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 06:05:25 +02:00
Compare commits
222 Commits
v0.23.0
...
1365-creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d5313dd8 | ||
|
|
3f3ff9670b | ||
|
|
7fb902551e | ||
|
|
a201b3f979 | ||
|
|
01d78e50fc | ||
|
|
6681ba7bbd | ||
|
|
0b71411c0e | ||
|
|
19f7465910 | ||
|
|
f33dd37571 | ||
|
|
a0031ed07f | ||
|
|
2ca4e264c4 | ||
|
|
fa81d04fb3 | ||
|
|
bd8745393b | ||
|
|
691c83c256 | ||
|
|
6bd85e9216 | ||
|
|
79c29fa92d | ||
|
|
89f71fe889 | ||
|
|
bddafe294d | ||
|
|
94829daf15 | ||
|
|
2209d44ea5 | ||
|
|
b12c035527 | ||
|
|
baadba542f | ||
|
|
a8fc052cbf | ||
|
|
fa5994bd47 | ||
|
|
96d0810607 | ||
|
|
2d382ea1be | ||
|
|
d78974efc0 | ||
|
|
81040c899f | ||
|
|
c7344190b4 | ||
|
|
257c0eb106 | ||
|
|
c03b9509c8 | ||
|
|
d87205c4dc | ||
|
|
48aef798e4 | ||
|
|
baa5cd5c58 | ||
|
|
5aae36996e | ||
|
|
ec8fa9fefe | ||
|
|
d959f59c2d | ||
|
|
a1169795e4 | ||
|
|
10af7925db | ||
|
|
c64cdca2e8 | ||
|
|
a5b95d8cf3 | ||
|
|
78b60f7d8a | ||
|
|
58e6a14cd6 | ||
|
|
0aac6da554 | ||
|
|
978c4d85c5 | ||
|
|
70e08c96eb | ||
|
|
027853a361 | ||
|
|
43ebe4dc7c | ||
|
|
0113ebe7da | ||
|
|
c36b40aa29 | ||
|
|
caea934f88 | ||
|
|
9b2ea1cade | ||
|
|
3a82c4b27b | ||
|
|
22a26e9873 | ||
|
|
226a287ce7 | ||
|
|
320b927aac | ||
|
|
d799b460bd | ||
|
|
3ed9da147b | ||
|
|
636dec4f09 | ||
|
|
4dcf6cf4c3 | ||
|
|
7356d71626 | ||
|
|
76603f598c | ||
|
|
e050c218e2 | ||
|
|
46e0b5df75 | ||
|
|
5b36503a3f | ||
|
|
b9afc551db | ||
|
|
078ca19578 | ||
|
|
b7dc7bbf0c | ||
|
|
b9ee81aa59 | ||
|
|
b2d01a2889 | ||
|
|
5ec3d63ab2 | ||
|
|
f0ef06ed8c | ||
|
|
75b2c34a13 | ||
|
|
cd4533df9e | ||
|
|
65a3d8175a | ||
|
|
d3702d22f2 | ||
|
|
d4b74c54da | ||
|
|
80ede659fb | ||
|
|
c59ea57814 | ||
|
|
381186c9f1 | ||
|
|
05e0031daf | ||
|
|
1bacd42bf5 | ||
|
|
2bc12a20ba | ||
|
|
1943a9e8fa | ||
|
|
db9109a3be | ||
|
|
2f6084ec8f | ||
|
|
a62c9f63e1 | ||
|
|
ee2bbf5e37 | ||
|
|
d27dff4906 | ||
|
|
7874445510 | ||
|
|
5c73ced500 | ||
|
|
b1450d14ac | ||
|
|
c5e3b00990 | ||
|
|
cf3f44f686 | ||
|
|
119883e746 | ||
|
|
2918868166 | ||
|
|
715e44116d | ||
|
|
c15ee721ff | ||
|
|
71befc3a4b | ||
|
|
bca32f077b | ||
|
|
1afcecaec0 | ||
|
|
80b72dc75e | ||
|
|
5973d7b9b8 | ||
|
|
ab3a1504cf | ||
|
|
f51c51958f | ||
|
|
f3703e6f5e | ||
|
|
14cb6cecae | ||
|
|
6885b140eb | ||
|
|
8e8712e33d | ||
|
|
56fcaa8ccd | ||
|
|
d229284e5e | ||
|
|
3561b5cae6 | ||
|
|
a3192d6584 | ||
|
|
24ea8b7fbd | ||
|
|
e12df7b32e | ||
|
|
8001c9cfc2 | ||
|
|
a762b4b4ae | ||
|
|
9cfbd664c5 | ||
|
|
f9972bee60 | ||
|
|
3970cd452b | ||
|
|
6fc51e02a7 | ||
|
|
fa7db0dc75 | ||
|
|
107cdcee49 | ||
|
|
6521491e2f | ||
|
|
c5311f2a9f | ||
|
|
7726c8db21 | ||
|
|
b272f01a18 | ||
|
|
bddb07898e | ||
|
|
2fe7349889 | ||
|
|
5c1c969873 | ||
|
|
2f6f1b19e7 | ||
|
|
934ec9b16a | ||
|
|
c6d760a904 | ||
|
|
4f021a3f79 | ||
|
|
0c861585ed | ||
|
|
d15ccfe505 | ||
|
|
e158e05ad6 | ||
|
|
e21605030a | ||
|
|
f1c46f0d19 | ||
|
|
fbf3776548 | ||
|
|
46d84eaa71 | ||
|
|
2a2b947998 | ||
|
|
9974b2326f | ||
|
|
c042c8c0c5 | ||
|
|
819a310d48 | ||
|
|
12860a0736 | ||
|
|
392e2d66ec | ||
|
|
1f4ce2daf3 | ||
|
|
49edf17463 | ||
|
|
6eea02c098 | ||
|
|
a5bba9a11b | ||
|
|
37c7507507 | ||
|
|
ce88a0a5f2 | ||
|
|
40f28705cb | ||
|
|
03e04b7bce | ||
|
|
b920e7c0f1 | ||
|
|
15fde820a3 | ||
|
|
64293fce79 | ||
|
|
526f249d0e | ||
|
|
17c5a42d8e | ||
|
|
fac96b5db5 | ||
|
|
4796b0cf4e | ||
|
|
159a055bc6 | ||
|
|
cfade317f1 | ||
|
|
36ebefff16 | ||
|
|
7cc0603078 | ||
|
|
e0e42ac554 | ||
|
|
e004d8bd52 | ||
|
|
0c0912f606 | ||
|
|
8d81440d9b | ||
|
|
9ede3bd71b | ||
|
|
df6a72ea50 | ||
|
|
e79f8c4b72 | ||
|
|
26e2a24f63 | ||
|
|
830feabd70 | ||
|
|
122a3d110d | ||
|
|
c05edb313f | ||
|
|
1ec2853862 | ||
|
|
5c2709248c | ||
|
|
bc79074441 | ||
|
|
3b5428697b | ||
|
|
5ada451916 | ||
|
|
6b0d9240dd | ||
|
|
475c452451 | ||
|
|
1e31ebb9c2 | ||
|
|
207fe6f477 | ||
|
|
0b34676336 | ||
|
|
499022a328 | ||
|
|
fb5d2bd5b6 | ||
|
|
e42f6bc610 | ||
|
|
61cf426615 | ||
|
|
dde12e132a | ||
|
|
fd0f679d0f | ||
|
|
2a89be6efc | ||
|
|
412bb9e874 | ||
|
|
6290c217f1 | ||
|
|
4babdd45ea | ||
|
|
24bff96898 | ||
|
|
df8f1252a0 | ||
|
|
892f272108 | ||
|
|
fca537ee40 | ||
|
|
ae24aa8be5 | ||
|
|
b74d3995ee | ||
|
|
f7fd77f7e9 | ||
|
|
db8a4e6edf | ||
|
|
fa16cfec2a | ||
|
|
f35d084dd4 | ||
|
|
274daf52c0 | ||
|
|
da52d767eb | ||
|
|
8599f519a4 | ||
|
|
113e4ae4b5 | ||
|
|
7f0bdc7e00 | ||
|
|
b685a817fd | ||
|
|
6061a443d1 | ||
|
|
4c9835d1f3 | ||
|
|
f2671f9369 | ||
|
|
bb904bb011 | ||
|
|
9fb6ca2b3b | ||
|
|
9f146d7d80 | ||
|
|
cad628d155 | ||
|
|
cd8230b0e5 | ||
|
|
da0e726326 |
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:20.9-slim AS base
|
FROM node:20.16.0-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|||||||
23
GUIDES.md
23
GUIDES.md
@@ -16,28 +16,29 @@ Here's how to install docker on different operating systems:
|
|||||||
### Ubuntu
|
### Ubuntu
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Uninstall old versions
|
||||||
|
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
|
||||||
|
|
||||||
# Update package index
|
# Update package index
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|
||||||
# Install prerequisites
|
# Install prerequisites
|
||||||
sudo apt-get install \
|
sudo apt-get install ca-certificates curl
|
||||||
apt-transport-https \
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
gnupg \
|
|
||||||
lsb-release
|
|
||||||
|
|
||||||
# Add Docker's official GPG key
|
# Add Docker's official GPG key
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
# Set up stable repository
|
# Add the repository to Apt sources
|
||||||
echo \
|
echo \
|
||||||
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||||
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
# Install Docker Engine
|
# Install Docker Engine
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install docker-ce docker-ce-cli containerd.io
|
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Core License (Apache License 2.0)
|
## Core License (Apache License 2.0)
|
||||||
|
|
||||||
Copyright 2024 Mauricio Siu.
|
Copyright 2025 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.
|
||||||
|
|||||||
94
README.md
94
README.md
@@ -1,20 +1,16 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<div>
|
<a href="https://dokploy.com">
|
||||||
<a href="https://dokploy.com" target="_blank" rel="noopener">
|
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." align="center" width="100%" />
|
||||||
<img style="object-fit: cover;" align="center" width="100%"src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." />
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</br>
|
|
||||||
<div align="center">
|
|
||||||
<div>Join us on Discord for help, feedback, and discussions!</div>
|
|
||||||
</br>
|
</br>
|
||||||
|
</br>
|
||||||
|
<p>Join us on Discord for help, feedback, and discussions!</p>
|
||||||
<a href="https://discord.gg/2tBnJ3jDJc">
|
<a href="https://discord.gg/2tBnJ3jDJc">
|
||||||
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -61,76 +57,48 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
### Hero Sponsors 🎖
|
### Hero Sponsors 🎖
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div>
|
||||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
||||||
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
||||||
</a>
|
|
||||||
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
|
||||||
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
|
||||||
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
|
||||||
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Premium Supporters 🥇 -->
|
||||||
|
|
||||||
|
<!-- Add Premium Supporters here -->
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div>
|
||||||
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
|
<a href="https://supafort.com/?ref=dokploy"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="300"/></a>
|
||||||
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
|
<a href="https://agentdock.ai/?ref=dokploy"><img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/></a>
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
|
|
||||||
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Elite Contributors 🥈
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
|
||||||
|
|
||||||
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
|
|
||||||
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- Elite Contributors 🥈 -->
|
<!-- Elite Contributors 🥈 -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Add Elite Contributors here -->
|
<!-- Add Elite Contributors here -->
|
||||||
|
|
||||||
### Supporting Members 🥉
|
### Elite Contributors 🥈
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
|
||||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
|
||||||
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
|
||||||
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
|
||||||
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
|
||||||
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
|
||||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="https://americancloud.com/?ref=dokploy"><img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="300"/></a>
|
||||||
|
<a href="https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy"><img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Supporting Members 🥉
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<a href="https://cloudblast.io/?ref=dokploy"><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||||
|
|
||||||
|
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
|
||||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
|
||||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
|
||||||
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
#### Organizations:
|
#### Organizations:
|
||||||
|
|
||||||
[](https://opencollective.com/dokploy)
|
[Sponsors on Open Collective](https://opencollective.com/dokploy)
|
||||||
|
|
||||||
#### Individuals:
|
#### Individuals:
|
||||||
|
|
||||||
@@ -140,12 +108,12 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
|
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
|
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Video Tutorial
|
## Video Tutorial
|
||||||
|
|
||||||
<a href="https://youtu.be/mznYKPvhcfw">
|
<a href="https://youtu.be/mznYKPvhcfw">
|
||||||
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
@@ -10,24 +10,24 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dokploy/server": "workspace:*",
|
"@dokploy/server": "workspace:*",
|
||||||
"@hono/node-server": "^1.12.1",
|
"@hono/node-server": "^1.14.3",
|
||||||
"@hono/zod-validator": "0.3.0",
|
"@hono/zod-validator": "0.3.0",
|
||||||
"@nerimity/mimiqueue": "1.2.3",
|
"@nerimity/mimiqueue": "1.2.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.4.5",
|
||||||
"hono": "^4.5.8",
|
"hono": "^4.7.10",
|
||||||
"pino": "9.4.0",
|
"pino": "9.4.0",
|
||||||
"pino-pretty": "11.2.2",
|
"pino-pretty": "11.2.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"redis": "4.7.0",
|
"redis": "4.7.0",
|
||||||
"zod": "^3.23.4"
|
"zod": "^3.25.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.17.51",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.16.2",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.5.0"
|
"packageManager": "pnpm@9.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# License
|
|
||||||
|
|
||||||
## Core License (Apache License 2.0)
|
|
||||||
|
|
||||||
Copyright 2024 Mauricio Siu.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and limitations under the License.
|
|
||||||
|
|
||||||
## Additional Terms for Specific Features
|
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server 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, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
|
||||||
@@ -19,6 +19,8 @@ describe("createDomainLabels", () => {
|
|||||||
path: "/",
|
path: "/",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
previewDeploymentId: "",
|
previewDeploymentId: "",
|
||||||
|
internalPath: "/",
|
||||||
|
stripPath: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should create basic labels for web entrypoint", async () => {
|
it("should create basic labels for web entrypoint", async () => {
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ const baseDomain: Domain = {
|
|||||||
domainType: "application",
|
domainType: "application",
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
previewDeploymentId: "",
|
previewDeploymentId: "",
|
||||||
|
internalPath: "/",
|
||||||
|
stripPath: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseRedirect: Redirect = {
|
const baseRedirect: Redirect = {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str);
|
return JSON.parse(str);
|
||||||
} catch (_e) {
|
} catch {
|
||||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
@@ -270,8 +270,8 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
Swarm Settings
|
Swarm Settings
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-5xl p-0">
|
<DialogContent className="sm:max-w-5xl p-0">
|
||||||
<DialogHeader className="p-6">
|
<DialogHeader>
|
||||||
<DialogTitle>Swarm Settings</DialogTitle>
|
<DialogTitle>Swarm Settings</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Update certain settings using a json object.
|
Update certain settings using a json object.
|
||||||
@@ -753,7 +753,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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 ">
|
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-add-permissions"
|
form="hook-form-add-permissions"
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
} catch (_error) {
|
} catch {
|
||||||
toast.error("Error importing template");
|
toast.error("Error importing template");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -126,7 +126,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
setTemplateInfo(result);
|
setTemplateInfo(result);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
} catch (_error) {
|
} catch {
|
||||||
toast.error("Error processing template");
|
toast.error("Error processing template");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -185,7 +185,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||||
<DialogContent className="max-h-[80vh] max-w-[50vw] overflow-y-auto">
|
<DialogContent className="max-w-[50vw]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl font-bold">
|
<DialogTitle className="text-2xl font-bold">
|
||||||
Template Information
|
Template Information
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const AddPortSchema = z.object({
|
const AddPortSchema = z.object({
|
||||||
publishedPort: z.number().int().min(1).max(65535),
|
publishedPort: z.number().int().min(1).max(65535),
|
||||||
|
publishMode: z.enum(["ingress", "host"], {
|
||||||
|
required_error: "Publish mode is required",
|
||||||
|
}),
|
||||||
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"], {
|
||||||
required_error: "Protocol is required",
|
required_error: "Protocol is required",
|
||||||
@@ -80,6 +83,7 @@ export const HandlePorts = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
publishedPort: data?.publishedPort ?? 0,
|
publishedPort: data?.publishedPort ?? 0,
|
||||||
|
publishMode: data?.publishMode ?? "ingress",
|
||||||
targetPort: data?.targetPort ?? 0,
|
targetPort: data?.targetPort ?? 0,
|
||||||
protocol: data?.protocol ?? "tcp",
|
protocol: data?.protocol ?? "tcp",
|
||||||
});
|
});
|
||||||
@@ -120,7 +124,7 @@ export const HandlePorts = ({
|
|||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Ports</DialogTitle>
|
<DialogTitle>Ports</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -165,6 +169,32 @@ export const HandlePorts = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="publishMode"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<FormLabel>Published Port Mode</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a publish mode for the port" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"ingress"}>Ingress</SelectItem>
|
||||||
|
<SelectItem value={"host"}>Host</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetPort"
|
name="targetPort"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
{data?.ports.map((port) => (
|
{data?.ports.map((port) => (
|
||||||
<div key={port.portId}>
|
<div key={port.portId}>
|
||||||
<div className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4">
|
<div className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Published Port</span>
|
<span className="font-medium">Published Port</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -68,7 +68,13 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium"> Target Port</span>
|
<span className="font-medium">Published Port Mode</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{port?.publishMode?.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Target Port</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{port.targetPort}
|
{port.targetPort}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export const HandleRedirect = ({
|
|||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Redirects</DialogTitle>
|
<DialogTitle>Redirects</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const HandleSecurity = ({
|
|||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Security</DialogTitle>
|
<DialogTitle>Security</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button isLoading={isLoading}>Modify</Button>
|
<Button isLoading={isLoading}>Modify</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
<DialogDescription>Update the traefik config</DialogDescription>
|
<DialogDescription>Update the traefik config</DialogDescription>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -150,7 +151,7 @@ export const AddVolumes = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Volumes / Mounts</DialogTitle>
|
<DialogTitle>Volumes / Mounts</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -169,6 +170,23 @@ export const AddVolumes = ({
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-8 "
|
className="grid w-full gap-8 "
|
||||||
>
|
>
|
||||||
|
{type === "bind" && (
|
||||||
|
<AlertBlock>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>
|
||||||
|
Make sure the host path is a valid path and exists in the
|
||||||
|
host machine.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<strong>Cluster Warning:</strong> If you're using cluster
|
||||||
|
features, bind mounts may cause deployment failures since
|
||||||
|
the path must exist on all worker/manager nodes. Consider
|
||||||
|
using external tools to distribute the folder across nodes
|
||||||
|
or use named volumes instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={form.control._defaultValues.type}
|
defaultValue={form.control._defaultValues.type}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export const UpdateVolume = ({
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update</DialogTitle>
|
<DialogTitle>Update</DialogTitle>
|
||||||
<DialogDescription>Update the mount</DialogDescription>
|
<DialogDescription>Update the mount</DialogDescription>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const ShowDeployment = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription className="flex items-center gap-2">
|
<DialogDescription className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ interface Props {
|
|||||||
| "schedule"
|
| "schedule"
|
||||||
| "server"
|
| "server"
|
||||||
| "backup"
|
| "backup"
|
||||||
| "previewDeployment";
|
| "previewDeployment"
|
||||||
|
| "volumeBackup";
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -49,7 +50,7 @@ export const ShowDeploymentsModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl p-0">
|
<DialogContent className="sm:max-w-5xl p-0">
|
||||||
<ShowDeployments
|
<ShowDeployments
|
||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ interface Props {
|
|||||||
| "schedule"
|
| "schedule"
|
||||||
| "server"
|
| "server"
|
||||||
| "backup"
|
| "backup"
|
||||||
| "previewDeployment";
|
| "previewDeployment"
|
||||||
|
| "volumeBackup";
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
@@ -62,6 +63,8 @@ export const ShowDeployments = ({
|
|||||||
|
|
||||||
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
||||||
api.rollback.rollback.useMutation();
|
api.rollback.rollback.useMutation();
|
||||||
|
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
|
||||||
|
api.deployment.killProcess.useMutation();
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -170,6 +173,32 @@ export const ShowDeployments = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
{deployment.pid && deployment.status === "running" && (
|
||||||
|
<DialogAction
|
||||||
|
title="Kill Process"
|
||||||
|
description="Are you sure you want to kill the process?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await killProcess({
|
||||||
|
deploymentId: deployment.deploymentId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Process killed successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error killing process");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isKillingProcess}
|
||||||
|
>
|
||||||
|
Kill Process
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveLog(deployment);
|
setActiveLog(deployment);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const DnsHelperModal = ({ domain, serverIp }: Props) => {
|
|||||||
<HelpCircle className="size-4" />
|
<HelpCircle className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Server className="size-5" />
|
<Server className="size-5" />
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export const domain = z
|
|||||||
.object({
|
.object({
|
||||||
host: z.string().min(1, { message: "Add a hostname" }),
|
host: z.string().min(1, { message: "Add a hostname" }),
|
||||||
path: z.string().min(1).optional(),
|
path: z.string().min(1).optional(),
|
||||||
|
internalPath: z.string().optional(),
|
||||||
|
stripPath: z.boolean().optional(),
|
||||||
port: z
|
port: z
|
||||||
.number()
|
.number()
|
||||||
.min(1, { message: "Port must be at least 1" })
|
.min(1, { message: "Port must be at least 1" })
|
||||||
@@ -84,6 +86,29 @@ export const domain = z
|
|||||||
message: "Required",
|
message: "Required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate stripPath requires a valid path
|
||||||
|
if (input.stripPath && (!input.path || input.path === "/")) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["stripPath"],
|
||||||
|
message:
|
||||||
|
"Strip path can only be enabled when a path other than '/' is specified",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate internalPath starts with /
|
||||||
|
if (
|
||||||
|
input.internalPath &&
|
||||||
|
input.internalPath !== "/" &&
|
||||||
|
!input.internalPath.startsWith("/")
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["internalPath"],
|
||||||
|
message: "Internal path must start with '/'",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
@@ -162,6 +187,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
host: "",
|
host: "",
|
||||||
path: undefined,
|
path: undefined,
|
||||||
|
internalPath: undefined,
|
||||||
|
stripPath: false,
|
||||||
port: undefined,
|
port: undefined,
|
||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
@@ -182,6 +209,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
...data,
|
...data,
|
||||||
/* Convert null to undefined */
|
/* Convert null to undefined */
|
||||||
path: data?.path || undefined,
|
path: data?.path || undefined,
|
||||||
|
internalPath: data?.internalPath || undefined,
|
||||||
|
stripPath: data?.stripPath || false,
|
||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
certificateType: data?.certificateType || undefined,
|
certificateType: data?.certificateType || undefined,
|
||||||
customCertResolver: data?.customCertResolver || undefined,
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
@@ -194,6 +223,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
host: "",
|
host: "",
|
||||||
path: undefined,
|
path: undefined,
|
||||||
|
internalPath: undefined,
|
||||||
|
stripPath: false,
|
||||||
port: undefined,
|
port: undefined,
|
||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
@@ -261,7 +292,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
@@ -469,6 +500,49 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="internalPath"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Internal Path</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
The path where your application expects to receive
|
||||||
|
requests internally (defaults to "/")
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"/"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="stripPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Strip Path</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Remove the external path from the request before
|
||||||
|
forwarding to the application
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="port"
|
name="port"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
currentBuildArgs !== (data?.buildArgs || "");
|
currentBuildArgs !== (data?.buildArgs || "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && !hasChanges) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
env: data.env || "",
|
env: data.env || "",
|
||||||
buildArgs: data.buildArgs || "",
|
buildArgs: data.buildArgs || "",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { api } from "@/utils/api";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } 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";
|
||||||
@@ -96,6 +96,16 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
const repository = form.watch("repository");
|
const repository = form.watch("repository");
|
||||||
const gitlabId = form.watch("gitlabId");
|
const gitlabId = form.watch("gitlabId");
|
||||||
|
|
||||||
|
const gitlabUrl = useMemo(() => {
|
||||||
|
const url = gitlabProviders?.find(
|
||||||
|
(provider) => provider.gitlabId === gitlabId,
|
||||||
|
)?.gitlabUrl;
|
||||||
|
|
||||||
|
const gitlabUrl = url?.replace(/\/$/, "");
|
||||||
|
|
||||||
|
return gitlabUrl || "https://gitlab.com";
|
||||||
|
}, [gitlabId, gitlabProviders]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: repositories,
|
data: repositories,
|
||||||
isLoading: isLoadingRepositories,
|
isLoading: isLoadingRepositories,
|
||||||
@@ -224,7 +234,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Repository</FormLabel>
|
<FormLabel>Repository</FormLabel>
|
||||||
{field.value.owner && field.value.repo && (
|
{field.value.owner && field.value.repo && (
|
||||||
<Link
|
<Link
|
||||||
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
|
href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
@@ -278,7 +288,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
{repositories?.map((repo) => {
|
{repositories?.map((repo) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -299,7 +309,8 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.url ===
|
||||||
|
field.value.gitlabPathNamespace
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
setSab(e as TabState);
|
setSab(e as TabState);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="github"
|
value="github"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const AddPreviewDomain = ({
|
|||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
{children}
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
Configure
|
Configure
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
<DialogContent className="sm:max-w-5xl w-full">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
108
apps/dokploy/components/dashboard/application/rollbacks/Backup
Normal file
108
apps/dokploy/components/dashboard/application/rollbacks/Backup
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
Backup
|
||||||
|
# license-namedbackups-abxelc
|
||||||
|
1. docker ps --filter "label=com.docker.swarm.service.name=license-namedbackups-abxelc" --format "{{.Names}}"
|
||||||
|
2. docker run --rm \
|
||||||
|
--volumes-from "license-namedbackups-abxelc.1.m3cxy78ocj3w0zu42kmgamc5y" \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
ubuntu \
|
||||||
|
tar cvf /backup/backup.tar /var/lib/postgresql/data
|
||||||
|
|
||||||
|
|
||||||
|
# Official Command Backup
|
||||||
|
|
||||||
|
1. Backup
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v license-namedbackups-abxelc-data:/volume_data \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
ubuntu \
|
||||||
|
bash -c "cd /volume_data && tar cvf /backup/generic_backup.tar ."
|
||||||
|
|
||||||
|
|
||||||
|
2. Restore
|
||||||
|
|
||||||
|
docker service scale license-namedbackups-abxelc=0
|
||||||
|
|
||||||
|
docker volume rm license-namedbackups-abxelc-data
|
||||||
|
|
||||||
|
2. docker run --rm \
|
||||||
|
-v license-namedbackups-abxelc-data:/volume_data \
|
||||||
|
-v $(pwd):/backup \
|
||||||
|
ubuntu \
|
||||||
|
bash -c "cd /volume_data && tar xvf /backup/generic_backup.tar ."
|
||||||
|
|
||||||
|
docker service scale license-namedbackups-abxelc=1
|
||||||
|
|
||||||
|
|
||||||
|
root@srv594061:~# docker volume inspect n8n_data-data
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"CreatedAt": "2025-06-28T18:07:44Z",
|
||||||
|
"Driver": "local",
|
||||||
|
"Labels": null,
|
||||||
|
"Mountpoint": "/var/lib/docker/volumes/n8n_data-data/_data",
|
||||||
|
"Name": "n8n_data-data",
|
||||||
|
"Options": null,
|
||||||
|
"Scope": "local"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Archivos funcuionando creados por N8N
|
||||||
|
|
||||||
|
# root@srv594061:~# cd /var/lib/docker/volumes/n8n_data-data/_data
|
||||||
|
# root@srv594061:/var/lib/docker/volumes/n8n_data-data/_data# ls
|
||||||
|
# binaryData config crash.journal database.sqlite git n8nEventLog.log ssh
|
||||||
|
|
||||||
|
Luego que intente hacer el backup con el comando de backup
|
||||||
|
|
||||||
|
|
||||||
|
root@srv594061:~# docker run --rm -v n8n_data-data:/volume_data -v $(pwd):/backup ubuntu bash -c "cd /volume_data && tar cvf /backup/generic_backup6.tar ."
|
||||||
|
./
|
||||||
|
./config
|
||||||
|
./crash.journal
|
||||||
|
./binaryData/
|
||||||
|
./git/
|
||||||
|
./database.sqlite
|
||||||
|
./ssh/
|
||||||
|
./n8nEventLog.log
|
||||||
|
root@srv594061:~#
|
||||||
|
|
||||||
|
# Paramos la aplicacion
|
||||||
|
docker service scale n8n=0
|
||||||
|
|
||||||
|
# Haciendo el restore
|
||||||
|
root@srv594061:~# docker volume rm n8n_data-data
|
||||||
|
n8n_data-data
|
||||||
|
root@srv594061:~# docker run --rm -v n8n_data-data:/volume_data -v $(pwd):/backup ubuntu bash -c "cd /volume_data && tar xvf /backup/generic_backup6.tar && chown -R 999:999 ."
|
||||||
|
./
|
||||||
|
./config
|
||||||
|
./crash.journal
|
||||||
|
./binaryData/
|
||||||
|
./git/
|
||||||
|
./database.sqlite
|
||||||
|
./ssh/
|
||||||
|
./n8nEventLog.log
|
||||||
|
|
||||||
|
# Tenemos los archivos en el volumen
|
||||||
|
root@srv594061:~# ls /var/lib/docker/volumes/n8n_data-data/_data
|
||||||
|
binaryData config crash.journal database.sqlite git n8nEventLog.log ssh
|
||||||
|
root@srv594061:~#
|
||||||
|
|
||||||
|
docker service scale n8n=1
|
||||||
|
|
||||||
|
# Luego en N8N Cuando se que el volumen tiene la data
|
||||||
|
Permissions 0644 for n8n settings file /home/node/.n8n/config are too wide. This is ignored for now, but in the future n8n will attempt to change the permissions automatically. To automatically enforce correct permissions now set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true (recommended), or turn this check off set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false.
|
||||||
|
User settings loaded from: /home/node/.n8n/config
|
||||||
|
Last session crashed
|
||||||
|
Error: EACCES: permission denied, open '/home/node/.n8n/crash.journal'
|
||||||
|
at open (node:internal/fs/promises:639:25)
|
||||||
|
at touchFile (/usr/local/lib/node_modules/n8n/dist/crash-journal.js:18:20)
|
||||||
|
at Object.init (/usr/local/lib/node_modules/n8n/dist/crash-journal.js:32:5)
|
||||||
|
at Start.initCrashJournal (/usr/local/lib/node_modules/n8n/dist/commands/base-command.js:113:9)
|
||||||
|
at Start.init (/usr/local/lib/node_modules/n8n/dist/commands/start.js:141:9)
|
||||||
|
at Start._run (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/command.js:301:13)
|
||||||
|
at Config.runCommand (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/config/config.js:424:25)
|
||||||
|
at run (/usr/local/lib/node_modules/n8n/node_modules/@oclif/core/lib/main.js:94:16)
|
||||||
|
at /usr/local/lib/node_modules/n8n/bin/n8n:71:2
|
||||||
|
TypeError: Cannot read properties of undefined (reading 'error')
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -79,6 +80,11 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
|
|||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Configure how rollbacks work for this application
|
Configure how rollbacks work for this application
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
<AlertBlock>
|
||||||
|
Having rollbacks enabled increases storage usage. Be careful with
|
||||||
|
this option. Note that manually cleaning the cache may delete
|
||||||
|
rollback images, making them unavailable for future rollbacks.
|
||||||
|
</AlertBlock>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -232,14 +233,17 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-screen overflow-y-auto",
|
|
||||||
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
||||||
? "max-h-[95vh] sm:max-w-2xl"
|
? "sm:max-w-2xl"
|
||||||
: " sm:max-w-lg",
|
: "sm:max-w-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{scheduleId ? "Manage" : "Create"} a schedule to run a task at a
|
||||||
|
specific time or interval.
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={schedule.scheduleId}
|
key={schedule.scheduleId}
|
||||||
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
@@ -166,12 +166,16 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
|
|
||||||
await runManually({
|
await runManually({
|
||||||
scheduleId: schedule.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
}).then(async () => {
|
})
|
||||||
await new Promise((resolve) =>
|
.then(async () => {
|
||||||
setTimeout(resolve, 1500),
|
await new Promise((resolve) =>
|
||||||
);
|
setTimeout(resolve, 1500),
|
||||||
refetchSchedules();
|
);
|
||||||
});
|
refetchSchedules();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error running schedule");
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Play className="size-4 transition-colors" />
|
<Play className="size-4 transition-colors" />
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Application</DialogTitle>
|
<DialogTitle>Modify Application</DialogTitle>
|
||||||
<DialogDescription>Update the application data</DialogDescription>
|
<DialogDescription>Update the application data</DialogDescription>
|
||||||
|
|||||||
@@ -0,0 +1,672 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
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 {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
DatabaseZap,
|
||||||
|
Info,
|
||||||
|
PenBoxIcon,
|
||||||
|
PlusCircle,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { CacheType } from "../domains/handle-domain";
|
||||||
|
import { commonCronExpressions } from "../schedules/handle-schedules";
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||||
|
volumeName: z.string().min(1, "Volume name is required"),
|
||||||
|
prefix: z.string(),
|
||||||
|
// keepLatestCount: z.coerce.number().optional(),
|
||||||
|
turnOff: z.boolean().default(false),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
serviceType: z.enum([
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"postgres",
|
||||||
|
"mariadb",
|
||||||
|
"mongo",
|
||||||
|
"mysql",
|
||||||
|
"redis",
|
||||||
|
]),
|
||||||
|
serviceName: z.string(),
|
||||||
|
destinationId: z.string().min(1, "Destination required"),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.serviceType === "compose" && !data.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required",
|
||||||
|
path: ["serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.serviceType === "compose" && !data.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required",
|
||||||
|
path: ["serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
volumeBackupId?: string;
|
||||||
|
volumeBackupType?:
|
||||||
|
| "application"
|
||||||
|
| "compose"
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HandleVolumeBackups = ({
|
||||||
|
id,
|
||||||
|
volumeBackupId,
|
||||||
|
volumeBackupType,
|
||||||
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
cronExpression: "",
|
||||||
|
volumeName: "",
|
||||||
|
prefix: "",
|
||||||
|
// keepLatestCount: undefined,
|
||||||
|
turnOff: false,
|
||||||
|
enabled: true,
|
||||||
|
serviceName: "",
|
||||||
|
serviceType: volumeBackupType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serviceTypeForm = volumeBackupType;
|
||||||
|
const { data: destinations } = api.destination.all.useQuery();
|
||||||
|
const { data: volumeBackup } = api.volumeBackups.one.useQuery(
|
||||||
|
{ volumeBackupId: volumeBackupId || "" },
|
||||||
|
{ enabled: !!volumeBackupId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: mounts } = api.mounts.allNamedByApplicationId.useQuery(
|
||||||
|
{ applicationId: id || "" },
|
||||||
|
{ enabled: !!id && volumeBackupType === "application" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id || "",
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: !!id && volumeBackupType === "compose",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const serviceName = form.watch("serviceName");
|
||||||
|
|
||||||
|
const { data: mountsByService } = api.compose.loadMountsByService.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id || "",
|
||||||
|
serviceName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id && volumeBackupType === "compose" && !!serviceName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (volumeBackupId && volumeBackup) {
|
||||||
|
form.reset({
|
||||||
|
name: volumeBackup.name,
|
||||||
|
cronExpression: volumeBackup.cronExpression,
|
||||||
|
volumeName: volumeBackup.volumeName || "",
|
||||||
|
prefix: volumeBackup.prefix,
|
||||||
|
// keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
||||||
|
turnOff: volumeBackup.turnOff,
|
||||||
|
enabled: volumeBackup.enabled || false,
|
||||||
|
serviceName: volumeBackup.serviceName || "",
|
||||||
|
destinationId: volumeBackup.destinationId,
|
||||||
|
serviceType: volumeBackup.serviceType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, volumeBackup, volumeBackupId]);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = volumeBackupId
|
||||||
|
? api.volumeBackups.update.useMutation()
|
||||||
|
: api.volumeBackups.create.useMutation();
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
if (!id && !volumeBackupId) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
...values,
|
||||||
|
destinationId: values.destinationId,
|
||||||
|
volumeBackupId: volumeBackupId || "",
|
||||||
|
serviceType: volumeBackupType,
|
||||||
|
...(volumeBackupType === "application" && {
|
||||||
|
applicationId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "compose" && {
|
||||||
|
composeId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "postgres" && {
|
||||||
|
serverId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "postgres" && {
|
||||||
|
postgresId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "mariadb" && {
|
||||||
|
mariadbId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "mongo" && {
|
||||||
|
mongoId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "mysql" && {
|
||||||
|
mysqlId: id || "",
|
||||||
|
}),
|
||||||
|
...(volumeBackupType === "redis" && {
|
||||||
|
redisId: id || "",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
`Volume backup ${volumeBackupId ? "updated" : "created"} successfully`,
|
||||||
|
);
|
||||||
|
utils.volumeBackups.list.invalidate({
|
||||||
|
id,
|
||||||
|
volumeBackupType,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "An unknown error occurred",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{volumeBackupId ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button>
|
||||||
|
<PlusCircle className="w-4 h-4 mr-2" />
|
||||||
|
Add Volume Backup
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
"overflow-y-auto",
|
||||||
|
volumeBackupType === "compose" || volumeBackupType === "application"
|
||||||
|
? "max-h-[95vh] sm:max-w-2xl"
|
||||||
|
: " sm:max-w-lg",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{volumeBackupId ? "Edit" : "Create"} Volume Backup
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a volume backup to backup your volume to a destination
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Task Name
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Daily Database Backup" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
A descriptive name for your scheduled task
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cronExpression"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Schedule
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Cron expression format: minute hour day month
|
||||||
|
weekday
|
||||||
|
</p>
|
||||||
|
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a predefined schedule" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{commonCronExpressions.map((expr) => (
|
||||||
|
<SelectItem key={expr.value} value={expr.value}>
|
||||||
|
{expr.label} ({expr.value})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="relative">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
Choose a predefined schedule or enter a custom cron
|
||||||
|
expression
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="destinationId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Destination</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a destination" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{destinations?.map((destination) => (
|
||||||
|
<SelectItem
|
||||||
|
key={destination.destinationId}
|
||||||
|
value={destination.destinationId}
|
||||||
|
>
|
||||||
|
{destination.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose the backup destination where files will be stored
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{serviceTypeForm === "compose" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-full gap-4">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock
|
||||||
|
type="warning"
|
||||||
|
className="[overflow-wrap:anywhere]"
|
||||||
|
>
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services from the
|
||||||
|
last deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{mountsByService && mountsByService.length > 0 && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volumes</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a volume name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{mountsByService?.map((volume) => (
|
||||||
|
<SelectItem
|
||||||
|
key={volume.Name}
|
||||||
|
value={volume.Name || ""}
|
||||||
|
>
|
||||||
|
{volume.Name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose the volume to backup, if you dont see the
|
||||||
|
volume here, you can type the volume name manually
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{serviceTypeForm === "application" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volumes</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a volume name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{mounts?.map((mount) => (
|
||||||
|
<SelectItem key={mount.Name} value={mount.Name || ""}>
|
||||||
|
{mount.Name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose the volume to backup, if you dont see the volume
|
||||||
|
here, you can type the volume name manually
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volume Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-volume-name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The name of the Docker volume to backup
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="prefix"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Backup Prefix</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="backup-" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Prefix for backup files (optional)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <FormField
|
||||||
|
control={form.control}
|
||||||
|
name="keepLatestCount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Keep Latest Count</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="5"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(Number(e.target.value) || undefined)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Number of backup files to keep (optional)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="turnOff"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
Turn Off Container During Backup
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription className="text-amber-600 dark:text-amber-400">
|
||||||
|
⚠️ The container will be temporarily stopped during backup to
|
||||||
|
prevent file corruption. This ensures data integrity but may
|
||||||
|
cause temporary service interruption.
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isLoading} className="w-full">
|
||||||
|
{volumeBackupId ? "Update" : "Create"} Volume Backup
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
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 {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
import { formatBytes } from "../../database/backups/restore-backup";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RestoreBackupSchema = z.object({
|
||||||
|
destinationId: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please select a destination",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Destination is required",
|
||||||
|
}),
|
||||||
|
backupFile: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please select a backup file",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Backup file is required",
|
||||||
|
}),
|
||||||
|
volumeName: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please enter a volume name",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Volume name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
|
|
||||||
|
const { data: destinations = [] } = api.destination.all.useQuery();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof RestoreBackupSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
destinationId: "",
|
||||||
|
backupFile: "",
|
||||||
|
volumeName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const destinationId = form.watch("destinationId");
|
||||||
|
const volumeName = form.watch("volumeName");
|
||||||
|
const backupFile = form.watch("backupFile");
|
||||||
|
|
||||||
|
const debouncedSetSearch = debounce((value: string) => {
|
||||||
|
setDebouncedSearchTerm(value);
|
||||||
|
}, 350);
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
debouncedSetSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
||||||
|
{
|
||||||
|
destinationId: destinationId,
|
||||||
|
search: debouncedSearchTerm,
|
||||||
|
serverId: serverId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isOpen && !!destinationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
|
||||||
|
api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
serviceType: type,
|
||||||
|
serverId,
|
||||||
|
destinationId,
|
||||||
|
volumeName,
|
||||||
|
backupFileName: backupFile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isDeploying,
|
||||||
|
onData(log) {
|
||||||
|
if (!isDrawerOpen) {
|
||||||
|
setIsDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log === "Restore completed successfully!") {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
const parsedLogs = parseLogs(log);
|
||||||
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error("Restore logs error:", error);
|
||||||
|
setIsDeploying(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
Restore Volume Backup
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center">
|
||||||
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
Restore Volume Backup
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select a destination and search for volume backup files
|
||||||
|
</DialogDescription>
|
||||||
|
<AlertBlock>
|
||||||
|
Make sure the volume name is not being used by another container.
|
||||||
|
</AlertBlock>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-restore-backup"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="destinationId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="">
|
||||||
|
<FormLabel>Destination</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? destinations.find(
|
||||||
|
(d) => d.destinationId === field.value,
|
||||||
|
)?.name
|
||||||
|
: "Select Destination"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search destinations..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<CommandEmpty>No destinations found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<CommandGroup>
|
||||||
|
{destinations.map((destination) => (
|
||||||
|
<CommandItem
|
||||||
|
value={destination.destinationId}
|
||||||
|
key={destination.destinationId}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue(
|
||||||
|
"destinationId",
|
||||||
|
destination.destinationId,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{destination.name}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
destination.destinationId === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="backupFile"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="">
|
||||||
|
<FormLabel className="flex items-center">
|
||||||
|
Search Backup Files
|
||||||
|
{field.value && (
|
||||||
|
<Badge variant="outline" className="truncate w-52">
|
||||||
|
{field.value}
|
||||||
|
<Copy
|
||||||
|
className="ml-2 size-4 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
copy(field.value);
|
||||||
|
toast.success("Backup file copied to clipboard");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<Popover modal>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate text-left flex-1 w-52">
|
||||||
|
{field.value || "Search and select a backup file"}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search backup files..."
|
||||||
|
value={search}
|
||||||
|
onValueChange={handleSearchChange}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
Loading backup files...
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 && search ? (
|
||||||
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
No backup files found for "{search}"
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
No backup files available
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<CommandGroup className="w-96">
|
||||||
|
{files?.map((file) => (
|
||||||
|
<CommandItem
|
||||||
|
value={file.Path}
|
||||||
|
key={file.Path}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("backupFile", file.Path);
|
||||||
|
if (file.IsDir) {
|
||||||
|
setSearch(`${file.Path}/`);
|
||||||
|
setDebouncedSearchTerm(`${file.Path}/`);
|
||||||
|
} else {
|
||||||
|
setSearch(file.Path);
|
||||||
|
setDebouncedSearchTerm(file.Path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-col gap-1">
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<span className="font-medium">
|
||||||
|
{file.Path}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
file.Path === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
Size: {formatBytes(file.Size)}
|
||||||
|
</span>
|
||||||
|
{file.IsDir && (
|
||||||
|
<span className="text-blue-500">
|
||||||
|
Directory
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{file.Hashes?.MD5 && (
|
||||||
|
<span>MD5: {file.Hashes.MD5}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Volume Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Enter volume name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isDeploying}
|
||||||
|
form="hook-form-restore-backup"
|
||||||
|
type="submit"
|
||||||
|
// disabled={
|
||||||
|
// !form.watch("backupFile") ||
|
||||||
|
// (backupType === "compose" && !form.watch("databaseType"))
|
||||||
|
// }
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<DrawerLogs
|
||||||
|
isOpen={isDrawerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
// refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
ClipboardList,
|
||||||
|
DatabaseBackup,
|
||||||
|
Loader2,
|
||||||
|
Play,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { HandleVolumeBackups } from "./handle-volume-backups";
|
||||||
|
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||||
|
import { RestoreVolumeBackups } from "./restore-volume-backups";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type?: "application" | "compose";
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowVolumeBackups = ({
|
||||||
|
id,
|
||||||
|
type = "application",
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
data: volumeBackups,
|
||||||
|
isLoading: isLoadingVolumeBackups,
|
||||||
|
refetch: refetchVolumeBackups,
|
||||||
|
} = api.volumeBackups.list.useQuery(
|
||||||
|
{
|
||||||
|
id: id || "",
|
||||||
|
volumeBackupType: type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
|
||||||
|
api.volumeBackups.delete.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: runManually, isLoading } =
|
||||||
|
api.volumeBackups.runManually.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
|
<CardHeader className="px-0">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
|
Volume Backups
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Schedule volume backups to run automatically at specified
|
||||||
|
intervals.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{volumeBackups && volumeBackups.length > 0 && (
|
||||||
|
<>
|
||||||
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RestoreVolumeBackups
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
serverId={serverId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-0">
|
||||||
|
{isLoadingVolumeBackups ? (
|
||||||
|
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||||
|
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||||
|
<span className="text-sm text-muted-foreground/70">
|
||||||
|
Loading volume backups...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : volumeBackups && volumeBackups.length > 0 ? (
|
||||||
|
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
|
||||||
|
{volumeBackups.map((volumeBackup) => {
|
||||||
|
const serverId =
|
||||||
|
volumeBackup.application?.serverId ||
|
||||||
|
volumeBackup.postgres?.serverId ||
|
||||||
|
volumeBackup.mysql?.serverId ||
|
||||||
|
volumeBackup.mariadb?.serverId ||
|
||||||
|
volumeBackup.mongo?.serverId ||
|
||||||
|
volumeBackup.redis?.serverId ||
|
||||||
|
volumeBackup.compose?.serverId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={volumeBackup.volumeBackupId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
|
<DatabaseBackup className="size-4 text-primary/70" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-sm font-medium leading-none">
|
||||||
|
{volumeBackup.name}
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
volumeBackup.enabled ? "default" : "secondary"
|
||||||
|
}
|
||||||
|
className="text-[10px] px-1 py-0"
|
||||||
|
>
|
||||||
|
{volumeBackup.enabled ? "Enabled" : "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono text-[10px] bg-transparent"
|
||||||
|
>
|
||||||
|
Cron: {volumeBackup.cronExpression}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ShowDeploymentsModal
|
||||||
|
id={volumeBackup.volumeBackupId}
|
||||||
|
type="volumeBackup"
|
||||||
|
serverId={serverId || undefined}
|
||||||
|
>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<ClipboardList className="size-4 transition-colors " />
|
||||||
|
</Button>
|
||||||
|
</ShowDeploymentsModal>
|
||||||
|
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
toast.success("Volume backup run successfully");
|
||||||
|
|
||||||
|
await runManually({
|
||||||
|
volumeBackupId: volumeBackup.volumeBackupId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 1500),
|
||||||
|
);
|
||||||
|
refetchVolumeBackups();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error running volume backup");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play className="size-4 transition-colors" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Run Manual Volume Backup
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<HandleVolumeBackups
|
||||||
|
volumeBackupId={volumeBackup.volumeBackupId}
|
||||||
|
id={id}
|
||||||
|
volumeBackupType={type}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Volume Backup"
|
||||||
|
description="Are you sure you want to delete this volume backup?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteVolumeBackup({
|
||||||
|
volumeBackupId: volumeBackup.volumeBackupId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
utils.volumeBackups.list.invalidate({
|
||||||
|
id,
|
||||||
|
volumeBackupType: type,
|
||||||
|
});
|
||||||
|
toast.success("Volume backup deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting volume backup");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||||
|
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
|
||||||
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
|
No volume backups
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Create your first volume backup to automate your workflows
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
|
<RestoreVolumeBackups id={id} type={type} serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -126,7 +126,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((_e) => {
|
.catch(() => {
|
||||||
toast.error("Error updating the Compose config");
|
toast.error("Error updating the Compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
{repositories?.map((repo) => {
|
{repositories?.map((repo) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -301,7 +301,8 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.url ===
|
||||||
|
field.value.gitlabPathNamespace
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
setSab(e as TabState);
|
setSab(e as TabState);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="github"
|
value="github"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch((_err) => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
Preview Compose
|
Preview Compose
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
<DialogContent className="sm:max-w-6xl max-h-[50rem]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Converted Compose</DialogTitle>
|
<DialogTitle>Converted Compose</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const ShowUtilities = ({ composeId }: Props) => {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">Show Utilities</Button>
|
<Button variant="ghost">Show Utilities</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
<DialogContent className="sm:max-w-5xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Utilities </DialogTitle>
|
<DialogTitle>Utilities </DialogTitle>
|
||||||
<DialogDescription>Modify the application data</DialogDescription>
|
<DialogDescription>Modify the application data</DialogDescription>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Compose</DialogTitle>
|
<DialogTitle>Modify Compose</DialogTitle>
|
||||||
<DialogDescription>Update the compose data</DialogDescription>
|
<DialogDescription>Update the compose data</DialogDescription>
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ export const HandleBackup = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl max-h-screen overflow-y-auto">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{backupId ? "Update Backup" : "Create Backup"}
|
{backupId ? "Update Backup" : "Create Backup"}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ const RestoreBackupSchema = z
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatBytes = (bytes: number): string => {
|
export const formatBytes = (bytes: number): string => {
|
||||||
if (bytes === 0) return "0 Bytes";
|
if (bytes === 0) return "0 Bytes";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||||
@@ -324,7 +324,7 @@ export const RestoreBackup = ({
|
|||||||
Restore Backup
|
Restore Backup
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center">
|
<DialogTitle className="flex items-center">
|
||||||
<RotateCcw className="mr-2 size-4" />
|
<RotateCcw className="mr-2 size-4" />
|
||||||
@@ -415,7 +415,7 @@ export const RestoreBackup = ({
|
|||||||
<FormLabel className="flex items-center justify-between">
|
<FormLabel className="flex items-center justify-between">
|
||||||
Search Backup Files
|
Search Backup Files
|
||||||
{field.value && (
|
{field.value && (
|
||||||
<Badge variant="outline">
|
<Badge variant="outline" className="truncate">
|
||||||
{field.value}
|
{field.value}
|
||||||
<Copy
|
<Copy
|
||||||
className="ml-2 size-4 cursor-pointer"
|
className="ml-2 size-4 cursor-pointer"
|
||||||
@@ -439,7 +439,9 @@ export const RestoreBackup = ({
|
|||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field.value || "Search and select a backup file"}
|
<span className="truncate text-left flex-1 w-52">
|
||||||
|
{field.value || "Search and select a backup file"}
|
||||||
|
</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
|||||||
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 bg-card overflow-y-auto max-h-[80vh]">
|
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
|
||||||
<code>
|
<code>
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const ShowDockerModalLogs = ({
|
|||||||
{children}
|
{children}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
<DialogContent className="sm:max-w-7xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>View Logs</DialogTitle>
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const ShowDockerModalStackLogs = ({
|
|||||||
{children}
|
{children}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
<DialogContent className="sm:max-w-7xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>View Logs</DialogTitle>
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const DockerTerminalModal = ({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="max-h-screen overflow-y-auto sm:max-w-7xl"
|
className="sm:max-w-7xl"
|
||||||
onEscapeKeyDown={(event) => event.preventDefault()}
|
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify MariaDB</DialogTitle>
|
<DialogTitle>Modify MariaDB</DialogTitle>
|
||||||
<DialogDescription>Update the MariaDB data</DialogDescription>
|
<DialogDescription>Update the MariaDB data</DialogDescription>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify MongoDB</DialogTitle>
|
<DialogTitle>Modify MongoDB</DialogTitle>
|
||||||
<DialogDescription>Update the MongoDB data</DialogDescription>
|
<DialogDescription>Update the MongoDB data</DialogDescription>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
|||||||
? queryError.message
|
? queryError.message
|
||||||
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
||||||
</p>
|
</p>
|
||||||
<p className=" text-sm text-muted-foreground">URL: {baseUrl}</p>
|
<p className="text-sm text-muted-foreground">URL: {baseUrl}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const ShowPaidMonitoring = ({
|
|||||||
? queryError.message
|
? queryError.message
|
||||||
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
||||||
</p>
|
</p>
|
||||||
<p className=" text-sm text-muted-foreground">URL: {BASE_URL}</p>
|
<p className="text-sm text-muted-foreground">URL: {BASE_URL}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify MySQL</DialogTitle>
|
<DialogTitle>Modify MySQL</DialogTitle>
|
||||||
<DialogDescription>Update the MySQL data</DialogDescription>
|
<DialogDescription>Update the MySQL data</DialogDescription>
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="logo"
|
name="logo"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className=" gap-4">
|
<FormItem className="gap-4">
|
||||||
<FormLabel className="text-right">Logo URL</FormLabel>
|
<FormLabel className="text-right">Logo URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -169,7 +169,7 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter className="mt-4">
|
<DialogFooter>
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
{organizationId ? "Update organization" : "Create organization"}
|
{organizationId ? "Update organization" : "Create organization"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Postgres</DialogTitle>
|
<DialogTitle>Modify Postgres</DialogTitle>
|
||||||
<DialogDescription>Update the Postgres data</DialogDescription>
|
<DialogDescription>Update the Postgres data</DialogDescription>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((_e) => {
|
.catch(() => {
|
||||||
toast.error("Error creating the service");
|
toast.error("Error creating the service");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -119,7 +119,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
<span>Application</span>
|
<span>Application</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create</DialogTitle>
|
<DialogTitle>Create</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
<span>Compose</span>
|
<span>Compose</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
|
<DialogContent className="sm:max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Compose</DialogTitle>
|
<DialogTitle>Create Compose</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
<span>Database</span>
|
<span>Database</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="md:max-h-[90vh] sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Databases</DialogTitle>
|
<DialogTitle>Databases</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
<span>Template</span>
|
<span>Template</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen sm:max-w-[90vw] p-0">
|
<DialogContent className="sm:max-w-[90vw] p-0">
|
||||||
<DialogHeader className="sticky top-0 z-10 bg-background p-6 border-b">
|
<DialogHeader className="sticky top-0 z-10 bg-background p-6 border-b">
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
<AccordionItem value="description">
|
<AccordionItem value="description">
|
||||||
<AccordionTrigger>Description</AccordionTrigger>
|
<AccordionTrigger>Description</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ScrollArea className=" w-full rounded-md border p-4">
|
<ScrollArea className="w-full rounded-md border p-4">
|
||||||
<ReactMarkdown className="text-muted-foreground text-sm">
|
<ReactMarkdown className="text-muted-foreground text-sm">
|
||||||
{selectedVariant?.description}
|
{selectedVariant?.description}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
@@ -289,7 +289,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
<AccordionItem value="env-variables">
|
<AccordionItem value="env-variables">
|
||||||
<AccordionTrigger>Environment Variables</AccordionTrigger>
|
<AccordionTrigger>Environment Variables</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ScrollArea className=" w-full rounded-md border">
|
<ScrollArea className="w-full rounded-md border">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{selectedVariant?.envVariables.map((env, index) => (
|
{selectedVariant?.envVariables.map((env, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -364,7 +364,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
<AccordionItem value="domains">
|
<AccordionItem value="domains">
|
||||||
<AccordionTrigger>Domains</AccordionTrigger>
|
<AccordionTrigger>Domains</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ScrollArea className=" w-full rounded-md border">
|
<ScrollArea className="w-full rounded-md border">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{selectedVariant?.domains.map((domain, index) => (
|
{selectedVariant?.domains.map((domain, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const TemplateGenerator = ({ projectId }: Props) => {
|
|||||||
<span>AI Assistant</span>
|
<span>AI Assistant</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl w-full flex flex-col">
|
<DialogContent className="sm:max-w-4xl w-full flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>AI Assistant</DialogTitle>
|
<DialogTitle>AI Assistant</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl">
|
<DialogContent className="sm:max-w-6xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Project Environment</DialogTitle>
|
<DialogTitle>Project Environment</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Redis</DialogTitle>
|
<DialogTitle>Modify Redis</DialogTitle>
|
||||||
<DialogDescription>Update the redis data</DialogDescription>
|
<DialogDescription>Update the redis data</DialogDescription>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const columns: ColumnDef<LogEntry>[] = [
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const log = row.original;
|
const log = row.original;
|
||||||
return (
|
return (
|
||||||
<div className=" flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center flex-row gap-3 ">
|
<div className="flex items-center flex-row gap-3 ">
|
||||||
{log.RequestMethod}{" "}
|
{log.RequestMethod}{" "}
|
||||||
<div className="inline-flex items-center gap-2 bg-muted px-1.5 py-1 rounded-lg">
|
<div className="inline-flex items-center gap-2 bg-muted px-1.5 py-1 rounded-lg">
|
||||||
@@ -86,7 +86,7 @@ export const columns: ColumnDef<LogEntry>[] = [
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const log = row.original;
|
const log = row.original;
|
||||||
return (
|
return (
|
||||||
<div className=" flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-row gap-3 w-full">
|
<div className="flex flex-row gap-3 w-full">
|
||||||
{format(new Date(log.StartUTC), "yyyy-MM-dd HH:mm:ss")}
|
{format(new Date(log.StartUTC), "yyyy-MM-dd HH:mm:ss")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const AddApiKey = () => {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>Generate New Key</Button>
|
<Button>Generate New Key</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-xl max-h-[90vh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Generate API Key</DialogTitle>
|
<DialogTitle>Generate API Key</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export const ShowBilling = () => {
|
|||||||
)}
|
)}
|
||||||
{isAnnual ? (
|
{isAnnual ? (
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(
|
{calculatePrice(
|
||||||
serverQuantity,
|
serverQuantity,
|
||||||
@@ -180,7 +180,7 @@ export const ShowBilling = () => {
|
|||||||
USD
|
USD
|
||||||
</p>
|
</p>
|
||||||
|
|
|
|
||||||
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
${" "}
|
${" "}
|
||||||
{(
|
{(
|
||||||
calculatePrice(serverQuantity, isAnnual) / 12
|
calculatePrice(serverQuantity, isAnnual) / 12
|
||||||
@@ -189,7 +189,7 @@ export const ShowBilling = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const ShowWelcomeDokploy = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
<DialogContent className="sm:max-w-xl max-h-screen overflow-y-auto">
|
<DialogContent className="sm:max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl font-semibold text-center">
|
<DialogTitle className="text-2xl font-semibold text-center">
|
||||||
Welcome to Dokploy Cloud 🎉
|
Welcome to Dokploy Cloud 🎉
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const AddCertificate = () => {
|
|||||||
Add Certificate
|
Add Certificate
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add New Certificate</DialogTitle>
|
<DialogTitle>Add New Certificate</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -222,7 +222,7 @@ export const AddCertificate = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-end pt-3">
|
<DialogFooter className="flex w-full flex-row !justify-end">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-add-certificate"
|
form="hook-form-add-certificate"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const AddNode = ({ serverId }: Props) => {
|
|||||||
Add Node
|
Add Node
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add Node</DialogTitle>
|
<DialogTitle>Add Node</DialogTitle>
|
||||||
<DialogDescription className="flex flex-col gap-2">
|
<DialogDescription className="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const ShowNodeData = ({ data }: Props) => {
|
|||||||
View Config
|
View Config
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Node Config</DialogTitle>
|
<DialogTitle>Node Config</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const ShowNodesModal = ({ serverId }: Props) => {
|
|||||||
Show Swarm Nodes
|
Show Swarm Nodes
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
<DialogContent className="min-w-[70vw]">
|
||||||
<div className="grid w-full gap-1">
|
<div className="grid w-full gap-1">
|
||||||
<ShowNodes serverId={serverId} />
|
<ShowNodes serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const ShowNodes = ({ serverId }: Props) => {
|
|||||||
</TableCaption>
|
</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Hostname</TableHead>
|
<TableHead className="text-left">Hostname</TableHead>
|
||||||
<TableHead className="text-right">Status</TableHead>
|
<TableHead className="text-right">Status</TableHead>
|
||||||
<TableHead className="text-right">Role</TableHead>
|
<TableHead className="text-right">Role</TableHead>
|
||||||
<TableHead className="text-right">Availability</TableHead>
|
<TableHead className="text-right">Availability</TableHead>
|
||||||
@@ -104,7 +104,7 @@ export const ShowNodes = ({ serverId }: Props) => {
|
|||||||
const isManager = node.Spec.Role === "manager";
|
const isManager = node.Spec.Role === "manager";
|
||||||
return (
|
return (
|
||||||
<TableRow key={node.ID}>
|
<TableRow key={node.ID}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="text-left">
|
||||||
{node.Description.Hostname}
|
{node.Description.Hostname}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl max-h-screen overflow-y-auto">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add a external registry</DialogTitle>
|
<DialogTitle>Add a external registry</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -316,7 +316,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col col-span-2 mt-6">
|
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col col-span-2">
|
||||||
<div className="flex flex-row gap-2 justify-between">
|
<div className="flex flex-row gap-2 justify-between">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{destinationId ? "Update" : "Add"} Destination
|
{destinationId ? "Update" : "Add"} Destination
|
||||||
@@ -359,7 +359,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
<DialogFooter
|
<DialogFooter
|
||||||
className={cn(
|
className={cn(
|
||||||
isCloud ? "!flex-col" : "flex-row",
|
isCloud ? "!flex-col" : "flex-row",
|
||||||
"flex w-full !justify-between pt-3 gap-4",
|
"flex w-full !justify-between gap-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCloud ? (
|
{isCloud ? (
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const AddBitbucketProvider = () => {
|
|||||||
<span>Bitbucket</span>
|
<span>Bitbucket</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
<DialogContent className="sm:max-w-2xl ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
Bitbucket Provider <BitbucketIcon className="size-5" />
|
Bitbucket Provider <BitbucketIcon className="size-5" />
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
<DialogContent className="sm:max-w-2xl ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
Update Bitbucket <BitbucketIcon className="size-5" />
|
Update Bitbucket <BitbucketIcon className="size-5" />
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const AddGiteaProvider = () => {
|
|||||||
<span>Gitea</span>
|
<span>Gitea</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
Gitea Provider <GiteaIcon className="size-5" />
|
Gitea Provider <GiteaIcon className="size-5" />
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
<DialogContent className="sm:max-w-2xl ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
Update Github <GithubIcon className="size-5" />
|
Update Github <GithubIcon className="size-5" />
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const AddGitlabProvider = () => {
|
|||||||
<span>GitLab</span>
|
<span>GitLab</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-2xl ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
GitLab Provider <GitlabIcon className="size-5" />
|
GitLab Provider <GitlabIcon className="size-5" />
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
|
|||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
<DialogContent className="sm:max-w-2xl ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
Update GitLab <GitlabIcon className="size-5" />
|
Update GitLab <GitlabIcon className="size-5" />
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const ShowGitProviders = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
<CardHeader className="">
|
<CardHeader className="">
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
@@ -73,14 +73,14 @@ export const ShowGitProviders = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GitBranch className="size-8 self-center text-muted-foreground" />
|
<GitBranch className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
Create your first Git Provider
|
Create your first Git Provider
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
||||||
<div className="flex items-center gap-4 p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex flex-wrap items-center gap-4 p-3.5 rounded-lg bg-background border w-full [&>button]:grow">
|
||||||
<AddGithubProvider />
|
<AddGithubProvider />
|
||||||
<AddGitlabProvider />
|
<AddGitlabProvider />
|
||||||
<AddBitbucketProvider />
|
<AddBitbucketProvider />
|
||||||
@@ -90,13 +90,13 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
<div className="flex flex-col gap-2 rounded-lg ">
|
<div className="flex flex-col gap-2 rounded-lg ">
|
||||||
<span className="text-base font-medium">
|
<span className="text-base font-medium">
|
||||||
Available Providers
|
Available Providers
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
||||||
<div className="flex items-center gap-4 p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex flex-wrap items-center gap-4 p-3.5 rounded-lg bg-background border w-full [&>button]:grow">
|
||||||
<AddGithubProvider />
|
<AddGithubProvider />
|
||||||
<AddGitlabProvider />
|
<AddGitlabProvider />
|
||||||
<AddBitbucketProvider />
|
<AddBitbucketProvider />
|
||||||
@@ -158,7 +158,7 @@ export const ShowGitProviders = () => {
|
|||||||
|
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
{!haveGithubRequirements && isGithub && (
|
{!haveGithubRequirements && isGithub && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Link
|
<Link
|
||||||
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
|
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
@@ -171,7 +171,7 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{haveGithubRequirements && isGithub && (
|
{haveGithubRequirements && isGithub && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Link
|
<Link
|
||||||
href={`${gitProvider?.github?.githubAppName}`}
|
href={`${gitProvider?.github?.githubAppName}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -185,7 +185,7 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!haveGitlabRequirements && isGitlab && (
|
{!haveGitlabRequirements && isGitlab && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Link
|
<Link
|
||||||
href={getGitlabUrl(
|
href={getGitlabUrl(
|
||||||
gitProvider.gitlab?.applicationId || "",
|
gitProvider.gitlab?.applicationId || "",
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{notificationId ? "Update" : "Add"} Notification
|
{notificationId ? "Update" : "Add"} Notification
|
||||||
@@ -907,7 +907,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="appBuildError"
|
name="appBuildError"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>App Build Error</FormLabel>
|
<FormLabel>App Build Error</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -928,7 +928,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="databaseBackup"
|
name="databaseBackup"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Database Backup</FormLabel>
|
<FormLabel>Database Backup</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -949,7 +949,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerCleanup"
|
name="dockerCleanup"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Docker Cleanup</FormLabel>
|
<FormLabel>Docker Cleanup</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -972,7 +972,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="dokployRestart"
|
name="dokployRestart"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -995,7 +995,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="serverThreshold"
|
name="serverThreshold"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Server Threshold</FormLabel>
|
<FormLabel>Server Threshold</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -1063,7 +1063,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast.success("Connection Success");
|
toast.success("Connection Success");
|
||||||
} catch (_err) {
|
} catch {
|
||||||
toast.error("Error testing the provider");
|
toast.error("Error testing the provider");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const Disable2FA = () => {
|
|||||||
toast.success("2FA disabled successfully");
|
toast.success("2FA disabled successfully");
|
||||||
utils.user.get.invalidate();
|
utils.user.get.invalidate();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
} catch (_error) {
|
} catch {
|
||||||
form.setError("password", {
|
form.setError("password", {
|
||||||
message: "Connection error. Please try again.",
|
message: "Connection error. Please try again.",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export const Enable2FA = () => {
|
|||||||
Enable 2FA
|
Enable 2FA
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
|
<DialogContent className="sm:max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>2FA Setup</DialogTitle>
|
<DialogTitle>2FA Setup</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const ShowServerActions = ({ serverId }: Props) => {
|
|||||||
View Actions
|
View Actions
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen">
|
<DialogContent className="sm:max-w-xl">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
||||||
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
}
|
}
|
||||||
toast.success("Docker Cleanup updated");
|
toast.success("Docker Cleanup updated");
|
||||||
} catch (_error) {
|
} catch {
|
||||||
toast.error("Docker Cleanup Error");
|
toast.error("Docker Cleanup Error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const EditScript = ({ serverId }: Props) => {
|
|||||||
<FileTerminal className="size-4 text-muted-foreground" />
|
<FileTerminal className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl overflow-x-hidden">
|
<DialogContent className="sm:max-w-5xl overflow-x-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Script</DialogTitle>
|
<DialogTitle>Modify Script</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const GPUSupportModal = () => {
|
|||||||
<span>GPU Setup</span>
|
<span>GPU Setup</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-4xl overflow-y-auto max-h-screen">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
Dokploy Server GPU Setup
|
Dokploy Server GPU Setup
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
|
|||||||
try {
|
try {
|
||||||
await utils.settings.checkGPUStatus.invalidate({ serverId });
|
await utils.settings.checkGPUStatus.invalidate({ serverId });
|
||||||
await refetch();
|
await refetch();
|
||||||
} catch (_error) {
|
} catch {
|
||||||
toast.error("Failed to refresh GPU status");
|
toast.error("Failed to refresh GPU status");
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
@@ -74,7 +74,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await setupGPU.mutateAsync({ serverId });
|
await setupGPU.mutateAsync({ serverId });
|
||||||
} catch (_error) {
|
} catch {
|
||||||
// Error handling is done in mutation's onError
|
// Error handling is done in mutation's onError
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
Setup Server
|
Setup Server
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-4xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-4xl ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
@@ -152,7 +152,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
Copy Public Key ({server?.sshKey?.name})
|
Copy Public Key ({server?.sshKey?.name})
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className=" right-2 top-8"
|
className="right-2 top-8"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copy(
|
copy(
|
||||||
server?.sshKey?.publicKey || "Generate a SSH Key",
|
server?.sshKey?.publicKey || "Generate a SSH Key",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const ShowDockerContainersModal = ({ serverId }: Props) => {
|
|||||||
Show Docker Containers
|
Show Docker Containers
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-7xl ">
|
||||||
<div className="grid w-full gap-1">
|
<div className="grid w-full gap-1">
|
||||||
<ShowContainers serverId={serverId} />
|
<ShowContainers serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const ShowMonitoringModal = ({ url, token }: Props) => {
|
|||||||
Show Monitoring
|
Show Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-7xl ">
|
||||||
<div className="flex gap-4 py-4 w-full">
|
<div className="flex gap-4 py-4 w-full">
|
||||||
<ShowPaidMonitoring BASE_URL={url} token={token} />
|
<ShowPaidMonitoring BASE_URL={url} token={token} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const ShowSchedulesModal = ({ serverId }: Props) => {
|
|||||||
Show Schedules
|
Show Schedules
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-5xl ">
|
||||||
<ShowSchedules id={serverId} scheduleType="server" />
|
<ShowSchedules id={serverId} scheduleType="server" />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const ShowServers = () => {
|
|||||||
</TableCaption>
|
</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Name</TableHead>
|
<TableHead className="text-left">Name</TableHead>
|
||||||
{isCloud && (
|
{isCloud && (
|
||||||
<TableHead className="text-center">
|
<TableHead className="text-center">
|
||||||
Status
|
Status
|
||||||
@@ -173,7 +173,7 @@ export const ShowServers = () => {
|
|||||||
const isActive = server.serverStatus === "active";
|
const isActive = server.serverStatus === "active";
|
||||||
return (
|
return (
|
||||||
<TableRow key={server.serverId}>
|
<TableRow key={server.serverId}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="text-left">
|
||||||
{server.name}
|
{server.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{isCloud && (
|
{isCloud && (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const ShowSwarmOverviewModal = ({ serverId }: Props) => {
|
|||||||
Show Swarm Overview
|
Show Swarm Overview
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-7xl ">
|
||||||
<div className="grid w-full gap-1">
|
<div className="grid w-full gap-1">
|
||||||
<SwarmMonitorCard serverId={serverId} />
|
<SwarmMonitorCard serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const ShowTraefikFileSystemModal = ({ serverId }: Props) => {
|
|||||||
Show Traefik File System
|
Show Traefik File System
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-7xl ">
|
||||||
<ShowTraefikSystem serverId={serverId} />
|
<ShowTraefikSystem serverId={serverId} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ export const CreateServer = ({ stepper }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="pt-5">
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
disabled={!canCreateMoreServers}
|
disabled={!canCreateMoreServers}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user