mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfdc73f8d1 | ||
|
|
525a711c55 | ||
|
|
e0a0f97f70 | ||
|
|
45aae520d8 | ||
|
|
c8f14c322e | ||
|
|
334466f28f | ||
|
|
22328eeb83 | ||
|
|
c2201a304e | ||
|
|
f77de1bc52 | ||
|
|
74d6b2d591 | ||
|
|
b7cc3283e2 | ||
|
|
5dfa6c5194 | ||
|
|
59d662396e | ||
|
|
cbfcae4ab6 | ||
|
|
152a54b251 | ||
|
|
1113669bfd | ||
|
|
4d251271b9 | ||
|
|
21263a217c | ||
|
|
3322fdf937 | ||
|
|
b415e2fba1 | ||
|
|
8888a4ce5c | ||
|
|
dd3ba7e836 | ||
|
|
cb6d8fceb4 | ||
|
|
d8641b7b4e | ||
|
|
997a8395b1 | ||
|
|
d2420ed6e8 | ||
|
|
64ada7020a | ||
|
|
faf24dfa25 | ||
|
|
6f4bf428c7 | ||
|
|
a43627d869 | ||
|
|
addd102d39 | ||
|
|
0d85fd0e3c | ||
|
|
86fc59d850 | ||
|
|
3cd3db6828 | ||
|
|
2c6fd0f52b | ||
|
|
5fb682b58d | ||
|
|
b77f330222 | ||
|
|
f0b8f3eaa0 | ||
|
|
36af22630b | ||
|
|
2d53a700f6 | ||
|
|
00eeffee13 | ||
|
|
7a32698031 | ||
|
|
9d834e1a79 | ||
|
|
91819c2488 | ||
|
|
f64392469d | ||
|
|
889e72d21e | ||
|
|
86165d1104 | ||
|
|
54adab16cf | ||
|
|
2e3b0ddcde | ||
|
|
ed0f3cadd6 | ||
|
|
898880634a | ||
|
|
b4e154fb28 | ||
|
|
2e6489d315 | ||
|
|
210fed30a2 | ||
|
|
60521c1025 | ||
|
|
1a496e35c0 | ||
|
|
f37f98aade | ||
|
|
0eb7b3ecb1 | ||
|
|
1a7c602861 | ||
|
|
4706adc0c0 | ||
|
|
85f025c729 | ||
|
|
06005eb333 | ||
|
|
0c01efb249 | ||
|
|
7e9e9dc865 | ||
|
|
b28bf5f9ec | ||
|
|
c071be6ad9 | ||
|
|
059a98c575 | ||
|
|
d2a07195b0 | ||
|
|
4ff1b3c19f | ||
|
|
39abd7e374 | ||
|
|
899d7565f6 | ||
|
|
3cfc2d6cd8 | ||
|
|
817fa91173 | ||
|
|
4d8a4f713d | ||
|
|
4865f4f969 | ||
|
|
0b7feb5483 | ||
|
|
1c139b9503 | ||
|
|
d47b7e62e6 | ||
|
|
33940a345a | ||
|
|
f230bda1f7 | ||
|
|
687524d154 | ||
|
|
e01d92d1d9 | ||
|
|
8f0a4f0886 | ||
|
|
01794c7742 | ||
|
|
19bbe69ee3 | ||
|
|
92c4e769ab | ||
|
|
0801e91816 | ||
|
|
b360cc2af4 | ||
|
|
ba5d8feba2 | ||
|
|
43be9d0171 | ||
|
|
a6bbf5d96b | ||
|
|
7e6e43adfe | ||
|
|
e8a4611ab7 | ||
|
|
608db3d401 | ||
|
|
0add62f14d | ||
|
|
1754f63352 | ||
|
|
edcc7544fb | ||
|
|
1edf30546d | ||
|
|
ad806437af | ||
|
|
f0eecf354b | ||
|
|
c5ace67b3f | ||
|
|
fe22890311 | ||
|
|
3f2eeaf386 | ||
|
|
7826ba5bb5 | ||
|
|
bdc488e179 | ||
|
|
fc2abac989 | ||
|
|
101bbd44d8 | ||
|
|
68c2272e98 | ||
|
|
bc28464430 | ||
|
|
7df415a386 | ||
|
|
9fbd3039a6 | ||
|
|
3c00937b94 | ||
|
|
ae4226531e | ||
|
|
a57a776500 | ||
|
|
323e2f54ba | ||
|
|
2b7c7632f4 | ||
|
|
7db662e332 | ||
|
|
cd1a686b59 | ||
|
|
78d573c4f3 | ||
|
|
0ef9b1427b | ||
|
|
993d6b52f2 | ||
|
|
b9ab4a4d1a | ||
|
|
83153471b8 | ||
|
|
909e536f45 | ||
|
|
295cf50060 | ||
|
|
dd16baf234 | ||
|
|
72c366aa10 | ||
|
|
9a4f79f9e6 | ||
|
|
30b81834fc | ||
|
|
4e3aaa2a69 | ||
|
|
3dcd89cc32 | ||
|
|
41dc388bb0 | ||
|
|
44a592f7a7 | ||
|
|
1a4f5607dc | ||
|
|
d54c6e4ac9 | ||
|
|
936cf76a4c | ||
|
|
65254f1686 | ||
|
|
b312b1d7e0 | ||
|
|
1b7244e841 | ||
|
|
7eb1716d71 | ||
|
|
b8a8e32659 | ||
|
|
eea00d28cd | ||
|
|
449a61e208 | ||
|
|
037f3ed118 | ||
|
|
471802c4da | ||
|
|
502ff638d6 | ||
|
|
750c12ded5 | ||
|
|
7fb34ade00 | ||
|
|
0c197c095b | ||
|
|
4cbff731d1 | ||
|
|
b9bff95c3d | ||
|
|
113df9ae12 | ||
|
|
7f13fd24ec | ||
|
|
ce200185bb | ||
|
|
ff274d4f6b | ||
|
|
306f02f5cd | ||
|
|
8f9d21c0f8 | ||
|
|
1df6db738e | ||
|
|
290228116a | ||
|
|
36eb2edc76 | ||
|
|
a310065766 | ||
|
|
7cb299a4bb | ||
|
|
71be1ed180 | ||
|
|
aa094e8472 | ||
|
|
4d3e3426ca | ||
|
|
ed6e4e8e73 | ||
|
|
b244aaa9de | ||
|
|
8a0ffbe754 | ||
|
|
d34aadde43 | ||
|
|
e174101377 | ||
|
|
eef0bf6ff7 | ||
|
|
41bdfdf78f | ||
|
|
874d1f9fe4 | ||
|
|
0867bc20bd | ||
|
|
fae180f157 | ||
|
|
bf2516a495 | ||
|
|
ec54c79e80 | ||
|
|
422187cd4b | ||
|
|
2362130927 | ||
|
|
0f025182f1 | ||
|
|
3a59edbc0f | ||
|
|
5f42bf63a6 | ||
|
|
baecc49d86 | ||
|
|
506fe074df | ||
|
|
7961591009 | ||
|
|
f784a4f989 | ||
|
|
957e5066aa | ||
|
|
bf133536ba | ||
|
|
683a62f418 | ||
|
|
5a70e616e6 | ||
|
|
d52f66a716 | ||
|
|
5806068e2e | ||
|
|
667067811c | ||
|
|
eda33e095e | ||
|
|
976d1f312f | ||
|
|
42e9aa1834 | ||
|
|
744f800700 | ||
|
|
08517d6f36 | ||
|
|
d19dec8010 | ||
|
|
c45017e204 | ||
|
|
6c792564ae | ||
|
|
e9245cee2c | ||
|
|
832fc184af |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: #
|
||||
open_collective: dokploy
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before opening a new issue, please do a search of existing issues.
|
||||
|
||||
If you need help with your own project, you can start a discussion in the [Q&A Section](https://github.com/Dokploy/dokploy/discussions).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: A step-by-step description of how to reproduce the issue, or a link to the reproducible repository.
|
||||
placeholder: |
|
||||
1. Create a application
|
||||
2. Click X
|
||||
3. Y will happen
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current vs. Expected behavior
|
||||
description: A clear and concise description of what the bug is, and what you expected to happen.
|
||||
placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Provide environment information
|
||||
description: Please provide the following information about your environment.
|
||||
render: bash
|
||||
placeholder: |
|
||||
Operating System:
|
||||
OS: Ubuntu 20.04
|
||||
Arch: arm64
|
||||
Dokploy version: 0.2.2'
|
||||
VPS Provider: DigitalOcean, Hetzner, Linode, etc.
|
||||
What applications/services are you tying to deploy?
|
||||
eg - Database, Nextjs App, laravel, etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Which area(s) are affected? (Select all that apply)
|
||||
multiple: true
|
||||
options:
|
||||
- 'Installation'
|
||||
- 'Application'
|
||||
- 'Databases'
|
||||
- 'Docker Compose'
|
||||
- 'Traefik'
|
||||
- 'Docker'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Any extra information that might help us investigate.
|
||||
placeholder: |
|
||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions?
|
||||
url: https://github.com/Dokploy/dokploy/discussions
|
||||
about: Ask your questions here.
|
||||
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement to the project
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What problem will this feature address?
|
||||
description: A clear and concise description of what the problem is.
|
||||
placeholder: |
|
||||
I'm always frustrated when I can't do X
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: Add X to the core
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
placeholder: |
|
||||
Maybe use Y as a workaround?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
2
.github/workflows/pull-request.yml
vendored
2
.github/workflows/pull-request.yml
vendored
@@ -30,6 +30,8 @@ jobs:
|
||||
run: pnpm install
|
||||
- name: Run Build
|
||||
run: pnpm build
|
||||
- name: Run Tests
|
||||
run: pnpm run test
|
||||
|
||||
build-and-push-docker-on-push:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ yarn-error.log*
|
||||
|
||||
*.lockb
|
||||
*.rdb
|
||||
.idea
|
||||
|
||||
@@ -151,3 +151,92 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ FROM node:18-slim AS production
|
||||
# Install dependencies only for production
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN corepack enable && apt-get update && apt-get install -y curl && apt-get install -y apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -47,7 +47,6 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-l
|
||||
# Install docker
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
||||
|
||||
|
||||
# Install Nixpacks and tsx
|
||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
|
||||
11
LICENSE.MD
11
LICENSE.MD
@@ -1,5 +1,7 @@
|
||||
# License
|
||||
|
||||
## Core License (Apache License 2.0)
|
||||
|
||||
Copyright 2024 Mauricio Siu.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -13,11 +15,12 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
## Appendix
|
||||
## Additional Terms for Specific Features
|
||||
|
||||
In the event of a conflict, the provisions in this appendix shall take precedence over those in the Apache License.
|
||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
|
||||
- **Modification Distribution:** Any modifications to the software must be distributed freely.
|
||||
- **Future Paid Features:** Any future paid features of Dokploy cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
||||
|
||||
For further inquiries or permissions, please contact us directly.
|
||||
|
||||
@@ -39,10 +39,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
||||
|
||||
Getestete Systems:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 Dokumentation
|
||||
|
||||
|
||||
11
README-ru.md
11
README-ru.md
@@ -40,10 +40,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
||||
|
||||
Проверенные системы:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 Документация
|
||||
Для подробной документации посетите docs.dokploy.com/docs.
|
||||
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||
|
||||
@@ -43,10 +43,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
||||
|
||||
经过测试的系统:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 文档
|
||||
|
||||
|
||||
91
README.md
91
README.md
@@ -1,36 +1,37 @@
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
<div>
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center" style="width:100%;">
|
||||
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Reflex Logo" style="width:60%;">
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<br />
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases using Docker and Traefik. Designed to enhance efficiency and security, Dokploy allows you to deploy your applications on any VPS.
|
||||
Dokploy include multiples features to make your life easier.
|
||||
|
||||
|
||||
|
||||
## Explanation
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
||||
* **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||
* **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
||||
* **Backups**: Automate backups for databases to a external storage destination.
|
||||
* **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||
* **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
||||
* **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
||||
* **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
* **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||
* **Docker Management**: Easily deploy and manage Docker containers.
|
||||
* **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
||||
* **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
|
||||
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease.
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis, and more.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage.
|
||||
- **Database Backups**: Automate backups with support for multiple storage destinations.
|
||||
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
To get started run the following command in a VPS:
|
||||
@@ -40,14 +41,54 @@ To get started run the following command in a VPS:
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
```
|
||||
|
||||
Tested Systems:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
For detailed documentation, visit [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
|
||||
## Video Tutorial
|
||||
<a href="https://youtu.be/mznYKPvhcfw">
|
||||
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||
</a>
|
||||
|
||||
|
||||
## Donations
|
||||
|
||||
If you like dokploy, and want to support the project to cover the costs of hosting, testing and development new features, you can donate to the project using the following link:
|
||||
|
||||
Thanks to all the supporters!
|
||||
|
||||
https://opencollective.com/dokploy
|
||||
|
||||
|
||||
<a href="https://opencollective.com/dokploy"><img src="https://opencollective.com/dokploy/individuals.svg?width=890"></a>
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
## Support OS
|
||||
|
||||
- Ubuntu 24.04 LTS
|
||||
- Ubuntu 23.10
|
||||
- Ubuntu 22.04 LTS
|
||||
- Ubuntu 20.04 LTS
|
||||
- Ubuntu 18.04 LTS
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
|
||||
## Explanation
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
||||
|
||||
|
||||
|
||||
476
__test__/compose/compose.test.ts
Normal file
476
__test__/compose/compose.test.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { load } from "js-yaml";
|
||||
import { addPrefixToAllProperties } from "@/server/utils/docker/compose";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
|
||||
const composeFile1 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
container_name: web_container
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- frontend
|
||||
volumes_from:
|
||||
- data
|
||||
links:
|
||||
- db
|
||||
extends:
|
||||
service: base_service
|
||||
configs:
|
||||
- source: web_config
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
networks:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
networks:
|
||||
- backend
|
||||
|
||||
data:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
base_service:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
backend:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
web_data:
|
||||
driver: local
|
||||
|
||||
configs:
|
||||
web_config:
|
||||
file: ./web_config.yml
|
||||
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile1 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web-testhash:
|
||||
image: nginx:latest
|
||||
container_name: web_container-testhash
|
||||
depends_on:
|
||||
- app-testhash
|
||||
networks:
|
||||
- frontend-testhash
|
||||
volumes_from:
|
||||
- data-testhash
|
||||
links:
|
||||
- db-testhash
|
||||
extends:
|
||||
service: base_service-testhash
|
||||
configs:
|
||||
- source: web_config-testhash
|
||||
|
||||
app-testhash:
|
||||
image: node:14
|
||||
networks:
|
||||
- backend-testhash
|
||||
- frontend-testhash
|
||||
|
||||
db-testhash:
|
||||
image: postgres:13
|
||||
networks:
|
||||
- backend-testhash
|
||||
|
||||
data-testhash:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
base_service-testhash:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
frontend-testhash:
|
||||
driver: bridge
|
||||
backend-testhash:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
web_data-testhash:
|
||||
driver: local
|
||||
|
||||
configs:
|
||||
web_config-testhash:
|
||||
file: ./web_config.yml
|
||||
|
||||
secrets:
|
||||
db_password-testhash:
|
||||
file: ./db_password.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all properties in compose file 1", () => {
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile1);
|
||||
});
|
||||
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: nginx:latest
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- public
|
||||
volumes_from:
|
||||
- logs
|
||||
links:
|
||||
- cache
|
||||
extends:
|
||||
service: shared_service
|
||||
secrets:
|
||||
- db_password
|
||||
|
||||
backend:
|
||||
image: node:14
|
||||
networks:
|
||||
- private
|
||||
- public
|
||||
|
||||
cache:
|
||||
image: redis:latest
|
||||
networks:
|
||||
- private
|
||||
|
||||
logs:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /logs
|
||||
|
||||
shared_service:
|
||||
image: shared:latest
|
||||
|
||||
networks:
|
||||
public:
|
||||
driver: bridge
|
||||
private:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
logs:
|
||||
driver: local
|
||||
|
||||
configs:
|
||||
app_config:
|
||||
file: ./app_config.yml
|
||||
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
frontend-testhash:
|
||||
image: nginx:latest
|
||||
depends_on:
|
||||
- backend-testhash
|
||||
networks:
|
||||
- public-testhash
|
||||
volumes_from:
|
||||
- logs-testhash
|
||||
links:
|
||||
- cache-testhash
|
||||
extends:
|
||||
service: shared_service-testhash
|
||||
secrets:
|
||||
- db_password-testhash
|
||||
|
||||
backend-testhash:
|
||||
image: node:14
|
||||
networks:
|
||||
- private-testhash
|
||||
- public-testhash
|
||||
|
||||
cache-testhash:
|
||||
image: redis:latest
|
||||
networks:
|
||||
- private-testhash
|
||||
|
||||
logs-testhash:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /logs
|
||||
|
||||
shared_service-testhash:
|
||||
image: shared:latest
|
||||
|
||||
networks:
|
||||
public-testhash:
|
||||
driver: bridge
|
||||
private-testhash:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
logs-testhash:
|
||||
driver: local
|
||||
|
||||
configs:
|
||||
app_config-testhash:
|
||||
file: ./app_config.yml
|
||||
|
||||
secrets:
|
||||
db_password-testhash:
|
||||
file: ./db_password.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all properties in compose file 2", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile2);
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
service_a:
|
||||
image: service_a:latest
|
||||
depends_on:
|
||||
- service_b
|
||||
networks:
|
||||
- net_a
|
||||
volumes_from:
|
||||
- data_volume
|
||||
links:
|
||||
- service_c
|
||||
extends:
|
||||
service: common_service
|
||||
configs:
|
||||
- source: service_a_config
|
||||
|
||||
service_b:
|
||||
image: service_b:latest
|
||||
networks:
|
||||
- net_b
|
||||
- net_a
|
||||
|
||||
service_c:
|
||||
image: service_c:latest
|
||||
networks:
|
||||
- net_b
|
||||
|
||||
data_volume:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
common_service:
|
||||
image: common:latest
|
||||
|
||||
networks:
|
||||
net_a:
|
||||
driver: bridge
|
||||
net_b:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
data_volume:
|
||||
driver: local
|
||||
|
||||
configs:
|
||||
service_a_config:
|
||||
file: ./service_a_config.yml
|
||||
|
||||
secrets:
|
||||
service_secret:
|
||||
file: ./service_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
service_a-testhash:
|
||||
image: service_a:latest
|
||||
depends_on:
|
||||
- service_b-testhash
|
||||
networks:
|
||||
- net_a-testhash
|
||||
volumes_from:
|
||||
- data_volume-testhash
|
||||
links:
|
||||
- service_c-testhash
|
||||
extends:
|
||||
service: common_service-testhash
|
||||
configs:
|
||||
- source: service_a_config-testhash
|
||||
|
||||
service_b-testhash:
|
||||
image: service_b:latest
|
||||
networks:
|
||||
- net_b-testhash
|
||||
- net_a-testhash
|
||||
|
||||
service_c-testhash:
|
||||
image: service_c:latest
|
||||
networks:
|
||||
- net_b-testhash
|
||||
|
||||
data_volume-testhash:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
common_service-testhash:
|
||||
image: common:latest
|
||||
|
||||
networks:
|
||||
net_a-testhash:
|
||||
driver: bridge
|
||||
net_b-testhash:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
data_volume-testhash:
|
||||
driver: local
|
||||
|
||||
configs:
|
||||
service_a_config-testhash:
|
||||
file: ./service_a_config.yml
|
||||
|
||||
secrets:
|
||||
service_secret-testhash:
|
||||
file: ./service_secret.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all properties in compose file 3", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile3);
|
||||
});
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
plausible_db:
|
||||
image: postgres:16-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
plausible_events_db:
|
||||
image: clickhouse/clickhouse-server:24.3.3.102-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- event-data:/var/lib/clickhouse
|
||||
- event-logs:/var/log/clickhouse-server
|
||||
- ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
|
||||
- ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 262144
|
||||
hard: 262144
|
||||
|
||||
plausible:
|
||||
image: ghcr.io/plausible/community-edition:v2.1.0
|
||||
restart: always
|
||||
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
|
||||
depends_on:
|
||||
- plausible_db
|
||||
- plausible_events_db
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
||||
env_file:
|
||||
- plausible-conf.env
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
driver: local
|
||||
event-data:
|
||||
driver: local
|
||||
event-logs:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFile = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
plausible_db-testhash:
|
||||
image: postgres:16-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- db-data-testhash:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
plausible_events_db-testhash:
|
||||
image: clickhouse/clickhouse-server:24.3.3.102-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- event-data-testhash:/var/lib/clickhouse
|
||||
- event-logs-testhash:/var/log/clickhouse-server
|
||||
- ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
|
||||
- ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 262144
|
||||
hard: 262144
|
||||
|
||||
plausible-testhash:
|
||||
image: ghcr.io/plausible/community-edition:v2.1.0
|
||||
restart: always
|
||||
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
|
||||
depends_on:
|
||||
- plausible_db-testhash
|
||||
- plausible_events_db-testhash
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
||||
env_file:
|
||||
- plausible-conf.env
|
||||
|
||||
volumes:
|
||||
db-data-testhash:
|
||||
driver: local
|
||||
event-data-testhash:
|
||||
driver: local
|
||||
event-logs-testhash:
|
||||
driver: local
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all properties in Plausible compose file", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile);
|
||||
});
|
||||
178
__test__/compose/config/config-root.test.ts
Normal file
178
__test__/compose/config/config-root.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToConfigsRoot } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
configs:
|
||||
web-config:
|
||||
file: ./web-config.yml
|
||||
`;
|
||||
|
||||
test("Add prefix to configs in root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
|
||||
expect(configs).toBeDefined();
|
||||
for (const configKey of Object.keys(configs)) {
|
||||
expect(configKey).toContain(`-${prefix}`);
|
||||
expect(configs[configKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
const composeFileMultipleConfigs = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web-config
|
||||
target: /etc/nginx/nginx.conf
|
||||
- source: another-config
|
||||
target: /etc/nginx/another.conf
|
||||
|
||||
configs:
|
||||
web-config:
|
||||
file: ./web-config.yml
|
||||
another-config:
|
||||
file: ./another-config.yml
|
||||
`;
|
||||
|
||||
test("Add prefix to multiple configs in root property", () => {
|
||||
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
|
||||
expect(configs).toBeDefined();
|
||||
for (const configKey of Object.keys(configs)) {
|
||||
expect(configKey).toContain(`-${prefix}`);
|
||||
expect(configs[configKey]).toBeDefined();
|
||||
}
|
||||
expect(configs).toHaveProperty(`web-config-${prefix}`);
|
||||
expect(configs).toHaveProperty(`another-config-${prefix}`);
|
||||
});
|
||||
|
||||
const composeFileDifferentProperties = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
configs:
|
||||
web-config:
|
||||
file: ./web-config.yml
|
||||
special-config:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add prefix to configs with different properties in root property", () => {
|
||||
const composeData = load(
|
||||
composeFileDifferentProperties,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
|
||||
expect(configs).toBeDefined();
|
||||
for (const configKey of Object.keys(configs)) {
|
||||
expect(configKey).toContain(`-${prefix}`);
|
||||
expect(configs[configKey]).toBeDefined();
|
||||
}
|
||||
expect(configs).toHaveProperty(`web-config-${prefix}`);
|
||||
expect(configs).toHaveProperty(`special-config-${prefix}`);
|
||||
});
|
||||
|
||||
const composeFileConfigRoot = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
app:
|
||||
image: node:latest
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
configs:
|
||||
web_config:
|
||||
file: ./web-config.yml
|
||||
|
||||
app_config:
|
||||
file: ./app-config.json
|
||||
|
||||
db_config:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFileConfigRoot = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
app:
|
||||
image: node:latest
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
configs:
|
||||
web_config-testhash:
|
||||
file: ./web-config.yml
|
||||
|
||||
app_config-testhash:
|
||||
file: ./app-config.json
|
||||
|
||||
db_config-testhash:
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to configs in root property", () => {
|
||||
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
const updatedComposeData = { ...composeData, configs };
|
||||
|
||||
// Verificar que el resultado coincide con el archivo esperado
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileConfigRoot);
|
||||
});
|
||||
197
__test__/compose/config/config-service.test.ts
Normal file
197
__test__/compose/config/config-service.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToConfigsInServices } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web-config
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
configs:
|
||||
web-config:
|
||||
file: ./web-config.yml
|
||||
`;
|
||||
|
||||
test("Add prefix to configs in services", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToConfigsInServices(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.web?.configs).toContainEqual({
|
||||
source: `web-config-${prefix}`,
|
||||
target: "/etc/nginx/nginx.conf",
|
||||
});
|
||||
});
|
||||
|
||||
const composeFileSingleServiceConfig = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web-config
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
configs:
|
||||
web-config:
|
||||
file: ./web-config.yml
|
||||
`;
|
||||
|
||||
test("Add prefix to configs in services with single config", () => {
|
||||
const composeData = load(
|
||||
composeFileSingleServiceConfig,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToConfigsInServices(composeData.services, prefix);
|
||||
|
||||
expect(services).toBeDefined();
|
||||
for (const serviceKey of Object.keys(services)) {
|
||||
const serviceConfigs = services?.[serviceKey]?.configs;
|
||||
if (serviceConfigs) {
|
||||
for (const config of serviceConfigs) {
|
||||
if (typeof config === "object") {
|
||||
expect(config.source).toContain(`-${prefix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const composeFileMultipleServicesConfigs = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web-config
|
||||
target: /etc/nginx/nginx.conf
|
||||
- source: common-config
|
||||
target: /etc/nginx/common.conf
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
configs:
|
||||
- source: app-config
|
||||
target: /usr/src/app/config.json
|
||||
- source: common-config
|
||||
target: /usr/src/app/common.json
|
||||
|
||||
configs:
|
||||
web-config:
|
||||
file: ./web-config.yml
|
||||
app-config:
|
||||
file: ./app-config.json
|
||||
common-config:
|
||||
file: ./common-config.yml
|
||||
`;
|
||||
|
||||
test("Add prefix to configs in services with multiple configs", () => {
|
||||
const composeData = load(
|
||||
composeFileMultipleServicesConfigs,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToConfigsInServices(composeData.services, prefix);
|
||||
|
||||
expect(services).toBeDefined();
|
||||
for (const serviceKey of Object.keys(services)) {
|
||||
const serviceConfigs = services?.[serviceKey]?.configs;
|
||||
if (serviceConfigs) {
|
||||
for (const config of serviceConfigs) {
|
||||
if (typeof config === "object") {
|
||||
expect(config.source).toContain(`-${prefix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const composeFileConfigServices = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:latest
|
||||
configs:
|
||||
- source: app_config
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
configs:
|
||||
- source: db_config
|
||||
target: /etc/postgresql/postgresql.conf
|
||||
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFileConfigServices = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config-testhash
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:latest
|
||||
configs:
|
||||
- source: app_config-testhash
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
configs:
|
||||
- source: db_config-testhash
|
||||
target: /etc/postgresql/postgresql.conf
|
||||
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to configs in services", () => {
|
||||
const composeData = load(composeFileConfigServices) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToConfigsInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileConfigServices);
|
||||
});
|
||||
249
__test__/compose/config/config.test.ts
Normal file
249
__test__/compose/config/config.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addPrefixToAllConfigs,
|
||||
addPrefixToConfigsRoot,
|
||||
} from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFileCombinedConfigs = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
configs:
|
||||
- source: app_config
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
configs:
|
||||
- source: db_config
|
||||
target: /etc/postgresql/postgresql.conf
|
||||
|
||||
configs:
|
||||
web_config:
|
||||
file: ./web-config.yml
|
||||
|
||||
app_config:
|
||||
file: ./app-config.json
|
||||
|
||||
db_config:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedConfigs = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config-testhash
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
configs:
|
||||
- source: app_config-testhash
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
configs:
|
||||
- source: db_config-testhash
|
||||
target: /etc/postgresql/postgresql.conf
|
||||
|
||||
configs:
|
||||
web_config-testhash:
|
||||
file: ./web-config.yml
|
||||
|
||||
app_config-testhash:
|
||||
file: ./app-config.json
|
||||
|
||||
db_config-testhash:
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all configs in root and services", () => {
|
||||
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedConfigs);
|
||||
});
|
||||
|
||||
const composeFileWithEnvAndExternal = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config
|
||||
target: /etc/nginx/nginx.conf
|
||||
environment:
|
||||
- NGINX_CONFIG=/etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
configs:
|
||||
- source: app_config
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
configs:
|
||||
- source: db_config
|
||||
target: /etc/postgresql/postgresql.conf
|
||||
|
||||
configs:
|
||||
web_config:
|
||||
external: true
|
||||
|
||||
app_config:
|
||||
file: ./app-config.json
|
||||
|
||||
db_config:
|
||||
environment: dev
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileWithEnvAndExternal = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config-testhash
|
||||
target: /etc/nginx/nginx.conf
|
||||
environment:
|
||||
- NGINX_CONFIG=/etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
configs:
|
||||
- source: app_config-testhash
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
configs:
|
||||
- source: db_config-testhash
|
||||
target: /etc/postgresql/postgresql.conf
|
||||
|
||||
configs:
|
||||
web_config-testhash:
|
||||
external: true
|
||||
|
||||
app_config-testhash:
|
||||
file: ./app-config.json
|
||||
|
||||
db_config-testhash:
|
||||
environment: dev
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to configs with environment and external", () => {
|
||||
const composeData = load(
|
||||
composeFileWithEnvAndExternal,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileWithEnvAndExternal);
|
||||
});
|
||||
|
||||
const composeFileWithTemplateDriverAndLabels = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
configs:
|
||||
- source: app_config
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
configs:
|
||||
web_config:
|
||||
file: ./web-config.yml
|
||||
template_driver: golang
|
||||
|
||||
app_config:
|
||||
file: ./app-config.json
|
||||
labels:
|
||||
- app=frontend
|
||||
|
||||
db_config:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileWithTemplateDriverAndLabels = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
configs:
|
||||
- source: web_config-testhash
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
configs:
|
||||
- source: app_config-testhash
|
||||
target: /usr/src/app/config.json
|
||||
|
||||
configs:
|
||||
web_config-testhash:
|
||||
file: ./web-config.yml
|
||||
template_driver: golang
|
||||
|
||||
app_config-testhash:
|
||||
file: ./app-config.json
|
||||
labels:
|
||||
- app=frontend
|
||||
|
||||
db_config-testhash:
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to configs with template driver and labels", () => {
|
||||
const composeData = load(
|
||||
composeFileWithTemplateDriverAndLabels,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(
|
||||
expectedComposeFileWithTemplateDriverAndLabels,
|
||||
);
|
||||
});
|
||||
281
__test__/compose/network/network-root.test.ts
Normal file
281
__test__/compose/network/network-root.test.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.driver.mtu: 1200
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
external_network:
|
||||
external: true
|
||||
|
||||
`;
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
test("Add prefix to networks root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const volumeKey of Object.keys(networks)) {
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: myapp:latest
|
||||
networks:
|
||||
- app_net
|
||||
|
||||
networks:
|
||||
app_net:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.driver.mtu: 1500
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
|
||||
database_net:
|
||||
driver: overlay
|
||||
attachable: true
|
||||
|
||||
monitoring_net:
|
||||
driver: bridge
|
||||
internal: true
|
||||
`;
|
||||
|
||||
test("Add prefix to advanced networks root property (2 TRY)", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
external:
|
||||
name: my_external_network
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
labels:
|
||||
- "com.example.description=Backend network"
|
||||
- "com.example.environment=production"
|
||||
|
||||
external_network:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add prefix to networks with external properties", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile4 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:13
|
||||
networks:
|
||||
- db_net
|
||||
|
||||
networks:
|
||||
db_net:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 192.168.1.0/24
|
||||
- gateway: 192.168.1.1
|
||||
- aux_addresses:
|
||||
host1: 192.168.1.2
|
||||
host2: 192.168.1.3
|
||||
|
||||
external_network:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add prefix to networks with IPAM configurations", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile5 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
- api_net
|
||||
|
||||
networks:
|
||||
api_net:
|
||||
driver: bridge
|
||||
options:
|
||||
com.docker.network.bridge.name: br0
|
||||
enable_ipv6: true
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: "2001:db8:1::/64"
|
||||
- gateway: "2001:db8:1::1"
|
||||
|
||||
external_network:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add prefix to networks with custom options", () => {
|
||||
const composeData = load(composeFile5) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile6 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.driver.mtu: 1200
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
external_network:
|
||||
external: true
|
||||
`;
|
||||
|
||||
// Expected compose file with static prefix `testhash`
|
||||
const expectedComposeFile6 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend-testhash
|
||||
|
||||
networks:
|
||||
frontend-testhash:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.driver.mtu: 1200
|
||||
|
||||
backend-testhash:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
external_network-testhash:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add prefix to networks with static prefix", () => {
|
||||
const composeData = load(composeFile6) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
const expectedComposeData = load(
|
||||
expectedComposeFile6,
|
||||
) as ComposeSpecification;
|
||||
expect(networks).toStrictEqual(expectedComposeData.networks);
|
||||
});
|
||||
181
__test__/compose/network/network-service.test.ts
Normal file
181
__test__/compose/network/network-service.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNetworks } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
- backend
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData?.services?.web?.networks).toContain(
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
|
||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
|
||||
const apiNetworks = actualComposeData?.services?.api?.networks;
|
||||
|
||||
expect(apiNetworks).toBeDefined();
|
||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services with aliases", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.api?.networks).toHaveProperty(
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
|
||||
const networkConfig =
|
||||
actualComposeData?.services?.api?.networks[`frontend-${prefix}`];
|
||||
expect(networkConfig).toBeDefined();
|
||||
expect(networkConfig?.aliases).toContain("api");
|
||||
|
||||
expect(actualComposeData.services?.api?.networks).not.toHaveProperty(
|
||||
"frontend-ash",
|
||||
);
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend:
|
||||
|
||||
networks:
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services (Object with simple networks)", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.redis?.networks).toHaveProperty(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
const composeFileCombined = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend:
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services (combined case)", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
// Caso 1: ListOfStrings
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const apiNetworks = actualComposeData.services?.api?.networks;
|
||||
expect(apiNetworks).toHaveProperty(`frontend-${prefix}`);
|
||||
expect(apiNetworks[`frontend-${prefix}`]).toBeDefined();
|
||||
expect(apiNetworks).not.toHaveProperty("frontend");
|
||||
|
||||
// Caso 3: Objeto con redes simples
|
||||
const redisNetworks = actualComposeData.services?.redis?.networks;
|
||||
expect(redisNetworks).toHaveProperty(`backend-${prefix}`);
|
||||
expect(redisNetworks).not.toHaveProperty("backend");
|
||||
});
|
||||
254
__test__/compose/network/network.test.ts
Normal file
254
__test__/compose/network/network.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addPrefixToAllNetworks,
|
||||
addPrefixToServiceNetworks,
|
||||
} from "@/server/utils/docker/compose/network";
|
||||
import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFileCombined = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend:
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services and root (combined case)", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
// Prefijo para redes definidas en el root
|
||||
if (composeData.networks) {
|
||||
composeData.networks = addPrefixToNetworksRoot(
|
||||
composeData.networks,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
// Prefijo para redes definidas en los servicios
|
||||
if (composeData.services) {
|
||||
composeData.services = addPrefixToServiceNetworks(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
const actualComposeData = { ...composeData };
|
||||
|
||||
// Verificar redes en root
|
||||
expect(actualComposeData.networks).toHaveProperty(`frontend-${prefix}`);
|
||||
expect(actualComposeData.networks).toHaveProperty(`backend-${prefix}`);
|
||||
expect(actualComposeData.networks).not.toHaveProperty("frontend");
|
||||
expect(actualComposeData.networks).not.toHaveProperty("backend");
|
||||
|
||||
// Caso 1: ListOfStrings
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const apiNetworks = actualComposeData.services?.api?.networks;
|
||||
expect(apiNetworks).toHaveProperty(`frontend-${prefix}`);
|
||||
expect(apiNetworks[`frontend-${prefix}`]?.aliases).toContain("api");
|
||||
expect(apiNetworks).not.toHaveProperty("frontend");
|
||||
|
||||
// Caso 3: Objeto con redes simples
|
||||
const redisNetworks = actualComposeData.services?.redis?.networks;
|
||||
expect(redisNetworks).toHaveProperty(`backend-${prefix}`);
|
||||
expect(redisNetworks).not.toHaveProperty("backend");
|
||||
});
|
||||
|
||||
const expectedComposeFile = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend-testhash
|
||||
- backend-testhash
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend-testhash:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend-testhash:
|
||||
|
||||
networks:
|
||||
frontend-testhash:
|
||||
driver: bridge
|
||||
|
||||
backend-testhash:
|
||||
driver: bridge
|
||||
`);
|
||||
|
||||
test("Add prefix to networks in compose file", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile);
|
||||
});
|
||||
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
networks:
|
||||
backend:
|
||||
aliases:
|
||||
- db
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
external: true
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend-testhash
|
||||
- backend-testhash
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
networks:
|
||||
backend-testhash:
|
||||
aliases:
|
||||
- db
|
||||
|
||||
networks:
|
||||
frontend-testhash:
|
||||
external: true
|
||||
|
||||
backend-testhash:
|
||||
driver: bridge
|
||||
`);
|
||||
|
||||
test("Add prefix to networks in compose file with external and internal networks", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile2);
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: myapp:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- app
|
||||
backend:
|
||||
|
||||
worker:
|
||||
image: worker:latest
|
||||
networks:
|
||||
- backend
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.bridge.enable_icc: "true"
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: myapp:latest
|
||||
networks:
|
||||
frontend-testhash:
|
||||
aliases:
|
||||
- app
|
||||
backend-testhash:
|
||||
|
||||
worker:
|
||||
image: worker:latest
|
||||
networks:
|
||||
- backend-testhash
|
||||
|
||||
networks:
|
||||
frontend-testhash:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
backend-testhash:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.bridge.enable_icc: "true"
|
||||
`);
|
||||
|
||||
test("Add prefix to networks in compose file with multiple services and complex network configurations", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile3);
|
||||
});
|
||||
103
__test__/compose/secrets/secret-root.test.ts
Normal file
103
__test__/compose/secrets/secret-root.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { load, dump } from "js-yaml";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { addPrefixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFileSecretsRoot = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
test("Add prefix to secrets in root property", () => {
|
||||
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
return;
|
||||
}
|
||||
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
|
||||
expect(secrets).toBeDefined();
|
||||
if (secrets) {
|
||||
for (const secretKey of Object.keys(secrets)) {
|
||||
expect(secretKey).toContain(`-${prefix}`);
|
||||
expect(secrets[secretKey]).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const composeFileSecretsRoot1 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
secrets:
|
||||
api_key:
|
||||
file: ./api_key.txt
|
||||
`;
|
||||
|
||||
test("Add prefix to secrets in root property (Test 1)", () => {
|
||||
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification;
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
return;
|
||||
}
|
||||
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
|
||||
expect(secrets).toBeDefined();
|
||||
|
||||
if (secrets) {
|
||||
for (const secretKey of Object.keys(secrets)) {
|
||||
expect(secretKey).toContain(`-${prefix}`);
|
||||
expect(secrets[secretKey]).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const composeFileSecretsRoot2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: nginx:latest
|
||||
|
||||
secrets:
|
||||
frontend_secret:
|
||||
file: ./frontend_secret.txt
|
||||
db_password:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add prefix to secrets in root property (Test 2)", () => {
|
||||
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification;
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
return;
|
||||
}
|
||||
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
|
||||
expect(secrets).toBeDefined();
|
||||
|
||||
if (secrets) {
|
||||
for (const secretKey of Object.keys(secrets)) {
|
||||
expect(secretKey).toContain(`-${prefix}`);
|
||||
expect(secrets[secretKey]).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
113
__test__/compose/secrets/secret-services.test.ts
Normal file
113
__test__/compose/secrets/secret-services.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFileSecretsServices = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:latest
|
||||
secrets:
|
||||
- db_password
|
||||
|
||||
secrets:
|
||||
db_password:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
test("Add prefix to secrets in services", () => {
|
||||
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addPrefixToSecretsInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.db?.secrets).toContain(
|
||||
`db_password-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
const composeFileSecretsServices1 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: node:14
|
||||
secrets:
|
||||
- app_secret
|
||||
|
||||
secrets:
|
||||
app_secret:
|
||||
file: ./app_secret.txt
|
||||
`;
|
||||
|
||||
test("Add prefix to secrets in services (Test 1)", () => {
|
||||
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addPrefixToSecretsInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.app?.secrets).toContain(
|
||||
`app_secret-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
const composeFileSecretsServices2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
backend:
|
||||
image: backend:latest
|
||||
secrets:
|
||||
- backend_secret
|
||||
frontend:
|
||||
image: frontend:latest
|
||||
secrets:
|
||||
- frontend_secret
|
||||
|
||||
secrets:
|
||||
backend_secret:
|
||||
file: ./backend_secret.txt
|
||||
frontend_secret:
|
||||
file: ./frontend_secret.txt
|
||||
`;
|
||||
|
||||
test("Add prefix to secrets in services (Test 2)", () => {
|
||||
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addPrefixToSecretsInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.backend?.secrets).toContain(
|
||||
`backend_secret-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.frontend?.secrets).toContain(
|
||||
`frontend_secret-${prefix}`,
|
||||
);
|
||||
});
|
||||
159
__test__/compose/secrets/secret.test.ts
Normal file
159
__test__/compose/secrets/secret.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { addPrefixToAllSecrets } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFileCombinedSecrets = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
secrets:
|
||||
- web_secret
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
secrets:
|
||||
- app_secret
|
||||
|
||||
secrets:
|
||||
web_secret:
|
||||
file: ./web_secret.txt
|
||||
|
||||
app_secret:
|
||||
file: ./app_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
secrets:
|
||||
- web_secret-testhash
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
secrets:
|
||||
- app_secret-testhash
|
||||
|
||||
secrets:
|
||||
web_secret-testhash:
|
||||
file: ./web_secret.txt
|
||||
|
||||
app_secret-testhash:
|
||||
file: ./app_secret.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all secrets", () => {
|
||||
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets);
|
||||
});
|
||||
|
||||
const composeFileCombinedSecrets3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
image: myapi:latest
|
||||
secrets:
|
||||
- api_key
|
||||
|
||||
cache:
|
||||
image: redis:latest
|
||||
secrets:
|
||||
- cache_secret
|
||||
|
||||
secrets:
|
||||
api_key:
|
||||
file: ./api_key.txt
|
||||
cache_secret:
|
||||
file: ./cache_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
image: myapi:latest
|
||||
secrets:
|
||||
- api_key-testhash
|
||||
|
||||
cache:
|
||||
image: redis:latest
|
||||
secrets:
|
||||
- cache_secret-testhash
|
||||
|
||||
secrets:
|
||||
api_key-testhash:
|
||||
file: ./api_key.txt
|
||||
cache_secret-testhash:
|
||||
file: ./cache_secret.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all secrets (3rd Case)", () => {
|
||||
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets3);
|
||||
});
|
||||
|
||||
const composeFileCombinedSecrets4 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
secrets:
|
||||
- web_secret
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
secrets:
|
||||
- db_password
|
||||
|
||||
secrets:
|
||||
web_secret:
|
||||
file: ./web_secret.txt
|
||||
db_password:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets4 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
secrets:
|
||||
- web_secret-testhash
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
secrets:
|
||||
- db_password-testhash
|
||||
|
||||
secrets:
|
||||
web_secret-testhash:
|
||||
file: ./web_secret.txt
|
||||
db_password-testhash:
|
||||
file: ./db_password.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all secrets (4th Case)", () => {
|
||||
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets4);
|
||||
});
|
||||
59
__test__/compose/service/service-container-name.test.ts
Normal file
59
__test__/compose/service/service-container-name.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
container_name: web_container
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
test("Add prefix to service names with container_name in compose file", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que el nombre del contenedor ha cambiado correctamente
|
||||
expect(actualComposeData.services[`web-${prefix}`].container_name).toBe(
|
||||
`web_container-${prefix}`,
|
||||
);
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
150
__test__/compose/service/service-depends-on.test.ts
Normal file
150
__test__/compose/service/service-depends-on.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFile4 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
depends_on:
|
||||
- db
|
||||
- api
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to service names with depends_on (array) in compose file", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en depends_on tienen el prefijo
|
||||
expect(actualComposeData.services[`web-${prefix}`].depends_on).toContain(
|
||||
`db-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services[`web-${prefix}`].depends_on).toContain(
|
||||
`api-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que los servicios `db` y `api` también tienen el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("db");
|
||||
expect(actualComposeData.services[`db-${prefix}`].image).toBe(
|
||||
"postgres:latest",
|
||||
);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
|
||||
const composeFile5 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
api:
|
||||
condition: service_started
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to service names with depends_on (object) in compose file", () => {
|
||||
const composeData = load(composeFile5) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en depends_on tienen el prefijo
|
||||
const webDependsOn = actualComposeData.services[`web-${prefix}`]
|
||||
.depends_on as Record<string, any>;
|
||||
expect(webDependsOn).toHaveProperty(`db-${prefix}`);
|
||||
expect(webDependsOn).toHaveProperty(`api-${prefix}`);
|
||||
expect(webDependsOn[`db-${prefix}`].condition).toBe("service_healthy");
|
||||
expect(webDependsOn[`api-${prefix}`].condition).toBe("service_started");
|
||||
|
||||
// Verificar que los servicios `db` y `api` también tienen el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("db");
|
||||
expect(actualComposeData.services[`db-${prefix}`].image).toBe(
|
||||
"postgres:latest",
|
||||
);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
131
__test__/compose/service/service-extends.test.ts
Normal file
131
__test__/compose/service/service-extends.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFile6 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
extends: base_service
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
base_service:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to service names with extends (string) in compose file", () => {
|
||||
const composeData = load(composeFile6) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que el nombre en extends tiene el prefijo
|
||||
expect(actualComposeData.services[`web-${prefix}`].extends).toBe(
|
||||
`base_service-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que el servicio `base_service` también tiene el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("base_service");
|
||||
expect(actualComposeData.services[`base_service-${prefix}`].image).toBe(
|
||||
"base:latest",
|
||||
);
|
||||
});
|
||||
|
||||
const composeFile7 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
extends:
|
||||
service: base_service
|
||||
file: docker-compose.base.yml
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
base_service:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to service names with extends (object) in compose file", () => {
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que el nombre en extends.service tiene el prefijo
|
||||
const webExtends = actualComposeData.services[`web-${prefix}`].extends;
|
||||
if (typeof webExtends !== "string") {
|
||||
expect(webExtends.service).toBe(`base_service-${prefix}`);
|
||||
}
|
||||
|
||||
// Verificar que el servicio `base_service` también tiene el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("base_service");
|
||||
expect(actualComposeData.services[`base_service-${prefix}`].image).toBe(
|
||||
"base:latest",
|
||||
);
|
||||
});
|
||||
76
__test__/compose/service/service-links.test.ts
Normal file
76
__test__/compose/service/service-links.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
links:
|
||||
- db
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to service names with links in compose file", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en links tienen el prefijo
|
||||
expect(actualComposeData.services[`web-${prefix}`].links).toContain(
|
||||
`db-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que los servicios `db` y `api` también tienen el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("db");
|
||||
expect(actualComposeData.services[`db-${prefix}`].image).toBe(
|
||||
"postgres:latest",
|
||||
);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
49
__test__/compose/service/service-names.test.ts
Normal file
49
__test__/compose/service/service-names.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to service names in compose file", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que los nombres de los servicios han cambiado correctamente
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
// Verificar que las claves originales no existen
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
});
|
||||
375
__test__/compose/service/service.test.ts
Normal file
375
__test__/compose/service/service.test.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import {
|
||||
addPrefixToAllServiceNames,
|
||||
addPrefixToServiceNames,
|
||||
} from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFileCombinedAllCases = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
container_name: web_container
|
||||
links:
|
||||
- api
|
||||
depends_on:
|
||||
- api
|
||||
extends: base_service
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes_from:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
base_service:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web-testhash:
|
||||
image: nginx:latest
|
||||
container_name: web_container-testhash
|
||||
links:
|
||||
- api-testhash
|
||||
depends_on:
|
||||
- api-testhash
|
||||
extends: base_service-testhash
|
||||
|
||||
api-testhash:
|
||||
image: myapi:latest
|
||||
depends_on:
|
||||
db-testhash:
|
||||
condition: service_healthy
|
||||
volumes_from:
|
||||
- db-testhash
|
||||
|
||||
db-testhash:
|
||||
image: postgres:latest
|
||||
|
||||
base_service-testhash:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`);
|
||||
|
||||
test("Add prefix to all service names in compose file", () => {
|
||||
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFile);
|
||||
});
|
||||
|
||||
const composeFile1 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
container_name: web_container
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- frontend
|
||||
volumes_from:
|
||||
- data
|
||||
links:
|
||||
- db
|
||||
extends:
|
||||
service: base_service
|
||||
|
||||
app:
|
||||
image: node:14
|
||||
networks:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
db:
|
||||
image: postgres:13
|
||||
networks:
|
||||
- backend
|
||||
|
||||
data:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
base_service:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile1 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web-testhash:
|
||||
image: nginx:latest
|
||||
container_name: web_container-testhash
|
||||
depends_on:
|
||||
- app-testhash
|
||||
networks:
|
||||
- frontend
|
||||
volumes_from:
|
||||
- data-testhash
|
||||
links:
|
||||
- db-testhash
|
||||
extends:
|
||||
service: base_service-testhash
|
||||
|
||||
app-testhash:
|
||||
image: node:14
|
||||
networks:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
db-testhash:
|
||||
image: postgres:13
|
||||
networks:
|
||||
- backend
|
||||
|
||||
data-testhash:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
base_service-testhash:
|
||||
image: base:latest
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
backend:
|
||||
driver: bridge
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all service names in compose file 1", () => {
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile1);
|
||||
});
|
||||
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: nginx:latest
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- public
|
||||
volumes_from:
|
||||
- logs
|
||||
links:
|
||||
- cache
|
||||
extends:
|
||||
service: shared_service
|
||||
|
||||
backend:
|
||||
image: node:14
|
||||
networks:
|
||||
- private
|
||||
- public
|
||||
|
||||
cache:
|
||||
image: redis:latest
|
||||
networks:
|
||||
- private
|
||||
|
||||
logs:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /logs
|
||||
|
||||
shared_service:
|
||||
image: shared:latest
|
||||
|
||||
networks:
|
||||
public:
|
||||
driver: bridge
|
||||
private:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
frontend-testhash:
|
||||
image: nginx:latest
|
||||
depends_on:
|
||||
- backend-testhash
|
||||
networks:
|
||||
- public
|
||||
volumes_from:
|
||||
- logs-testhash
|
||||
links:
|
||||
- cache-testhash
|
||||
extends:
|
||||
service: shared_service-testhash
|
||||
|
||||
backend-testhash:
|
||||
image: node:14
|
||||
networks:
|
||||
- private
|
||||
- public
|
||||
|
||||
cache-testhash:
|
||||
image: redis:latest
|
||||
networks:
|
||||
- private
|
||||
|
||||
logs-testhash:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /logs
|
||||
|
||||
shared_service-testhash:
|
||||
image: shared:latest
|
||||
|
||||
networks:
|
||||
public:
|
||||
driver: bridge
|
||||
private:
|
||||
driver: bridge
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all service names in compose file 2", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile2);
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
service_a:
|
||||
image: service_a:latest
|
||||
depends_on:
|
||||
- service_b
|
||||
networks:
|
||||
- net_a
|
||||
volumes_from:
|
||||
- data_volume
|
||||
links:
|
||||
- service_c
|
||||
extends:
|
||||
service: common_service
|
||||
|
||||
service_b:
|
||||
image: service_b:latest
|
||||
networks:
|
||||
- net_b
|
||||
- net_a
|
||||
|
||||
service_c:
|
||||
image: service_c:latest
|
||||
networks:
|
||||
- net_b
|
||||
|
||||
data_volume:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
common_service:
|
||||
image: common:latest
|
||||
|
||||
networks:
|
||||
net_a:
|
||||
driver: bridge
|
||||
net_b:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
service_a-testhash:
|
||||
image: service_a:latest
|
||||
depends_on:
|
||||
- service_b-testhash
|
||||
networks:
|
||||
- net_a
|
||||
volumes_from:
|
||||
- data_volume-testhash
|
||||
links:
|
||||
- service_c-testhash
|
||||
extends:
|
||||
service: common_service-testhash
|
||||
|
||||
service_b-testhash:
|
||||
image: service_b:latest
|
||||
networks:
|
||||
- net_b
|
||||
- net_a
|
||||
|
||||
service_c-testhash:
|
||||
image: service_c:latest
|
||||
networks:
|
||||
- net_b
|
||||
|
||||
data_volume-testhash:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
common_service-testhash:
|
||||
image: common:latest
|
||||
|
||||
networks:
|
||||
net_a:
|
||||
driver: bridge
|
||||
net_b:
|
||||
driver: bridge
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to all service names in compose file 3", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile3);
|
||||
});
|
||||
76
__test__/compose/service/sevice-volumes-from.test.ts
Normal file
76
__test__/compose/service/sevice-volumes-from.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
volumes_from:
|
||||
- shared
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
volumes_from:
|
||||
- shared
|
||||
|
||||
shared:
|
||||
image: busybox
|
||||
volumes:
|
||||
- /data
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to service names with volumes_from in compose file", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en volumes_from tienen el prefijo
|
||||
expect(actualComposeData.services[`web-${prefix}`].volumes_from).toContain(
|
||||
`shared-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services[`api-${prefix}`].volumes_from).toContain(
|
||||
`shared-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que el servicio shared también tiene el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`shared-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("shared");
|
||||
expect(actualComposeData.services[`shared-${prefix}`].image).toBe("busybox");
|
||||
});
|
||||
1120
__test__/compose/volume/volume-2.test.ts
Normal file
1120
__test__/compose/volume/volume-2.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
195
__test__/compose/volume/volume-root.test.ts
Normal file
195
__test__/compose/volume/volume-root.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToVolumesRoot } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- web_data:/var/lib/nginx/data
|
||||
|
||||
volumes:
|
||||
web_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
test("Add prefix to volumes in root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
expect(volumes).toBeDefined();
|
||||
for (const volumeKey of Object.keys(volumes)) {
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
expect(volumes[volumeKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: node:latest
|
||||
volumes:
|
||||
- app_data:/var/lib/app/data
|
||||
|
||||
volumes:
|
||||
app_data:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: addr=10.0.0.1,rw
|
||||
device: ":/exported/path"
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to volumes in root property (Case 2)", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
expect(volumes).toBeDefined();
|
||||
for (const volumeKey of Object.keys(volumes)) {
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
expect(volumes[volumeKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
external: true
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to volumes in root property (Case 3)", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
|
||||
expect(volumes).toBeDefined();
|
||||
for (const volumeKey of Object.keys(volumes)) {
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
expect(volumes[volumeKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
const composeFile4 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
app:
|
||||
image: node:latest
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
volumes:
|
||||
web_data:
|
||||
driver: local
|
||||
|
||||
app_data:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: addr=10.0.0.1,rw
|
||||
device: ":/exported/path"
|
||||
|
||||
db_data:
|
||||
external: true
|
||||
|
||||
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFile4 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
|
||||
app:
|
||||
image: node:latest
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
|
||||
volumes:
|
||||
web_data-testhash:
|
||||
driver: local
|
||||
|
||||
app_data-testhash:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: addr=10.0.0.1,rw
|
||||
device: ":/exported/path"
|
||||
|
||||
db_data-testhash:
|
||||
external: true
|
||||
|
||||
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to volumes in root property", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
const updatedComposeData = { ...composeData, volumes };
|
||||
|
||||
// Verificar que el resultado coincide con el archivo esperado
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile4);
|
||||
});
|
||||
81
__test__/compose/volume/volume-services.test.ts
Normal file
81
__test__/compose/volume/volume-services.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToVolumesInServices } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
const composeFile1 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
`;
|
||||
|
||||
test("Add prefix to volumes declared directly in services", () => {
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addPrefixToVolumesInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
expect(actualComposeData.services?.db?.volumes).toContain(
|
||||
`db_data-${prefix}:/var/lib/postgresql/data`,
|
||||
);
|
||||
});
|
||||
|
||||
const composeFileTypeVolume = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- type: volume
|
||||
source: db-test
|
||||
target: /var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
db-test:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
test("Add prefix to volumes declared directly in services (Case 2)", () => {
|
||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addPrefixToVolumesInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.db?.volumes).toEqual([
|
||||
{
|
||||
type: "volume",
|
||||
source: `db-test-${prefix}`,
|
||||
target: "/var/lib/postgresql/data",
|
||||
},
|
||||
]);
|
||||
});
|
||||
288
__test__/compose/volume/volume.test.ts
Normal file
288
__test__/compose/volume/volume.test.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addPrefixToAllVolumes,
|
||||
addPrefixToVolumesInServices,
|
||||
} from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFileTypeVolume = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db1:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- "db-test:/var/lib/postgresql/data"
|
||||
db2:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- type: volume
|
||||
source: db-test
|
||||
target: /var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
db-test:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db1:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- "db-test-testhash:/var/lib/postgresql/data"
|
||||
db2:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- type: volume
|
||||
source: db-test-testhash
|
||||
target: /var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
db-test-testhash:
|
||||
driver: local
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to volumes with type: volume in services", () => {
|
||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume);
|
||||
});
|
||||
|
||||
const composeFileTypeVolume1 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- "web-data:/var/www/html"
|
||||
- type: volume
|
||||
source: web-logs
|
||||
target: /var/log/nginx
|
||||
|
||||
volumes:
|
||||
web-data:
|
||||
driver: local
|
||||
web-logs:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume1 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- "web-data-testhash:/var/www/html"
|
||||
- type: volume
|
||||
source: web-logs-testhash
|
||||
target: /var/log/nginx
|
||||
|
||||
volumes:
|
||||
web-data-testhash:
|
||||
driver: local
|
||||
web-logs-testhash:
|
||||
driver: local
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to mixed volumes in services", () => {
|
||||
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume1);
|
||||
});
|
||||
|
||||
const composeFileTypeVolume2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: node:latest
|
||||
volumes:
|
||||
- "app-data:/usr/src/app"
|
||||
- type: volume
|
||||
source: app-logs
|
||||
target: /var/log/app
|
||||
volume:
|
||||
nocopy: true
|
||||
|
||||
volumes:
|
||||
app-data:
|
||||
driver: local
|
||||
app-logs:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/app/logs
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: node:latest
|
||||
volumes:
|
||||
- "app-data-testhash:/usr/src/app"
|
||||
- type: volume
|
||||
source: app-logs-testhash
|
||||
target: /var/log/app
|
||||
volume:
|
||||
nocopy: true
|
||||
|
||||
volumes:
|
||||
app-data-testhash:
|
||||
driver: local
|
||||
app-logs-testhash:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/app/logs
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to complex volume configurations in services", () => {
|
||||
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume2);
|
||||
});
|
||||
|
||||
const composeFileTypeVolume3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- "web-data:/usr/share/nginx/html"
|
||||
- type: volume
|
||||
source: web-logs
|
||||
target: /var/log/nginx
|
||||
volume:
|
||||
nocopy: true
|
||||
|
||||
api:
|
||||
image: node:latest
|
||||
volumes:
|
||||
- "api-data:/usr/src/app"
|
||||
- type: volume
|
||||
source: api-logs
|
||||
target: /var/log/app
|
||||
volume:
|
||||
nocopy: true
|
||||
- type: volume
|
||||
source: shared-logs
|
||||
target: /shared/logs
|
||||
|
||||
volumes:
|
||||
web-data:
|
||||
driver: local
|
||||
web-logs:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/web/logs
|
||||
|
||||
api-data:
|
||||
driver: local
|
||||
api-logs:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/api/logs
|
||||
|
||||
shared-logs:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/shared/logs
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- "web-data-testhash:/usr/share/nginx/html"
|
||||
- type: volume
|
||||
source: web-logs-testhash
|
||||
target: /var/log/nginx
|
||||
volume:
|
||||
nocopy: true
|
||||
|
||||
api:
|
||||
image: node:latest
|
||||
volumes:
|
||||
- "api-data-testhash:/usr/src/app"
|
||||
- type: volume
|
||||
source: api-logs-testhash
|
||||
target: /var/log/app
|
||||
volume:
|
||||
nocopy: true
|
||||
- type: volume
|
||||
source: shared-logs-testhash
|
||||
target: /shared/logs
|
||||
|
||||
volumes:
|
||||
web-data-testhash:
|
||||
driver: local
|
||||
web-logs-testhash:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/web/logs
|
||||
|
||||
api-data-testhash:
|
||||
driver: local
|
||||
api-logs-testhash:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/api/logs
|
||||
|
||||
shared-logs-testhash:
|
||||
driver: local
|
||||
driver_opts:
|
||||
o: bind
|
||||
type: none
|
||||
device: /path/to/shared/logs
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add prefix to complex nested volumes configuration in services", () => {
|
||||
const composeData = load(composeFileTypeVolume3) as ComposeSpecification;
|
||||
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume3);
|
||||
});
|
||||
16
__test__/vitest.config.ts
Normal file
16
__test__/vitest.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
root: "./",
|
||||
projects: ["tsconfig.json"],
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||
pool: "forks",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,764 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { HelpCircle, Settings } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const HealthCheckSwarmSchema = z
|
||||
.object({
|
||||
Test: z.array(z.string()).optional(),
|
||||
Interval: z.number().optional(),
|
||||
Timeout: z.number().optional(),
|
||||
StartPeriod: z.number().optional(),
|
||||
Retries: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const RestartPolicySwarmSchema = z
|
||||
.object({
|
||||
Condition: z.string().optional(),
|
||||
Delay: z.number().optional(),
|
||||
MaxAttempts: z.number().optional(),
|
||||
Window: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PreferenceSchema = z
|
||||
.object({
|
||||
Spread: z.object({
|
||||
SpreadDescriptor: z.string(),
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PlatformSchema = z
|
||||
.object({
|
||||
Architecture: z.string(),
|
||||
OS: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PlacementSwarmSchema = z
|
||||
.object({
|
||||
Constraints: z.array(z.string()).optional(),
|
||||
Preferences: z.array(PreferenceSchema).optional(),
|
||||
MaxReplicas: z.number().optional(),
|
||||
Platforms: z.array(PlatformSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const UpdateConfigSwarmSchema = z
|
||||
.object({
|
||||
Parallelism: z.number(),
|
||||
Delay: z.number().optional(),
|
||||
FailureAction: z.string().optional(),
|
||||
Monitor: z.number().optional(),
|
||||
MaxFailureRatio: z.number().optional(),
|
||||
Order: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ReplicatedSchema = z
|
||||
.object({
|
||||
Replicas: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ReplicatedJobSchema = z
|
||||
.object({
|
||||
MaxConcurrent: z.number().optional(),
|
||||
TotalCompletions: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ServiceModeSwarmSchema = z
|
||||
.object({
|
||||
Replicated: ReplicatedSchema.optional(),
|
||||
Global: z.object({}).optional(),
|
||||
ReplicatedJob: ReplicatedJobSchema.optional(),
|
||||
GlobalJob: z.object({}).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const NetworkSwarmSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
Target: z.string().optional(),
|
||||
Aliases: z.array(z.string()).optional(),
|
||||
DriverOpts: z.object({}).optional(),
|
||||
})
|
||||
.strict(),
|
||||
);
|
||||
|
||||
const LabelsSwarmSchema = z.record(z.string());
|
||||
|
||||
const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
||||
return z
|
||||
.string()
|
||||
.transform((str, ctx) => {
|
||||
if (str === null || str === "") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||
return z.NEVER;
|
||||
}
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Object cannot be empty",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parseResult = schema.safeParse(data);
|
||||
if (!parseResult.success) {
|
||||
for (const error of parseResult.error.issues) {
|
||||
const path = error.path.join(".");
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `${path} ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addSwarmSettings = z.object({
|
||||
healthCheckSwarm: createStringToJSONSchema(HealthCheckSwarmSchema).nullable(),
|
||||
restartPolicySwarm: createStringToJSONSchema(
|
||||
RestartPolicySwarmSchema,
|
||||
).nullable(),
|
||||
placementSwarm: createStringToJSONSchema(PlacementSwarmSchema).nullable(),
|
||||
updateConfigSwarm: createStringToJSONSchema(
|
||||
UpdateConfigSwarmSchema,
|
||||
).nullable(),
|
||||
rollbackConfigSwarm: createStringToJSONSchema(
|
||||
UpdateConfigSwarmSchema,
|
||||
).nullable(),
|
||||
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
|
||||
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
||||
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
||||
});
|
||||
|
||||
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.application.update.useMutation();
|
||||
|
||||
const form = useForm<AddSwarmSettings>({
|
||||
defaultValues: {
|
||||
healthCheckSwarm: null,
|
||||
restartPolicySwarm: null,
|
||||
placementSwarm: null,
|
||||
updateConfigSwarm: null,
|
||||
rollbackConfigSwarm: null,
|
||||
modeSwarm: null,
|
||||
labelsSwarm: null,
|
||||
networkSwarm: null,
|
||||
},
|
||||
resolver: zodResolver(addSwarmSettings),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
healthCheckSwarm: data.healthCheckSwarm
|
||||
? JSON.stringify(data.healthCheckSwarm, null, 2)
|
||||
: null,
|
||||
restartPolicySwarm: data.restartPolicySwarm
|
||||
? JSON.stringify(data.restartPolicySwarm, null, 2)
|
||||
: null,
|
||||
placementSwarm: data.placementSwarm
|
||||
? JSON.stringify(data.placementSwarm, null, 2)
|
||||
: null,
|
||||
updateConfigSwarm: data.updateConfigSwarm
|
||||
? JSON.stringify(data.updateConfigSwarm, null, 2)
|
||||
: null,
|
||||
rollbackConfigSwarm: data.rollbackConfigSwarm
|
||||
? JSON.stringify(data.rollbackConfigSwarm, null, 2)
|
||||
: null,
|
||||
modeSwarm: data.modeSwarm
|
||||
? JSON.stringify(data.modeSwarm, null, 2)
|
||||
: null,
|
||||
labelsSwarm: data.labelsSwarm
|
||||
? JSON.stringify(data.labelsSwarm, null, 2)
|
||||
: null,
|
||||
networkSwarm: data.networkSwarm
|
||||
? JSON.stringify(data.networkSwarm, null, 2)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddSwarmSettings) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
healthCheckSwarm: data.healthCheckSwarm,
|
||||
restartPolicySwarm: data.restartPolicySwarm,
|
||||
placementSwarm: data.placementSwarm,
|
||||
updateConfigSwarm: data.updateConfigSwarm,
|
||||
rollbackConfigSwarm: data.rollbackConfigSwarm,
|
||||
modeSwarm: data.modeSwarm,
|
||||
labelsSwarm: data.labelsSwarm,
|
||||
networkSwarm: data.networkSwarm,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Swarm settings updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the swarm settings");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" className="cursor-pointer w-fit">
|
||||
<Settings className="size-4 text-muted-foreground" />
|
||||
Swarm Settings
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-5xl p-0">
|
||||
<DialogHeader className="p-6">
|
||||
<DialogTitle>Swarm Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update certain settings using a json object.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-permissions"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="healthCheckSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Health Check</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Test?: string[] | undefined;
|
||||
Interval?: number | undefined;
|
||||
Timeout?: number | undefined;
|
||||
StartPeriod?: number | undefined;
|
||||
Retries?: number | undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
|
||||
"Interval" : 10000,
|
||||
"Timeout" : 10000,
|
||||
"StartPeriod" : 10000,
|
||||
"Retries" : 10
|
||||
}`}
|
||||
className="h-[12rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="restartPolicySwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Restart Policy</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Condition?: string | undefined;
|
||||
Delay?: number | undefined;
|
||||
MaxAttempts?: number | undefined;
|
||||
Window?: number | undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Condition" : "on-failure",
|
||||
"Delay" : 10000,
|
||||
"MaxAttempts" : 10,
|
||||
"Window" : 10000
|
||||
} `}
|
||||
className="h-[12rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="placementSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Placement</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Constraints?: string[] | undefined;
|
||||
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
|
||||
MaxReplicas?: number | undefined;
|
||||
Platforms?:
|
||||
| Array<{
|
||||
Architecture: string;
|
||||
OS: string;
|
||||
}>
|
||||
| undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Constraints" : ["node.role==manager"],
|
||||
"Preferences" : [{
|
||||
"Spread" : {
|
||||
"SpreadDescriptor" : "node.labels.region"
|
||||
}
|
||||
}],
|
||||
"MaxReplicas" : 10,
|
||||
"Platforms" : [{
|
||||
"Architecture" : "amd64",
|
||||
"OS" : "linux"
|
||||
}]
|
||||
} `}
|
||||
className="h-[21rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="updateConfigSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Update Config</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Parallelism?: number;
|
||||
Delay?: number | undefined;
|
||||
FailureAction?: string | undefined;
|
||||
Monitor?: number | undefined;
|
||||
MaxFailureRatio?: number | undefined;
|
||||
Order: string;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Parallelism" : 1,
|
||||
"Delay" : 10000,
|
||||
"FailureAction" : "continue",
|
||||
"Monitor" : 10000,
|
||||
"MaxFailureRatio" : 10,
|
||||
"Order" : "start-first"
|
||||
}`}
|
||||
className="h-[21rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rollbackConfigSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Rollback Config</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Parallelism?: number;
|
||||
Delay?: number | undefined;
|
||||
FailureAction?: string | undefined;
|
||||
Monitor?: number | undefined;
|
||||
MaxFailureRatio?: number | undefined;
|
||||
Order: string;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Parallelism" : 1,
|
||||
"Delay" : 10000,
|
||||
"FailureAction" : "continue",
|
||||
"Monitor" : 10000,
|
||||
"MaxFailureRatio" : 10,
|
||||
"Order" : "start-first"
|
||||
}`}
|
||||
className="h-[17rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="modeSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Mode</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="center"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
Replicated?: { Replicas?: number | undefined } | undefined;
|
||||
Global?: {} | undefined;
|
||||
ReplicatedJob?:
|
||||
| {
|
||||
MaxConcurrent?: number | undefined;
|
||||
TotalCompletions?: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
GlobalJob?: {} | undefined;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"Replicated" : {
|
||||
"Replicas" : 1
|
||||
},
|
||||
"Global" : {},
|
||||
"ReplicatedJob" : {
|
||||
"MaxConcurrent" : 1,
|
||||
"TotalCompletions" : 1
|
||||
},
|
||||
"GlobalJob" : {}
|
||||
}`}
|
||||
className="h-[17rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="networkSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Network</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`[
|
||||
{
|
||||
"Target" : string | undefined;
|
||||
"Aliases" : string[] | undefined;
|
||||
"DriverOpts" : { [key: string]: string } | undefined;
|
||||
}
|
||||
]`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`[
|
||||
{
|
||||
"Target" : "dokploy-network",
|
||||
"Aliases" : ["dokploy-network"],
|
||||
"DriverOpts" : {
|
||||
"com.docker.network.driver.mtu" : "1500",
|
||||
"com.docker.network.driver.host_binding" : "true",
|
||||
"com.docker.network.driver.mtu" : "1500",
|
||||
"com.docker.network.driver.host_binding" : "true"
|
||||
}
|
||||
}
|
||||
]`}
|
||||
className="h-[20rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="labelsSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
|
||||
<FormLabel>Labels</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Check the interface
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`{
|
||||
[name: string]: string;
|
||||
}`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
placeholder={`{
|
||||
"com.example.app.name" : "my-app",
|
||||
"com.example.app.version" : "1.0.0"
|
||||
}`}
|
||||
className="h-[20rem] font-mono"
|
||||
{...field}
|
||||
value={field?.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border p-2 ">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-permissions"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { toast } from "sonner";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import Link from "next/link";
|
||||
import { Server } from "lucide-react";
|
||||
import { AddSwarmSettings } from "./modify-swarm-settings";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
replicas: z.number(),
|
||||
registryId: z.string(),
|
||||
});
|
||||
|
||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
||||
|
||||
export const ShowClusterSettings = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { data: registries } = api.registry.all.useQuery();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading } = api.application.update.useMutation();
|
||||
|
||||
const form = useForm<AddCommand>({
|
||||
defaultValues: {
|
||||
registryId: data?.registryId || "",
|
||||
replicas: data?.replicas || 1,
|
||||
},
|
||||
resolver: zodResolver(AddRedirectchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.command) {
|
||||
form.reset({
|
||||
registryId: data?.registryId || "",
|
||||
replicas: data?.replicas || 1,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]);
|
||||
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
registryId: data?.registryId === "none" ? null : data?.registryId,
|
||||
replicas: data?.replicas,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Command Updated");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the command");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Cluster Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Add the registry and the replicas of the application
|
||||
</CardDescription>
|
||||
</div>
|
||||
<AddSwarmSettings applicationId={applicationId} />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after modify the cluster settings to
|
||||
apply the changes.
|
||||
</AlertBlock>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="replicas"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Replicas</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number(e.target.value));
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{registries && registries?.length === 0 ? (
|
||||
<div className="pt-10">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Server className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To use a cluster feature, you need to configure at least a
|
||||
registry first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/cluster"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="registryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select a registry</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a registry" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{registries?.map((registry) => (
|
||||
<SelectItem
|
||||
key={registry.registryId}
|
||||
value={registry.registryId}
|
||||
>
|
||||
{registry.registryName}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
<SelectLabel>
|
||||
Registries ({registries?.length})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button isLoading={isLoading} type="submit" className="w-fit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -183,8 +183,7 @@ export const AddPort = ({
|
||||
<SelectValue placeholder="Select a protocol" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent defaultValue={"none"}>
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
<SelectContent>
|
||||
<SelectItem value={"tcp"}>TCP</SelectItem>
|
||||
<SelectItem value={"udp"}>UDP</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Rss } from "lucide-react";
|
||||
import { AddPort } from "./add-port";
|
||||
import { DeletePort } from "./delete-port";
|
||||
import { UpdatePort } from "./update-port";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
@@ -47,7 +48,11 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting the ports to apply the changes.
|
||||
</AlertBlock>
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.ports.map((port) => (
|
||||
<div key={port.portId}>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -99,7 +99,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -94,7 +94,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,7 +89,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -21,6 +21,7 @@ import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
const addResourcesApplication = z.object({
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
@@ -84,6 +85,10 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after modify the resources to apply
|
||||
the changes.
|
||||
</AlertBlock>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { File } from "lucide-react";
|
||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
@@ -43,11 +44,13 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 relative">
|
||||
<div className="flex flex-col gap-6 bg-input p-4 rounded-md max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<div>
|
||||
<pre className="font-sans">{data || "Empty"}</pre>
|
||||
</div>
|
||||
<div className="flex justify-end absolute z-50 right-6">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<CodeEditor
|
||||
value={data || "Empty"}
|
||||
disabled
|
||||
className="font-mono"
|
||||
/>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-6">
|
||||
<UpdateTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,15 +16,15 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import jsyaml from "js-yaml";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const UpdateTraefikConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
@@ -58,6 +58,7 @@ export const validateAndFormatYAML = (yamlText: string) => {
|
||||
};
|
||||
|
||||
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
@@ -81,7 +82,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
}, [data]);
|
||||
|
||||
const onSubmit = async (data: UpdateTraefikConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
@@ -100,6 +101,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
refetch();
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
@@ -107,7 +110,12 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={(open) => {
|
||||
setOpen(open)
|
||||
if (!open) {
|
||||
form.reset();
|
||||
}
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
<Button isLoading={isLoading}>Modify</Button>
|
||||
</DialogTrigger>
|
||||
@@ -122,7 +130,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<form
|
||||
id="hook-form-update-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4"
|
||||
className="w-full space-y-4 overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
@@ -132,8 +140,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-[35rem] font-mono"
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
|
||||
@@ -27,6 +27,7 @@ import { PlusIcon } from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
interface Props {
|
||||
serviceId: string;
|
||||
serviceType:
|
||||
@@ -36,7 +37,8 @@ interface Props {
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "mariadb";
|
||||
| "mariadb"
|
||||
| "compose";
|
||||
refetch: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
@@ -77,7 +79,7 @@ export const AddVolumes = ({
|
||||
const { mutateAsync } = api.mounts.create.useMutation();
|
||||
const form = useForm<AddMount>({
|
||||
defaultValues: {
|
||||
type: "bind",
|
||||
type: serviceType === "compose" ? "file" : "bind",
|
||||
hostPath: "",
|
||||
mountPath: "",
|
||||
},
|
||||
@@ -176,41 +178,52 @@ export const AddVolumes = ({
|
||||
defaultValue={field.value}
|
||||
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="bind"
|
||||
id="bind"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="bind"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
Bind Mount
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="volume"
|
||||
id="volume"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="volume"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
Volume Mount
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
{serviceType !== "compose" && (
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="bind"
|
||||
id="bind"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="bind"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
Bind Mount
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
{serviceType !== "compose" && (
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="volume"
|
||||
id="volume"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="volume"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
Volume Mount
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
<FormItem
|
||||
className={cn(
|
||||
serviceType === "compose" && "col-span-3",
|
||||
"flex items-center space-x-3 space-y-0",
|
||||
)}
|
||||
>
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
@@ -220,7 +233,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="file"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
File Mount
|
||||
</Label>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { api } from "@/utils/api";
|
||||
import { AlertTriangle, Package } from "lucide-react";
|
||||
import { AddVolumes } from "./add-volumes";
|
||||
import { DeleteVolume } from "./delete-volume";
|
||||
import { UpdateVolume } from "./update-volume";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
@@ -59,15 +61,12 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
</AddVolumes>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||
<AlertTriangle className="text-yellow-600 size-5 sm:size-8 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 pt-6">
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</AlertBlock>
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.mounts.map((mount) => (
|
||||
<div key={mount.mountId}>
|
||||
<div
|
||||
@@ -114,7 +113,12 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateVolume
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
const mountSchema = z.object({
|
||||
mountPath: z.string().min(1, "Mount path required"),
|
||||
});
|
||||
|
||||
const mySchema = z.discriminatedUnion("type", [
|
||||
z
|
||||
.object({
|
||||
type: z.literal("bind"),
|
||||
hostPath: z.string().min(1, "Host path required"),
|
||||
})
|
||||
.merge(mountSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("volume"),
|
||||
volumeName: z.string().min(1, "Volume name required"),
|
||||
})
|
||||
.merge(mountSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("file"),
|
||||
content: z.string().optional(),
|
||||
})
|
||||
.merge(mountSchema),
|
||||
]);
|
||||
|
||||
type UpdateMount = z.infer<typeof mySchema>;
|
||||
|
||||
interface Props {
|
||||
mountId: string;
|
||||
type: "bind" | "volume" | "file";
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data } = api.mounts.one.useQuery(
|
||||
{
|
||||
mountId,
|
||||
},
|
||||
{
|
||||
enabled: !!mountId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.mounts.update.useMutation();
|
||||
|
||||
const form = useForm<UpdateMount>({
|
||||
defaultValues: {
|
||||
type,
|
||||
hostPath: "",
|
||||
mountPath: "",
|
||||
},
|
||||
resolver: zodResolver(mySchema),
|
||||
});
|
||||
|
||||
const typeForm = form.watch("type");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (typeForm === "bind") {
|
||||
form.reset({
|
||||
hostPath: data.hostPath || "",
|
||||
mountPath: data.mountPath,
|
||||
type: "bind",
|
||||
});
|
||||
} else if (typeForm === "volume") {
|
||||
form.reset({
|
||||
volumeName: data.volumeName || "",
|
||||
mountPath: data.mountPath,
|
||||
type: "volume",
|
||||
});
|
||||
} else if (typeForm === "file") {
|
||||
form.reset({
|
||||
content: data.content || "",
|
||||
mountPath: data.mountPath,
|
||||
type: "file",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateMount) => {
|
||||
if (data.type === "bind") {
|
||||
await mutateAsync({
|
||||
hostPath: data.hostPath,
|
||||
mountPath: data.mountPath,
|
||||
type: data.type,
|
||||
mountId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Update");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Bind mount");
|
||||
});
|
||||
} else if (data.type === "volume") {
|
||||
await mutateAsync({
|
||||
volumeName: data.volumeName,
|
||||
mountPath: data.mountPath,
|
||||
type: data.type,
|
||||
mountId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Update");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Volume mount");
|
||||
});
|
||||
} else if (data.type === "file") {
|
||||
await mutateAsync({
|
||||
content: data.content,
|
||||
mountPath: data.mountPath,
|
||||
type: data.type,
|
||||
mountId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Mount Update");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the File mount");
|
||||
});
|
||||
}
|
||||
refetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update</DialogTitle>
|
||||
<DialogDescription>Update the mount</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-update-volume"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{type === "bind" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hostPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Host Path" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{type === "volume" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="volumeName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Volume Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Volume Name"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "file" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Any content"
|
||||
className="h-64"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mountPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mount Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Mount Path" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-volume"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -25,8 +25,8 @@ export const DeleteApplication = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
|
||||
@@ -86,6 +86,11 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="text-sm capitalize text-muted-foreground">
|
||||
|
||||
@@ -24,8 +24,8 @@ export const DeleteDomain = ({ domainId }: Props) => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
|
||||
79
components/dashboard/application/domains/generate-domain.tsx
Normal file
79
components/dashboard/application/domains/generate-domain.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { GenerateTraefikMe } from "./generate-traefikme";
|
||||
import { GenerateWildCard } from "./generate-wildcard";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const GenerateDomain = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="secondary">
|
||||
Generate Domain
|
||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Generate Domain</DialogTitle>
|
||||
<DialogDescription>
|
||||
Generate Domains for your applications
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ul className="flex flex-col gap-4">
|
||||
<li className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-bold">
|
||||
1. Generate TraefikMe Domain
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
This option generates a free domain provided by{" "}
|
||||
<Link
|
||||
href="https://traefik.me"
|
||||
className="text-primary"
|
||||
target="_blank"
|
||||
>
|
||||
TraefikMe
|
||||
</Link>
|
||||
. We recommend using this for quick domain testing or if you
|
||||
don't have a domain yet.
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/* <li className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-base font-bold">
|
||||
2. Use Wildcard Domain
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
To use this option, you need to set up an 'A' record in your
|
||||
domain provider. For example, create a record for
|
||||
*.yourdomain.com.
|
||||
</div>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<div className="flex flex-row gap-4 w-full">
|
||||
<GenerateTraefikMe applicationId={applicationId} />
|
||||
{/* <GenerateWildCard applicationId={applicationId} /> */}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
export const GenerateTraefikMe = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Generate Domain
|
||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to generate a new domain?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will generate a new domain and will be used to access to the
|
||||
application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
toast.success("Generated Domain succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to generate Domain");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { SquareAsterisk } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
export const GenerateWildCard = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary" isLoading={isLoading}>
|
||||
Generate Wildcard Domain
|
||||
<SquareAsterisk className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to generate a new wildcard domain?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will generate a new domain and will be used to access to the
|
||||
application
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then((data) => {
|
||||
utils.domain.byApplicationId.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
utils.application.readTraefikConfig.invalidate({
|
||||
applicationId: applicationId,
|
||||
});
|
||||
toast.success("Generated Domain succesfully");
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(`Error to generate Domain: ${e.message}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { ExternalLink, GlobeIcon } from "lucide-react";
|
||||
import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,6 +14,7 @@ import { DeleteDomain } from "./delete-domain";
|
||||
import Link from "next/link";
|
||||
import { AddDomain } from "./add-domain";
|
||||
import { UpdateDomain } from "./update-domain";
|
||||
import { GenerateDomain } from "./generate-domain";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
@@ -31,7 +32,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">Domains</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -39,9 +40,16 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data?.length > 0 && (
|
||||
<AddDomain applicationId={applicationId} />
|
||||
)}
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
{data && data?.length > 0 && (
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
)}
|
||||
{data && data?.length > 0 && (
|
||||
<GenerateDomain applicationId={applicationId} />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-row gap-4">
|
||||
{data?.length === 0 ? (
|
||||
@@ -51,7 +59,13 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
To access to the application is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<AddDomain applicationId={applicationId}>Add Domain</AddDomain>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<AddDomain applicationId={applicationId}>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</AddDomain>
|
||||
|
||||
<GenerateDomain applicationId={applicationId} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
@@ -75,8 +89,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
<Button variant="outline" disabled>
|
||||
{item.https ? "HTTPS" : "HTTP"}
|
||||
</Button>
|
||||
<UpdateDomain domainId={item.domainId} />
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateDomain domainId={item.domainId} />
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -115,8 +115,8 @@ export const UpdateDomain = ({ domainId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button>
|
||||
<PenBoxIcon className="size-4" />
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
@@ -32,6 +34,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveEnvironment.useMutation();
|
||||
|
||||
@@ -71,22 +74,57 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
toast.error("Error to add environment");
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isEnvVisible) {
|
||||
if (data?.env) {
|
||||
const maskedLines = data.env
|
||||
.split("\n")
|
||||
.map((line) => "*".repeat(line.length))
|
||||
.join("\n");
|
||||
form.reset({
|
||||
environment: maskedLines,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
environment: "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset({
|
||||
environment: data?.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form, isEnvVisible]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle bold"
|
||||
pressed={isEnvVisible}
|
||||
onPressedChange={setIsEnvVisible}
|
||||
>
|
||||
{isEnvVisible ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Toggle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -94,8 +132,12 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="NODE_ENV=production"
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
@@ -107,7 +149,12 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
<Button
|
||||
disabled={isEnvVisible}
|
||||
isLoading={isLoading}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,6 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: markRunning } =
|
||||
api.application.markRunning.useMutation();
|
||||
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||
|
||||
return (
|
||||
@@ -47,28 +45,16 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await markRunning({
|
||||
toast.success("Deploying Application....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Application Deploying....");
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.message || "Error to deploy Application");
|
||||
});
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
|
||||
@@ -10,7 +10,7 @@ import { RedbuildApplication } from "../rebuild-application";
|
||||
import { StartApplication } from "../start-application";
|
||||
import { StopApplication } from "../stop-application";
|
||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { CheckCircle2, Terminal } from "lucide-react";
|
||||
import { DeployApplication } from "./deploy-application";
|
||||
import { ResetApplication } from "./reset-application";
|
||||
interface Props {
|
||||
@@ -55,8 +55,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
toast.error("Error to update Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center"
|
||||
>
|
||||
Autodeploy
|
||||
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
||||
</Toggle>
|
||||
<RedbuildApplication applicationId={applicationId} />
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
|
||||
@@ -25,8 +25,7 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { mutateAsync: markRunning } =
|
||||
api.application.markRunning.useMutation();
|
||||
|
||||
const { mutateAsync } = api.application.redeploy.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
@@ -54,22 +53,14 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await markRunning({
|
||||
toast.success("Redeploying Application....");
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
await mutateAsync({
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
toast.success("Application rebuild succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the application");
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the application");
|
||||
|
||||
@@ -90,7 +90,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
133
components/dashboard/compose/advanced/add-command.tsx
Normal file
133
components/dashboard/compose/advanced/add-command.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { toast } from "sonner";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const AddRedirectSchema = z.object({
|
||||
command: z.string(),
|
||||
});
|
||||
|
||||
type AddCommand = z.infer<typeof AddRedirectSchema>;
|
||||
|
||||
export const AddCommandCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const { data: defaultCommand, refetch } =
|
||||
api.compose.getDefaultCommand.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<AddCommand>({
|
||||
defaultValues: {
|
||||
command: "",
|
||||
},
|
||||
resolver: zodResolver(AddRedirectSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.command) {
|
||||
form.reset({
|
||||
command: data?.command || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]);
|
||||
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
command: data?.command,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Command Updated");
|
||||
refetch();
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the command");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||
<CardDescription>
|
||||
Append a custom command to the compose file
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="command"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Command</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Custom command" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Default Command ({defaultCommand})
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button isLoading={isLoading} type="submit" className="w-fit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
133
components/dashboard/compose/advanced/show-volumes.tsx
Normal file
133
components/dashboard/compose/advanced/show-volumes.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Package } from "lucide-react";
|
||||
import { DeleteVolume } from "../../application/advanced/volumes/delete-volume";
|
||||
import { AddVolumes } from "../../application/advanced/volumes/add-volumes";
|
||||
import { UpdateVolume } from "../../application/advanced/volumes/update-volume";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowVolumesCompose = ({ composeId }: Props) => {
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to persist data in this compose use the following config
|
||||
to setup the volumes
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data?.mounts.length > 0 && (
|
||||
<AddVolumes
|
||||
serviceId={composeId}
|
||||
refetch={refetch}
|
||||
serviceType="compose"
|
||||
>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{data?.mounts.length === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<Package className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No volumes/mounts configured
|
||||
</span>
|
||||
<AddVolumes
|
||||
serviceId={composeId}
|
||||
refetch={refetch}
|
||||
serviceType="compose"
|
||||
>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</AlertBlock>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.mounts.map((mount) => (
|
||||
<div key={mount.mountId}>
|
||||
<div
|
||||
key={mount.mountId}
|
||||
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Mount Type</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.type.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
{mount.type === "volume" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Volume Name</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.volumeName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mount.type === "file" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Content</span>
|
||||
<span className="text-sm text-muted-foreground w-40 truncate">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{mount.type === "bind" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Host Path</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.hostPath}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Mount Path</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{mount.mountPath}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateVolume
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
63
components/dashboard/compose/delete-compose.tsx
Normal file
63
components/dashboard/compose/delete-compose.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const DeleteCompose = ({ composeId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
compose and all its services.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
|
||||
toast.success("Compose delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Paintbrush } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
|
||||
Cancel Queues
|
||||
<Paintbrush className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to cancel the incoming deployments?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will cancel all the incoming deployments
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Queues are being cleaned");
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
export const RefreshTokenCompose = ({ composeId }: Props) => {
|
||||
const { mutateAsync } = api.compose.refreshToken.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger>
|
||||
<RefreshCcw className="h-4 w-4 cursor-pointer text-muted-foreground" />
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently change the token
|
||||
and all the previous tokens will be invalidated
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
})
|
||||
.then(() => {
|
||||
utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success("Refresh Token updated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the refresh token");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
logPath: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !logPath) return;
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
setData((currentData) => currentData + e.data);
|
||||
};
|
||||
|
||||
return () => ws.close();
|
||||
}, [logPath, open]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(e) => {
|
||||
onClose();
|
||||
if (!e) setData("");
|
||||
}}
|
||||
>
|
||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Deployment</DialogTitle>
|
||||
<DialogDescription>
|
||||
See all the details of this deployment
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem]">
|
||||
<code>
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{data || "Loading..."}
|
||||
</pre>
|
||||
<div ref={endOfLogsRef} />
|
||||
</code>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { RocketIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { ShowDeploymentCompose } from "./show-deployment-compose";
|
||||
import { RefreshTokenCompose } from "./refresh-token-compose";
|
||||
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
const { data: deployments } = api.deployment.allByCompose.useQuery(
|
||||
{ composeId },
|
||||
{
|
||||
enabled: !!composeId,
|
||||
refetchInterval: 5000,
|
||||
},
|
||||
);
|
||||
const [url, setUrl] = React.useState("");
|
||||
useEffect(() => {
|
||||
setUrl(document.location.origin);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||
<CardDescription>
|
||||
See all the 10 last deployments for this compose
|
||||
</CardDescription>
|
||||
</div>
|
||||
<CancelQueuesCompose composeId={composeId} />
|
||||
{/* <CancelQueues applicationId={applicationId} /> */}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<span>
|
||||
If you want to re-deploy this application use this URL in the config
|
||||
of your git provider or docker
|
||||
</span>
|
||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||
<span>Webhook URL: </span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="text-muted-foreground">
|
||||
{`${url}/api/deploy/compose/${data?.refreshToken}`}
|
||||
</span>
|
||||
<RefreshTokenCompose composeId={composeId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{data?.deployments?.length === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<RocketIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No deployments found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{deployments?.map((deployment) => (
|
||||
<div
|
||||
key={deployment.deploymentId}
|
||||
className="flex items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
{deployment.status}
|
||||
|
||||
<StatusTooltip
|
||||
status={deployment?.status}
|
||||
className="size-2.5"
|
||||
/>
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="text-sm capitalize text-muted-foreground">
|
||||
<DateTooltip date={deployment.createdAt} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment.logPath);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ShowDeploymentCompose
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
167
components/dashboard/compose/enviroment/show.tsx
Normal file
167
components/dashboard/compose/enviroment/show.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
});
|
||||
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
const form = useForm<EnvironmentSchema>({
|
||||
defaultValues: {
|
||||
environment: "",
|
||||
},
|
||||
resolver: zodResolver(addEnvironmentSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
environment: data.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: EnvironmentSchema) => {
|
||||
mutateAsync({
|
||||
env: data.environment,
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Environments Added");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add environment");
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEnvVisible) {
|
||||
if (data?.env) {
|
||||
const maskedLines = data.env
|
||||
.split("\n")
|
||||
.map((line) => "*".repeat(line.length))
|
||||
.join("\n");
|
||||
form.reset({
|
||||
environment: maskedLines,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
environment: "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset({
|
||||
environment: data?.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form, isEnvVisible]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle bold"
|
||||
pressed={isEnvVisible}
|
||||
onPressedChange={setIsEnvVisible}
|
||||
>
|
||||
{isEnvVisible ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Toggle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="environment"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button
|
||||
disabled={isEnvVisible}
|
||||
isLoading={isLoading}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
121
components/dashboard/compose/general/actions.tsx
Normal file
121
components/dashboard/compose/general/actions.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLink, Globe, Terminal } from "lucide-react";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { RedbuildCompose } from "./rebuild-compose";
|
||||
import { DeployCompose } from "./deploy-compose";
|
||||
import { StopCompose } from "./stop-compose";
|
||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
export const ComposeActions = ({ composeId }: Props) => {
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { mutateAsync: update } = api.compose.update.useMutation();
|
||||
|
||||
const extractDomains = (env: string) => {
|
||||
const lines = env.split("\n");
|
||||
const hostLines = lines.filter((line) => {
|
||||
const [key, value] = line.split("=");
|
||||
return key?.trim().endsWith("_HOST");
|
||||
});
|
||||
|
||||
const hosts = hostLines.map((line) => {
|
||||
const [key, value] = line.split("=");
|
||||
return value ? value.trim() : "";
|
||||
});
|
||||
|
||||
return hosts;
|
||||
};
|
||||
|
||||
const domains = extractDomains(data?.env || "");
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||
<DeployCompose composeId={composeId} />
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle italic"
|
||||
pressed={data?.autoDeploy || false}
|
||||
onPressedChange={async (enabled) => {
|
||||
await update({
|
||||
composeId,
|
||||
autoDeploy: enabled,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Auto Deploy Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update Auto Deploy");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Autodeploy
|
||||
</Toggle>
|
||||
<RedbuildCompose composeId={composeId} />
|
||||
{data?.composeType === "docker-compose" && (
|
||||
<StopCompose composeId={composeId} />
|
||||
)}
|
||||
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
{domains.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
Domains
|
||||
<Globe className="text-xs size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>Domains detected</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{domains.map((host, index) => {
|
||||
const url =
|
||||
host.startsWith("http://") || host.startsWith("https://")
|
||||
? host
|
||||
: `http://${host}`;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`domain-${index}`}
|
||||
className="cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<Link href={url} target="_blank">
|
||||
{host}
|
||||
<ExternalLink className="ml-2 text-xs text-muted-foreground" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
142
components/dashboard/compose/general/compose-file-editor.tsx
Normal file
142
components/dashboard/compose/general/compose-file-editor.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RandomizeCompose } from "./randomize-compose";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const AddComposeFile = z.object({
|
||||
composeFile: z.string(),
|
||||
});
|
||||
|
||||
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
||||
|
||||
export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<AddComposeFile>({
|
||||
defaultValues: {
|
||||
composeFile: "",
|
||||
},
|
||||
resolver: zodResolver(AddComposeFile),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
composeFile: data.composeFile || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddComposeFile) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.composeFile);
|
||||
if (!valid) {
|
||||
form.setError("composeFile", {
|
||||
type: "manual",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
form.clearErrors("composeFile");
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
composeFile: data.composeFile,
|
||||
sourceType: "raw",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose config Updated");
|
||||
refetch();
|
||||
await utils.compose.allServices.invalidate({
|
||||
composeId,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
toast.error("Error to update the compose config");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col lg:flex-row gap-4 ">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full relative space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composeFile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="overflow-auto">
|
||||
<FormControl className="">
|
||||
<div className="flex flex-col gap-4 w-full outline-none focus:outline-none overflow-auto">
|
||||
<CodeEditor
|
||||
// disabled
|
||||
value={field.value}
|
||||
className="font-mono"
|
||||
wrapperClassName="compose-file-editor"
|
||||
placeholder={`version: '3'
|
||||
services:
|
||||
web:
|
||||
image: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
`}
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
||||
<RandomizeCompose composeId={composeId} />
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
className="lg:w-fit w-full"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
64
components/dashboard/compose/general/deploy-compose.tsx
Normal file
64
components/dashboard/compose/general/deploy-compose.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const DeployCompose = ({ composeId }: Props) => {
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button isLoading={data?.composeStatus === "running"}>Deploy</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will deploy the compose
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
toast.success("Deploying Compose....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
composeId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,255 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon, LockIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GitProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
repositoryURL: z.string().min(1, {
|
||||
message: "Repository URL is required",
|
||||
}),
|
||||
branch: z.string().min(1, "Branch required"),
|
||||
});
|
||||
|
||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const { mutateAsync: generateSSHKey, isLoading: isGeneratingSSHKey } =
|
||||
api.compose.generateSSHKey.useMutation();
|
||||
|
||||
const { mutateAsync: removeSSHKey, isLoading: isRemovingSSHKey } =
|
||||
api.compose.removeSSHKey.useMutation();
|
||||
const form = useForm<GitProvider>({
|
||||
defaultValues: {
|
||||
branch: "",
|
||||
repositoryURL: "",
|
||||
composePath: "./docker-compose.yml",
|
||||
},
|
||||
resolver: zodResolver(GitProviderSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.customGitBranch || "",
|
||||
repositoryURL: data.customGitUrl || "",
|
||||
composePath: data.composePath,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (values: GitProvider) => {
|
||||
await mutateAsync({
|
||||
customGitBranch: values.branch,
|
||||
customGitUrl: values.repositoryURL,
|
||||
composeId,
|
||||
sourceType: "git",
|
||||
composePath: values.composePath,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Git Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Git provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4 ">
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repositoryURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row justify-between">
|
||||
Repository URL
|
||||
<div className="flex gap-2">
|
||||
<Dialog>
|
||||
<DialogTrigger className="flex flex-row gap-2">
|
||||
<LockIcon className="size-4 text-muted-foreground" />?
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Private Repository</DialogTitle>
|
||||
<DialogDescription>
|
||||
If your repository is private is necessary to
|
||||
generate SSH Keys to add to your git provider.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
placeholder="Please click on Generate SSH Key"
|
||||
className="no-scrollbar h-64 text-muted-foreground"
|
||||
disabled={!data?.customGitSSHKey}
|
||||
contentEditable={false}
|
||||
value={
|
||||
data?.customGitSSHKey ||
|
||||
"Please click on Generate SSH Key"
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2"
|
||||
onClick={() => {
|
||||
copy(
|
||||
data?.customGitSSHKey ||
|
||||
"Generate a SSH Key",
|
||||
);
|
||||
toast.success("SSH Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex sm:justify-between gap-3.5 flex-col sm:flex-col w-full">
|
||||
<div className="flex flex-row gap-2 w-full justify-between flex-wrap">
|
||||
{data?.customGitSSHKey && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={
|
||||
isGeneratingSSHKey || isRemovingSSHKey
|
||||
}
|
||||
className="max-sm:w-full"
|
||||
onClick={async () => {
|
||||
await removeSSHKey({
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("SSH Key Removed");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to remove the SSH Key",
|
||||
);
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Remove SSH Key
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
isLoading={
|
||||
isGeneratingSSHKey || isRemovingSSHKey
|
||||
}
|
||||
className="max-sm:w-full"
|
||||
onClick={async () => {
|
||||
await generateSSHKey({
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("SSH Key Generated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error to generate the SSH Key",
|
||||
);
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Generate SSH Key
|
||||
</Button>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Is recommended to remove the SSH Key if you want
|
||||
to deploy a public repository.
|
||||
</span>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="git@bitbucket.org" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Branch" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button type="submit" className="w-fit" isLoading={isLoading}>
|
||||
Save{" "}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,310 @@
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GithubProviderSchema = 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"),
|
||||
});
|
||||
|
||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGithubProvider } =
|
||||
api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<GithubProvider>({
|
||||
defaultValues: {
|
||||
composePath: "./docker-compose.yml",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GithubProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
|
||||
const { data: repositories, isLoading: isLoadingRepositories } =
|
||||
api.admin.getRepositories.useQuery();
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.admin.getBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
},
|
||||
{ enabled: !!repository?.owner && !!repository?.repo },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.branch || "",
|
||||
repository: {
|
||||
repo: data.repository || "",
|
||||
owner: data.owner || "",
|
||||
},
|
||||
composePath: data.composePath,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GithubProvider) => {
|
||||
console.log(data);
|
||||
await mutateAsync({
|
||||
branch: data.branch,
|
||||
repository: data.repository.repo,
|
||||
composeId: composeId,
|
||||
owner: data.repository.owner,
|
||||
sourceType: "github",
|
||||
composePath: data.composePath,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the github provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
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
|
||||
value={repo.url}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.login as string,
|
||||
repo: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<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"
|
||||
role="combobox"
|
||||
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 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) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.sha}
|
||||
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="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingGithubProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
97
components/dashboard/compose/general/generic/show.tsx
Normal file
97
components/dashboard/compose/general/generic/show.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import { GitBranch, LockIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||
import { ComposeFileEditor } from "../compose-file-editor";
|
||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||
|
||||
type TabState = "github" | "git" | "raw";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
const { data: haveGithubConfigured } =
|
||||
api.admin.haveGithubConfigured.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>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Select the source of your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={tab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<TabsList className="grid w-fit grid-cols-4 bg-transparent">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Github
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Git
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="raw"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Raw
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{haveGithubConfigured ? (
|
||||
<SaveGithubProviderCompose composeId={composeId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<LockIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using GitHub, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProviderCompose composeId={composeId} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="raw" className="w-full p-2 flex flex-col gap-4">
|
||||
<ComposeFileEditor composeId={composeId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
98
components/dashboard/compose/general/randomize-compose.tsx
Normal file
98
components/dashboard/compose/general/randomize-compose.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Dices } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [prefix, setPrefix] = useState<string>("");
|
||||
const [compose, setCompose] = useState<string>("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } =
|
||||
api.compose.randomizeCompose.useMutation();
|
||||
|
||||
const onSubmit = async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
prefix,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
toast.success("Compose randomized");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to randomize the compose");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild onClick={() => onSubmit()}>
|
||||
<Button className="max-lg:w-full" variant="outline">
|
||||
<Dices className="h-4 w-4" />
|
||||
Randomize Compose
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use this in case you want to deploy the same compose file and you
|
||||
have conflicts with some property like volumes, networks, etc.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||
<span>
|
||||
This will randomize the compose file and will add a prefix to the
|
||||
property to avoid conflicts
|
||||
</span>
|
||||
<ul className="list-disc list-inside">
|
||||
<li>volumes</li>
|
||||
<li>networks</li>
|
||||
<li>services</li>
|
||||
<li>configs</li>
|
||||
<li>secrets</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row gap-2">
|
||||
<Input
|
||||
placeholder="Enter a prefix (Optional, example: prod)"
|
||||
onChange={(e) => setPrefix(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
await onSubmit();
|
||||
}}
|
||||
className="lg:w-fit w-full"
|
||||
>
|
||||
Random
|
||||
</Button>
|
||||
</div>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="p-4 bg-secondary rounded-lg">
|
||||
<pre>
|
||||
<code className="language-yaml">{compose}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
75
components/dashboard/compose/general/rebuild-compose.tsx
Normal file
75
components/dashboard/compose/general/rebuild-compose.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Hammer } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const RedbuildCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { mutateAsync } = api.compose.redeploy.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={data?.composeStatus === "running"}
|
||||
>
|
||||
Rebuild
|
||||
<Hammer className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure to rebuild the compose?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Is required to deploy at least 1 time in order to reuse the same
|
||||
code
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
toast.success("Redeploying Compose....");
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to rebuild the compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
47
components/dashboard/compose/general/show.tsx
Normal file
47
components/dashboard/compose/general/show.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import React from "react";
|
||||
import { ShowProviderFormCompose } from "./generic/show";
|
||||
import { ComposeActions } from "./actions";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { api } from "@/utils/api";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowGeneralCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<div className="flex flex-row gap-2 justify-between flex-wrap">
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
<Badge>
|
||||
{data?.composeType === "docker-compose" ? "Compose" : "Stack"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<CardDescription>
|
||||
Create a compose file to deploy your compose
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 flex-wrap">
|
||||
<ComposeActions composeId={composeId} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ShowProviderFormCompose composeId={composeId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
69
components/dashboard/compose/general/stop-compose.tsx
Normal file
69
components/dashboard/compose/general/stop-compose.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { Ban } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const StopCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
Stop
|
||||
<Ban className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure to stop the compose?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will stop the compose services
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.compose.one.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success("Compose stopped succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to stop the compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
88
components/dashboard/compose/logs/show.tsx
Normal file
88
components/dashboard/compose/logs/show.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
export const DockerLogs = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||
(e) => e.DockerLogsId,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogsCompose = ({ appName }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
},
|
||||
);
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data?.length > 0) {
|
||||
setContainerId(data[0]?.containerId);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Logs</CardTitle>
|
||||
<CardDescription>
|
||||
Watch the logs of the application in real time
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{data?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.containerId}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
id="terminal"
|
||||
containerId={containerId || "select-a-container"}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
85
components/dashboard/compose/monitoring/show.tsx
Normal file
85
components/dashboard/compose/monitoring/show.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { DockerMonitoring } from "../../monitoring/docker/show";
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
}
|
||||
|
||||
export const ShowMonitoringCompose = ({
|
||||
appName,
|
||||
appType = "stack",
|
||||
}: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName: appName,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
},
|
||||
);
|
||||
|
||||
const [containerAppName, setContainerAppName] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data?.length > 0) {
|
||||
setContainerAppName(data[0]?.name);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
||||
<CardDescription>Watch the usage of your compose</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Label>Select a container to watch the monitoring</Label>
|
||||
<Select onValueChange={setContainerAppName} value={containerAppName}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{data?.map((container) => (
|
||||
<SelectItem
|
||||
key={container.containerId}
|
||||
value={container.name}
|
||||
>
|
||||
{container.name} ({container.containerId}) {container.state}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerMonitoring
|
||||
appName={containerAppName || ""}
|
||||
appType={appType}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
159
components/dashboard/compose/update-compose.tsx
Normal file
159
components/dashboard/compose/update-compose.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SquarePen } from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const updateComposeSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateCompose = z.infer<typeof updateComposeSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const UpdateCompose = ({ composeId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isLoading } =
|
||||
api.compose.update.useMutation();
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
const form = useForm<UpdateCompose>({
|
||||
defaultValues: {
|
||||
description: data?.description ?? "",
|
||||
name: data?.name ?? "",
|
||||
},
|
||||
resolver: zodResolver(updateComposeSchema),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
description: data.description ?? "",
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: UpdateCompose) => {
|
||||
await mutateAsync({
|
||||
name: formData.name,
|
||||
composeId: composeId,
|
||||
description: formData.description || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose updated succesfully");
|
||||
utils.compose.one.invalidate({
|
||||
composeId: composeId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the Compose");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modify Compose</DialogTitle>
|
||||
<DialogDescription>Update the compose data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-update-compose"
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tesla" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Description about your project..."
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-update-compose"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil, CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { Pencil, CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -115,7 +115,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -161,10 +161,6 @@ export const ShowContainers = () => {
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div className="space-x-2 flex flex-wrap">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -9,16 +9,15 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||
traefikConfig: z.string(),
|
||||
@@ -88,13 +87,12 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full relative"
|
||||
className="w-full relative z-[5]"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col overflow-auto">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
@@ -105,8 +103,8 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
{path}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-[35rem] font-mono"
|
||||
<CodeEditor
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
|
||||
@@ -14,7 +14,7 @@ export const ShowTraefikSystem = () => {
|
||||
|
||||
return (
|
||||
<div className={cn("mt-6 md:grid gap-4")}>
|
||||
<div className="flex flex-col md:flex-row gap-4 md:gap-10 w-full">
|
||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
@@ -27,7 +27,7 @@ export const ShowTraefikSystem = () => {
|
||||
<>
|
||||
<Tree
|
||||
data={directories}
|
||||
className="md:max-w-[19rem] w-full md:h-[660px] border rounded-lg"
|
||||
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
|
||||
onSelectChange={(item) => setFile(item?.id || null)}
|
||||
folderIcon={Folder}
|
||||
itemIcon={Workflow}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -21,213 +21,218 @@ import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
const addResourcesMariadb = z.object({
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
cpuLimit: z.number().nullable().optional(),
|
||||
memoryLimit: z.number().nullable().optional(),
|
||||
cpuReservation: z.number().nullable().optional(),
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
cpuLimit: z.number().nullable().optional(),
|
||||
memoryLimit: z.number().nullable().optional(),
|
||||
cpuReservation: z.number().nullable().optional(),
|
||||
});
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
mariadbId: string;
|
||||
}
|
||||
|
||||
type AddResourcesMariadb = z.infer<typeof addResourcesMariadb>;
|
||||
export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
const { data, refetch } = api.mariadb.one.useQuery(
|
||||
{
|
||||
mariadbId,
|
||||
},
|
||||
{ enabled: !!mariadbId },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mariadb.update.useMutation();
|
||||
const form = useForm<AddResourcesMariadb>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(addResourcesMariadb),
|
||||
});
|
||||
const { data, refetch } = api.mariadb.one.useQuery(
|
||||
{
|
||||
mariadbId,
|
||||
},
|
||||
{ enabled: !!mariadbId },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mariadb.update.useMutation();
|
||||
const form = useForm<AddResourcesMariadb>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(addResourcesMariadb),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
cpuLimit: data?.cpuLimit || undefined,
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
});
|
||||
}
|
||||
}, [data, form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
cpuLimit: data?.cpuLimit || undefined,
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
});
|
||||
}
|
||||
}, [data, form, form.formState.isSubmitSuccessful, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: AddResourcesMariadb) => {
|
||||
await mutateAsync({
|
||||
mariadbId,
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
memoryReservation: formData.memoryReservation || null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Resources Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to decrease or increase the resources to a specific
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
const onSubmit = async (formData: AddResourcesMariadb) => {
|
||||
await mutateAsync({
|
||||
mariadbId,
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
memoryReservation: formData.memoryReservation || null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Resources Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to decrease or increase the resources to a specific
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after modify the resources to apply
|
||||
the changes.
|
||||
</AlertBlock>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuReservation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuReservation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
mariadbId: string;
|
||||
}
|
||||
|
||||
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mariadbId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mariadbId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
@@ -85,7 +85,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -93,8 +93,11 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="MARIADB_PASSWORD=1234567678"
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@@ -50,17 +50,13 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mariadbId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
@@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
@@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Root Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databaseRootPassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||
/>
|
||||
|
||||
@@ -90,7 +90,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4" />
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -10,6 +10,8 @@ import { api } from "@/utils/api";
|
||||
import { AlertTriangle, Package } from "lucide-react";
|
||||
import { DeleteVolume } from "../../application/advanced/volumes/delete-volume";
|
||||
import { AddVolumes } from "../../application/advanced/volumes/add-volumes";
|
||||
import { UpdateVolume } from "../../application/advanced/volumes/update-volume";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
}
|
||||
@@ -59,15 +61,12 @@ export const ShowVolumes = ({ mariadbId }: Props) => {
|
||||
</AddVolumes>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||
<AlertTriangle className="text-yellow-600 size-5 sm:size-8 dark:text-yellow-400" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 pt-6">
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</AlertBlock>
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.mounts.map((mount) => (
|
||||
<div key={mount.mountId}>
|
||||
<div
|
||||
@@ -113,7 +112,12 @@ export const ShowVolumes = ({ mariadbId }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateVolume
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -21,213 +21,222 @@ import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
const addResourcesMongo = z.object({
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
cpuLimit: z.number().nullable().optional(),
|
||||
memoryLimit: z.number().nullable().optional(),
|
||||
cpuReservation: z.number().nullable().optional(),
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
cpuLimit: z.number().nullable().optional(),
|
||||
memoryLimit: z.number().nullable().optional(),
|
||||
cpuReservation: z.number().nullable().optional(),
|
||||
});
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
mongoId: string;
|
||||
}
|
||||
|
||||
type AddResourcesMongo = z.infer<typeof addResourcesMongo>;
|
||||
export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
const { data, refetch } = api.mongo.one.useQuery(
|
||||
{
|
||||
mongoId,
|
||||
},
|
||||
{ enabled: !!mongoId },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mongo.update.useMutation();
|
||||
const form = useForm<AddResourcesMongo>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(addResourcesMongo),
|
||||
});
|
||||
const { data, refetch } = api.mongo.one.useQuery(
|
||||
{
|
||||
mongoId,
|
||||
},
|
||||
{ enabled: !!mongoId },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mongo.update.useMutation();
|
||||
const form = useForm<AddResourcesMongo>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(addResourcesMongo),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
cpuLimit: data?.cpuLimit || undefined,
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
cpuLimit: data?.cpuLimit || undefined,
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: AddResourcesMongo) => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
memoryReservation: formData.memoryReservation || null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Resources Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to decrease or increase the resources to a specific
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
const onSubmit = async (formData: AddResourcesMongo) => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
memoryReservation: formData.memoryReservation || null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Resources Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to Update the resources");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to decrease or increase the resources to a specific
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after modify the resources to apply
|
||||
the changes.
|
||||
</AlertBlock>
|
||||
<Form {...form}>
|
||||
<AlertBlock type="info">
|
||||
Please remember to click Redeploy after modify the resources to
|
||||
apply the changes.
|
||||
</AlertBlock>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryReservation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="256 MB"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memoryLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1024 MB"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuReservation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuLimit"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"2"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cpuReservation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Cpu Reservation</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"1"}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
// Si el campo está vacío, establece el valor como null.
|
||||
field.onChange(null);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
// Solo actualiza el valor si se convierte a un número válido.
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -16,47 +16,47 @@ import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
mongoId: string;
|
||||
}
|
||||
|
||||
export const DeleteMongo = ({ mongoId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
||||
const { push } = useRouter();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground " />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
database
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
mongoId,
|
||||
})
|
||||
.then((data) => {
|
||||
push(`/dashboard/project/${data?.projectId}`);
|
||||
toast.success("Database delete succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete the database");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import React, { useEffect } from "react";
|
||||
@@ -85,7 +85,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -93,8 +93,11 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="MONGO_PASSWORD=1234567678"
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@@ -50,17 +50,13 @@ export const DeployMongo = ({ mongoId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mongoId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
@@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user