mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bdaa81263 | ||
|
|
baf36b6fb6 | ||
|
|
d632e83799 | ||
|
|
6f52edd845 | ||
|
|
9d0f5bc8cd | ||
|
|
3dc558c260 | ||
|
|
180aa34140 | ||
|
|
ffc85b04a8 | ||
|
|
dbcfc702d4 | ||
|
|
7805efc738 | ||
|
|
3910e22412 | ||
|
|
2f16034cb0 | ||
|
|
d4925dd2b7 | ||
|
|
5aba6c79a0 | ||
|
|
84f5627471 | ||
|
|
4eaf8fee0f | ||
|
|
adee87b6da | ||
|
|
e5e987fcf9 | ||
|
|
d0a6373dcc | ||
|
|
8ed44066ad | ||
|
|
befe2193a7 | ||
|
|
f20c73cdee | ||
|
|
16bfc09202 | ||
|
|
d54a61b2a4 | ||
|
|
60c09a6434 | ||
|
|
5361e9074f | ||
|
|
13d4dea504 | ||
|
|
ffc2d593e4 | ||
|
|
297439a348 | ||
|
|
ff3e067866 | ||
|
|
f008a45bf2 | ||
|
|
50c8503cf9 | ||
|
|
930a03de60 | ||
|
|
2d3d86e823 | ||
|
|
7bab166e1b | ||
|
|
7a6e1dbc1b | ||
|
|
17a859d26d | ||
|
|
d793c6a2ec | ||
|
|
3adb9d54f4 | ||
|
|
7144adbf0c | ||
|
|
55328468d1 | ||
|
|
6968cb6930 | ||
|
|
a431e4c58e | ||
|
|
fe967239b4 | ||
|
|
c5b4b85470 | ||
|
|
b1ef9d25b1 | ||
|
|
74f7c51530 | ||
|
|
4ba2b9fe8d | ||
|
|
413eda50f4 | ||
|
|
9f09681708 | ||
|
|
8eb174812d | ||
|
|
be77f114eb | ||
|
|
ca42708035 | ||
|
|
8b03454a87 | ||
|
|
fa7f749f84 | ||
|
|
3daecd7d71 | ||
|
|
0666b5b292 | ||
|
|
b288ddd826 | ||
|
|
beadcf871a | ||
|
|
ee49dadf0b | ||
|
|
46de83a1de | ||
|
|
fee5024b7d | ||
|
|
1f28a21835 | ||
|
|
0114b371f5 | ||
|
|
66d6cb5710 | ||
|
|
5927c7c3c5 | ||
|
|
84afcf0de5 | ||
|
|
e3527f7d69 | ||
|
|
39f4a35cc8 | ||
|
|
e0433e9f7b | ||
|
|
d29ff881fc | ||
|
|
568c3a1d06 | ||
|
|
e9fd280fa2 | ||
|
|
5d5913f39d | ||
|
|
f04c8a36af | ||
|
|
d5137d5d3a | ||
|
|
9535fca28f | ||
|
|
dd62d603e0 | ||
|
|
8d227e2a2c | ||
|
|
048c8ffc11 | ||
|
|
b59597630c | ||
|
|
707463f973 | ||
|
|
4b3e0805a4 | ||
|
|
148c30f604 | ||
|
|
95f79f2afb | ||
|
|
a067abd3e4 | ||
|
|
9359ee7a04 | ||
|
|
fc7eff94b6 | ||
|
|
ff3d444b89 | ||
|
|
530ad31aaa | ||
|
|
68d0a48843 | ||
|
|
a4e4d1c467 | ||
|
|
56d8defebe | ||
|
|
997e755b6f | ||
|
|
852011dde8 | ||
|
|
d7ef201adb | ||
|
|
91183056f0 | ||
|
|
03bd4398d0 | ||
|
|
8c260eff72 | ||
|
|
4eef65f1b7 | ||
|
|
a7535c6862 | ||
|
|
6e28196b0e | ||
|
|
18bacae175 | ||
|
|
f2be5a378e | ||
|
|
aef24296b9 | ||
|
|
7123b9b109 | ||
|
|
891dc840f5 | ||
|
|
bc78100613 | ||
|
|
ff22404b3b | ||
|
|
17330ca71a | ||
|
|
2898a5e575 | ||
|
|
172694be30 | ||
|
|
ea6cfc9d29 | ||
|
|
4fa5e10789 | ||
|
|
cb7fbb777c | ||
|
|
6a388fe370 | ||
|
|
0722182650 | ||
|
|
5e1095d199 | ||
|
|
c80a31e8c4 | ||
|
|
fac8ea7a30 | ||
|
|
9a11d0db97 | ||
|
|
3cdf4c426c | ||
|
|
7cb184dc97 | ||
|
|
fe57333f84 | ||
|
|
04fd77c3a9 | ||
|
|
7c17cfb5c7 | ||
|
|
c6a288781f | ||
|
|
724bed9832 | ||
|
|
2405e5a93a | ||
|
|
e97c8f42b3 | ||
|
|
d805f6a7aa | ||
|
|
45d05b2aa4 | ||
|
|
6d350a23a9 | ||
|
|
5965b73342 | ||
|
|
b8e06feaff | ||
|
|
3c5a005165 | ||
|
|
12d31c89f3 | ||
|
|
cf28640188 | ||
|
|
3cf7c697b8 | ||
|
|
75fc030984 | ||
|
|
060a170aee | ||
|
|
40718293a1 | ||
|
|
ea39b152f4 | ||
|
|
027406547e | ||
|
|
2974a8183e | ||
|
|
9ac68985e0 | ||
|
|
35ff8dcfe6 | ||
|
|
60c03e1ca7 | ||
|
|
d42fa738ea | ||
|
|
160742c2cf | ||
|
|
4c5bc541d6 | ||
|
|
d13871cd08 | ||
|
|
a12beb6748 | ||
|
|
4c90f4754f | ||
|
|
69fdda505d | ||
|
|
16e84e431a | ||
|
|
5d4db4d0f3 | ||
|
|
10d2493bcc | ||
|
|
ce97bc6c27 | ||
|
|
c2e05e86d9 | ||
|
|
5cd743eb10 | ||
|
|
cd32c55031 | ||
|
|
7f2ebab66c | ||
|
|
0bc2734925 | ||
|
|
f74d02381f | ||
|
|
d46afbef2d | ||
|
|
be64a1554d | ||
|
|
8d9d00d0c6 | ||
|
|
31164c9798 | ||
|
|
4d4de1424e | ||
|
|
fa954c3bbd | ||
|
|
005f73d665 | ||
|
|
bbe7d5bdc5 | ||
|
|
6f7a5609a3 | ||
|
|
c3a5e2a8d6 | ||
|
|
1ca965268e | ||
|
|
e323ade29e | ||
|
|
8c916bc431 | ||
|
|
0670f9b910 | ||
|
|
44f002d8d0 | ||
|
|
27f6c945e0 | ||
|
|
e61c216ea0 | ||
|
|
9f9492af79 | ||
|
|
68f608bdc9 | ||
|
|
8f671d1691 | ||
|
|
7afbe8b208 | ||
|
|
8c05214e78 | ||
|
|
07769e69d6 | ||
|
|
2ace36f035 | ||
|
|
b7196a3494 | ||
|
|
3b737ca55b | ||
|
|
581e590f65 | ||
|
|
ac0922d742 | ||
|
|
d66a5d55a3 | ||
|
|
47db6831b4 | ||
|
|
56cbd1abb3 | ||
|
|
cb40ac5c6b | ||
|
|
7218b3f79b | ||
|
|
19ea4d3fcd | ||
|
|
6edfd1e547 | ||
|
|
666a8ede97 | ||
|
|
08e4b8fe33 | ||
|
|
5fc265d14f | ||
|
|
c3887af5d1 | ||
|
|
6df0878ed4 | ||
|
|
a1bbfaebf4 | ||
|
|
ed89f5aa8a | ||
|
|
888e904d75 | ||
|
|
3e522b9cae | ||
|
|
7903ddba89 | ||
|
|
3a0dbc26d1 | ||
|
|
6df680e9da | ||
|
|
2bced3e9b6 | ||
|
|
911a7730f9 | ||
|
|
2902648188 |
2
.github/workflows/dokploy.yml
vendored
2
.github/workflows/dokploy.yml
vendored
@@ -2,7 +2,7 @@ name: Dokploy Docker Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, canary, "feat/better-auth-2"]
|
||||
branches: [main, canary, "1061-custom-docker-service-hostname"]
|
||||
|
||||
env:
|
||||
IMAGE_NAME: dokploy/dokploy
|
||||
|
||||
22
.github/workflows/format.yml
vendored
Normal file
22
.github/workflows/format.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: autofix.ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [canary]
|
||||
pull_request:
|
||||
branches: [canary]
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup biomeJs
|
||||
uses: biomejs/setup-biome@v2
|
||||
|
||||
- name: Run Biome formatter
|
||||
run: biome format . --write
|
||||
|
||||
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
|
||||
@@ -61,9 +61,9 @@ pnpm install
|
||||
cp apps/dokploy/.env.example apps/dokploy/.env
|
||||
```
|
||||
|
||||
## Development
|
||||
## Requirements
|
||||
|
||||
Is required to have **Docker** installed on your machine.
|
||||
- [Docker](/GUIDES.md#docker)
|
||||
|
||||
### Setup
|
||||
|
||||
@@ -165,86 +165,8 @@ Thank you for your contribution!
|
||||
|
||||
## Templates
|
||||
|
||||
To add a new template, go to `templates` folder and create a new folder with the name of the template.
|
||||
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
|
||||
|
||||
Let's take the example of `plausible` template.
|
||||
|
||||
1. create a folder in `templates/plausible`
|
||||
2. create a `docker-compose.yml` file inside the folder with the content of compose.
|
||||
3. create a `index.ts` file inside the folder with the following code as base:
|
||||
4. When creating a pull request, please provide a video of the template working in action.
|
||||
|
||||
```typescript
|
||||
// EXAMPLE
|
||||
import {
|
||||
generateBase64,
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
type DomainSchema,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const mainDomain = generateRandomDomain(schema);
|
||||
const secretBase = generateBase64(64);
|
||||
const toptKeyBase = generateBase64(32);
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 8000,
|
||||
serviceName: "plausible",
|
||||
},
|
||||
];
|
||||
|
||||
const envs = [
|
||||
`BASE_URL=http://${mainDomain}`,
|
||||
`SECRET_KEY_BASE=${secretBase}`,
|
||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
filePath: "./clickhouse/clickhouse-config.xml",
|
||||
content: "some content......",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
mounts,
|
||||
domains,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties:
|
||||
|
||||
**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.**
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: "plausible",
|
||||
name: "Plausible",
|
||||
version: "v2.1.0",
|
||||
description:
|
||||
"Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.",
|
||||
logo: "plausible.svg", // we defined the name and the extension of the logo
|
||||
links: {
|
||||
github: "https://github.com/plausible/plausible",
|
||||
website: "https://plausible.io/",
|
||||
docs: "https://plausible.io/docs",
|
||||
},
|
||||
tags: ["analytics"],
|
||||
load: () => import("./plausible/index").then((m) => m.generate),
|
||||
},
|
||||
```
|
||||
|
||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
||||
|
||||
### Recommendations
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
|
||||
49
GUIDES.md
Normal file
49
GUIDES.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Docker
|
||||
|
||||
Here's how to install docker on different operating systems:
|
||||
|
||||
## macOS
|
||||
|
||||
1. Visit [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop)
|
||||
2. Download the Docker Desktop installer
|
||||
3. Double-click the downloaded `.dmg` file
|
||||
4. Drag Docker to your Applications folder
|
||||
5. Open Docker Desktop from Applications
|
||||
6. Follow the onboarding tutorial if desired
|
||||
|
||||
## Linux
|
||||
|
||||
### Ubuntu
|
||||
|
||||
```bash
|
||||
# Update package index
|
||||
sudo apt-get update
|
||||
|
||||
# Install prerequisites
|
||||
sudo apt-get install \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
lsb-release
|
||||
|
||||
# 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
|
||||
|
||||
# Set up stable repository
|
||||
echo \
|
||||
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
||||
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
# Install Docker Engine
|
||||
sudo apt-get update
|
||||
sudo apt-get install docker-ce docker-ce-cli containerd.io
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
1. Enable WSL2 if not already enabled
|
||||
2. Visit [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop)
|
||||
3. Download the installer
|
||||
4. Run the installer and follow the prompts
|
||||
5. Start Docker Desktop from the Start menu
|
||||
@@ -1,242 +0,0 @@
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute.
|
||||
|
||||
|
||||
Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues.
|
||||
|
||||
We have a few guidelines to follow when contributing to this project:
|
||||
|
||||
- [Commit Convention](#commit-convention)
|
||||
- [Setup](#setup)
|
||||
- [Development](#development)
|
||||
- [Build](#build)
|
||||
- [Pull Request](#pull-request)
|
||||
|
||||
## Commit Convention
|
||||
|
||||
Before you craete a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
### Commit Message Format
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
#### Type
|
||||
Must be one of the following:
|
||||
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **docs**: Documentation only changes
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **perf**: A code change that improves performance
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
||||
* **chore**: Other changes that don't modify `src` or `test` files
|
||||
* **revert**: Reverts a previous commit
|
||||
|
||||
Example:
|
||||
```
|
||||
feat: add new feature
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dokploy/dokploy.git
|
||||
cd dokploy
|
||||
pnpm install
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Is required to have **Docker** installed on your machine.
|
||||
|
||||
|
||||
### Setup
|
||||
|
||||
Run the command that will spin up all the required services and files.
|
||||
|
||||
```bash
|
||||
pnpm run setup
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
|
||||
Go to http://localhost:3000 to see the development server
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
To build the docker image
|
||||
```bash
|
||||
pnpm run docker:build
|
||||
```
|
||||
|
||||
To push the docker image
|
||||
```bash
|
||||
pnpm run docker:push
|
||||
```
|
||||
|
||||
## Password Reset
|
||||
|
||||
In the case you lost your password, you can reset it using the following command
|
||||
|
||||
```bash
|
||||
pnpm run reset-password
|
||||
```
|
||||
|
||||
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
|
||||
|
||||
```bash
|
||||
bunx lt --port 3000
|
||||
```
|
||||
|
||||
If you run into permission issues of docker run the following command
|
||||
|
||||
```bash
|
||||
sudo chown -R USERNAME dokploy or sudo chown -R $(whoami) ~/.docker
|
||||
```
|
||||
|
||||
## Application deploy
|
||||
|
||||
In case you want to deploy the application on your machine and you selected nixpacks or buildpacks, you need to install first.
|
||||
|
||||
```bash
|
||||
# Install Nixpacks
|
||||
curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& chmod +x install.sh \
|
||||
&& ./install.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
# Install Buildpacks
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
```
|
||||
|
||||
|
||||
## Pull Request
|
||||
|
||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||
- Create a new branch for each feature or bug fix.
|
||||
- Make sure to add tests for your changes.
|
||||
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
|
||||
- When creating a pull request, please provide a clear and concise description of the changes made.
|
||||
- If you include a video or screenshot, would be awesome so we can see the changes in action.
|
||||
- If your pull request fixes an open issue, please reference the issue in the pull request description.
|
||||
- Once your pull request is merged, you will be automatically added as a contributor to the project.
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Templates
|
||||
|
||||
To add a new template, go to `templates` folder and create a new folder with the name of the template.
|
||||
|
||||
Let's take the example of `plausible` template.
|
||||
|
||||
1. create a folder in `templates/plausible`
|
||||
2. create a `docker-compose.yml` file inside the folder with the content of compose.
|
||||
3. create a `index.ts` file inside the folder with the following code as base:
|
||||
4. When creating a pull request, please provide a video of the template working in action.
|
||||
|
||||
```typescript
|
||||
// EXAMPLE
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
|
||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const secretBase = generateBase64(64);
|
||||
const toptKeyBase = generateBase64(32);
|
||||
|
||||
const envs = [
|
||||
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
||||
`PLAUSIBLE_HOST=${randomDomain}`,
|
||||
"PLAUSIBLE_PORT=8000",
|
||||
`BASE_URL=http://${randomDomain}`,
|
||||
`SECRET_KEY_BASE=${secretBase}`,
|
||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||
content: `some content......`,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
mounts,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties:
|
||||
|
||||
**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.**
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: "plausible",
|
||||
name: "Plausible",
|
||||
version: "v2.1.0",
|
||||
description:
|
||||
"Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.",
|
||||
logo: "plausible.svg", // we defined the name and the extension of the logo
|
||||
links: {
|
||||
github: "https://github.com/plausible/plausible",
|
||||
website: "https://plausible.io/",
|
||||
docs: "https://plausible.io/docs",
|
||||
},
|
||||
tags: ["analytics"],
|
||||
load: () => import("./plausible/index").then((m) => m.generate),
|
||||
},
|
||||
```
|
||||
|
||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
||||
|
||||
|
||||
### Recomendations
|
||||
- Use the same name of the folder as the id of the template.
|
||||
- The logo should be in the public folder.
|
||||
- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
||||
- Test first on a vps or a server to make sure the template works.
|
||||
|
||||
@@ -1006,7 +1006,7 @@ services:
|
||||
|
||||
volumes:
|
||||
db-config-testhash:
|
||||
`) as ComposeSpecification;
|
||||
`);
|
||||
|
||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
||||
@@ -1115,3 +1115,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
|
||||
});
|
||||
|
||||
const composeFileBackrest = `
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:v1.7.3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9898
|
||||
environment:
|
||||
- BACKREST_PORT=9898
|
||||
- BACKREST_DATA=/data
|
||||
- BACKREST_CONFIG=/config/config.json
|
||||
- XDG_CACHE_HOME=/cache
|
||||
- TZ=\${TZ}
|
||||
volumes:
|
||||
- backrest/data:/data
|
||||
- backrest/config:/config
|
||||
- backrest/cache:/cache
|
||||
- /:/userdata:ro
|
||||
|
||||
volumes:
|
||||
backrest:
|
||||
backrest-cache:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeBackrest = load(`
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:v1.7.3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9898
|
||||
environment:
|
||||
- BACKREST_PORT=9898
|
||||
- BACKREST_DATA=/data
|
||||
- BACKREST_CONFIG=/config/config.json
|
||||
- XDG_CACHE_HOME=/cache
|
||||
- TZ=\${TZ}
|
||||
volumes:
|
||||
- backrest-testhash/data:/data
|
||||
- backrest-testhash/config:/config
|
||||
- backrest-testhash/cache:/cache
|
||||
- /:/userdata:ro
|
||||
|
||||
volumes:
|
||||
backrest-testhash:
|
||||
backrest-cache-testhash:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Should handle volume paths with subdirectories correctly", () => {
|
||||
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerComposeBackrest);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,12 @@ if (typeof window === "undefined") {
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
herokuVersion: "",
|
||||
giteaBranch: "",
|
||||
giteaBuildPath: "",
|
||||
giteaId: "",
|
||||
giteaOwner: "",
|
||||
giteaRepository: "",
|
||||
cleanCache: false,
|
||||
watchPaths: [],
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Schema } from "@dokploy/server/templates";
|
||||
import type { CompleteTemplate } from "@dokploy/server/templates/processors";
|
||||
import { processTemplate } from "@dokploy/server/templates/processors";
|
||||
import type { Schema } from "@dokploy/server/templates";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("processTemplate", () => {
|
||||
// Mock schema for testing
|
||||
@@ -166,7 +166,38 @@ describe("processTemplate", () => {
|
||||
if (!baseUrl || !secretKey) return;
|
||||
|
||||
expect(baseUrl).toContain(mockSchema.projectName);
|
||||
expect(secretKey.split("=")[1]).toHaveLength(64);
|
||||
const base64Value = secretKey.split("=")[1];
|
||||
expect(base64Value).toBeDefined();
|
||||
if (!base64Value) return;
|
||||
expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
|
||||
expect(base64Value.length).toBeGreaterThanOrEqual(86);
|
||||
expect(base64Value.length).toBeLessThanOrEqual(88);
|
||||
});
|
||||
|
||||
it("should process env vars when provided as an array", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
domains: [],
|
||||
env: [
|
||||
'CLOUDFLARE_TUNNEL_TOKEN="<INSERT TOKEN>"',
|
||||
'ANOTHER_VAR="some value"',
|
||||
"DOMAIN=${domain}",
|
||||
],
|
||||
mounts: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = processTemplate(template, mockSchema);
|
||||
expect(result.envs).toHaveLength(3);
|
||||
|
||||
// Should preserve exact format for static values
|
||||
expect(result.envs[0]).toBe('CLOUDFLARE_TUNNEL_TOKEN="<INSERT TOKEN>"');
|
||||
expect(result.envs[1]).toBe('ANOTHER_VAR="some value"');
|
||||
|
||||
// Should process variables in array items
|
||||
expect(result.envs[2]).toContain(mockSchema.projectName);
|
||||
});
|
||||
|
||||
it("should allow using utility functions directly in env vars", () => {
|
||||
@@ -195,7 +226,55 @@ describe("processTemplate", () => {
|
||||
if (!randomDomainEnv || !secretKeyEnv) return;
|
||||
|
||||
expect(randomDomainEnv).toContain(mockSchema.projectName);
|
||||
expect(secretKeyEnv.split("=")[1]).toHaveLength(32);
|
||||
const base64Value = secretKeyEnv.split("=")[1];
|
||||
expect(base64Value).toBeDefined();
|
||||
if (!base64Value) return;
|
||||
expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
|
||||
expect(base64Value.length).toBeGreaterThanOrEqual(42);
|
||||
expect(base64Value.length).toBeLessThanOrEqual(44);
|
||||
});
|
||||
|
||||
it("should handle boolean values in env vars when provided as an array", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
domains: [],
|
||||
env: [
|
||||
"ENABLE_USER_SIGN_UP=false",
|
||||
"DEBUG_MODE=true",
|
||||
"SOME_NUMBER=42",
|
||||
],
|
||||
mounts: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = processTemplate(template, mockSchema);
|
||||
expect(result.envs).toHaveLength(3);
|
||||
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||
});
|
||||
|
||||
it("should handle boolean values in env vars when provided as an object", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
domains: [],
|
||||
env: {
|
||||
ENABLE_USER_SIGN_UP: false,
|
||||
DEBUG_MODE: true,
|
||||
SOME_NUMBER: 42,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = processTemplate(template, mockSchema);
|
||||
expect(result.envs).toHaveLength(3);
|
||||
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -325,8 +404,22 @@ describe("processTemplate", () => {
|
||||
if (!baseUrl || !secretKey || !totpKey) return;
|
||||
|
||||
expect(baseUrl).toContain(mockSchema.projectName);
|
||||
expect(secretKey.split("=")[1]).toHaveLength(64);
|
||||
expect(totpKey.split("=")[1]).toHaveLength(32);
|
||||
|
||||
// Check base64 lengths and format
|
||||
const secretKeyValue = secretKey.split("=")[1];
|
||||
const totpKeyValue = totpKey.split("=")[1];
|
||||
|
||||
expect(secretKeyValue).toBeDefined();
|
||||
expect(totpKeyValue).toBeDefined();
|
||||
if (!secretKeyValue || !totpKeyValue) return;
|
||||
|
||||
expect(secretKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
|
||||
expect(secretKeyValue.length).toBeGreaterThanOrEqual(86);
|
||||
expect(secretKeyValue.length).toBeLessThanOrEqual(88);
|
||||
|
||||
expect(totpKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
|
||||
expect(totpKeyValue.length).toBeGreaterThanOrEqual(42);
|
||||
expect(totpKeyValue.length).toBeLessThanOrEqual(44);
|
||||
|
||||
// Check mounts
|
||||
expect(result.mounts).toHaveLength(1);
|
||||
@@ -334,8 +427,8 @@ describe("processTemplate", () => {
|
||||
expect(mount).toBeDefined();
|
||||
if (!mount) return;
|
||||
expect(mount.content).toContain(mockSchema.projectName);
|
||||
expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{64}/);
|
||||
expect(mount.content).toMatch(/totp=[A-Za-z0-9+/]{32}/);
|
||||
expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{86,88}/);
|
||||
expect(mount.content).toMatch(/totp=[A-Za-z0-9+/]{42,44}/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ import { expect, test } from "vitest";
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
herokuVersion: "",
|
||||
giteaRepository: "",
|
||||
giteaOwner: "",
|
||||
giteaBranch: "",
|
||||
giteaBuildPath: "",
|
||||
giteaId: "",
|
||||
cleanCache: false,
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
|
||||
@@ -40,7 +40,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
replicas: z.number(),
|
||||
replicas: z.number().min(1, "Replicas must be at least 1"),
|
||||
registryId: z.string(),
|
||||
});
|
||||
|
||||
@@ -130,9 +130,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
placeholder="1"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number(e.target.value));
|
||||
const value = e.target.value;
|
||||
field.onChange(value === "" ? 0 : Number(value));
|
||||
}}
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -32,7 +33,6 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
const ImportSchema = z.object({
|
||||
base64: z.string(),
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
enum BuildType {
|
||||
export enum BuildType {
|
||||
dockerfile = "dockerfile",
|
||||
heroku_buildpacks = "heroku_buildpacks",
|
||||
paketo_buildpacks = "paketo_buildpacks",
|
||||
@@ -29,9 +29,18 @@ enum BuildType {
|
||||
railpack = "railpack",
|
||||
}
|
||||
|
||||
const buildTypeDisplayMap: Record<BuildType, string> = {
|
||||
[BuildType.dockerfile]: "Dockerfile",
|
||||
[BuildType.railpack]: "Railpack",
|
||||
[BuildType.nixpacks]: "Nixpacks",
|
||||
[BuildType.heroku_buildpacks]: "Heroku Buildpacks",
|
||||
[BuildType.paketo_buildpacks]: "Paketo Buildpacks",
|
||||
[BuildType.static]: "Static",
|
||||
};
|
||||
|
||||
const mySchema = z.discriminatedUnion("buildType", [
|
||||
z.object({
|
||||
buildType: z.literal("dockerfile"),
|
||||
buildType: z.literal(BuildType.dockerfile),
|
||||
dockerfile: z
|
||||
.string({
|
||||
required_error: "Dockerfile path is required",
|
||||
@@ -42,39 +51,88 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
dockerBuildStage: z.string().nullable().default(""),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("heroku_buildpacks"),
|
||||
buildType: z.literal(BuildType.heroku_buildpacks),
|
||||
herokuVersion: z.string().nullable().default(""),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("paketo_buildpacks"),
|
||||
buildType: z.literal(BuildType.paketo_buildpacks),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("nixpacks"),
|
||||
buildType: z.literal(BuildType.nixpacks),
|
||||
publishDirectory: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("static"),
|
||||
buildType: z.literal(BuildType.static),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("railpack"),
|
||||
buildType: z.literal(BuildType.railpack),
|
||||
}),
|
||||
]);
|
||||
|
||||
type AddTemplate = z.infer<typeof mySchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
interface ApplicationData {
|
||||
buildType: BuildType;
|
||||
dockerfile?: string | null;
|
||||
dockerContextPath?: string | null;
|
||||
dockerBuildStage?: string | null;
|
||||
herokuVersion?: string | null;
|
||||
publishDirectory?: string | null;
|
||||
}
|
||||
|
||||
function isValidBuildType(value: string): value is BuildType {
|
||||
return Object.values(BuildType).includes(value as BuildType);
|
||||
}
|
||||
|
||||
const resetData = (data: ApplicationData): AddTemplate => {
|
||||
switch (data.buildType) {
|
||||
case BuildType.dockerfile:
|
||||
return {
|
||||
buildType: BuildType.dockerfile,
|
||||
dockerfile: data.dockerfile || "",
|
||||
dockerContextPath: data.dockerContextPath || "",
|
||||
dockerBuildStage: data.dockerBuildStage || "",
|
||||
};
|
||||
case BuildType.heroku_buildpacks:
|
||||
return {
|
||||
buildType: BuildType.heroku_buildpacks,
|
||||
herokuVersion: data.herokuVersion || "",
|
||||
};
|
||||
case BuildType.nixpacks:
|
||||
return {
|
||||
buildType: BuildType.nixpacks,
|
||||
publishDirectory: data.publishDirectory || undefined,
|
||||
};
|
||||
case BuildType.paketo_buildpacks:
|
||||
return {
|
||||
buildType: BuildType.paketo_buildpacks,
|
||||
};
|
||||
case BuildType.static:
|
||||
return {
|
||||
buildType: BuildType.static,
|
||||
};
|
||||
case BuildType.railpack:
|
||||
return {
|
||||
buildType: BuildType.railpack,
|
||||
};
|
||||
default:
|
||||
const buildType = data.buildType as BuildType;
|
||||
return {
|
||||
buildType,
|
||||
} as AddTemplate;
|
||||
}
|
||||
};
|
||||
|
||||
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveBuildType.useMutation();
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
{ applicationId },
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const form = useForm<AddTemplate>({
|
||||
@@ -85,46 +143,36 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
});
|
||||
|
||||
const buildType = form.watch("buildType");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data.buildType === "dockerfile") {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
...(data.buildType && {
|
||||
dockerfile: data.dockerfile || "",
|
||||
dockerContextPath: data.dockerContextPath || "",
|
||||
dockerBuildStage: data.dockerBuildStage || "",
|
||||
}),
|
||||
});
|
||||
} else if (data.buildType === "heroku_buildpacks") {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
...(data.buildType && {
|
||||
herokuVersion: data.herokuVersion || "",
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
publishDirectory: data.publishDirectory || undefined,
|
||||
});
|
||||
}
|
||||
const typedData: ApplicationData = {
|
||||
...data,
|
||||
buildType: isValidBuildType(data.buildType)
|
||||
? (data.buildType as BuildType)
|
||||
: BuildType.nixpacks, // fallback
|
||||
};
|
||||
|
||||
form.reset(resetData(typedData));
|
||||
}
|
||||
}, [form.formState.isSubmitSuccessful, form.reset, data, form]);
|
||||
}, [data, form]);
|
||||
|
||||
const onSubmit = async (data: AddTemplate) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
buildType: data.buildType,
|
||||
publishDirectory:
|
||||
data.buildType === "nixpacks" ? data.publishDirectory : null,
|
||||
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
|
||||
data.buildType === BuildType.nixpacks ? data.publishDirectory : null,
|
||||
dockerfile:
|
||||
data.buildType === BuildType.dockerfile ? data.dockerfile : null,
|
||||
dockerContextPath:
|
||||
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
||||
data.buildType === BuildType.dockerfile ? data.dockerContextPath : null,
|
||||
dockerBuildStage:
|
||||
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
|
||||
data.buildType === BuildType.dockerfile ? data.dockerBuildStage : null,
|
||||
herokuVersion:
|
||||
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
|
||||
data.buildType === BuildType.heroku_buildpacks
|
||||
? data.herokuVersion
|
||||
: null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Build type saved");
|
||||
@@ -160,193 +208,143 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="buildType"
|
||||
defaultValue={form.control._defaultValues.buildType}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Build Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="dockerfile" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Dockerfile
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="railpack" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Railpack{" "}
|
||||
<Badge className="ml-1 text-xs px-1">New</Badge>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="nixpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Nixpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="heroku_buildpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Heroku Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="paketo_buildpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Paketo Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="static" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Static</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Build Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
{Object.entries(buildTypeDisplayMap).map(
|
||||
([value, label]) => (
|
||||
<FormItem
|
||||
key={value}
|
||||
className="flex items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<RadioGroupItem value={value} />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{label}
|
||||
{value === BuildType.railpack && (
|
||||
<Badge className="ml-2 px-1 text-xs">New</Badge>
|
||||
)}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
),
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{buildType === "heroku_buildpacks" && (
|
||||
{buildType === BuildType.heroku_buildpacks && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="herokuVersion"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Heroku Version (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Heroku Version (Default: 24)"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Heroku Version (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Heroku Version (Default: 24)"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{buildType === "dockerfile" && (
|
||||
{buildType === BuildType.dockerfile && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerfile"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker File</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Path of your docker file"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerContextPath"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker Context Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
"Path of your docker context default: ."
|
||||
}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerBuildStage"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Docker Build Stage</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to target a specific stage in a
|
||||
Multi-stage Dockerfile. If empty, Docker defaults to
|
||||
build the last defined stage.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"E.g. production"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{buildType === "nixpacks" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="publishDirectory"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish Directory</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to serve a single directory via NGINX after
|
||||
the build phase. Useful if the final build assets
|
||||
should be served as a static site.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormLabel>Docker File</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Publish Directory"}
|
||||
placeholder="Path of your docker file"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerContextPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Docker Context Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Path of your docker context (default: .)"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerBuildStage"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Docker Build Stage</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to target a specific stage in a Multi-stage
|
||||
Dockerfile. If empty, Docker defaults to build the
|
||||
last defined stage.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="E.g. production"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{buildType === BuildType.nixpacks && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="publishDirectory"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish Directory</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to serve a single directory via NGINX after
|
||||
the build phase. Useful if the final build assets should
|
||||
be served as a static site.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Publish Directory"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full justify-end">
|
||||
|
||||
@@ -41,6 +41,7 @@ import { toast } from "sonner";
|
||||
import { domain } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Dices } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
@@ -83,6 +84,13 @@ export const AddDomain = ({
|
||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||
api.domain.generateDomain.useMutation();
|
||||
|
||||
const { data: canGenerateTraefikMeDomains } =
|
||||
api.domain.canGenerateTraefikMeDomains.useQuery({
|
||||
serverId: application?.serverId || "",
|
||||
});
|
||||
|
||||
console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains);
|
||||
|
||||
const form = useForm<Domain>({
|
||||
resolver: zodResolver(domain),
|
||||
defaultValues: {
|
||||
@@ -186,6 +194,21 @@ export const AddDomain = ({
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
{!canGenerateTraefikMeDomains &&
|
||||
field.value.includes("traefik.me") && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
{application?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to make your traefik.me domain work.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Form } from "@/components/ui/form";
|
||||
import { Secrets } from "@/components/ui/secrets";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
env: z.string(),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const BitbucketProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
|
||||
@@ -115,7 +115,11 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Username" autoComplete="username" {...field} />
|
||||
<Input
|
||||
placeholder="Username"
|
||||
autoComplete="username"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -130,7 +134,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Password" autoComplete="one-time-code" {...field} type="password" />
|
||||
<Input
|
||||
placeholder="Password"
|
||||
autoComplete="one-time-code"
|
||||
{...field}
|
||||
type="password"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -26,15 +26,15 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
|
||||
const GitProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
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 { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
interface GiteaRepository {
|
||||
name: string;
|
||||
url: string;
|
||||
id: number;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GiteaBranch {
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
const GiteaProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||
watchPaths: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGiteaProvider } =
|
||||
api.application.saveGiteaProvider.useMutation();
|
||||
|
||||
const form = useForm<GiteaProvider>({
|
||||
defaultValues: {
|
||||
buildPath: "/",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
giteaId: "",
|
||||
branch: "",
|
||||
watchPaths: [],
|
||||
},
|
||||
resolver: zodResolver(GiteaProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const giteaId = form.watch("giteaId");
|
||||
|
||||
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
|
||||
{ giteaId },
|
||||
{
|
||||
enabled: !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
} = api.gitea.getGiteaRepositories.useQuery(
|
||||
{
|
||||
giteaId,
|
||||
},
|
||||
{
|
||||
enabled: !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.gitea.getGiteaBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repositoryName: repository?.repo,
|
||||
giteaId: giteaId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.giteaBranch || "",
|
||||
repository: {
|
||||
repo: data.giteaRepository || "",
|
||||
owner: data.giteaOwner || "",
|
||||
},
|
||||
buildPath: data.giteaBuildPath || "/",
|
||||
giteaId: data.giteaId || "",
|
||||
watchPaths: data.watchPaths || [],
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GiteaProvider) => {
|
||||
await mutateAsync({
|
||||
giteaBranch: data.branch,
|
||||
giteaRepository: data.repository.repo,
|
||||
giteaOwner: data.repository.owner,
|
||||
giteaBuildPath: data.buildPath,
|
||||
giteaId: data.giteaId,
|
||||
applicationId,
|
||||
watchPaths: data.watchPaths,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error saving the Gitea provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="giteaId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Gitea Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Gitea Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{giteaProviders?.map((giteaProvider) => (
|
||||
<SelectItem
|
||||
key={giteaProvider.giteaId}
|
||||
value={giteaProvider.giteaId}
|
||||
>
|
||||
{giteaProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
<Link
|
||||
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<GiteaIcon className="h-4 w-4" />
|
||||
<span>View Repository</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo: GiteaRepository) =>
|
||||
repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
|
||||
<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 repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories && repositories.length === 0 && (
|
||||
<CommandEmpty>
|
||||
No repositories found.
|
||||
</CommandEmpty>
|
||||
)}
|
||||
{repositories?.map((repo: GiteaRepository) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch: GiteaBranch) =>
|
||||
branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<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 branch..."
|
||||
className="h-9"
|
||||
/>
|
||||
{status === "loading" && fetchStatus === "fetching" && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Loading Branches....
|
||||
</span>
|
||||
)}
|
||||
{!repository?.owner && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a repository
|
||||
</span>
|
||||
)}
|
||||
<ScrollArea className="h-96">
|
||||
<CommandEmpty>No branch found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{branches?.map((branch: GiteaBranch) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.id}
|
||||
onSelect={() => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
<FormMessage />
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="watchPaths"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Add paths to watch for changes. When files in these
|
||||
paths change, a new deployment will be triggered.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{field.value?.map((path: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{path}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
onClick={() => {
|
||||
const newPaths = [...field.value];
|
||||
newPaths.splice(index, 1);
|
||||
field.onChange(newPaths);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const input = e.currentTarget;
|
||||
const path = input.value.trim();
|
||||
if (path) {
|
||||
field.onChange([...field.value, path]);
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder*="Enter a path"]',
|
||||
) as HTMLInputElement;
|
||||
const path = input.value.trim();
|
||||
if (path) {
|
||||
field.onChange([...field.value, path]);
|
||||
input.value = "";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingGiteaProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -34,17 +36,15 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Link from "next/link";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
|
||||
const GithubProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
@@ -468,16 +468,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
{/* create github link */}
|
||||
<div className="flex w-full justify-end">
|
||||
<Link
|
||||
href={`https://github.com/${repository?.owner}/${repository?.repo}`}
|
||||
target="_blank"
|
||||
className="w-fit"
|
||||
>
|
||||
Repository
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -35,17 +37,15 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Link from "next/link";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
|
||||
const GitlabProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
||||
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
@@ -18,7 +20,14 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||
|
||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
|
||||
type TabState =
|
||||
| "github"
|
||||
| "docker"
|
||||
| "git"
|
||||
| "drop"
|
||||
| "gitlab"
|
||||
| "bitbucket"
|
||||
| "gitea";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
@@ -29,6 +38,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
@@ -78,6 +88,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitea"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GiteaIcon className="size-4 text-current fill-current" />
|
||||
Gitea
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="docker"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
@@ -162,6 +179,26 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitea" className="w-full p-2">
|
||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||
<SaveGiteaProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using Gitea, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="docker" className="w-full p-2">
|
||||
<SaveDockerProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
Hammer,
|
||||
HelpCircle,
|
||||
RefreshCcw,
|
||||
Rocket,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -55,7 +55,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||
<DialogAction
|
||||
title="Deploy Application"
|
||||
description="Are you sure you want to deploy this application?"
|
||||
@@ -79,12 +79,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Deploy
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -114,9 +116,24 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" isLoading={isReloading}>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Reload the application without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
@@ -139,13 +156,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Rebuild
|
||||
<Hammer className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Hammer className="size-4 mr-1" />
|
||||
Rebuild
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -180,13 +198,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -219,13 +238,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -241,15 +261,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Autodeploy</span>
|
||||
<Switch
|
||||
aria-label="Toggle italic"
|
||||
aria-label="Toggle autodeploy"
|
||||
checked={data?.autoDeploy || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
@@ -264,7 +287,29 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center"
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Clean Cache</span>
|
||||
<Switch
|
||||
aria-label="Toggle clean cache"
|
||||
checked={data?.cleanCache || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
applicationId,
|
||||
cleanCache: enabled,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Clean Cache Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Clean Cache");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -121,7 +121,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import { domainCompose } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domainCompose>;
|
||||
@@ -102,6 +103,11 @@ export const AddDomainCompose = ({
|
||||
? api.domain.update.useMutation()
|
||||
: api.domain.create.useMutation();
|
||||
|
||||
const { data: canGenerateTraefikMeDomains } =
|
||||
api.domain.canGenerateTraefikMeDomains.useQuery({
|
||||
serverId: compose?.serverId || "",
|
||||
});
|
||||
|
||||
const form = useForm<Domain>({
|
||||
resolver: zodResolver(domainCompose),
|
||||
defaultValues: {
|
||||
@@ -313,6 +319,21 @@ export const AddDomainCompose = ({
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
{!canGenerateTraefikMeDomains &&
|
||||
field.value.includes("traefik.me") && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
{compose?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to make your traefik.me domain work.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||
@@ -34,7 +34,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
api.compose.stop.useMutation();
|
||||
return (
|
||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||
<DialogAction
|
||||
title="Deploy Compose"
|
||||
description="Are you sure you want to deploy this compose?"
|
||||
@@ -58,12 +58,14 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Deploy
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -74,36 +76,37 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Rebuild Compose"
|
||||
description="Are you sure you want to rebuild this compose?"
|
||||
title="Reload Compose"
|
||||
description="Are you sure you want to reload this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose rebuilt successfully");
|
||||
toast.success("Compose reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error rebuilding compose");
|
||||
toast.error("Error reloading compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Rebuild
|
||||
<Hammer className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Only rebuilds the compose without downloading new code</p>
|
||||
<p>Reload the compose without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
@@ -131,13 +134,14 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -169,13 +173,14 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -191,15 +196,18 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Autodeploy</span>
|
||||
<Switch
|
||||
aria-label="Toggle italic"
|
||||
aria-label="Toggle autodeploy"
|
||||
checked={data?.autoDeploy || false}
|
||||
onCheckedChange={async (enabled) => {
|
||||
await update({
|
||||
@@ -214,7 +222,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
toast.error("Error updating Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center"
|
||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const BitbucketProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -27,13 +28,12 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const GitProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import type { Repository } from "@/utils/gitea-utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GiteaProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
const { mutateAsync, isLoading: isSavingGiteaProvider } =
|
||||
api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<GiteaProvider>({
|
||||
defaultValues: {
|
||||
composePath: "./docker-compose.yml",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
giteaId: "",
|
||||
branch: "",
|
||||
watchPaths: [],
|
||||
},
|
||||
resolver: zodResolver(GiteaProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const giteaId = form.watch("giteaId");
|
||||
|
||||
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
|
||||
{ giteaId },
|
||||
{
|
||||
enabled: !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
} = api.gitea.getGiteaRepositories.useQuery<Repository[]>(
|
||||
{
|
||||
giteaId,
|
||||
},
|
||||
{
|
||||
enabled: !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.gitea.getGiteaBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repositoryName: repository?.repo,
|
||||
giteaId: giteaId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.giteaBranch || "",
|
||||
repository: {
|
||||
repo: data.giteaRepository || "",
|
||||
owner: data.giteaOwner || "",
|
||||
},
|
||||
composePath: data.composePath || "./docker-compose.yml",
|
||||
giteaId: data.giteaId || "",
|
||||
watchPaths: data.watchPaths || [],
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GiteaProvider) => {
|
||||
await mutateAsync({
|
||||
giteaBranch: data.branch,
|
||||
giteaRepository: data.repository.repo,
|
||||
giteaOwner: data.repository.owner,
|
||||
composePath: data.composePath,
|
||||
giteaId: data.giteaId,
|
||||
composeId,
|
||||
sourceType: "gitea",
|
||||
composeStatus: "idle",
|
||||
watchPaths: data.watchPaths,
|
||||
} as any)
|
||||
.then(async () => {
|
||||
toast.success("Service Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error saving the Gitea provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="giteaId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Gitea Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Gitea Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{giteaProviders?.map((giteaProvider) => (
|
||||
<SelectItem
|
||||
key={giteaProvider.giteaId}
|
||||
value={giteaProvider.giteaId}
|
||||
>
|
||||
{giteaProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
<Link
|
||||
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<GiteaIcon className="h-4 w-4" />
|
||||
<span>View Repository</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
<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 repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
key={repo.url}
|
||||
value={repo.name}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username,
|
||||
repo: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<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 branches..."
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandEmpty>No branches found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{branches?.map((branch) => (
|
||||
<CommandItem
|
||||
key={branch.name}
|
||||
value={branch.name}
|
||||
onSelect={() =>
|
||||
form.setValue("branch", branch.name)
|
||||
}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{branch.name}
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.branch && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Branch is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="watchPaths"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Add paths to watch for changes. When files in these
|
||||
paths change, a new deployment will be triggered.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const input = e.currentTarget;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
const newPaths = [...(field.value || []), value];
|
||||
form.setValue("watchPaths", newPaths);
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder*="Enter a path"]',
|
||||
) as HTMLInputElement;
|
||||
const path = input.value.trim();
|
||||
if (path) {
|
||||
field.onChange([...(field.value || []), path]);
|
||||
input.value = "";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" isLoading={isSavingGiteaProvider}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -39,12 +40,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const GithubProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const GitlabProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
@@ -14,10 +15,11 @@ import { ComposeFileEditor } from "../compose-file-editor";
|
||||
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
|
||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
||||
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
@@ -27,9 +29,11 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
@@ -54,21 +58,21 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<TabsTrigger
|
||||
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"
|
||||
>
|
||||
<GithubIcon className="size-4 text-current fill-current" />
|
||||
Github
|
||||
GitHub
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitlab"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitlabIcon className="size-4 text-current fill-current" />
|
||||
Gitlab
|
||||
GitLab
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="bitbucket"
|
||||
@@ -77,7 +81,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value="gitea"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GiteaIcon className="size-4 text-current fill-current" /> Gitea
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
@@ -89,11 +98,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
value="raw"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<CodeIcon className="size-4 " />
|
||||
<CodeIcon className="size-4" />
|
||||
Raw
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{githubProviders && githubProviders?.length > 0 ? (
|
||||
<SaveGithubProviderCompose composeId={composeId} />
|
||||
@@ -154,6 +164,26 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitea" className="w-full p-2">
|
||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||
<SaveGiteaProviderCompose composeId={composeId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using Gitea, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProviderCompose composeId={composeId} />
|
||||
</TabsContent>
|
||||
|
||||
@@ -147,7 +147,9 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Enable Isolated Deployment ({data?.appName})</FormLabel>
|
||||
<FormLabel>
|
||||
Enable Isolated Deployment ({data?.appName})
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Enable isolated deployment to the compose file.
|
||||
</FormDescription>
|
||||
|
||||
@@ -62,6 +62,11 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<AlertBlock type="info">
|
||||
Preview your docker-compose file with added domains. Note: At least
|
||||
one domain must be specified for this conversion to take effect.
|
||||
</AlertBlock>
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
@@ -121,7 +121,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
|
||||
|
||||
interface Props {
|
||||
databaseId: string;
|
||||
databaseType: "postgres" | "mariadb" | "mysql" | "mongo";
|
||||
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
database: "",
|
||||
database: databaseType === "web-server" ? "dokploy" : "",
|
||||
destinationId: "",
|
||||
enabled: true,
|
||||
prefix: "/",
|
||||
@@ -112,7 +112,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
? {
|
||||
mongoId: databaseId,
|
||||
}
|
||||
: undefined;
|
||||
: databaseType === "web-server"
|
||||
? {
|
||||
userId: databaseId,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await createBackup({
|
||||
destinationId: data.destinationId,
|
||||
@@ -236,7 +240,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Database</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"dokploy"} {...field} />
|
||||
<Input
|
||||
disabled={databaseType === "web-server"}
|
||||
placeholder={"dokploy"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -286,16 +294,21 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Keep the latest</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={"keeps all the backups if left empty"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional. If provided, only keeps the latest N backups in the cloud.
|
||||
Optional. If provided, only keeps the latest N backups
|
||||
in the cloud.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
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 { ServiceType } from "../../application/advanced/show-resources";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
|
||||
interface Props {
|
||||
databaseId: string;
|
||||
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
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",
|
||||
}),
|
||||
databaseName: z
|
||||
.string({
|
||||
required_error: "Please enter a database name",
|
||||
})
|
||||
.min(1, {
|
||||
message: "Database name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
|
||||
|
||||
export const RestoreBackup = ({
|
||||
databaseId,
|
||||
databaseType,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const { data: destinations = [] } = api.destination.all.useQuery();
|
||||
|
||||
const form = useForm<RestoreBackup>({
|
||||
defaultValues: {
|
||||
destinationId: "",
|
||||
backupFile: "",
|
||||
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||
},
|
||||
resolver: zodResolver(RestoreBackupSchema),
|
||||
});
|
||||
|
||||
const destionationId = form.watch("destinationId");
|
||||
|
||||
const debouncedSetSearch = debounce((value: string) => {
|
||||
setSearch(value);
|
||||
}, 300);
|
||||
|
||||
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
||||
{
|
||||
destinationId: destionationId,
|
||||
search,
|
||||
serverId: serverId ?? "",
|
||||
},
|
||||
{
|
||||
enabled: isOpen && !!destionationId,
|
||||
},
|
||||
);
|
||||
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
|
||||
// const { mutateAsync: restore, isLoading: isRestoring } =
|
||||
// api.backup.restoreBackup.useMutation();
|
||||
|
||||
api.backup.restoreBackupWithLogs.useSubscription(
|
||||
{
|
||||
databaseId,
|
||||
databaseType,
|
||||
databaseName: form.watch("databaseName"),
|
||||
backupFile: form.watch("backupFile"),
|
||||
destinationId: form.watch("destinationId"),
|
||||
},
|
||||
{
|
||||
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 (_data: RestoreBackup) => {
|
||||
setIsDeploying(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<RotateCcw className="mr-2 size-4" />
|
||||
Restore Backup
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<RotateCcw className="mr-2 size-4" />
|
||||
Restore Backup
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a destination and search for backup files
|
||||
</DialogDescription>
|
||||
</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 justify-between">
|
||||
Search Backup Files
|
||||
{field.value && (
|
||||
<Badge variant="outline">
|
||||
{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>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{field.value || "Search and select a backup file"}
|
||||
<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..."
|
||||
onValueChange={debouncedSetSearch}
|
||||
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>
|
||||
{files.map((file) => (
|
||||
<CommandItem
|
||||
value={file}
|
||||
key={file}
|
||||
onSelect={() => {
|
||||
form.setValue("backupFile", file);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full justify-between">
|
||||
<span>{file}</span>
|
||||
</div>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
file === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databaseName"
|
||||
render={({ field }) => (
|
||||
<FormItem className="">
|
||||
<FormLabel>Database Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={databaseType === "web-server"}
|
||||
{...field}
|
||||
placeholder="Enter database name"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isDeploying}
|
||||
form="hook-form-restore-backup"
|
||||
type="submit"
|
||||
disabled={!form.watch("backupFile")}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<DrawerLogs
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsDrawerOpen(false);
|
||||
setFilteredLogs([]);
|
||||
setIsDeploying(false);
|
||||
// refetch();
|
||||
}}
|
||||
filteredLogs={filteredLogs}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -14,20 +14,23 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||
import { AddBackup } from "./add-backup";
|
||||
import { RestoreBackup } from "./restore-backup";
|
||||
import { UpdateBackup } from "./update-backup";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: Exclude<ServiceType, "application" | "redis">;
|
||||
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||
}
|
||||
export const ShowBackups = ({ id, type }: Props) => {
|
||||
const [activeManualBackup, setActiveManualBackup] = useState<string | undefined>();
|
||||
const [activeManualBackup, setActiveManualBackup] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||
@@ -35,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
mariadb: () =>
|
||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||
"web-server": () => api.user.getBackups.useQuery(),
|
||||
};
|
||||
const { data } = api.destination.all.useQuery();
|
||||
const { data: postgres, refetch } = queryMap[type]
|
||||
@@ -46,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
||||
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
||||
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
||||
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
||||
@@ -61,7 +66,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="text-xl">Backups</CardTitle>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<Database className="size-6 text-muted-foreground" />
|
||||
Backups
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add backups to your database to save the data to a different
|
||||
provider.
|
||||
@@ -69,7 +77,20 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
</div>
|
||||
|
||||
{postgres && postgres?.backups?.length > 0 && (
|
||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||
{type !== "web-server" && (
|
||||
<AddBackup
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
<RestoreBackup
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
serverId={"serverId" in postgres ? postgres.serverId : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -96,11 +117,20 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No backups configured
|
||||
</span>
|
||||
<AddBackup
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||
<AddBackup
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<RestoreBackup
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
serverId={
|
||||
"serverId" in postgres ? postgres.serverId : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
@@ -142,7 +172,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Keep Latest</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{backup.keepLatestCount || 'All'}
|
||||
{backup.keepLatestCount || "All"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,7 +183,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
isLoading={isManualBackup && activeManualBackup === backup.backupId}
|
||||
isLoading={
|
||||
isManualBackup &&
|
||||
activeManualBackup === backup.backupId
|
||||
}
|
||||
onClick={async () => {
|
||||
setActiveManualBackup(backup.backupId);
|
||||
await manualBackup({
|
||||
@@ -178,6 +211,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
<TooltipContent>Run Manual Backup</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<UpdateBackup
|
||||
backupId={backup.backupId}
|
||||
refetch={refetch}
|
||||
|
||||
@@ -92,7 +92,9 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
enabled: backup.enabled || false,
|
||||
prefix: backup.prefix,
|
||||
schedule: backup.schedule,
|
||||
keepLatestCount: backup.keepLatestCount ? Number(backup.keepLatestCount) : undefined,
|
||||
keepLatestCount: backup.keepLatestCount
|
||||
? Number(backup.keepLatestCount)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, backup]);
|
||||
@@ -274,10 +276,15 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Keep the latest</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={"keeps all the backups if left empty"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional. If provided, only keeps the latest N backups in the cloud.
|
||||
Optional. If provided, only keeps the latest N backups
|
||||
in the cloud.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -119,7 +119,6 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
const wsUrl = `${protocol}//${
|
||||
window.location.host
|
||||
}/docker-container-logs?${params.toString()}`;
|
||||
console.log("Connecting to WebSocket:", wsUrl);
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
const resetNoDataTimeout = () => {
|
||||
@@ -136,7 +135,6 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
console.log("WebSocket connected");
|
||||
resetNoDataTimeout();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -106,6 +108,20 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
{!getIp && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
{data?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to fix the database url connection.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -8,15 +8,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
HelpCircle,
|
||||
RefreshCcw,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
@@ -92,12 +86,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Deploy
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -107,6 +103,8 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Reload Mariadb"
|
||||
description="Are you sure you want to reload this mariadb?"
|
||||
@@ -128,13 +126,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -144,7 +143,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
</TooltipProvider>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Start Mariadb"
|
||||
description="Are you sure you want to start this mariadb?"
|
||||
@@ -165,13 +166,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -184,7 +186,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
) : (
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Stop Mariadb"
|
||||
description="Are you sure you want to stop this mariadb?"
|
||||
@@ -204,13 +208,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -220,15 +225,29 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the MariaDB container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
|
||||
@@ -119,7 +119,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -106,6 +108,20 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
{!getIp && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
{data?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to fix the database url connection.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -8,15 +8,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
HelpCircle,
|
||||
RefreshCcw,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
@@ -91,12 +85,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Deploy
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -127,13 +123,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -164,13 +161,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -203,13 +201,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -225,9 +224,23 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the MongoDB container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
|
||||
@@ -121,7 +121,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -218,7 +218,7 @@ export const ContainerFreeMonitoring = ({
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Used: {currentData.cpu.value}%
|
||||
Used: {currentData.cpu.value}
|
||||
</span>
|
||||
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -106,6 +108,20 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
{!getIp && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
{data?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to fix the database url connection.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -8,15 +8,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
HelpCircle,
|
||||
RefreshCcw,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
@@ -77,7 +71,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Deploy Mysql"
|
||||
title="Deploy MySQL"
|
||||
description="Are you sure you want to deploy this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
@@ -89,12 +83,14 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Deploy
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -105,7 +101,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload Mysql"
|
||||
title="Reload MySQL"
|
||||
description="Are you sure you want to reload this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
@@ -114,24 +110,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mysql reloaded successfully");
|
||||
toast.success("MySQL reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Mysql");
|
||||
toast.error("Error reloading MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -143,7 +140,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</DialogAction>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start Mysql"
|
||||
title="Start MySQL"
|
||||
description="Are you sure you want to start this mysql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
@@ -151,24 +148,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
mysqlId: mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mysql started successfully");
|
||||
toast.success("MySQL started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Mysql");
|
||||
toast.error("Error starting MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -183,31 +181,32 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop Mysql"
|
||||
title="Stop MySQL"
|
||||
description="Are you sure you want to stop this mysql?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
mysqlId: mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mysql stopped successfully");
|
||||
toast.success("MySQL stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Mysql");
|
||||
toast.error("Error stopping MySQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -223,9 +222,23 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the MySQL container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
|
||||
@@ -119,7 +119,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -108,6 +110,20 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
{!getIp && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
{data?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to fix the database url connection.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -8,15 +8,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
HelpCircle,
|
||||
RefreshCcw,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
@@ -78,9 +72,9 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<TooltipProvider disableHoverableContent={false}>
|
||||
<DialogAction
|
||||
title="Deploy Postgres"
|
||||
title="Deploy PostgreSQL"
|
||||
description="Are you sure you want to deploy this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
@@ -92,12 +86,14 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Deploy
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -108,7 +104,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Reload Postgres"
|
||||
title="Reload PostgreSQL"
|
||||
description="Are you sure you want to reload this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
@@ -117,24 +113,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Postgres reloaded successfully");
|
||||
toast.success("PostgreSQL reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Postgres");
|
||||
toast.error("Error reloading PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -146,7 +143,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</DialogAction>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
<DialogAction
|
||||
title="Start Postgres"
|
||||
title="Start PostgreSQL"
|
||||
description="Are you sure you want to start this postgres?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
@@ -154,24 +151,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
postgresId: postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Postgres started successfully");
|
||||
toast.success("PostgreSQL started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Postgres");
|
||||
toast.error("Error starting PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -186,31 +184,32 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</DialogAction>
|
||||
) : (
|
||||
<DialogAction
|
||||
title="Stop Postgres"
|
||||
title="Stop PostgreSQL"
|
||||
description="Are you sure you want to stop this postgres?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
postgresId: postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Postgres stopped successfully");
|
||||
toast.success("PostgreSQL stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Postgres");
|
||||
toast.error("Error stopping PostgreSQL");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -226,9 +225,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the PostgreSQL container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { PenBox } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -94,9 +94,9 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
@@ -121,7 +121,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -151,6 +151,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-postgres"
|
||||
type="submit"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
|
||||
@@ -324,7 +324,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={`${customBaseUrl || "https://dokploy.github.io/templates"}/blueprints/${template.id}/${template.logo}`}
|
||||
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template.id}/${template.logo}`}
|
||||
className={cn(
|
||||
"object-contain",
|
||||
viewMode === "detailed" ? "size-24" : "size-16",
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { useState } from "react";
|
||||
|
||||
const examples = [
|
||||
"Make a personal blog",
|
||||
@@ -23,7 +22,7 @@ const examples = [
|
||||
"Sendgrid service opensource analogue",
|
||||
];
|
||||
|
||||
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
||||
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||
// Get servers from the API
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
|
||||
172
apps/dokploy/components/dashboard/project/duplicate-project.tsx
Normal file
172
apps/dokploy/components/dashboard/project/duplicate-project.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { Copy, Loader2 } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
serverId?: string | null;
|
||||
name: string;
|
||||
type:
|
||||
| "mariadb"
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status?: "idle" | "running" | "done" | "error";
|
||||
};
|
||||
|
||||
interface DuplicateProjectProps {
|
||||
projectId: string;
|
||||
services: Services[];
|
||||
selectedServiceIds: string[];
|
||||
}
|
||||
|
||||
export const DuplicateProject = ({
|
||||
projectId,
|
||||
services,
|
||||
selectedServiceIds,
|
||||
}: DuplicateProjectProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
|
||||
const selectedServices = services.filter((service) =>
|
||||
selectedServiceIds.includes(service.id),
|
||||
);
|
||||
|
||||
const { mutateAsync: duplicateProject, isLoading } =
|
||||
api.project.duplicate.useMutation({
|
||||
onSuccess: async (newProject) => {
|
||||
await utils.project.all.invalidate();
|
||||
toast.success("Project duplicated successfully");
|
||||
setOpen(false);
|
||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleDuplicate = async () => {
|
||||
if (!name) {
|
||||
toast.error("Project name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
await duplicateProject({
|
||||
sourceProjectId: projectId,
|
||||
name,
|
||||
description,
|
||||
includeServices: true,
|
||||
selectedServices: selectedServices.map((service) => ({
|
||||
id: service.id,
|
||||
type: service.type,
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
// Reset form when closing
|
||||
setName("");
|
||||
setDescription("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" className="w-full justify-start">
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Duplicate
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Duplicate Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new project with the selected services
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="New project name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Project description (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Selected services to duplicate</Label>
|
||||
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
||||
{selectedServices.map((service) => (
|
||||
<div key={service.id} className="flex items-center space-x-2">
|
||||
<span className="text-sm">
|
||||
{service.name} ({service.type})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDuplicate} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Duplicating...
|
||||
</>
|
||||
) : (
|
||||
"Duplicate"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -148,7 +148,7 @@ export const HandleProject = ({ projectId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -115,7 +115,7 @@ export const ShowProjects = () => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 flex-wrap gap-5">
|
||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
|
||||
{filteredProjects?.map((project) => {
|
||||
const emptyServices =
|
||||
project?.mariadb.length === 0 &&
|
||||
@@ -186,7 +186,9 @@ export const ShowProjects = () => {
|
||||
target="_blank"
|
||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||
>
|
||||
<span>{domain.host}</span>
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
@@ -222,7 +224,9 @@ export const ShowProjects = () => {
|
||||
target="_blank"
|
||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||
>
|
||||
<span>{domain.host}</span>
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -100,6 +102,20 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
{!getIp && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
{data?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to fix the database url connection.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -8,15 +8,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
HelpCircle,
|
||||
RefreshCcw,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
@@ -91,12 +85,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Deploy
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -127,13 +123,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Reload
|
||||
<RefreshCcw className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -164,13 +161,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Start
|
||||
<CheckCircle2 className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -203,13 +201,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
@@ -225,9 +224,23 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the Redis container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
|
||||
@@ -119,7 +119,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
|
||||
@@ -25,13 +25,13 @@ import {
|
||||
import { type RouterOutputs, api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
ArrowDownUp,
|
||||
AlertCircle,
|
||||
InfoIcon,
|
||||
ArrowDownUp,
|
||||
Calendar as CalendarIcon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||
import { RequestsTable } from "./requests-table";
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
@@ -17,22 +25,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -7,13 +9,11 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Clock, ExternalLinkIcon, KeyIcon, Tag, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { AddApiKey } from "./add-api-key";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export const ShowApiKeys = () => {
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -13,7 +14,11 @@ import Link from "next/link";
|
||||
import { AddManager } from "./manager/add-manager";
|
||||
import { AddWorker } from "./workers/add-worker";
|
||||
|
||||
export const AddNode = () => {
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const AddNode = ({ serverId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
@@ -44,6 +49,10 @@ export const AddNode = () => {
|
||||
Architecture
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Link>
|
||||
<AlertBlock type="warning">
|
||||
Make sure you use the same architecture as the node you are
|
||||
adding.
|
||||
</AlertBlock>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -52,11 +61,11 @@ export const AddNode = () => {
|
||||
<TabsTrigger value="worker">Worker</TabsTrigger>
|
||||
<TabsTrigger value="manager">Manager</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="worker" className="pt-4">
|
||||
<AddWorker />
|
||||
<TabsContent value="worker" className="pt-4 overflow-hidden">
|
||||
<AddWorker serverId={serverId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="manager" className="pt-4">
|
||||
<AddManager />
|
||||
<TabsContent value="manager" className="pt-4 overflow-hidden">
|
||||
<AddManager serverId={serverId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
DialogDescription,
|
||||
@@ -6,60 +7,74 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { CopyIcon, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const AddManager = () => {
|
||||
const { data } = api.cluster.addManager.useQuery();
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const AddManager = ({ serverId }: Props) => {
|
||||
const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a new manager</DialogTitle>
|
||||
<DialogDescription>Add a new manager</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>1. Go to your new server and run the following command</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||
<button
|
||||
type="button"
|
||||
className="self-center"
|
||||
onClick={() => {
|
||||
copy(
|
||||
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||
);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a new manager</DialogTitle>
|
||||
<DialogDescription>Add a new manager</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-full animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>
|
||||
1. Go to your new server and run the following command
|
||||
</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||
<button
|
||||
type="button"
|
||||
className="self-center"
|
||||
onClick={() => {
|
||||
copy(
|
||||
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||
);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>
|
||||
2. Run the following command to add the node(manager) to your
|
||||
cluster
|
||||
</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex">
|
||||
{data?.command}
|
||||
<button
|
||||
type="button"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
copy(data?.command || "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>
|
||||
2. Run the following command to add the node(manager) to your
|
||||
cluster
|
||||
</span>
|
||||
|
||||
<span className="bg-muted rounded-lg p-2 flex">
|
||||
{data?.command}
|
||||
<button
|
||||
type="button"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
copy(data?.command || "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { useState } from "react";
|
||||
import { ShowNodes } from "./show-nodes";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ShowNodesModal = ({ serverId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Show Swarm Nodes
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
||||
<div className="grid w-full gap-1">
|
||||
<ShowNodes serverId={serverId} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -32,13 +32,25 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { Boxes, HelpCircle, LockIcon, MoreHorizontal } from "lucide-react";
|
||||
import {
|
||||
Boxes,
|
||||
HelpCircle,
|
||||
Loader2,
|
||||
LockIcon,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AddNode } from "./add-node";
|
||||
import { ShowNodeData } from "./show-node-data";
|
||||
|
||||
export const ShowNodes = () => {
|
||||
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery();
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowNodes = ({ serverId }: Props) => {
|
||||
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
|
||||
serverId,
|
||||
});
|
||||
const { data: registry } = api.registry.all.useQuery();
|
||||
|
||||
const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation();
|
||||
@@ -58,14 +70,17 @@ export const ShowNodes = () => {
|
||||
</div>
|
||||
{haveAtLeastOneRegistry && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<AddNode />
|
||||
<AddNode serverId={serverId} />
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t min-h-[35vh]">
|
||||
{haveAtLeastOneRegistry ? (
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center w-full h-[40vh]">
|
||||
<Loader2 className="size-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : haveAtLeastOneRegistry ? (
|
||||
<div className="grid md:grid-cols-1 gap-4">
|
||||
{isLoading && <div>Loading...</div>}
|
||||
<Table>
|
||||
<TableCaption>
|
||||
A list of your managers / workers.
|
||||
@@ -129,7 +144,7 @@ export const ShowNodes = () => {
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<ShowNodeData data={node} />
|
||||
{node?.ManagerStatus?.Leader && (
|
||||
{!node?.ManagerStatus?.Leader && (
|
||||
<DialogAction
|
||||
title="Delete Node"
|
||||
description="Are you sure you want to delete this node from the cluster?"
|
||||
@@ -137,6 +152,7 @@ export const ShowNodes = () => {
|
||||
onClick={async () => {
|
||||
await deleteNode({
|
||||
nodeId: node.ID,
|
||||
serverId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
DialogDescription,
|
||||
@@ -6,58 +7,70 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { CopyIcon, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const AddWorker = () => {
|
||||
const { data } = api.cluster.addWorker.useQuery();
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const AddWorker = ({ serverId }: Props) => {
|
||||
const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a new worker</DialogTitle>
|
||||
<DialogDescription>Add a new worker</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>1. Go to your new server and run the following command</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||
<button
|
||||
type="button"
|
||||
className="self-center"
|
||||
onClick={() => {
|
||||
copy(
|
||||
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||
);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a new worker</DialogTitle>
|
||||
<DialogDescription>Add a new worker</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
{isLoading ? (
|
||||
<Loader2 className="w-full animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>1. Go to your new server and run the following command</span>
|
||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||
<button
|
||||
type="button"
|
||||
className="self-center"
|
||||
onClick={() => {
|
||||
copy(
|
||||
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||
);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>
|
||||
2. Run the following command to add the node(worker) to your cluster
|
||||
</span>
|
||||
<div className="flex flex-col gap-2.5 text-sm">
|
||||
<span>
|
||||
2. Run the following command to add the node(worker) to your
|
||||
cluster
|
||||
</span>
|
||||
|
||||
<span className="bg-muted rounded-lg p-2 flex">
|
||||
{data?.command}
|
||||
<button
|
||||
type="button"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
copy(data?.command || "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
<span className="bg-muted rounded-lg p-2 flex">
|
||||
{data?.command}
|
||||
<button
|
||||
type="button"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
copy(data?.command || "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
type GiteaProviderResponse,
|
||||
getGiteaOAuthUrl,
|
||||
} from "@/utils/gitea-utils";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
giteaUrl: z.string().min(1, {
|
||||
message: "Gitea URL is required",
|
||||
}),
|
||||
clientId: z.string().min(1, {
|
||||
message: "Client ID is required",
|
||||
}),
|
||||
clientSecret: z.string().min(1, {
|
||||
message: "Client Secret is required",
|
||||
}),
|
||||
redirectUri: z.string().min(1, {
|
||||
message: "Redirect URI is required",
|
||||
}),
|
||||
organizationName: z.string().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const AddGiteaProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const urlObj = useUrl();
|
||||
const baseUrl =
|
||||
typeof urlObj === "string" ? urlObj : (urlObj as any)?.url || "";
|
||||
|
||||
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
|
||||
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
redirectUri: webhookUrl,
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
const giteaUrl = form.watch("giteaUrl");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
redirectUri: webhookUrl,
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
});
|
||||
}, [form, webhookUrl, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
try {
|
||||
// Send the form data to create the Gitea provider
|
||||
const result = (await mutateAsync({
|
||||
clientId: data.clientId,
|
||||
clientSecret: data.clientSecret,
|
||||
name: data.name,
|
||||
redirectUri: data.redirectUri,
|
||||
giteaUrl: data.giteaUrl,
|
||||
organizationName: data.organizationName,
|
||||
})) as unknown as GiteaProviderResponse;
|
||||
|
||||
// Check if we have a giteaId from the response
|
||||
if (!result || !result.giteaId) {
|
||||
toast.error("Failed to get Gitea ID from response");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate OAuth URL using the shared utility
|
||||
const authUrl = getGiteaOAuthUrl(
|
||||
result.giteaId,
|
||||
data.clientId,
|
||||
data.giteaUrl,
|
||||
baseUrl,
|
||||
);
|
||||
|
||||
// Open the Gitea OAuth URL
|
||||
if (authUrl !== "#") {
|
||||
window.open(authUrl, "_blank");
|
||||
} else {
|
||||
toast.error("Configuration Incomplete", {
|
||||
description: "Please fill in Client ID and Gitea URL first.",
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Gitea provider created successfully");
|
||||
setIsOpen(false);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(`Error configuring Gitea: ${error.message}`);
|
||||
} else {
|
||||
toast.error("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
|
||||
>
|
||||
<GiteaIcon />
|
||||
<span>Gitea</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Gitea Provider <GiteaIcon className="size-5" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-gitea"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-1"
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your Gitea account, you need to create a new
|
||||
application in your Gitea settings. Follow these steps:
|
||||
</p>
|
||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||
<li className="flex flex-row gap-2 items-center">
|
||||
Go to your Gitea settings{" "}
|
||||
<Link
|
||||
href={`${giteaUrl}/user/settings/applications`}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="w-fit text-primary size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
Navigate to Applications {"->"} Create new OAuth2
|
||||
Application
|
||||
</li>
|
||||
<li>
|
||||
Create a new application with the following details:
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
<li>Name: Dokploy</li>
|
||||
<li>
|
||||
Redirect URI:{" "}
|
||||
<span className="text-primary">{webhookUrl}</span>{" "}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
After creating, you'll receive an ID and Secret, copy them
|
||||
and paste them below.
|
||||
</li>
|
||||
</ol>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="giteaUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://gitea.com/" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="redirectUri"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Redirect URI</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Client ID" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Client Secret"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button isLoading={form.formState.isSubmitting}>
|
||||
Configure Gitea App
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
giteaUrl: z.string().min(1, "Gitea URL is required"),
|
||||
clientId: z.string().min(1, "Client ID is required"),
|
||||
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
giteaId: string;
|
||||
}
|
||||
|
||||
export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const {
|
||||
data: gitea,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = api.gitea.one.useQuery({ giteaId });
|
||||
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading: isTesting } =
|
||||
api.gitea.testConnection.useMutation();
|
||||
const url = useUrl();
|
||||
const utils = api.useUtils();
|
||||
|
||||
useEffect(() => {
|
||||
const { connected, error } = router.query;
|
||||
|
||||
if (!router.isReady) return;
|
||||
|
||||
if (connected) {
|
||||
toast.success("Successfully connected to Gitea", {
|
||||
description: "Your Gitea provider has been authorized.",
|
||||
id: "gitea-connection-success",
|
||||
});
|
||||
refetch();
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
toast.error("Gitea Connection Failed", {
|
||||
description: decodeURIComponent(error as string),
|
||||
id: "gitea-connection-error",
|
||||
});
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}
|
||||
}, [router.query, router.isReady, refetch]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (gitea) {
|
||||
form.reset({
|
||||
name: gitea.gitProvider?.name || "",
|
||||
giteaUrl: gitea.giteaUrl || "https://gitea.com",
|
||||
clientId: gitea.clientId || "",
|
||||
clientSecret: gitea.clientSecret || "",
|
||||
});
|
||||
}
|
||||
}, [gitea, form]);
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
await mutateAsync({
|
||||
giteaId: giteaId,
|
||||
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
|
||||
name: values.name,
|
||||
giteaUrl: values.giteaUrl,
|
||||
clientId: values.clientId,
|
||||
clientSecret: values.clientSecret,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Gitea provider updated successfully");
|
||||
await refetch();
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Gitea provider");
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
try {
|
||||
const result = await testConnection({ giteaId });
|
||||
toast.success("Gitea Connection Verified", {
|
||||
description: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
const formValues = form.getValues();
|
||||
const authUrl =
|
||||
error.authorizationUrl ||
|
||||
getGiteaOAuthUrl(
|
||||
giteaId,
|
||||
formValues.clientId,
|
||||
formValues.giteaUrl,
|
||||
typeof url === "string" ? url : (url as any).url || "",
|
||||
);
|
||||
|
||||
toast.error("Gitea Not Connected", {
|
||||
description:
|
||||
error.message || "Please complete the OAuth authorization process.",
|
||||
action:
|
||||
authUrl && authUrl !== "#"
|
||||
? {
|
||||
label: "Authorize Now",
|
||||
onClick: () => window.open(authUrl, "_blank"),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Button variant="ghost" size="icon" disabled>
|
||||
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Function to handle dialog open state
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<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>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Gitea Provider</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update your Gitea provider details.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="My Gitea"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="giteaUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://gitea.example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Client ID" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Client Secret"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
isLoading={isTesting}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const formValues = form.getValues();
|
||||
const authUrl = getGiteaOAuthUrl(
|
||||
giteaId,
|
||||
formValues.clientId,
|
||||
formValues.giteaUrl,
|
||||
typeof url === "string" ? url : (url as any).url || "",
|
||||
);
|
||||
if (authUrl !== "#") {
|
||||
window.open(authUrl, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connect to Gitea
|
||||
</Button>
|
||||
|
||||
<Button type="submit" isLoading={isUpdating}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
@@ -26,6 +27,8 @@ import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||
import { AddGiteaProvider } from "./gitea/add-gitea-provider";
|
||||
import { EditGiteaProvider } from "./gitea/edit-gitea-provider";
|
||||
import { AddGithubProvider } from "./github/add-github-provider";
|
||||
import { EditGithubProvider } from "./github/edit-github-provider";
|
||||
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
||||
@@ -36,19 +39,18 @@ export const ShowGitProviders = () => {
|
||||
const { mutateAsync, isLoading: isRemoving } =
|
||||
api.gitProvider.remove.useMutation();
|
||||
const url = useUrl();
|
||||
|
||||
const getGitlabUrl = (
|
||||
clientId: string,
|
||||
gitlabId: string,
|
||||
gitlabUrl: string,
|
||||
) => {
|
||||
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
||||
|
||||
const scope = "api read_user read_repository";
|
||||
|
||||
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
@@ -82,6 +84,7 @@ export const ShowGitProviders = () => {
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
<AddGiteaProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +100,7 @@ export const ShowGitProviders = () => {
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
<AddGiteaProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,13 +111,16 @@ export const ShowGitProviders = () => {
|
||||
const isGitlab = gitProvider.providerType === "gitlab";
|
||||
const isBitbucket =
|
||||
gitProvider.providerType === "bitbucket";
|
||||
const isGitea = gitProvider.providerType === "gitea";
|
||||
|
||||
const haveGithubRequirements =
|
||||
gitProvider.providerType === "github" &&
|
||||
isGithub &&
|
||||
gitProvider.github?.githubPrivateKey &&
|
||||
gitProvider.github?.githubAppId &&
|
||||
gitProvider.github?.githubInstallationId;
|
||||
|
||||
const haveGitlabRequirements =
|
||||
isGitlab &&
|
||||
gitProvider.gitlab?.accessToken &&
|
||||
gitProvider.gitlab?.refreshToken;
|
||||
|
||||
@@ -122,18 +129,19 @@ export const ShowGitProviders = () => {
|
||||
key={gitProvider.gitProviderId}
|
||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex flex-col items-center justify-between">
|
||||
<div className="flex gap-2 flex-row items-center">
|
||||
{gitProvider.providerType === "github" && (
|
||||
{isGithub && (
|
||||
<GithubIcon className="size-5" />
|
||||
)}
|
||||
{gitProvider.providerType === "gitlab" && (
|
||||
{isGitlab && (
|
||||
<GitlabIcon className="size-5" />
|
||||
)}
|
||||
{gitProvider.providerType === "bitbucket" && (
|
||||
{isBitbucket && (
|
||||
<BitbucketIcon className="size-5" />
|
||||
)}
|
||||
{isGitea && <GiteaIcon className="size-5" />}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium">
|
||||
{gitProvider.name}
|
||||
@@ -194,26 +202,33 @@ export const ShowGitProviders = () => {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={gitProvider.github.githubId}
|
||||
githubId={gitProvider.github?.githubId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={gitProvider.gitlab.gitlabId}
|
||||
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={
|
||||
gitProvider.bitbucket.bitbucketId
|
||||
gitProvider.bitbucket?.bitbucketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitea && (
|
||||
<EditGiteaProvider
|
||||
giteaId={gitProvider.gitea?.giteaId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description="Are you sure you want to delete this Git Provider?"
|
||||
@@ -238,7 +253,7 @@ export const ShowGitProviders = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
|
||||
@@ -663,13 +663,16 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value) {
|
||||
if (value === "") {
|
||||
field.onChange(undefined);
|
||||
} else {
|
||||
const port = Number.parseInt(value);
|
||||
if (port > 0 && port < 65536) {
|
||||
field.onChange(port);
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={field.value || ""}
|
||||
type="number"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -64,12 +64,12 @@ export const Enable2FA = () => {
|
||||
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||
setIsPasswordLoading(true);
|
||||
try {
|
||||
const { data: enableData } = await authClient.twoFactor.enable({
|
||||
const { data: enableData, error } = await authClient.twoFactor.enable({
|
||||
password: formData.password,
|
||||
});
|
||||
|
||||
if (!enableData) {
|
||||
throw new Error("No data received from server");
|
||||
throw new Error(error?.message || "Error enabling 2FA");
|
||||
}
|
||||
|
||||
if (enableData.backupCodes) {
|
||||
@@ -95,7 +95,8 @@ export const Enable2FA = () => {
|
||||
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||
);
|
||||
passwordForm.setError("password", {
|
||||
message: "Error verifying password",
|
||||
message:
|
||||
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||
});
|
||||
} finally {
|
||||
setIsPasswordLoading(false);
|
||||
|
||||
@@ -59,9 +59,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||
.then(async () => {
|
||||
toast.success("Traefik Reloaded");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Traefik");
|
||||
});
|
||||
.catch(() => {});
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
const _utils = api.useUtils();
|
||||
|
||||
return (
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -36,10 +36,12 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<LockKeyhole className="size-5" />
|
||||
<CardTitle className="text-xl">
|
||||
Setup Security Sugestions
|
||||
Setup Security Suggestions
|
||||
</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Check the security sugestions</CardDescription>
|
||||
<CardDescription>
|
||||
Check the security suggestions
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isRefreshing}
|
||||
@@ -120,36 +122,36 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
<div className="grid gap-2.5">
|
||||
<StatusRow
|
||||
label="Enabled"
|
||||
isEnabled={data?.ssh.enabled}
|
||||
isEnabled={data?.ssh?.enabled}
|
||||
description={
|
||||
data?.ssh.enabled
|
||||
data?.ssh?.enabled
|
||||
? "Enabled"
|
||||
: "Not Enabled (SSH should be enabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Key Auth"
|
||||
isEnabled={data?.ssh.keyAuth}
|
||||
isEnabled={data?.ssh?.keyAuth}
|
||||
description={
|
||||
data?.ssh.keyAuth
|
||||
data?.ssh?.keyAuth
|
||||
? "Enabled (Recommended)"
|
||||
: "Not Enabled (Key Authentication should be enabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Password Auth"
|
||||
isEnabled={data?.ssh.passwordAuth === "no"}
|
||||
isEnabled={data?.ssh?.passwordAuth === "no"}
|
||||
description={
|
||||
data?.ssh.passwordAuth === "no"
|
||||
data?.ssh?.passwordAuth === "no"
|
||||
? "Disabled (Recommended)"
|
||||
: "Enabled (Password Authentication should be disabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Use PAM"
|
||||
isEnabled={data?.ssh.usePam === "no"}
|
||||
isEnabled={data?.ssh?.usePam === "no"}
|
||||
description={
|
||||
data?.ssh.usePam === "no"
|
||||
data?.ssh?.usePam === "no"
|
||||
? "Disabled (Recommended for key-based auth)"
|
||||
: "Enabled (Should be disabled when using key-based auth)"
|
||||
}
|
||||
@@ -166,9 +168,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
<div className="grid gap-2.5">
|
||||
<StatusRow
|
||||
label="Installed"
|
||||
isEnabled={data?.fail2ban.installed}
|
||||
isEnabled={data?.fail2ban?.installed}
|
||||
description={
|
||||
data?.fail2ban.installed
|
||||
data?.fail2ban?.installed
|
||||
? "Installed (Recommended)"
|
||||
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
||||
}
|
||||
@@ -176,18 +178,18 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
|
||||
<StatusRow
|
||||
label="Enabled"
|
||||
isEnabled={data?.fail2ban.enabled}
|
||||
isEnabled={data?.fail2ban?.enabled}
|
||||
description={
|
||||
data?.fail2ban.enabled
|
||||
data?.fail2ban?.enabled
|
||||
? "Enabled (Recommended)"
|
||||
: "Not Enabled (Fail2Ban service should be enabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Active"
|
||||
isEnabled={data?.fail2ban.active}
|
||||
isEnabled={data?.fail2ban?.active}
|
||||
description={
|
||||
data?.fail2ban.active
|
||||
data?.fail2ban?.active
|
||||
? "Active (Recommended)"
|
||||
: "Not Active (Fail2Ban service should be running)"
|
||||
}
|
||||
@@ -195,9 +197,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
|
||||
<StatusRow
|
||||
label="SSH Protection"
|
||||
isEnabled={data?.fail2ban.sshEnabled === "true"}
|
||||
isEnabled={data?.fail2ban?.sshEnabled === "true"}
|
||||
description={
|
||||
data?.fail2ban.sshEnabled === "true"
|
||||
data?.fail2ban?.sshEnabled === "true"
|
||||
? "Enabled (Recommended)"
|
||||
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
|
||||
}
|
||||
@@ -205,11 +207,11 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
|
||||
<StatusRow
|
||||
label="SSH Mode"
|
||||
isEnabled={data?.fail2ban.sshMode === "aggressive"}
|
||||
isEnabled={data?.fail2ban?.sshMode === "aggressive"}
|
||||
description={
|
||||
data?.fail2ban.sshMode === "aggressive"
|
||||
data?.fail2ban?.sshMode === "aggressive"
|
||||
? "Aggressive Mode (Recommended)"
|
||||
: `Mode: ${data?.fail2ban.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
||||
: `Mode: ${data?.fail2ban?.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,7 @@ import { useTranslation } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
||||
import { TerminalModal } from "../web-server/terminal-modal";
|
||||
import { ShowServerActions } from "./actions/show-server-actions";
|
||||
import { HandleServers } from "./handle-servers";
|
||||
@@ -328,6 +329,9 @@ export const ShowServers = () => {
|
||||
<ShowSwarmOverviewModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowNodesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -112,15 +112,17 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
||||
toast.error("Error generating the SSH Key");
|
||||
});
|
||||
|
||||
const downloadKey = (
|
||||
content: string,
|
||||
defaultFilename: string,
|
||||
keyType: "private" | "public",
|
||||
) => {
|
||||
const downloadKey = (content: string, keyType: "private" | "public") => {
|
||||
const keyName = form.watch("name");
|
||||
const publicKey = form.watch("publicKey");
|
||||
|
||||
// Extract algorithm type from public key
|
||||
const isEd25519 = publicKey.startsWith("ssh-ed25519");
|
||||
const defaultName = isEd25519 ? "id_ed25519" : "id_rsa";
|
||||
|
||||
const filename = keyName
|
||||
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}`
|
||||
: `${keyType}_${defaultFilename}`;
|
||||
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultName}${keyType === "public" ? ".pub" : ""}`
|
||||
: `${defaultName}${keyType === "public" ? ".pub" : ""}`;
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
@@ -273,7 +275,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={() =>
|
||||
downloadKey(form.watch("privateKey"), "id_rsa", "private")
|
||||
downloadKey(form.watch("privateKey"), "private")
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
@@ -287,11 +289,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={() =>
|
||||
downloadKey(
|
||||
form.watch("publicKey"),
|
||||
"id_rsa.pub",
|
||||
"public",
|
||||
)
|
||||
downloadKey(form.watch("publicKey"), "public")
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,6 +24,7 @@ import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { badgeStateColor } from "../../application/logs/show";
|
||||
|
||||
const Terminal = dynamic(
|
||||
() =>
|
||||
@@ -109,7 +111,10 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
|
||||
@@ -19,13 +19,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
||||
@@ -44,7 +37,6 @@ interface Props {
|
||||
const PortSchema = z.object({
|
||||
targetPort: z.number().min(1, "Target port is required"),
|
||||
publishedPort: z.number().min(1, "Published port is required"),
|
||||
publishMode: z.enum(["ingress", "host"]),
|
||||
});
|
||||
|
||||
const TraefikPortsSchema = z.object({
|
||||
@@ -88,7 +80,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
}, [currentPorts, form]);
|
||||
|
||||
const handleAddPort = () => {
|
||||
append({ targetPort: 0, publishedPort: 0, publishMode: "host" });
|
||||
append({ targetPort: 0, publishedPort: 0 });
|
||||
};
|
||||
|
||||
const onSubmit = async (data: TraefikPortsForm) => {
|
||||
@@ -99,9 +91,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
});
|
||||
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
||||
setOpen(false);
|
||||
} catch (_error) {
|
||||
toast.error(t("settings.server.webServer.traefik.portsUpdateError"));
|
||||
}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -154,7 +144,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
<div className="grid gap-4">
|
||||
{fields.map((field, index) => (
|
||||
<Card key={field.id}>
|
||||
<CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent">
|
||||
<CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ports.${index}.targetPort`}
|
||||
@@ -169,9 +159,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
field.onChange(
|
||||
value === ""
|
||||
? undefined
|
||||
: Number(value),
|
||||
);
|
||||
}}
|
||||
value={field.value || ""}
|
||||
className="w-full dark:bg-black"
|
||||
placeholder="e.g. 8080"
|
||||
/>
|
||||
@@ -195,9 +191,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
field.onChange(
|
||||
value === ""
|
||||
? undefined
|
||||
: Number(value),
|
||||
);
|
||||
}}
|
||||
value={field.value || ""}
|
||||
className="w-full dark:bg-black"
|
||||
placeholder="e.g. 80"
|
||||
/>
|
||||
@@ -207,39 +209,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ports.${index}.publishMode`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm font-medium text-muted-foreground">
|
||||
{t(
|
||||
"settings.server.webServer.traefik.publishMode",
|
||||
)}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="dark:bg-black">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="host">
|
||||
Host Mode
|
||||
</SelectItem>
|
||||
<SelectItem value="ingress">
|
||||
Ingress Mode
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
onClick={() => remove(index)}
|
||||
@@ -263,30 +232,23 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
<span className="text-sm">
|
||||
<strong>
|
||||
Each port mapping defines how external traffic reaches
|
||||
your containers.
|
||||
your containers through Traefik.
|
||||
</strong>
|
||||
<ul className="pt-2">
|
||||
<li>
|
||||
<strong>Host Mode:</strong> Directly binds the port
|
||||
to the host machine.
|
||||
<ul className="p-2 list-inside list-disc">
|
||||
<li>
|
||||
Best for single-node deployments or when you
|
||||
need guaranteed port availability.
|
||||
</li>
|
||||
</ul>
|
||||
<strong>Target Port:</strong> The port inside your
|
||||
container that the service is listening on.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Ingress Mode:</strong> Routes through Docker
|
||||
Swarm's load balancer.
|
||||
<ul className="p-2 list-inside list-disc">
|
||||
<li>
|
||||
Recommended for multi-node deployments and
|
||||
better scalability.
|
||||
</li>
|
||||
</ul>
|
||||
<strong>Published Port:</strong> The port on your
|
||||
host machine that will be mapped to the target port.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2">
|
||||
All ports are bound directly to the host machine,
|
||||
allowing Traefik to handle incoming traffic and route
|
||||
it appropriately to your services.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</AlertBlock>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -21,6 +22,7 @@ import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { badgeStateColor } from "../../application/logs/show";
|
||||
|
||||
export const DockerLogsId = dynamic(
|
||||
() =>
|
||||
@@ -90,7 +92,10 @@ export const ShowModalLogs = ({
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
{container.name} ({container.containerId}){" "}
|
||||
<Badge variant={badgeStateColor(container.state)}>
|
||||
{container.state}
|
||||
</Badge>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { DatabaseIcon, AlertTriangle } from "lucide-react";
|
||||
import { AlertTriangle, DatabaseIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
||||
import { RebuildDatabase } from "./rebuild-database";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
|
||||
{nodes.map((node) => (
|
||||
<NodeCard key={node.ID} node={node} serverId={serverId} />
|
||||
))}
|
||||
|
||||
@@ -238,6 +238,41 @@ export const BitbucketIcon = ({ className }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const GiteaIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
id="main_outline"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="5.67 143.05 628.65 387.55"
|
||||
enableBackground="new 0 0 640 640"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
id="teabag"
|
||||
style={{ fill: "#FFFFFF" }}
|
||||
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#609926" }}
|
||||
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#609926" }}
|
||||
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const DockerIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -908,7 +908,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Settings</SidebarGroupLabel>
|
||||
<SidebarMenu className="gap-2">
|
||||
<SidebarMenu className="gap-1">
|
||||
{filteredSettings.map((item) => {
|
||||
const isSingle = item.isSingle !== false;
|
||||
const isActive = isSingle
|
||||
@@ -1068,7 +1068,7 @@ export default function Page({ children }: Props) {
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col w-full gap-4 p-4 pt-0">{children}</div>
|
||||
<div className="flex flex-col w-full p-4 pt-0">{children}</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { api } from "@/utils/api";
|
||||
import type { IUpdateData } from "@dokploy/server/index";
|
||||
import { Download } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
||||
import { Button } from "../ui/button";
|
||||
import { Download } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
||||
@@ -37,7 +37,9 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||
)}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{_index + 1 < list.length && <BreadcrumbSeparator className="block" />}
|
||||
{_index + 1 < list.length && (
|
||||
<BreadcrumbSeparator className="block" />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
|
||||
@@ -3,18 +3,18 @@ import { json } from "@codemirror/lang-json";
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { StreamLanguage } from "@codemirror/language";
|
||||
|
||||
import {
|
||||
type Completion,
|
||||
type CompletionContext,
|
||||
type CompletionResult,
|
||||
autocompletion,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
autocompletion,
|
||||
type CompletionContext,
|
||||
type CompletionResult,
|
||||
type Completion,
|
||||
} from "@codemirror/autocomplete";
|
||||
|
||||
// Docker Compose completion options
|
||||
const dockerComposeServices = [
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
|
||||
1
apps/dokploy/drizzle/0078_uneven_omega_sentinel.sql
Normal file
1
apps/dokploy/drizzle/0078_uneven_omega_sentinel.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "cleanCache" boolean DEFAULT false;
|
||||
31
apps/dokploy/drizzle/0079_bizarre_wendell_rand.sql
Normal file
31
apps/dokploy/drizzle/0079_bizarre_wendell_rand.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
ALTER TYPE "public"."sourceType" ADD VALUE 'gitea' BEFORE 'drop';--> statement-breakpoint
|
||||
ALTER TYPE "public"."sourceTypeCompose" ADD VALUE 'gitea' BEFORE 'raw';--> statement-breakpoint
|
||||
ALTER TYPE "public"."gitProviderType" ADD VALUE 'gitea';--> statement-breakpoint
|
||||
CREATE TABLE "gitea" (
|
||||
"giteaId" text PRIMARY KEY NOT NULL,
|
||||
"giteaUrl" text DEFAULT 'https://gitea.com' NOT NULL,
|
||||
"redirect_uri" text,
|
||||
"client_id" text,
|
||||
"client_secret" text,
|
||||
"gitProviderId" text NOT NULL,
|
||||
"gitea_username" text,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"expires_at" integer,
|
||||
"scopes" text DEFAULT 'repo,repo:status,read:user,read:org',
|
||||
"last_authenticated_at" integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaProjectId" integer;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaBuildPath" text DEFAULT '/';--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaId" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaId" text;--> statement-breakpoint
|
||||
ALTER TABLE "gitea" ADD CONSTRAINT "gitea_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD CONSTRAINT "compose_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;
|
||||
1
apps/dokploy/drizzle/0080_sleepy_sinister_six.sql
Normal file
1
apps/dokploy/drizzle/0080_sleepy_sinister_six.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" DROP COLUMN "giteaProjectId";
|
||||
1
apps/dokploy/drizzle/0081_lovely_mentallo.sql
Normal file
1
apps/dokploy/drizzle/0081_lovely_mentallo.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "gitea" DROP COLUMN "gitea_username";
|
||||
2
apps/dokploy/drizzle/0082_clean_mandarin.sql
Normal file
2
apps/dokploy/drizzle/0082_clean_mandarin.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "backup" ADD COLUMN "userId" text;--> statement-breakpoint
|
||||
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;
|
||||
1
apps/dokploy/drizzle/0083_parallel_stranger.sql
Normal file
1
apps/dokploy/drizzle/0083_parallel_stranger.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."databaseType" ADD VALUE 'web-server';
|
||||
5163
apps/dokploy/drizzle/meta/0078_snapshot.json
Normal file
5163
apps/dokploy/drizzle/meta/0078_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5354
apps/dokploy/drizzle/meta/0079_snapshot.json
Normal file
5354
apps/dokploy/drizzle/meta/0079_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5348
apps/dokploy/drizzle/meta/0080_snapshot.json
Normal file
5348
apps/dokploy/drizzle/meta/0080_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user