mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
195 Commits
v0.20.6
...
feat/caddy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a8508e623 | ||
|
|
e14f2780af | ||
|
|
33ab87f3db | ||
|
|
571d73a5b6 | ||
|
|
a630909612 | ||
|
|
8eaa006f0f | ||
|
|
8e8bc3e71e | ||
|
|
f4cd617107 | ||
|
|
48cfe66a6b | ||
|
|
bdc10cacef | ||
|
|
8fbad8a26e | ||
|
|
0f36bcb04e | ||
|
|
f4054453b4 | ||
|
|
dbd36fc024 | ||
|
|
850d06a32c | ||
|
|
dfd3dc180d | ||
|
|
3d42bfc81b | ||
|
|
d2eaa4b40b | ||
|
|
7d7f2b4b1f | ||
|
|
8e97c63faa | ||
|
|
74ec8f4594 | ||
|
|
76c0bff13a | ||
|
|
9b5cd0f5fe | ||
|
|
efee798880 | ||
|
|
1c470b8ba7 | ||
|
|
692864ced1 | ||
|
|
9ca61476d2 | ||
|
|
773a610be1 | ||
|
|
37f9e073f0 | ||
|
|
d335a9515d | ||
|
|
7a5a3de43d | ||
|
|
ee6ad07c0a | ||
|
|
48fe26b204 | ||
|
|
3ede89fe8a | ||
|
|
fa698d173e | ||
|
|
1279fac137 | ||
|
|
0e1f0b42ee | ||
|
|
05f43ad06b | ||
|
|
8f0697b0e9 | ||
|
|
61a20f13e2 | ||
|
|
148b1ff2db | ||
|
|
1beceb7ee7 | ||
|
|
bea0316bbd | ||
|
|
b2a8572d10 | ||
|
|
2352939e87 | ||
|
|
48ec0a74ad | ||
|
|
bca6af77fd | ||
|
|
b3bd9ba1ce | ||
|
|
5a9c763c4f | ||
|
|
4b51744d0d | ||
|
|
e5a3e56e13 | ||
|
|
42fa4008ab | ||
|
|
1605aedd6e | ||
|
|
14bc26e065 | ||
|
|
6c8eb3b711 | ||
|
|
cb20950dd9 | ||
|
|
350bed217c | ||
|
|
7ac7481343 | ||
|
|
d9c34c4524 | ||
|
|
e83efa3379 | ||
|
|
5863e45c13 | ||
|
|
2c09b63bf9 | ||
|
|
eff2657e70 | ||
|
|
36172491a4 | ||
|
|
d43b098a7a | ||
|
|
8479f20205 | ||
|
|
6cb4159d54 | ||
|
|
031d0ce315 | ||
|
|
131a1acbbe | ||
|
|
9a839de022 | ||
|
|
b9de05015f | ||
|
|
e176def5b6 | ||
|
|
94c947e288 | ||
|
|
0bdaa81263 | ||
|
|
baf36b6fb6 | ||
|
|
d632e83799 | ||
|
|
6f52edd845 | ||
|
|
9d0f5bc8cd | ||
|
|
3dc558c260 | ||
|
|
180aa34140 | ||
|
|
ffc85b04a8 | ||
|
|
dbcfc702d4 | ||
|
|
7805efc738 | ||
|
|
3910e22412 | ||
|
|
2f16034cb0 | ||
|
|
d4925dd2b7 | ||
|
|
5aba6c79a0 | ||
|
|
84f5627471 | ||
|
|
4eaf8fee0f | ||
|
|
adee87b6da | ||
|
|
e5e987fcf9 | ||
|
|
d0a6373dcc | ||
|
|
8ed44066ad | ||
|
|
befe2193a7 | ||
|
|
f20c73cdee | ||
|
|
16bfc09202 | ||
|
|
d54a61b2a4 | ||
|
|
60c09a6434 | ||
|
|
5361e9074f | ||
|
|
13d4dea504 | ||
|
|
ffc2d593e4 | ||
|
|
297439a348 | ||
|
|
ff3e067866 | ||
|
|
f008a45bf2 | ||
|
|
50c8503cf9 | ||
|
|
930a03de60 | ||
|
|
2d3d86e823 | ||
|
|
7bab166e1b | ||
|
|
7a6e1dbc1b | ||
|
|
17a859d26d | ||
|
|
d793c6a2ec | ||
|
|
3adb9d54f4 | ||
|
|
7144adbf0c | ||
|
|
55328468d1 | ||
|
|
6968cb6930 | ||
|
|
a431e4c58e | ||
|
|
fe967239b4 | ||
|
|
c5b4b85470 | ||
|
|
b1ef9d25b1 | ||
|
|
74f7c51530 | ||
|
|
4ba2b9fe8d | ||
|
|
413eda50f4 | ||
|
|
9f09681708 | ||
|
|
8eb174812d | ||
|
|
be77f114eb | ||
|
|
ca42708035 | ||
|
|
8b03454a87 | ||
|
|
fa7f749f84 | ||
|
|
3daecd7d71 | ||
|
|
0666b5b292 | ||
|
|
b288ddd826 | ||
|
|
beadcf871a | ||
|
|
ee49dadf0b | ||
|
|
46de83a1de | ||
|
|
fee5024b7d | ||
|
|
1f28a21835 | ||
|
|
0114b371f5 | ||
|
|
66d6cb5710 | ||
|
|
5927c7c3c5 | ||
|
|
84afcf0de5 | ||
|
|
e3527f7d69 | ||
|
|
39f4a35cc8 | ||
|
|
e0433e9f7b | ||
|
|
d29ff881fc | ||
|
|
568c3a1d06 | ||
|
|
e9fd280fa2 | ||
|
|
5d5913f39d | ||
|
|
f04c8a36af | ||
|
|
d5137d5d3a | ||
|
|
9535fca28f | ||
|
|
dd62d603e0 | ||
|
|
8d227e2a2c | ||
|
|
048c8ffc11 | ||
|
|
b59597630c | ||
|
|
707463f973 | ||
|
|
4b3e0805a4 | ||
|
|
148c30f604 | ||
|
|
95f79f2afb | ||
|
|
a067abd3e4 | ||
|
|
9359ee7a04 | ||
|
|
fc7eff94b6 | ||
|
|
ff3d444b89 | ||
|
|
530ad31aaa | ||
|
|
68d0a48843 | ||
|
|
a4e4d1c467 | ||
|
|
56d8defebe | ||
|
|
997e755b6f | ||
|
|
852011dde8 | ||
|
|
d7ef201adb | ||
|
|
91183056f0 | ||
|
|
03bd4398d0 | ||
|
|
8c260eff72 | ||
|
|
4eef65f1b7 | ||
|
|
a7535c6862 | ||
|
|
6e28196b0e | ||
|
|
18bacae175 | ||
|
|
f2be5a378e | ||
|
|
aef24296b9 | ||
|
|
7123b9b109 | ||
|
|
891dc840f5 | ||
|
|
bc78100613 | ||
|
|
ff22404b3b | ||
|
|
17330ca71a | ||
|
|
2898a5e575 | ||
|
|
fac8ea7a30 | ||
|
|
9a11d0db97 | ||
|
|
3cdf4c426c | ||
|
|
7cb184dc97 | ||
|
|
fe57333f84 | ||
|
|
04fd77c3a9 | ||
|
|
cf28640188 | ||
|
|
ea39b152f4 | ||
|
|
027406547e | ||
|
|
2974a8183e | ||
|
|
ac0922d742 |
2
.github/workflows/dokploy.yml
vendored
2
.github/workflows/dokploy.yml
vendored
@@ -2,7 +2,7 @@ name: Dokploy Docker Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, canary, "feat/better-auth-2"]
|
branches: [main, canary, "1061-custom-docker-service-hostname"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: dokploy/dokploy
|
IMAGE_NAME: dokploy/dokploy
|
||||||
|
|||||||
22
.github/workflows/format.yml
vendored
Normal file
22
.github/workflows/format.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: autofix.ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [canary]
|
||||||
|
pull_request:
|
||||||
|
branches: [canary]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup biomeJs
|
||||||
|
uses: biomejs/setup-biome@v2
|
||||||
|
|
||||||
|
- name: Run Biome formatter
|
||||||
|
run: biome format . --write
|
||||||
|
|
||||||
|
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
|
||||||
@@ -52,7 +52,7 @@ feat: add new feature
|
|||||||
|
|
||||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||||
|
|
||||||
We use Node v20.9.0
|
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dokploy/dokploy.git
|
git clone https://github.com/dokploy/dokploy.git
|
||||||
@@ -61,9 +61,9 @@ pnpm install
|
|||||||
cp apps/dokploy/.env.example apps/dokploy/.env
|
cp apps/dokploy/.env.example apps/dokploy/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Requirements
|
||||||
|
|
||||||
Is required to have **Docker** installed on your machine.
|
- [Docker](/GUIDES.md#docker)
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
@@ -87,6 +87,8 @@ pnpm run dokploy:dev
|
|||||||
|
|
||||||
Go to http://localhost:3000 to see the development server
|
Go to http://localhost:3000 to see the development server
|
||||||
|
|
||||||
|
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only the necessary files
|
# Copy only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
|
|||||||
49
GUIDES.md
Normal file
49
GUIDES.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
Here's how to install docker on different operating systems:
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
1. Visit [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop)
|
||||||
|
2. Download the Docker Desktop installer
|
||||||
|
3. Double-click the downloaded `.dmg` file
|
||||||
|
4. Drag Docker to your Applications folder
|
||||||
|
5. Open Docker Desktop from Applications
|
||||||
|
6. Follow the onboarding tutorial if desired
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
### Ubuntu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update package index
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
# Install prerequisites
|
||||||
|
sudo apt-get install \
|
||||||
|
apt-transport-https \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gnupg \
|
||||||
|
lsb-release
|
||||||
|
|
||||||
|
# Add Docker's official GPG key
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||||
|
|
||||||
|
# Set up stable repository
|
||||||
|
echo \
|
||||||
|
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
||||||
|
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
# Install Docker Engine
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install docker-ce docker-ce-cli containerd.io
|
||||||
|
```
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
1. Enable WSL2 if not already enabled
|
||||||
|
2. Visit [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop)
|
||||||
|
3. Download the installer
|
||||||
|
4. Run the installer and follow the prompts
|
||||||
|
5. Start Docker Desktop from the Start menu
|
||||||
@@ -1006,7 +1006,7 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-config-testhash:
|
db-config-testhash:
|
||||||
`) as ComposeSpecification;
|
`);
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
const composeData = load(composeFileComplex) as ComposeSpecification;
|
||||||
@@ -1115,3 +1115,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
|||||||
|
|
||||||
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
|
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const composeFileBackrest = `
|
||||||
|
services:
|
||||||
|
backrest:
|
||||||
|
image: garethgeorge/backrest:v1.7.3
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 9898
|
||||||
|
environment:
|
||||||
|
- BACKREST_PORT=9898
|
||||||
|
- BACKREST_DATA=/data
|
||||||
|
- BACKREST_CONFIG=/config/config.json
|
||||||
|
- XDG_CACHE_HOME=/cache
|
||||||
|
- TZ=\${TZ}
|
||||||
|
volumes:
|
||||||
|
- backrest/data:/data
|
||||||
|
- backrest/config:/config
|
||||||
|
- backrest/cache:/cache
|
||||||
|
- /:/userdata:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backrest:
|
||||||
|
backrest-cache:
|
||||||
|
`;
|
||||||
|
|
||||||
|
const expectedDockerComposeBackrest = load(`
|
||||||
|
services:
|
||||||
|
backrest:
|
||||||
|
image: garethgeorge/backrest:v1.7.3
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 9898
|
||||||
|
environment:
|
||||||
|
- BACKREST_PORT=9898
|
||||||
|
- BACKREST_DATA=/data
|
||||||
|
- BACKREST_CONFIG=/config/config.json
|
||||||
|
- XDG_CACHE_HOME=/cache
|
||||||
|
- TZ=\${TZ}
|
||||||
|
volumes:
|
||||||
|
- backrest-testhash/data:/data
|
||||||
|
- backrest-testhash/config:/config
|
||||||
|
- backrest-testhash/cache:/cache
|
||||||
|
- /:/userdata:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backrest-testhash:
|
||||||
|
backrest-cache-testhash:
|
||||||
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
|
test("Should handle volume paths with subdirectories correctly", () => {
|
||||||
|
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
||||||
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
|
|
||||||
|
expect(updatedComposeData).toEqual(expectedDockerComposeBackrest);
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ if (typeof window === "undefined") {
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
|
giteaBranch: "",
|
||||||
|
giteaBuildPath: "",
|
||||||
|
giteaId: "",
|
||||||
|
giteaOwner: "",
|
||||||
|
giteaRepository: "",
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import type { Schema } from "@dokploy/server/templates";
|
||||||
import type { CompleteTemplate } from "@dokploy/server/templates/processors";
|
import type { CompleteTemplate } from "@dokploy/server/templates/processors";
|
||||||
import { processTemplate } from "@dokploy/server/templates/processors";
|
import { processTemplate } from "@dokploy/server/templates/processors";
|
||||||
import type { Schema } from "@dokploy/server/templates";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("processTemplate", () => {
|
describe("processTemplate", () => {
|
||||||
// Mock schema for testing
|
// Mock schema for testing
|
||||||
@@ -233,6 +233,49 @@ describe("processTemplate", () => {
|
|||||||
expect(base64Value.length).toBeGreaterThanOrEqual(42);
|
expect(base64Value.length).toBeGreaterThanOrEqual(42);
|
||||||
expect(base64Value.length).toBeLessThanOrEqual(44);
|
expect(base64Value.length).toBeLessThanOrEqual(44);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle boolean values in env vars when provided as an array", () => {
|
||||||
|
const template: CompleteTemplate = {
|
||||||
|
metadata: {} as any,
|
||||||
|
variables: {},
|
||||||
|
config: {
|
||||||
|
domains: [],
|
||||||
|
env: [
|
||||||
|
"ENABLE_USER_SIGN_UP=false",
|
||||||
|
"DEBUG_MODE=true",
|
||||||
|
"SOME_NUMBER=42",
|
||||||
|
],
|
||||||
|
mounts: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = processTemplate(template, mockSchema);
|
||||||
|
expect(result.envs).toHaveLength(3);
|
||||||
|
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||||
|
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||||
|
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle boolean values in env vars when provided as an object", () => {
|
||||||
|
const template: CompleteTemplate = {
|
||||||
|
metadata: {} as any,
|
||||||
|
variables: {},
|
||||||
|
config: {
|
||||||
|
domains: [],
|
||||||
|
env: {
|
||||||
|
ENABLE_USER_SIGN_UP: false,
|
||||||
|
DEBUG_MODE: true,
|
||||||
|
SOME_NUMBER: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = processTemplate(template, mockSchema);
|
||||||
|
expect(result.envs).toHaveLength(3);
|
||||||
|
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||||
|
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||||
|
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("mounts processing", () => {
|
describe("mounts processing", () => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: User = {
|
const baseAdmin: User = {
|
||||||
|
https: false,
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
@@ -73,7 +74,6 @@ beforeEach(() => {
|
|||||||
|
|
||||||
test("Should read the configuration file", () => {
|
test("Should read the configuration file", () => {
|
||||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
|
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
|
||||||
"dokploy-service-app",
|
"dokploy-service-app",
|
||||||
);
|
);
|
||||||
@@ -83,6 +83,7 @@ test("Should apply redirect-to-https", () => {
|
|||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{
|
{
|
||||||
...baseAdmin,
|
...baseAdmin,
|
||||||
|
https: true,
|
||||||
certificateType: "letsencrypt",
|
certificateType: "letsencrypt",
|
||||||
},
|
},
|
||||||
"example.com",
|
"example.com",
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { expect, test } from "vitest";
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
|
giteaRepository: "",
|
||||||
|
giteaOwner: "",
|
||||||
|
giteaBranch: "",
|
||||||
|
giteaBuildPath: "",
|
||||||
|
giteaId: "",
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
|
|||||||
61
apps/dokploy/__test__/utils/backups.test.ts
Normal file
61
apps/dokploy/__test__/utils/backups.test.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
|
||||||
|
|
||||||
|
describe("normalizeS3Path", () => {
|
||||||
|
test("should handle empty and whitespace-only prefix", () => {
|
||||||
|
expect(normalizeS3Path("")).toBe("");
|
||||||
|
expect(normalizeS3Path("/")).toBe("");
|
||||||
|
expect(normalizeS3Path(" ")).toBe("");
|
||||||
|
expect(normalizeS3Path("\t")).toBe("");
|
||||||
|
expect(normalizeS3Path("\n")).toBe("");
|
||||||
|
expect(normalizeS3Path(" \n \t ")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should trim whitespace from prefix", () => {
|
||||||
|
expect(normalizeS3Path(" prefix")).toBe("prefix/");
|
||||||
|
expect(normalizeS3Path("prefix ")).toBe("prefix/");
|
||||||
|
expect(normalizeS3Path(" prefix ")).toBe("prefix/");
|
||||||
|
expect(normalizeS3Path("\tprefix\t")).toBe("prefix/");
|
||||||
|
expect(normalizeS3Path(" prefix/nested ")).toBe("prefix/nested/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should remove leading slashes", () => {
|
||||||
|
expect(normalizeS3Path("/prefix")).toBe("prefix/");
|
||||||
|
expect(normalizeS3Path("///prefix")).toBe("prefix/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should remove trailing slashes", () => {
|
||||||
|
expect(normalizeS3Path("prefix/")).toBe("prefix/");
|
||||||
|
expect(normalizeS3Path("prefix///")).toBe("prefix/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should remove both leading and trailing slashes", () => {
|
||||||
|
expect(normalizeS3Path("/prefix/")).toBe("prefix/");
|
||||||
|
expect(normalizeS3Path("///prefix///")).toBe("prefix/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle nested paths", () => {
|
||||||
|
expect(normalizeS3Path("prefix/nested")).toBe("prefix/nested/");
|
||||||
|
expect(normalizeS3Path("/prefix/nested/")).toBe("prefix/nested/");
|
||||||
|
expect(normalizeS3Path("///prefix/nested///")).toBe("prefix/nested/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should preserve middle slashes", () => {
|
||||||
|
expect(normalizeS3Path("prefix/nested/deep")).toBe("prefix/nested/deep/");
|
||||||
|
expect(normalizeS3Path("/prefix/nested/deep/")).toBe("prefix/nested/deep/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle special characters", () => {
|
||||||
|
expect(normalizeS3Path("prefix-with-dashes")).toBe("prefix-with-dashes/");
|
||||||
|
expect(normalizeS3Path("prefix_with_underscores")).toBe(
|
||||||
|
"prefix_with_underscores/",
|
||||||
|
);
|
||||||
|
expect(normalizeS3Path("prefix.with.dots")).toBe("prefix.with.dots/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle the cases from the bug report", () => {
|
||||||
|
expect(normalizeS3Path("instance-backups/")).toBe("instance-backups/");
|
||||||
|
expect(normalizeS3Path("/instance-backups/")).toBe("instance-backups/");
|
||||||
|
expect(normalizeS3Path("instance-backups")).toBe("instance-backups/");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,7 +40,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectchema = z.object({
|
||||||
replicas: z.number(),
|
replicas: z.number().min(1, "Replicas must be at least 1"),
|
||||||
registryId: z.string(),
|
registryId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,9 +130,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
placeholder="1"
|
placeholder="1"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(Number(e.target.value));
|
const value = e.target.value;
|
||||||
|
field.onChange(value === "" ? 0 : Number(value));
|
||||||
}}
|
}}
|
||||||
type="number"
|
type="number"
|
||||||
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -32,7 +33,6 @@ import { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
|
|
||||||
const ImportSchema = z.object({
|
const ImportSchema = z.object({
|
||||||
base64: z.string(),
|
base64: z.string(),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
enum BuildType {
|
export enum BuildType {
|
||||||
dockerfile = "dockerfile",
|
dockerfile = "dockerfile",
|
||||||
heroku_buildpacks = "heroku_buildpacks",
|
heroku_buildpacks = "heroku_buildpacks",
|
||||||
paketo_buildpacks = "paketo_buildpacks",
|
paketo_buildpacks = "paketo_buildpacks",
|
||||||
@@ -29,9 +29,18 @@ enum BuildType {
|
|||||||
railpack = "railpack",
|
railpack = "railpack",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildTypeDisplayMap: Record<BuildType, string> = {
|
||||||
|
[BuildType.dockerfile]: "Dockerfile",
|
||||||
|
[BuildType.railpack]: "Railpack",
|
||||||
|
[BuildType.nixpacks]: "Nixpacks",
|
||||||
|
[BuildType.heroku_buildpacks]: "Heroku Buildpacks",
|
||||||
|
[BuildType.paketo_buildpacks]: "Paketo Buildpacks",
|
||||||
|
[BuildType.static]: "Static",
|
||||||
|
};
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("buildType", [
|
const mySchema = z.discriminatedUnion("buildType", [
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("dockerfile"),
|
buildType: z.literal(BuildType.dockerfile),
|
||||||
dockerfile: z
|
dockerfile: z
|
||||||
.string({
|
.string({
|
||||||
required_error: "Dockerfile path is required",
|
required_error: "Dockerfile path is required",
|
||||||
@@ -42,39 +51,88 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
dockerBuildStage: z.string().nullable().default(""),
|
dockerBuildStage: z.string().nullable().default(""),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("heroku_buildpacks"),
|
buildType: z.literal(BuildType.heroku_buildpacks),
|
||||||
herokuVersion: z.string().nullable().default(""),
|
herokuVersion: z.string().nullable().default(""),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("paketo_buildpacks"),
|
buildType: z.literal(BuildType.paketo_buildpacks),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("nixpacks"),
|
buildType: z.literal(BuildType.nixpacks),
|
||||||
publishDirectory: z.string().optional(),
|
publishDirectory: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("static"),
|
buildType: z.literal(BuildType.static),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("railpack"),
|
buildType: z.literal(BuildType.railpack),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type AddTemplate = z.infer<typeof mySchema>;
|
type AddTemplate = z.infer<typeof mySchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApplicationData {
|
||||||
|
buildType: BuildType;
|
||||||
|
dockerfile?: string | null;
|
||||||
|
dockerContextPath?: string | null;
|
||||||
|
dockerBuildStage?: string | null;
|
||||||
|
herokuVersion?: string | null;
|
||||||
|
publishDirectory?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidBuildType(value: string): value is BuildType {
|
||||||
|
return Object.values(BuildType).includes(value as BuildType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetData = (data: ApplicationData): AddTemplate => {
|
||||||
|
switch (data.buildType) {
|
||||||
|
case BuildType.dockerfile:
|
||||||
|
return {
|
||||||
|
buildType: BuildType.dockerfile,
|
||||||
|
dockerfile: data.dockerfile || "",
|
||||||
|
dockerContextPath: data.dockerContextPath || "",
|
||||||
|
dockerBuildStage: data.dockerBuildStage || "",
|
||||||
|
};
|
||||||
|
case BuildType.heroku_buildpacks:
|
||||||
|
return {
|
||||||
|
buildType: BuildType.heroku_buildpacks,
|
||||||
|
herokuVersion: data.herokuVersion || "",
|
||||||
|
};
|
||||||
|
case BuildType.nixpacks:
|
||||||
|
return {
|
||||||
|
buildType: BuildType.nixpacks,
|
||||||
|
publishDirectory: data.publishDirectory || undefined,
|
||||||
|
};
|
||||||
|
case BuildType.paketo_buildpacks:
|
||||||
|
return {
|
||||||
|
buildType: BuildType.paketo_buildpacks,
|
||||||
|
};
|
||||||
|
case BuildType.static:
|
||||||
|
return {
|
||||||
|
buildType: BuildType.static,
|
||||||
|
};
|
||||||
|
case BuildType.railpack:
|
||||||
|
return {
|
||||||
|
buildType: BuildType.railpack,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
const buildType = data.buildType as BuildType;
|
||||||
|
return {
|
||||||
|
buildType,
|
||||||
|
} as AddTemplate;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.application.saveBuildType.useMutation();
|
api.application.saveBuildType.useMutation();
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{ applicationId },
|
||||||
applicationId,
|
{ enabled: !!applicationId },
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!applicationId,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = useForm<AddTemplate>({
|
const form = useForm<AddTemplate>({
|
||||||
@@ -85,46 +143,36 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const buildType = form.watch("buildType");
|
const buildType = form.watch("buildType");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.buildType === "dockerfile") {
|
const typedData: ApplicationData = {
|
||||||
form.reset({
|
...data,
|
||||||
buildType: data.buildType,
|
buildType: isValidBuildType(data.buildType)
|
||||||
...(data.buildType && {
|
? (data.buildType as BuildType)
|
||||||
dockerfile: data.dockerfile || "",
|
: BuildType.nixpacks, // fallback
|
||||||
dockerContextPath: data.dockerContextPath || "",
|
};
|
||||||
dockerBuildStage: data.dockerBuildStage || "",
|
|
||||||
}),
|
form.reset(resetData(typedData));
|
||||||
});
|
|
||||||
} else if (data.buildType === "heroku_buildpacks") {
|
|
||||||
form.reset({
|
|
||||||
buildType: data.buildType,
|
|
||||||
...(data.buildType && {
|
|
||||||
herokuVersion: data.herokuVersion || "",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.reset({
|
|
||||||
buildType: data.buildType,
|
|
||||||
publishDirectory: data.publishDirectory || undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}, [data, form]);
|
||||||
}, [form.formState.isSubmitSuccessful, form.reset, data, form]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddTemplate) => {
|
const onSubmit = async (data: AddTemplate) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
buildType: data.buildType,
|
buildType: data.buildType,
|
||||||
publishDirectory:
|
publishDirectory:
|
||||||
data.buildType === "nixpacks" ? data.publishDirectory : null,
|
data.buildType === BuildType.nixpacks ? data.publishDirectory : null,
|
||||||
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
|
dockerfile:
|
||||||
|
data.buildType === BuildType.dockerfile ? data.dockerfile : null,
|
||||||
dockerContextPath:
|
dockerContextPath:
|
||||||
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
data.buildType === BuildType.dockerfile ? data.dockerContextPath : null,
|
||||||
dockerBuildStage:
|
dockerBuildStage:
|
||||||
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
|
data.buildType === BuildType.dockerfile ? data.dockerBuildStage : null,
|
||||||
herokuVersion:
|
herokuVersion:
|
||||||
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
|
data.buildType === BuildType.heroku_buildpacks
|
||||||
|
? data.herokuVersion
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Build type saved");
|
toast.success("Build type saved");
|
||||||
@@ -160,8 +208,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="buildType"
|
name="buildType"
|
||||||
defaultValue={form.control._defaultValues.buildType}
|
defaultValue={form.control._defaultValues.buildType}
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
|
||||||
<FormItem className="space-y-3">
|
<FormItem className="space-y-3">
|
||||||
<FormLabel>Build Type</FormLabel>
|
<FormLabel>Build Type</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -170,183 +217,134 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
value={field.value}
|
value={field.value}
|
||||||
className="flex flex-col space-y-1"
|
className="flex flex-col space-y-1"
|
||||||
>
|
>
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
{Object.entries(buildTypeDisplayMap).map(
|
||||||
|
([value, label]) => (
|
||||||
|
<FormItem
|
||||||
|
key={value}
|
||||||
|
className="flex items-center space-x-3 space-y-0"
|
||||||
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value="dockerfile" />
|
<RadioGroupItem value={value} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="font-normal">
|
<FormLabel className="font-normal">
|
||||||
Dockerfile
|
{label}
|
||||||
|
{value === BuildType.railpack && (
|
||||||
|
<Badge className="ml-2 px-1 text-xs">New</Badge>
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
),
|
||||||
<FormControl>
|
)}
|
||||||
<RadioGroupItem value="railpack" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Railpack{" "}
|
|
||||||
<Badge className="ml-1 text-xs px-1">New</Badge>
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="nixpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Nixpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="heroku_buildpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Heroku Buildpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="paketo_buildpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Paketo Buildpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="static" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">Static</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{buildType === "heroku_buildpacks" && (
|
{buildType === BuildType.heroku_buildpacks && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="herokuVersion"
|
name="herokuVersion"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Heroku Version (Optional)</FormLabel>
|
<FormLabel>Heroku Version (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"Heroku Version (Default: 24)"}
|
placeholder="Heroku Version (Default: 24)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{buildType === "dockerfile" && (
|
{buildType === BuildType.dockerfile && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerfile"
|
name="dockerfile"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Docker File</FormLabel>
|
<FormLabel>Docker File</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"Path of your docker file"}
|
placeholder="Path of your docker file"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerContextPath"
|
name="dockerContextPath"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Docker Context Path</FormLabel>
|
<FormLabel>Docker Context Path</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={
|
placeholder="Path of your docker context (default: .)"
|
||||||
"Path of your docker context default: ."
|
|
||||||
}
|
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerBuildStage"
|
name="dockerBuildStage"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Docker Build Stage</FormLabel>
|
<FormLabel>Docker Build Stage</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Allows you to target a specific stage in a
|
Allows you to target a specific stage in a Multi-stage
|
||||||
Multi-stage Dockerfile. If empty, Docker defaults to
|
Dockerfile. If empty, Docker defaults to build the
|
||||||
build the last defined stage.
|
last defined stage.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"E.g. production"}
|
placeholder="E.g. production"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{buildType === BuildType.nixpacks && (
|
||||||
{buildType === "nixpacks" && (
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="publishDirectory"
|
name="publishDirectory"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Publish Directory</FormLabel>
|
<FormLabel>Publish Directory</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Allows you to serve a single directory via NGINX after
|
Allows you to serve a single directory via NGINX after
|
||||||
the build phase. Useful if the final build assets
|
the build phase. Useful if the final build assets should
|
||||||
should be served as a static site.
|
be served as a static site.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"Publish Directory"}
|
placeholder="Publish Directory"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ import { toast } from "sonner";
|
|||||||
import { domain } from "@/server/db/validations/domain";
|
import { domain } from "@/server/db/validations/domain";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Dices } from "lucide-react";
|
import { Dices } from "lucide-react";
|
||||||
import type z from "zod";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { Form } from "@/components/ui/form";
|
|||||||
import { Secrets } from "@/components/ui/secrets";
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const BitbucketProviderSchema = z.object({
|
const BitbucketProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -115,7 +115,11 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Username" autoComplete="username" {...field} />
|
<Input
|
||||||
|
placeholder="Username"
|
||||||
|
autoComplete="username"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -130,7 +134,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Password" autoComplete="one-time-code" {...field} type="password" />
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
|
|
||||||
const GitProviderSchema = z.object({
|
const GitProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -0,0 +1,515 @@
|
|||||||
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
interface GiteaRepository {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
id: number;
|
||||||
|
owner: {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GiteaBranch {
|
||||||
|
name: string;
|
||||||
|
commit: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const GiteaProviderSchema = z.object({
|
||||||
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
repository: z
|
||||||
|
.object({
|
||||||
|
repo: z.string().min(1, "Repo is required"),
|
||||||
|
owner: z.string().min(1, "Owner is required"),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
branch: z.string().min(1, "Branch is required"),
|
||||||
|
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading: isSavingGiteaProvider } =
|
||||||
|
api.application.saveGiteaProvider.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<GiteaProvider>({
|
||||||
|
defaultValues: {
|
||||||
|
buildPath: "/",
|
||||||
|
repository: {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
},
|
||||||
|
giteaId: "",
|
||||||
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(GiteaProviderSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const repository = form.watch("repository");
|
||||||
|
const giteaId = form.watch("giteaId");
|
||||||
|
|
||||||
|
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
|
||||||
|
{ giteaId },
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: repositories,
|
||||||
|
isLoading: isLoadingRepositories,
|
||||||
|
error,
|
||||||
|
} = api.gitea.getGiteaRepositories.useQuery(
|
||||||
|
{
|
||||||
|
giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: branches,
|
||||||
|
fetchStatus,
|
||||||
|
status,
|
||||||
|
} = api.gitea.getGiteaBranches.useQuery(
|
||||||
|
{
|
||||||
|
owner: repository?.owner,
|
||||||
|
repositoryName: repository?.repo,
|
||||||
|
giteaId: giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
branch: data.giteaBranch || "",
|
||||||
|
repository: {
|
||||||
|
repo: data.giteaRepository || "",
|
||||||
|
owner: data.giteaOwner || "",
|
||||||
|
},
|
||||||
|
buildPath: data.giteaBuildPath || "/",
|
||||||
|
giteaId: data.giteaId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
|
await mutateAsync({
|
||||||
|
giteaBranch: data.branch,
|
||||||
|
giteaRepository: data.repository.repo,
|
||||||
|
giteaOwner: data.repository.owner,
|
||||||
|
giteaBuildPath: data.buildPath,
|
||||||
|
giteaId: data.giteaId,
|
||||||
|
applicationId,
|
||||||
|
watchPaths: data.watchPaths,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Service Provider Saved");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error saving the Gitea provider");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4 py-3"
|
||||||
|
>
|
||||||
|
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<FormLabel>Gitea Account</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Gitea Account" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{giteaProviders?.map((giteaProvider) => (
|
||||||
|
<SelectItem
|
||||||
|
key={giteaProvider.giteaId}
|
||||||
|
value={giteaProvider.giteaId}
|
||||||
|
>
|
||||||
|
{giteaProvider.gitProvider.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="repository"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GiteaIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLoadingRepositories
|
||||||
|
? "Loading...."
|
||||||
|
: field.value.owner
|
||||||
|
? repositories?.find(
|
||||||
|
(repo: GiteaRepository) =>
|
||||||
|
repo.name === field.value.repo,
|
||||||
|
)?.name
|
||||||
|
: "Select repository"}
|
||||||
|
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search repository..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoadingRepositories && (
|
||||||
|
<span className="py-6 text-center text-sm">
|
||||||
|
Loading Repositories....
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandGroup>
|
||||||
|
{repositories && repositories.length === 0 && (
|
||||||
|
<CommandEmpty>
|
||||||
|
No repositories found.
|
||||||
|
</CommandEmpty>
|
||||||
|
)}
|
||||||
|
{repositories?.map((repo: GiteaRepository) => {
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
value={repo.name}
|
||||||
|
key={repo.url}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: repo.owner.username as string,
|
||||||
|
repo: repo.name,
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>{repo.name}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{repo.owner.username}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
repo.name === field.value.repo
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{form.formState.errors.repository && (
|
||||||
|
<p className={cn("text-sm font-medium text-destructive")}>
|
||||||
|
Repository is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="branch"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="block w-full">
|
||||||
|
<FormLabel>Branch</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
" w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{status === "loading" && fetchStatus === "fetching"
|
||||||
|
? "Loading...."
|
||||||
|
: field.value
|
||||||
|
? branches?.find(
|
||||||
|
(branch: GiteaBranch) =>
|
||||||
|
branch.name === field.value,
|
||||||
|
)?.name
|
||||||
|
: "Select branch"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search branch..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{status === "loading" && fetchStatus === "fetching" && (
|
||||||
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
Loading Branches....
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!repository?.owner && (
|
||||||
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
Select a repository
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandEmpty>No branch found.</CommandEmpty>
|
||||||
|
|
||||||
|
<CommandGroup>
|
||||||
|
{branches?.map((branch: GiteaBranch) => (
|
||||||
|
<CommandItem
|
||||||
|
value={branch.name}
|
||||||
|
key={branch.commit.id}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("branch", branch.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{branch.name}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
branch.name === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</Popover>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="buildPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Build Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="/" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path: string, index: number) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...field.value];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
field.onChange(newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...field.value, path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a path"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...field.value, path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
isLoading={isSavingGiteaProvider}
|
||||||
|
type="submit"
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -34,17 +36,15 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -35,17 +37,15 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
|
|
||||||
const GitlabProviderSchema = z.object({
|
const GitlabProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||||
|
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
||||||
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
DockerIcon,
|
DockerIcon,
|
||||||
GitIcon,
|
GitIcon,
|
||||||
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
@@ -18,7 +20,14 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
|||||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||||
|
|
||||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
|
type TabState =
|
||||||
|
| "github"
|
||||||
|
| "docker"
|
||||||
|
| "git"
|
||||||
|
| "drop"
|
||||||
|
| "gitlab"
|
||||||
|
| "bitbucket"
|
||||||
|
| "gitea";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -29,6 +38,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: bitbucketProviders } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||||
@@ -78,6 +88,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||||
Bitbucket
|
Bitbucket
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="gitea"
|
||||||
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
>
|
||||||
|
<GiteaIcon className="size-4 text-current fill-current" />
|
||||||
|
Gitea
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="docker"
|
value="docker"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
@@ -162,6 +179,26 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="gitea" className="w-full p-2">
|
||||||
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
|
<SaveGiteaProvider applicationId={applicationId} />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
To deploy using Gitea, you need to configure your account
|
||||||
|
first. Please, go to{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/git-providers"
|
||||||
|
className="text-foreground"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Link>{" "}
|
||||||
|
to do so.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="docker" className="w-full p-2">
|
<TabsContent value="docker" className="w-full p-2">
|
||||||
<SaveDockerProvider applicationId={applicationId} />
|
<SaveDockerProvider applicationId={applicationId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -298,7 +298,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
toast.success("Preview deployments enabled");
|
toast.success(
|
||||||
|
checked
|
||||||
|
? "Preview deployments enabled"
|
||||||
|
: "Preview deployments disabled",
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ import {
|
|||||||
import { domainCompose } from "@/server/db/validations/domain";
|
import { domainCompose } from "@/server/db/validations/domain";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||||
import type z from "zod";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
type Domain = z.infer<typeof domainCompose>;
|
type Domain = z.infer<typeof domainCompose>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const BitbucketProviderSchema = z.object({
|
const BitbucketProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -27,13 +28,12 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const GitProviderSchema = z.object({
|
const GitProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -0,0 +1,483 @@
|
|||||||
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import type { Repository } from "@/utils/gitea-utils";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const GiteaProviderSchema = z.object({
|
||||||
|
composePath: z.string().min(1),
|
||||||
|
repository: z
|
||||||
|
.object({
|
||||||
|
repo: z.string().min(1, "Repo is required"),
|
||||||
|
owner: z.string().min(1, "Owner is required"),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
branch: z.string().min(1, "Branch is required"),
|
||||||
|
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||||
|
const { mutateAsync, isLoading: isSavingGiteaProvider } =
|
||||||
|
api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<GiteaProvider>({
|
||||||
|
defaultValues: {
|
||||||
|
composePath: "./docker-compose.yml",
|
||||||
|
repository: {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
},
|
||||||
|
giteaId: "",
|
||||||
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(GiteaProviderSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const repository = form.watch("repository");
|
||||||
|
const giteaId = form.watch("giteaId");
|
||||||
|
|
||||||
|
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
|
||||||
|
{ giteaId },
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: repositories,
|
||||||
|
isLoading: isLoadingRepositories,
|
||||||
|
error,
|
||||||
|
} = api.gitea.getGiteaRepositories.useQuery<Repository[]>(
|
||||||
|
{
|
||||||
|
giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: branches,
|
||||||
|
fetchStatus,
|
||||||
|
status,
|
||||||
|
} = api.gitea.getGiteaBranches.useQuery(
|
||||||
|
{
|
||||||
|
owner: repository?.owner,
|
||||||
|
repositoryName: repository?.repo,
|
||||||
|
giteaId: giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
branch: data.giteaBranch || "",
|
||||||
|
repository: {
|
||||||
|
repo: data.giteaRepository || "",
|
||||||
|
owner: data.giteaOwner || "",
|
||||||
|
},
|
||||||
|
composePath: data.composePath || "./docker-compose.yml",
|
||||||
|
giteaId: data.giteaId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
|
await mutateAsync({
|
||||||
|
giteaBranch: data.branch,
|
||||||
|
giteaRepository: data.repository.repo,
|
||||||
|
giteaOwner: data.repository.owner,
|
||||||
|
composePath: data.composePath,
|
||||||
|
giteaId: data.giteaId,
|
||||||
|
composeId,
|
||||||
|
sourceType: "gitea",
|
||||||
|
composeStatus: "idle",
|
||||||
|
watchPaths: data.watchPaths,
|
||||||
|
} as any)
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Service Provider Saved");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error saving the Gitea provider");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4 py-3"
|
||||||
|
>
|
||||||
|
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<FormLabel>Gitea Account</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Gitea Account" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{giteaProviders?.map((giteaProvider) => (
|
||||||
|
<SelectItem
|
||||||
|
key={giteaProvider.giteaId}
|
||||||
|
value={giteaProvider.giteaId}
|
||||||
|
>
|
||||||
|
{giteaProvider.gitProvider.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="repository"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GiteaIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLoadingRepositories
|
||||||
|
? "Loading...."
|
||||||
|
: field.value.owner
|
||||||
|
? repositories?.find(
|
||||||
|
(repo) => repo.name === field.value.repo,
|
||||||
|
)?.name
|
||||||
|
: "Select repository"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search repository..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoadingRepositories && (
|
||||||
|
<span className="py-6 text-center text-sm">
|
||||||
|
Loading Repositories....
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandGroup>
|
||||||
|
{repositories?.map((repo) => (
|
||||||
|
<CommandItem
|
||||||
|
key={repo.url}
|
||||||
|
value={repo.name}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: repo.owner.username,
|
||||||
|
repo: repo.name,
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>{repo.name}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{repo.owner.username}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
repo.name === field.value.repo
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{form.formState.errors.repository && (
|
||||||
|
<p className={cn("text-sm font-medium text-destructive")}>
|
||||||
|
Repository is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="branch"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="block w-full">
|
||||||
|
<FormLabel>Branch</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{status === "loading" && fetchStatus === "fetching"
|
||||||
|
? "Loading...."
|
||||||
|
: field.value
|
||||||
|
? branches?.find(
|
||||||
|
(branch) => branch.name === field.value,
|
||||||
|
)?.name
|
||||||
|
: "Select branch"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search branches..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<CommandEmpty>No branches found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandGroup>
|
||||||
|
{branches?.map((branch) => (
|
||||||
|
<CommandItem
|
||||||
|
key={branch.name}
|
||||||
|
value={branch.name}
|
||||||
|
onSelect={() =>
|
||||||
|
form.setValue("branch", branch.name)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{branch.name}
|
||||||
|
</span>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
branch.name === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{form.formState.errors.branch && (
|
||||||
|
<p className={cn("text-sm font-medium text-destructive")}>
|
||||||
|
Branch is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="composePath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Compose Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="docker-compose.yml" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a path"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...(field.value || []), path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" isLoading={isSavingGiteaProvider}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -39,12 +40,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const GitlabProviderSchema = z.object({
|
const GitlabProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
GitIcon,
|
GitIcon,
|
||||||
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
@@ -14,10 +15,11 @@ import { ComposeFileEditor } from "../compose-file-editor";
|
|||||||
import { ShowConvertedCompose } from "../show-converted-compose";
|
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||||
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
||||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||||
|
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
|
||||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||||
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
||||||
|
|
||||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
|
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
@@ -27,9 +29,11 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: bitbucketProviders } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative w-full bg-transparent">
|
<Card className="group relative w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -55,20 +59,20 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="github"
|
value="github"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
>
|
>
|
||||||
<GithubIcon className="size-4 text-current fill-current" />
|
<GithubIcon className="size-4 text-current fill-current" />
|
||||||
Github
|
GitHub
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="gitlab"
|
value="gitlab"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
>
|
>
|
||||||
<GitlabIcon className="size-4 text-current fill-current" />
|
<GitlabIcon className="size-4 text-current fill-current" />
|
||||||
Gitlab
|
GitLab
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="bitbucket"
|
value="bitbucket"
|
||||||
@@ -77,7 +81,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||||
Bitbucket
|
Bitbucket
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="gitea"
|
||||||
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
>
|
||||||
|
<GiteaIcon className="size-4 text-current fill-current" /> Gitea
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="git"
|
value="git"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
@@ -94,6 +103,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="github" className="w-full p-2">
|
<TabsContent value="github" className="w-full p-2">
|
||||||
{githubProviders && githubProviders?.length > 0 ? (
|
{githubProviders && githubProviders?.length > 0 ? (
|
||||||
<SaveGithubProviderCompose composeId={composeId} />
|
<SaveGithubProviderCompose composeId={composeId} />
|
||||||
@@ -154,6 +164,26 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="gitea" className="w-full p-2">
|
||||||
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
|
<SaveGiteaProviderCompose composeId={composeId} />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
To deploy using Gitea, you need to configure your account
|
||||||
|
first. Please, go to{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/git-providers"
|
||||||
|
className="text-foreground"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Link>{" "}
|
||||||
|
to do so.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="git" className="w-full p-2">
|
<TabsContent value="git" className="w-full p-2">
|
||||||
<SaveGitProviderCompose composeId={composeId} />
|
<SaveGitProviderCompose composeId={composeId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Enable Isolated Deployment ({data?.appName})</FormLabel>
|
<FormLabel>
|
||||||
|
Enable Isolated Deployment ({data?.appName})
|
||||||
|
</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enable isolated deployment to the compose file.
|
Enable isolated deployment to the compose file.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Preview your docker-compose file with added domains. Note: At least
|
||||||
|
one domain must be specified for this conversion to take effect.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseType: "postgres" | "mariadb" | "mysql" | "mongo";
|
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
database: "",
|
database: databaseType === "web-server" ? "dokploy" : "",
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
prefix: "/",
|
prefix: "/",
|
||||||
@@ -112,6 +112,10 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
? {
|
? {
|
||||||
mongoId: databaseId,
|
mongoId: databaseId,
|
||||||
}
|
}
|
||||||
|
: databaseType === "web-server"
|
||||||
|
? {
|
||||||
|
userId: databaseId,
|
||||||
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
await createBackup({
|
await createBackup({
|
||||||
@@ -236,7 +240,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Database</FormLabel>
|
<FormLabel>Database</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={"dokploy"} {...field} />
|
<Input
|
||||||
|
disabled={databaseType === "web-server"}
|
||||||
|
placeholder={"dokploy"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -286,10 +294,15 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Keep the latest</FormLabel>
|
<FormLabel>Keep the latest</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={"keeps all the backups if left empty"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Optional. If provided, only keeps the latest N backups in the cloud.
|
Optional. If provided, only keeps the latest N backups
|
||||||
|
in the cloud.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -32,23 +35,20 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { debounce } from "lodash";
|
||||||
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { debounce } from "lodash";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseType: Exclude<ServiceType, "application" | "redis">;
|
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||||
serverId: string | null;
|
serverId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RestoreBackupSchema = z.object({
|
const RestoreBackupSchema = z.object({
|
||||||
@@ -84,6 +84,7 @@ export const RestoreBackup = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
|
|
||||||
const { data: destinations = [] } = api.destination.all.useQuery();
|
const { data: destinations = [] } = api.destination.all.useQuery();
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ export const RestoreBackup = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
backupFile: "",
|
backupFile: "",
|
||||||
databaseName: "",
|
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(RestoreBackupSchema),
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
});
|
});
|
||||||
@@ -99,13 +100,18 @@ export const RestoreBackup = ({
|
|||||||
const destionationId = form.watch("destinationId");
|
const destionationId = form.watch("destinationId");
|
||||||
|
|
||||||
const debouncedSetSearch = debounce((value: string) => {
|
const debouncedSetSearch = debounce((value: string) => {
|
||||||
|
setDebouncedSearchTerm(value);
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
}, 300);
|
debouncedSetSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
||||||
{
|
{
|
||||||
destinationId: destionationId,
|
destinationId: destionationId,
|
||||||
search,
|
search: debouncedSearchTerm,
|
||||||
serverId: serverId ?? "",
|
serverId: serverId ?? "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -284,7 +290,8 @@ export const RestoreBackup = ({
|
|||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search backup files..."
|
placeholder="Search backup files..."
|
||||||
onValueChange={debouncedSetSearch}
|
value={search}
|
||||||
|
onValueChange={handleSearchChange}
|
||||||
className="h-9"
|
className="h-9"
|
||||||
/>
|
/>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -308,6 +315,8 @@ export const RestoreBackup = ({
|
|||||||
key={file}
|
key={file}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("backupFile", file);
|
form.setValue("backupFile", file);
|
||||||
|
setSearch(file);
|
||||||
|
setDebouncedSearchTerm(file);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
@@ -340,7 +349,11 @@ export const RestoreBackup = ({
|
|||||||
<FormItem className="">
|
<FormItem className="">
|
||||||
<FormLabel>Database Name</FormLabel>
|
<FormLabel>Database Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="Enter database name" />
|
<Input
|
||||||
|
disabled={databaseType === "web-server"}
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter database name"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { AddBackup } from "./add-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
import { UpdateBackup } from "./update-backup";
|
|
||||||
import { RestoreBackup } from "./restore-backup";
|
import { RestoreBackup } from "./restore-backup";
|
||||||
import { useState } from "react";
|
import { UpdateBackup } from "./update-backup";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: Exclude<ServiceType, "application" | "redis">;
|
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||||
}
|
}
|
||||||
export const ShowBackups = ({ id, type }: Props) => {
|
export const ShowBackups = ({ id, type }: Props) => {
|
||||||
const [activeManualBackup, setActiveManualBackup] = useState<
|
const [activeManualBackup, setActiveManualBackup] = useState<
|
||||||
@@ -38,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
mariadb: () =>
|
mariadb: () =>
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
"web-server": () => api.user.getBackups.useQuery(),
|
||||||
};
|
};
|
||||||
const { data } = api.destination.all.useQuery();
|
const { data } = api.destination.all.useQuery();
|
||||||
const { data: postgres, refetch } = queryMap[type]
|
const { data: postgres, refetch } = queryMap[type]
|
||||||
@@ -49,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
||||||
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
||||||
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
||||||
|
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
||||||
@@ -64,7 +66,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
|
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
|
<Database className="size-6 text-muted-foreground" />
|
||||||
|
Backups
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backups to your database to save the data to a different
|
Add backups to your database to save the data to a different
|
||||||
provider.
|
provider.
|
||||||
@@ -73,11 +78,17 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
|
|
||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
{type !== "web-server" && (
|
||||||
|
<AddBackup
|
||||||
|
databaseId={id}
|
||||||
|
databaseType={type}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
databaseId={id}
|
databaseId={id}
|
||||||
databaseType={type}
|
databaseType={type}
|
||||||
serverId={postgres.serverId}
|
serverId={"serverId" in postgres ? postgres.serverId : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -115,7 +126,9 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
databaseId={id}
|
databaseId={id}
|
||||||
databaseType={type}
|
databaseType={type}
|
||||||
serverId={postgres.serverId}
|
serverId={
|
||||||
|
"serverId" in postgres ? postgres.serverId : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
enabled: backup.enabled || false,
|
enabled: backup.enabled || false,
|
||||||
prefix: backup.prefix,
|
prefix: backup.prefix,
|
||||||
schedule: backup.schedule,
|
schedule: backup.schedule,
|
||||||
keepLatestCount: backup.keepLatestCount ? Number(backup.keepLatestCount) : undefined,
|
keepLatestCount: backup.keepLatestCount
|
||||||
|
? Number(backup.keepLatestCount)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, backup]);
|
}, [form, form.reset, backup]);
|
||||||
@@ -274,10 +276,15 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Keep the latest</FormLabel>
|
<FormLabel>Keep the latest</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={"keeps all the backups if left empty"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Optional. If provided, only keeps the latest N backups in the cloud.
|
Optional. If provided, only keeps the latest N backups
|
||||||
|
in the cloud.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!postgresId,
|
enabled: !!postgresId,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const form = useForm<UpdatePostgres>({
|
const form = useForm<UpdatePostgres>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
|
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
|
||||||
@@ -307,7 +307,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
>
|
>
|
||||||
{templates?.map((template) => (
|
{templates?.map((template) => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template?.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col border rounded-lg overflow-hidden relative",
|
"flex flex-col border rounded-lg overflow-hidden relative",
|
||||||
viewMode === "icon" && "h-[200px]",
|
viewMode === "icon" && "h-[200px]",
|
||||||
@@ -315,7 +315,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Badge className="absolute top-2 right-2" variant="blue">
|
<Badge className="absolute top-2 right-2" variant="blue">
|
||||||
{template.version}
|
{template?.version}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -324,21 +324,21 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template.id}/${template.logo}`}
|
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"object-contain",
|
"object-contain",
|
||||||
viewMode === "detailed" ? "size-24" : "size-16",
|
viewMode === "detailed" ? "size-24" : "size-16",
|
||||||
)}
|
)}
|
||||||
alt={template.name}
|
alt={template?.name}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<span className="text-sm font-medium line-clamp-1">
|
<span className="text-sm font-medium line-clamp-1">
|
||||||
{template.name}
|
{template?.name}
|
||||||
</span>
|
</span>
|
||||||
{viewMode === "detailed" &&
|
{viewMode === "detailed" &&
|
||||||
template.tags.length > 0 && (
|
template?.tags?.length > 0 && (
|
||||||
<div className="flex flex-wrap justify-center gap-1.5">
|
<div className="flex flex-wrap justify-center gap-1.5">
|
||||||
{template.tags.map((tag) => (
|
{template?.tags?.map((tag) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={tag}
|
key={tag}
|
||||||
variant="green"
|
variant="green"
|
||||||
@@ -356,7 +356,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
{viewMode === "detailed" && (
|
{viewMode === "detailed" && (
|
||||||
<ScrollArea className="flex-1 p-6">
|
<ScrollArea className="flex-1 p-6">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{template.description}
|
{template?.description}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
@@ -372,25 +372,27 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
>
|
>
|
||||||
{viewMode === "detailed" && (
|
{viewMode === "detailed" && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{template?.links?.github && (
|
||||||
<Link
|
<Link
|
||||||
href={template.links.github}
|
href={template?.links?.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
<GithubIcon className="size-5" />
|
<GithubIcon className="size-5" />
|
||||||
</Link>
|
</Link>
|
||||||
{template.links.website && (
|
)}
|
||||||
|
{template?.links?.website && (
|
||||||
<Link
|
<Link
|
||||||
href={template.links.website}
|
href={template?.links?.website}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
<Globe className="size-5" />
|
<Globe className="size-5" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{template.links.docs && (
|
{template?.links?.docs && (
|
||||||
<Link
|
<Link
|
||||||
href={template.links.docs}
|
href={template?.links?.docs}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
@@ -419,7 +421,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This will create an application from the{" "}
|
This will create an application from the{" "}
|
||||||
{template.name} template and add it to your
|
{template?.name} template and add it to your
|
||||||
project.
|
project.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const examples = [
|
const examples = [
|
||||||
"Make a personal blog",
|
"Make a personal blog",
|
||||||
@@ -23,7 +22,7 @@ const examples = [
|
|||||||
"Sendgrid service opensource analogue",
|
"Sendgrid service opensource analogue",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||||
// Get servers from the API
|
// Get servers from the API
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
|
|||||||
172
apps/dokploy/components/dashboard/project/duplicate-project.tsx
Normal file
172
apps/dokploy/components/dashboard/project/duplicate-project.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Copy, Loader2 } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export type Services = {
|
||||||
|
appName: string;
|
||||||
|
serverId?: string | null;
|
||||||
|
name: string;
|
||||||
|
type:
|
||||||
|
| "mariadb"
|
||||||
|
| "application"
|
||||||
|
| "postgres"
|
||||||
|
| "mysql"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "compose";
|
||||||
|
description?: string | null;
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
status?: "idle" | "running" | "done" | "error";
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DuplicateProjectProps {
|
||||||
|
projectId: string;
|
||||||
|
services: Services[];
|
||||||
|
selectedServiceIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DuplicateProject = ({
|
||||||
|
projectId,
|
||||||
|
services,
|
||||||
|
selectedServiceIds,
|
||||||
|
}: DuplicateProjectProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const selectedServices = services.filter((service) =>
|
||||||
|
selectedServiceIds.includes(service.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: duplicateProject, isLoading } =
|
||||||
|
api.project.duplicate.useMutation({
|
||||||
|
onSuccess: async (newProject) => {
|
||||||
|
await utils.project.all.invalidate();
|
||||||
|
toast.success("Project duplicated successfully");
|
||||||
|
setOpen(false);
|
||||||
|
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDuplicate = async () => {
|
||||||
|
if (!name) {
|
||||||
|
toast.error("Project name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await duplicateProject({
|
||||||
|
sourceProjectId: projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
includeServices: true,
|
||||||
|
selectedServices: selectedServices.map((service) => ({
|
||||||
|
id: service.id,
|
||||||
|
type: service.type,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
setOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
// Reset form when closing
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" className="w-full justify-start">
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Duplicate
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Duplicate Project</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new project with the selected services
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="New project name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Project description (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Selected services to duplicate</Label>
|
||||||
|
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
|
||||||
|
{selectedServices.map((service) => (
|
||||||
|
<div key={service.id} className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm">
|
||||||
|
{service.name} ({service.type})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDuplicate} disabled={isLoading}>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Duplicating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Duplicate"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -31,8 +31,13 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const AddProjectSchema = z.object({
|
const AddProjectSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-zA-Z]/, {
|
||||||
|
message: "Project name cannot start with a number",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -97,18 +102,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// useEffect(() => {
|
|
||||||
// const getUsers = async () => {
|
|
||||||
// const users = await authClient.admin.listUsers({
|
|
||||||
// query: {
|
|
||||||
// limit: 100,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// console.log(users);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// getUsers();
|
|
||||||
// });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const ShowProjects = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
|
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
|
||||||
{filteredProjects?.map((project) => {
|
{filteredProjects?.map((project) => {
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
project?.mariadb.length === 0 &&
|
project?.mariadb.length === 0 &&
|
||||||
@@ -186,7 +186,9 @@ export const ShowProjects = () => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">{domain.host}</span>
|
<span className="truncate">
|
||||||
|
{domain.host}
|
||||||
|
</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -222,7 +224,9 @@ export const ShowProjects = () => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">{domain.host}</span>
|
<span className="truncate">
|
||||||
|
{domain.host}
|
||||||
|
</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ const DockerProviderSchema = z.object({
|
|||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { api } from "@/utils/api";
|
|
||||||
import {
|
import {
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ import {
|
|||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
ArrowDownUp,
|
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
InfoIcon,
|
ArrowDownUp,
|
||||||
Calendar as CalendarIcon,
|
Calendar as CalendarIcon,
|
||||||
|
InfoIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||||
import { RequestsTable } from "./requests-table";
|
import { RequestsTable } from "./requests-table";
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogDescription,
|
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -17,22 +25,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
FormDescription,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -7,13 +9,11 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { Clock, ExternalLinkIcon, KeyIcon, Tag, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { AddApiKey } from "./add-api-key";
|
import { AddApiKey } from "./add-api-key";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
|
|
||||||
export const ShowApiKeys = () => {
|
export const ShowApiKeys = () => {
|
||||||
const { data, refetch } = api.user.get.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -48,6 +49,10 @@ export const AddNode = ({ serverId }: Props) => {
|
|||||||
Architecture
|
Architecture
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Make sure you use the same architecture as the node you are
|
||||||
|
adding.
|
||||||
|
</AlertBlock>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -56,10 +61,10 @@ export const AddNode = ({ serverId }: Props) => {
|
|||||||
<TabsTrigger value="worker">Worker</TabsTrigger>
|
<TabsTrigger value="worker">Worker</TabsTrigger>
|
||||||
<TabsTrigger value="manager">Manager</TabsTrigger>
|
<TabsTrigger value="manager">Manager</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="worker" className="pt-4">
|
<TabsContent value="worker" className="pt-4 overflow-hidden">
|
||||||
<AddWorker serverId={serverId} />
|
<AddWorker serverId={serverId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="manager" className="pt-4">
|
<TabsContent value="manager" className="pt-4 overflow-hidden">
|
||||||
<AddManager serverId={serverId} />
|
<AddManager serverId={serverId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
@@ -6,7 +7,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { CopyIcon } from "lucide-react";
|
import { CopyIcon, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,18 +15,26 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddManager = ({ serverId }: Props) => {
|
export const AddManager = ({ serverId }: Props) => {
|
||||||
const { data } = api.cluster.addManager.useQuery({ serverId });
|
const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
|
||||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add a new manager</DialogTitle>
|
<DialogTitle>Add a new manager</DialogTitle>
|
||||||
<DialogDescription>Add a new manager</DialogDescription>
|
<DialogDescription>Add a new manager</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 className="w-full animate-spin text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="flex flex-col gap-2.5 text-sm">
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
<span>1. Go to your new server and run the following command</span>
|
<span>
|
||||||
|
1. Go to your new server and run the following command
|
||||||
|
</span>
|
||||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||||
curl https://get.docker.com | sh -s -- --version {data?.version}
|
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||||
<button
|
<button
|
||||||
@@ -48,6 +57,7 @@ export const AddManager = ({ serverId }: Props) => {
|
|||||||
2. Run the following command to add the node(manager) to your
|
2. Run the following command to add the node(manager) to your
|
||||||
cluster
|
cluster
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="bg-muted rounded-lg p-2 flex">
|
<span className="bg-muted rounded-lg p-2 flex">
|
||||||
{data?.command}
|
{data?.command}
|
||||||
<button
|
<button
|
||||||
@@ -62,8 +72,9 @@ export const AddManager = ({ serverId }: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const ShowNodesModal = ({ serverId }: Props) => {
|
|||||||
className="w-full cursor-pointer "
|
className="w-full cursor-pointer "
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
Show Nodes
|
Show Swarm Nodes
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ import { api } from "@/utils/api";
|
|||||||
import {
|
import {
|
||||||
Boxes,
|
Boxes,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
Loader2,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Loader2,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddNode } from "./add-node";
|
import { AddNode } from "./add-node";
|
||||||
@@ -144,7 +144,7 @@ export const ShowNodes = ({ serverId }: Props) => {
|
|||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<ShowNodeData data={node} />
|
<ShowNodeData data={node} />
|
||||||
{node?.ManagerStatus?.Leader && (
|
{!node?.ManagerStatus?.Leader && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Node"
|
title="Delete Node"
|
||||||
description="Are you sure you want to delete this node from the cluster?"
|
description="Are you sure you want to delete this node from the cluster?"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
@@ -6,7 +7,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { CopyIcon } from "lucide-react";
|
import { CopyIcon, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,15 +15,21 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddWorker = ({ serverId }: Props) => {
|
export const AddWorker = ({ serverId }: Props) => {
|
||||||
const { data } = api.cluster.addWorker.useQuery({ serverId });
|
const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
|
||||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add a new worker</DialogTitle>
|
<DialogTitle>Add a new worker</DialogTitle>
|
||||||
<DialogDescription>Add a new worker</DialogDescription>
|
<DialogDescription>Add a new worker</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 className="w-full animate-spin text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="flex flex-col gap-2.5 text-sm">
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
<span>1. Go to your new server and run the following command</span>
|
<span>1. Go to your new server and run the following command</span>
|
||||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||||
@@ -44,7 +51,8 @@ export const AddWorker = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2.5 text-sm">
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
<span>
|
<span>
|
||||||
2. Run the following command to add the node(worker) to your cluster
|
2. Run the following command to add the node(worker) to your
|
||||||
|
cluster
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="bg-muted rounded-lg p-2 flex">
|
<span className="bg-muted rounded-lg p-2 flex">
|
||||||
@@ -61,7 +69,8 @@ export const AddWorker = ({ serverId }: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
type GiteaProviderResponse,
|
||||||
|
getGiteaOAuthUrl,
|
||||||
|
} from "@/utils/gitea-utils";
|
||||||
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
|
giteaUrl: z.string().min(1, {
|
||||||
|
message: "Gitea URL is required",
|
||||||
|
}),
|
||||||
|
clientId: z.string().min(1, {
|
||||||
|
message: "Client ID is required",
|
||||||
|
}),
|
||||||
|
clientSecret: z.string().min(1, {
|
||||||
|
message: "Client Secret is required",
|
||||||
|
}),
|
||||||
|
redirectUri: z.string().min(1, {
|
||||||
|
message: "Redirect URI is required",
|
||||||
|
}),
|
||||||
|
organizationName: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
|
export const AddGiteaProvider = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const urlObj = useUrl();
|
||||||
|
const baseUrl =
|
||||||
|
typeof urlObj === "string" ? urlObj : (urlObj as any)?.url || "";
|
||||||
|
|
||||||
|
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
|
||||||
|
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
redirectUri: webhookUrl,
|
||||||
|
name: "",
|
||||||
|
giteaUrl: "https://gitea.com",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(Schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const giteaUrl = form.watch("giteaUrl");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
redirectUri: webhookUrl,
|
||||||
|
name: "",
|
||||||
|
giteaUrl: "https://gitea.com",
|
||||||
|
});
|
||||||
|
}, [form, webhookUrl, isOpen]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
try {
|
||||||
|
// Send the form data to create the Gitea provider
|
||||||
|
const result = (await mutateAsync({
|
||||||
|
clientId: data.clientId,
|
||||||
|
clientSecret: data.clientSecret,
|
||||||
|
name: data.name,
|
||||||
|
redirectUri: data.redirectUri,
|
||||||
|
giteaUrl: data.giteaUrl,
|
||||||
|
organizationName: data.organizationName,
|
||||||
|
})) as unknown as GiteaProviderResponse;
|
||||||
|
|
||||||
|
// Check if we have a giteaId from the response
|
||||||
|
if (!result || !result.giteaId) {
|
||||||
|
toast.error("Failed to get Gitea ID from response");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate OAuth URL using the shared utility
|
||||||
|
const authUrl = getGiteaOAuthUrl(
|
||||||
|
result.giteaId,
|
||||||
|
data.clientId,
|
||||||
|
data.giteaUrl,
|
||||||
|
baseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open the Gitea OAuth URL
|
||||||
|
if (authUrl !== "#") {
|
||||||
|
window.open(authUrl, "_blank");
|
||||||
|
} else {
|
||||||
|
toast.error("Configuration Incomplete", {
|
||||||
|
description: "Please fill in Client ID and Gitea URL first.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Gitea provider created successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(`Error configuring Gitea: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
toast.error("An unknown error occurred.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
|
||||||
|
>
|
||||||
|
<GiteaIcon />
|
||||||
|
<span>Gitea</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
Gitea Provider <GiteaIcon className="size-5" />
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-add-gitea"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-1"
|
||||||
|
>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
To integrate your Gitea account, you need to create a new
|
||||||
|
application in your Gitea settings. Follow these steps:
|
||||||
|
</p>
|
||||||
|
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||||
|
<li className="flex flex-row gap-2 items-center">
|
||||||
|
Go to your Gitea settings{" "}
|
||||||
|
<Link
|
||||||
|
href={`${giteaUrl}/user/settings/applications`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-fit text-primary size-4" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Navigate to Applications {"->"} Create new OAuth2
|
||||||
|
Application
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Create a new application with the following details:
|
||||||
|
<ul className="list-disc list-inside ml-4">
|
||||||
|
<li>Name: Dokploy</li>
|
||||||
|
<li>
|
||||||
|
Redirect URI:{" "}
|
||||||
|
<span className="text-primary">{webhookUrl}</span>{" "}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
After creating, you'll receive an ID and Secret, copy them
|
||||||
|
and paste them below.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Random Name eg(my-personal-account)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Gitea URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://gitea.com/" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="redirectUri"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Redirect URI</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
placeholder="Random Name eg(my-personal-account)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Client ID" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Secret</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Client Secret"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button isLoading={form.formState.isSubmitting}>
|
||||||
|
Configure Gitea App
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
|
||||||
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
giteaUrl: z.string().min(1, "Gitea URL is required"),
|
||||||
|
clientId: z.string().min(1, "Client ID is required"),
|
||||||
|
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
giteaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const {
|
||||||
|
data: gitea,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = api.gitea.one.useQuery({ giteaId });
|
||||||
|
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
|
||||||
|
const { mutateAsync: testConnection, isLoading: isTesting } =
|
||||||
|
api.gitea.testConnection.useMutation();
|
||||||
|
const url = useUrl();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { connected, error } = router.query;
|
||||||
|
|
||||||
|
if (!router.isReady) return;
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
toast.success("Successfully connected to Gitea", {
|
||||||
|
description: "Your Gitea provider has been authorized.",
|
||||||
|
id: "gitea-connection-success",
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error("Gitea Connection Failed", {
|
||||||
|
description: decodeURIComponent(error as string),
|
||||||
|
id: "gitea-connection-error",
|
||||||
|
});
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [router.query, router.isReady, refetch]);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
giteaUrl: "https://gitea.com",
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gitea) {
|
||||||
|
form.reset({
|
||||||
|
name: gitea.gitProvider?.name || "",
|
||||||
|
giteaUrl: gitea.giteaUrl || "https://gitea.com",
|
||||||
|
clientId: gitea.clientId || "",
|
||||||
|
clientSecret: gitea.clientSecret || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [gitea, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
await mutateAsync({
|
||||||
|
giteaId: giteaId,
|
||||||
|
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
|
||||||
|
name: values.name,
|
||||||
|
giteaUrl: values.giteaUrl,
|
||||||
|
clientId: values.clientId,
|
||||||
|
clientSecret: values.clientSecret,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.gitProvider.getAll.invalidate();
|
||||||
|
toast.success("Gitea provider updated successfully");
|
||||||
|
await refetch();
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating Gitea provider");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
try {
|
||||||
|
const result = await testConnection({ giteaId });
|
||||||
|
toast.success("Gitea Connection Verified", {
|
||||||
|
description: result,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
const formValues = form.getValues();
|
||||||
|
const authUrl =
|
||||||
|
error.authorizationUrl ||
|
||||||
|
getGiteaOAuthUrl(
|
||||||
|
giteaId,
|
||||||
|
formValues.clientId,
|
||||||
|
formValues.giteaUrl,
|
||||||
|
typeof url === "string" ? url : (url as any).url || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.error("Gitea Not Connected", {
|
||||||
|
description:
|
||||||
|
error.message || "Please complete the OAuth authorization process.",
|
||||||
|
action:
|
||||||
|
authUrl && authUrl !== "#"
|
||||||
|
? {
|
||||||
|
label: "Authorize Now",
|
||||||
|
onClick: () => window.open(authUrl, "_blank"),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" size="icon" disabled>
|
||||||
|
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle dialog open state
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
setOpen(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Gitea Provider</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update your Gitea provider details.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="My Gitea"
|
||||||
|
{...field}
|
||||||
|
autoFocus={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Gitea URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://gitea.example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Client ID" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Secret</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Client Secret"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleTestConnection}
|
||||||
|
isLoading={isTesting}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const formValues = form.getValues();
|
||||||
|
const authUrl = getGiteaOAuthUrl(
|
||||||
|
giteaId,
|
||||||
|
formValues.clientId,
|
||||||
|
formValues.giteaUrl,
|
||||||
|
typeof url === "string" ? url : (url as any).url || "",
|
||||||
|
);
|
||||||
|
if (authUrl !== "#") {
|
||||||
|
window.open(authUrl, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Connect to Gitea
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isUpdating}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -248,7 +248,9 @@ export const AddGitlabProvider = () => {
|
|||||||
name="groupName"
|
name="groupName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Group Name (Optional)</FormLabel>
|
<FormLabel>
|
||||||
|
Group Name (Optional, Comma-Separated List)
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
|
|||||||
name="groupName"
|
name="groupName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Group Name (Optional)</FormLabel>
|
<FormLabel>
|
||||||
|
Group Name (Optional, Comma-Separated List)
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
@@ -26,6 +27,8 @@ import Link from "next/link";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||||
|
import { AddGiteaProvider } from "./gitea/add-gitea-provider";
|
||||||
|
import { EditGiteaProvider } from "./gitea/edit-gitea-provider";
|
||||||
import { AddGithubProvider } from "./github/add-github-provider";
|
import { AddGithubProvider } from "./github/add-github-provider";
|
||||||
import { EditGithubProvider } from "./github/edit-github-provider";
|
import { EditGithubProvider } from "./github/edit-github-provider";
|
||||||
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
||||||
@@ -36,19 +39,18 @@ export const ShowGitProviders = () => {
|
|||||||
const { mutateAsync, isLoading: isRemoving } =
|
const { mutateAsync, isLoading: isRemoving } =
|
||||||
api.gitProvider.remove.useMutation();
|
api.gitProvider.remove.useMutation();
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
const getGitlabUrl = (
|
const getGitlabUrl = (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
gitlabId: string,
|
gitlabId: string,
|
||||||
gitlabUrl: string,
|
gitlabUrl: string,
|
||||||
) => {
|
) => {
|
||||||
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
||||||
|
|
||||||
const scope = "api read_user read_repository";
|
const scope = "api read_user read_repository";
|
||||||
|
|
||||||
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||||
|
|
||||||
return authUrl;
|
return authUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
@@ -82,6 +84,7 @@ export const ShowGitProviders = () => {
|
|||||||
<AddGithubProvider />
|
<AddGithubProvider />
|
||||||
<AddGitlabProvider />
|
<AddGitlabProvider />
|
||||||
<AddBitbucketProvider />
|
<AddBitbucketProvider />
|
||||||
|
<AddGiteaProvider />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,6 +100,7 @@ export const ShowGitProviders = () => {
|
|||||||
<AddGithubProvider />
|
<AddGithubProvider />
|
||||||
<AddGitlabProvider />
|
<AddGitlabProvider />
|
||||||
<AddBitbucketProvider />
|
<AddBitbucketProvider />
|
||||||
|
<AddGiteaProvider />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,13 +111,16 @@ export const ShowGitProviders = () => {
|
|||||||
const isGitlab = gitProvider.providerType === "gitlab";
|
const isGitlab = gitProvider.providerType === "gitlab";
|
||||||
const isBitbucket =
|
const isBitbucket =
|
||||||
gitProvider.providerType === "bitbucket";
|
gitProvider.providerType === "bitbucket";
|
||||||
|
const isGitea = gitProvider.providerType === "gitea";
|
||||||
|
|
||||||
const haveGithubRequirements =
|
const haveGithubRequirements =
|
||||||
gitProvider.providerType === "github" &&
|
isGithub &&
|
||||||
gitProvider.github?.githubPrivateKey &&
|
gitProvider.github?.githubPrivateKey &&
|
||||||
gitProvider.github?.githubAppId &&
|
gitProvider.github?.githubAppId &&
|
||||||
gitProvider.github?.githubInstallationId;
|
gitProvider.github?.githubInstallationId;
|
||||||
|
|
||||||
const haveGitlabRequirements =
|
const haveGitlabRequirements =
|
||||||
|
isGitlab &&
|
||||||
gitProvider.gitlab?.accessToken &&
|
gitProvider.gitlab?.accessToken &&
|
||||||
gitProvider.gitlab?.refreshToken;
|
gitProvider.gitlab?.refreshToken;
|
||||||
|
|
||||||
@@ -125,15 +132,16 @@ export const ShowGitProviders = () => {
|
|||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<div className="flex flex-col items-center justify-between">
|
<div className="flex flex-col items-center justify-between">
|
||||||
<div className="flex gap-2 flex-row items-center">
|
<div className="flex gap-2 flex-row items-center">
|
||||||
{gitProvider.providerType === "github" && (
|
{isGithub && (
|
||||||
<GithubIcon className="size-5" />
|
<GithubIcon className="size-5" />
|
||||||
)}
|
)}
|
||||||
{gitProvider.providerType === "gitlab" && (
|
{isGitlab && (
|
||||||
<GitlabIcon className="size-5" />
|
<GitlabIcon className="size-5" />
|
||||||
)}
|
)}
|
||||||
{gitProvider.providerType === "bitbucket" && (
|
{isBitbucket && (
|
||||||
<BitbucketIcon className="size-5" />
|
<BitbucketIcon className="size-5" />
|
||||||
)}
|
)}
|
||||||
|
{isGitea && <GiteaIcon className="size-5" />}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{gitProvider.name}
|
{gitProvider.name}
|
||||||
@@ -194,26 +202,33 @@ export const ShowGitProviders = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGithub && haveGithubRequirements && (
|
{isGithub && haveGithubRequirements && (
|
||||||
<EditGithubProvider
|
<EditGithubProvider
|
||||||
githubId={gitProvider.github.githubId}
|
githubId={gitProvider.github?.githubId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGitlab && (
|
{isGitlab && (
|
||||||
<EditGitlabProvider
|
<EditGitlabProvider
|
||||||
gitlabId={gitProvider.gitlab.gitlabId}
|
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isBitbucket && (
|
{isBitbucket && (
|
||||||
<EditBitbucketProvider
|
<EditBitbucketProvider
|
||||||
bitbucketId={
|
bitbucketId={
|
||||||
gitProvider.bitbucket.bitbucketId
|
gitProvider.bitbucket?.bitbucketId
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isGitea && (
|
||||||
|
<EditGiteaProvider
|
||||||
|
giteaId={gitProvider.gitea?.giteaId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Git Provider"
|
title="Delete Git Provider"
|
||||||
description="Are you sure you want to delete this Git Provider?"
|
description="Are you sure you want to delete this Git Provider?"
|
||||||
|
|||||||
@@ -663,13 +663,16 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value) {
|
if (value === "") {
|
||||||
|
field.onChange(undefined);
|
||||||
|
} else {
|
||||||
const port = Number.parseInt(value);
|
const port = Number.parseInt(value);
|
||||||
if (port > 0 && port < 65536) {
|
if (port > 0 && port < 65536) {
|
||||||
field.onChange(port);
|
field.onChange(port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
value={field.value || ""}
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const PasswordSchema = z.object({
|
|||||||
password: z.string().min(8, {
|
password: z.string().min(8, {
|
||||||
message: "Password is required",
|
message: "Password is required",
|
||||||
}),
|
}),
|
||||||
|
issuer: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const PinSchema = z.object({
|
const PinSchema = z.object({
|
||||||
@@ -60,12 +61,86 @@ export const Enable2FA = () => {
|
|||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [step, setStep] = useState<"password" | "verify">("password");
|
const [step, setStep] = useState<"password" | "verify">("password");
|
||||||
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
|
const [otpValue, setOtpValue] = useState("");
|
||||||
|
|
||||||
|
const handleVerifySubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const result = await authClient.twoFactor.verifyTotp({
|
||||||
|
code: otpValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
|
||||||
|
toast.error("Invalid verification code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.data) {
|
||||||
|
throw new Error("No response received from server");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("2FA configured successfully");
|
||||||
|
utils.user.get.invalidate();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const errorMessage =
|
||||||
|
error.message === "Failed to fetch"
|
||||||
|
? "Connection error. Please check your internet connection."
|
||||||
|
: error.message;
|
||||||
|
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} else {
|
||||||
|
toast.error("Error verifying 2FA code", {
|
||||||
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordForm = useForm<PasswordForm>({
|
||||||
|
resolver: zodResolver(PasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pinForm = useForm<PinForm>({
|
||||||
|
resolver: zodResolver(PinSchema),
|
||||||
|
defaultValues: {
|
||||||
|
pin: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDialogOpen) {
|
||||||
|
setStep("password");
|
||||||
|
setData(null);
|
||||||
|
setBackupCodes([]);
|
||||||
|
setOtpValue("");
|
||||||
|
passwordForm.reset({
|
||||||
|
password: "",
|
||||||
|
issuer: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isDialogOpen, passwordForm]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (step === "verify") {
|
||||||
|
setOtpValue("");
|
||||||
|
}
|
||||||
|
}, [step]);
|
||||||
|
|
||||||
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||||
setIsPasswordLoading(true);
|
setIsPasswordLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data: enableData, error } = await authClient.twoFactor.enable({
|
const { data: enableData, error } = await authClient.twoFactor.enable({
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
|
issuer: formData.issuer,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!enableData) {
|
if (!enableData) {
|
||||||
@@ -103,75 +178,6 @@ export const Enable2FA = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerifySubmit = async (formData: PinForm) => {
|
|
||||||
try {
|
|
||||||
const result = await authClient.twoFactor.verifyTotp({
|
|
||||||
code: formData.pin,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
|
|
||||||
pinForm.setError("pin", {
|
|
||||||
message: "Invalid code. Please try again.",
|
|
||||||
});
|
|
||||||
toast.error("Invalid verification code");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw result.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.data) {
|
|
||||||
throw new Error("No response received from server");
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("2FA configured successfully");
|
|
||||||
utils.user.get.invalidate();
|
|
||||||
setIsDialogOpen(false);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
const errorMessage =
|
|
||||||
error.message === "Failed to fetch"
|
|
||||||
? "Connection error. Please check your internet connection."
|
|
||||||
: error.message;
|
|
||||||
|
|
||||||
pinForm.setError("pin", {
|
|
||||||
message: errorMessage,
|
|
||||||
});
|
|
||||||
toast.error(errorMessage);
|
|
||||||
} else {
|
|
||||||
pinForm.setError("pin", {
|
|
||||||
message: "Error verifying code",
|
|
||||||
});
|
|
||||||
toast.error("Error verifying 2FA code");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const passwordForm = useForm<PasswordForm>({
|
|
||||||
resolver: zodResolver(PasswordSchema),
|
|
||||||
defaultValues: {
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pinForm = useForm<PinForm>({
|
|
||||||
resolver: zodResolver(PinSchema),
|
|
||||||
defaultValues: {
|
|
||||||
pin: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isDialogOpen) {
|
|
||||||
setStep("password");
|
|
||||||
setData(null);
|
|
||||||
setBackupCodes([]);
|
|
||||||
passwordForm.reset();
|
|
||||||
pinForm.reset();
|
|
||||||
}
|
|
||||||
}, [isDialogOpen, passwordForm, pinForm]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -217,6 +223,27 @@ export const Enable2FA = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={passwordForm.control}
|
||||||
|
name="issuer"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Issuer</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter your issuer"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Use a custom issuer to identify the service you're
|
||||||
|
authenticating with.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -228,11 +255,7 @@ export const Enable2FA = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
) : (
|
) : (
|
||||||
<Form {...pinForm}>
|
<Form {...pinForm}>
|
||||||
<form
|
<form onSubmit={handleVerifySubmit} className="space-y-6">
|
||||||
id="pin-form"
|
|
||||||
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-6 justify-center items-center">
|
<div className="flex flex-col gap-6 justify-center items-center">
|
||||||
{data?.qrCodeUrl ? (
|
{data?.qrCodeUrl ? (
|
||||||
<>
|
<>
|
||||||
@@ -284,14 +307,14 @@ export const Enable2FA = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<div className="flex flex-col justify-center items-center">
|
||||||
control={pinForm.control}
|
|
||||||
name="pin"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col justify-center items-center">
|
|
||||||
<FormLabel>Verification Code</FormLabel>
|
<FormLabel>Verification Code</FormLabel>
|
||||||
<FormControl>
|
<InputOTP
|
||||||
<InputOTP maxLength={6} {...field}>
|
maxLength={6}
|
||||||
|
value={otpValue}
|
||||||
|
onChange={setOtpValue}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<InputOTPGroup>
|
<InputOTPGroup>
|
||||||
<InputOTPSlot index={0} />
|
<InputOTPSlot index={0} />
|
||||||
<InputOTPSlot index={1} />
|
<InputOTPSlot index={1} />
|
||||||
@@ -301,19 +324,16 @@ export const Enable2FA = () => {
|
|||||||
<InputOTPSlot index={5} />
|
<InputOTPSlot index={5} />
|
||||||
</InputOTPGroup>
|
</InputOTPGroup>
|
||||||
</InputOTP>
|
</InputOTP>
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enter the 6-digit code from your authenticator app
|
Enter the 6-digit code from your authenticator app
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
</div>
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
isLoading={isPasswordLoading}
|
isLoading={isPasswordLoading}
|
||||||
|
disabled={otpValue.length !== 6}
|
||||||
>
|
>
|
||||||
Enable 2FA
|
Enable 2FA
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const randomImages = [
|
|||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const _utils = api.useUtils();
|
const _utils = api.useUtils();
|
||||||
const { data, refetch, isLoading } = api.user.get.useQuery();
|
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync,
|
mutateAsync,
|
||||||
isLoading: isUpdating,
|
isLoading: isUpdating,
|
||||||
@@ -84,12 +85,17 @@ export const ProfileForm = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset(
|
||||||
|
{
|
||||||
email: data?.user?.email || "",
|
email: data?.user?.email || "",
|
||||||
password: "",
|
password: form.getValues("password") || "",
|
||||||
image: data?.user?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: "",
|
currentPassword: form.getValues("currentPassword") || "",
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
keepValues: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (data.user.email) {
|
if (data.user.email) {
|
||||||
generateSHA256Hash(data.user.email).then((hash) => {
|
generateSHA256Hash(data.user.email).then((hash) => {
|
||||||
@@ -97,8 +103,7 @@ export const ProfileForm = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
form.reset();
|
}, [form, data]);
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (values: Profile) => {
|
const onSubmit = async (values: Profile) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -110,7 +115,12 @@ export const ProfileForm = () => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
toast.success("Profile Updated");
|
toast.success("Profile Updated");
|
||||||
form.reset();
|
form.reset({
|
||||||
|
email: values.email,
|
||||||
|
password: "",
|
||||||
|
image: values.image,
|
||||||
|
currentPassword: "",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the profile");
|
toast.error("Error updating the profile");
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const _utils = api.useUtils();
|
|
||||||
return (
|
return (
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -36,10 +36,12 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LockKeyhole className="size-5" />
|
<LockKeyhole className="size-5" />
|
||||||
<CardTitle className="text-xl">
|
<CardTitle className="text-xl">
|
||||||
Setup Security Sugestions
|
Setup Security Suggestions
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Check the security sugestions</CardDescription>
|
<CardDescription>
|
||||||
|
Check the security suggestions
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isRefreshing}
|
isLoading={isRefreshing}
|
||||||
@@ -120,36 +122,36 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
<div className="grid gap-2.5">
|
<div className="grid gap-2.5">
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
isEnabled={data?.ssh.enabled}
|
isEnabled={data?.ssh?.enabled}
|
||||||
description={
|
description={
|
||||||
data?.ssh.enabled
|
data?.ssh?.enabled
|
||||||
? "Enabled"
|
? "Enabled"
|
||||||
: "Not Enabled (SSH should be enabled)"
|
: "Not Enabled (SSH should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Key Auth"
|
label="Key Auth"
|
||||||
isEnabled={data?.ssh.keyAuth}
|
isEnabled={data?.ssh?.keyAuth}
|
||||||
description={
|
description={
|
||||||
data?.ssh.keyAuth
|
data?.ssh?.keyAuth
|
||||||
? "Enabled (Recommended)"
|
? "Enabled (Recommended)"
|
||||||
: "Not Enabled (Key Authentication should be enabled)"
|
: "Not Enabled (Key Authentication should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Password Auth"
|
label="Password Auth"
|
||||||
isEnabled={data?.ssh.passwordAuth === "no"}
|
isEnabled={data?.ssh?.passwordAuth === "no"}
|
||||||
description={
|
description={
|
||||||
data?.ssh.passwordAuth === "no"
|
data?.ssh?.passwordAuth === "no"
|
||||||
? "Disabled (Recommended)"
|
? "Disabled (Recommended)"
|
||||||
: "Enabled (Password Authentication should be disabled)"
|
: "Enabled (Password Authentication should be disabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Use PAM"
|
label="Use PAM"
|
||||||
isEnabled={data?.ssh.usePam === "no"}
|
isEnabled={data?.ssh?.usePam === "no"}
|
||||||
description={
|
description={
|
||||||
data?.ssh.usePam === "no"
|
data?.ssh?.usePam === "no"
|
||||||
? "Disabled (Recommended for key-based auth)"
|
? "Disabled (Recommended for key-based auth)"
|
||||||
: "Enabled (Should be disabled when using key-based auth)"
|
: "Enabled (Should be disabled when using key-based auth)"
|
||||||
}
|
}
|
||||||
@@ -166,9 +168,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
<div className="grid gap-2.5">
|
<div className="grid gap-2.5">
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Installed"
|
label="Installed"
|
||||||
isEnabled={data?.fail2ban.installed}
|
isEnabled={data?.fail2ban?.installed}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.installed
|
data?.fail2ban?.installed
|
||||||
? "Installed (Recommended)"
|
? "Installed (Recommended)"
|
||||||
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
||||||
}
|
}
|
||||||
@@ -176,18 +178,18 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
isEnabled={data?.fail2ban.enabled}
|
isEnabled={data?.fail2ban?.enabled}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.enabled
|
data?.fail2ban?.enabled
|
||||||
? "Enabled (Recommended)"
|
? "Enabled (Recommended)"
|
||||||
: "Not Enabled (Fail2Ban service should be enabled)"
|
: "Not Enabled (Fail2Ban service should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Active"
|
label="Active"
|
||||||
isEnabled={data?.fail2ban.active}
|
isEnabled={data?.fail2ban?.active}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.active
|
data?.fail2ban?.active
|
||||||
? "Active (Recommended)"
|
? "Active (Recommended)"
|
||||||
: "Not Active (Fail2Ban service should be running)"
|
: "Not Active (Fail2Ban service should be running)"
|
||||||
}
|
}
|
||||||
@@ -195,9 +197,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="SSH Protection"
|
label="SSH Protection"
|
||||||
isEnabled={data?.fail2ban.sshEnabled === "true"}
|
isEnabled={data?.fail2ban?.sshEnabled === "true"}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.sshEnabled === "true"
|
data?.fail2ban?.sshEnabled === "true"
|
||||||
? "Enabled (Recommended)"
|
? "Enabled (Recommended)"
|
||||||
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
|
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
|
||||||
}
|
}
|
||||||
@@ -205,11 +207,11 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="SSH Mode"
|
label="SSH Mode"
|
||||||
isEnabled={data?.fail2ban.sshMode === "aggressive"}
|
isEnabled={data?.fail2ban?.sshMode === "aggressive"}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.sshMode === "aggressive"
|
data?.fail2ban?.sshMode === "aggressive"
|
||||||
? "Aggressive Mode (Recommended)"
|
? "Aggressive Mode (Recommended)"
|
||||||
: `Mode: ${data?.fail2ban.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
: `Mode: ${data?.fail2ban?.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { useTranslation } from "next-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
||||||
import { TerminalModal } from "../web-server/terminal-modal";
|
import { TerminalModal } from "../web-server/terminal-modal";
|
||||||
import { ShowServerActions } from "./actions/show-server-actions";
|
import { ShowServerActions } from "./actions/show-server-actions";
|
||||||
import { HandleServers } from "./handle-servers";
|
import { HandleServers } from "./handle-servers";
|
||||||
@@ -42,7 +43,6 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
|
|||||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|||||||
@@ -112,15 +112,17 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
toast.error("Error generating the SSH Key");
|
toast.error("Error generating the SSH Key");
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadKey = (
|
const downloadKey = (content: string, keyType: "private" | "public") => {
|
||||||
content: string,
|
|
||||||
defaultFilename: string,
|
|
||||||
keyType: "private" | "public",
|
|
||||||
) => {
|
|
||||||
const keyName = form.watch("name");
|
const keyName = form.watch("name");
|
||||||
|
const publicKey = form.watch("publicKey");
|
||||||
|
|
||||||
|
// Extract algorithm type from public key
|
||||||
|
const isEd25519 = publicKey.startsWith("ssh-ed25519");
|
||||||
|
const defaultName = isEd25519 ? "id_ed25519" : "id_rsa";
|
||||||
|
|
||||||
const filename = keyName
|
const filename = keyName
|
||||||
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}`
|
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultName}${keyType === "public" ? ".pub" : ""}`
|
||||||
: `${keyType}_${defaultFilename}`;
|
: `${defaultName}${keyType === "public" ? ".pub" : ""}`;
|
||||||
const blob = new Blob([content], { type: "text/plain" });
|
const blob = new Blob([content], { type: "text/plain" });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
@@ -273,7 +275,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="default"
|
size="default"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadKey(form.watch("privateKey"), "id_rsa", "private")
|
downloadKey(form.watch("privateKey"), "private")
|
||||||
}
|
}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -287,11 +289,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="default"
|
size="default"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadKey(
|
downloadKey(form.watch("publicKey"), "public")
|
||||||
form.watch("publicKey"),
|
|
||||||
"id_rsa.pub",
|
|
||||||
"public",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { GlobeIcon } from "lucide-react";
|
import { GlobeIcon } from "lucide-react";
|
||||||
@@ -33,11 +35,19 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const addServerDomain = z
|
const addServerDomain = z
|
||||||
.object({
|
.object({
|
||||||
domain: z.string().min(1, { message: "URL is required" }),
|
domain: z.string(),
|
||||||
letsEncryptEmail: z.string(),
|
letsEncryptEmail: z.string(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.https && !data.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@@ -61,15 +71,18 @@ export const WebDomain = () => {
|
|||||||
domain: "",
|
domain: "",
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
letsEncryptEmail: "",
|
letsEncryptEmail: "",
|
||||||
|
https: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addServerDomain),
|
resolver: zodResolver(addServerDomain),
|
||||||
});
|
});
|
||||||
|
const https = form.watch("https");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
domain: data?.user?.host || "",
|
domain: data?.user?.host || "",
|
||||||
certificateType: data?.user?.certificateType,
|
certificateType: data?.user?.certificateType,
|
||||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||||
|
https: data?.user?.https || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -79,6 +92,7 @@ export const WebDomain = () => {
|
|||||||
host: data.domain,
|
host: data.domain,
|
||||||
letsEncryptEmail: data.letsEncryptEmail,
|
letsEncryptEmail: data.letsEncryptEmail,
|
||||||
certificateType: data.certificateType,
|
certificateType: data.certificateType,
|
||||||
|
https: data.https,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
@@ -153,6 +167,28 @@ export const WebDomain = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm w-full col-span-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{https && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="certificateType"
|
name="certificateType"
|
||||||
@@ -193,6 +229,7 @@ export const WebDomain = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex w-full justify-end col-span-2">
|
<div className="flex w-full justify-end col-span-2">
|
||||||
<Button isLoading={isLoading} type="submit">
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
|||||||
@@ -159,9 +159,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
field.onChange(Number(e.target.value))
|
const value = e.target.value;
|
||||||
}
|
field.onChange(
|
||||||
|
value === ""
|
||||||
|
? undefined
|
||||||
|
: Number(value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
className="w-full dark:bg-black"
|
||||||
placeholder="e.g. 8080"
|
placeholder="e.g. 8080"
|
||||||
/>
|
/>
|
||||||
@@ -185,9 +191,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
field.onChange(Number(e.target.value))
|
const value = e.target.value;
|
||||||
}
|
field.onChange(
|
||||||
|
value === ""
|
||||||
|
? undefined
|
||||||
|
: Number(value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
className="w-full dark:bg-black"
|
||||||
placeholder="e.g. 80"
|
placeholder="e.g. 80"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -22,7 +23,6 @@ import dynamic from "next/dynamic";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { badgeStateColor } from "../../application/logs/show";
|
import { badgeStateColor } from "../../application/logs/show";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
|
|
||||||
export const DockerLogsId = dynamic(
|
export const DockerLogsId = dynamic(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { DatabaseIcon, AlertTriangle } from "lucide-react";
|
import { AlertTriangle, DatabaseIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
|
||||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||||
|
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
||||||
import { RebuildDatabase } from "./rebuild-database";
|
import { RebuildDatabase } from "./rebuild-database";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -238,6 +238,41 @@ export const BitbucketIcon = ({ className }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GiteaIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
version="1.1"
|
||||||
|
id="main_outline"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="5.67 143.05 628.65 387.55"
|
||||||
|
enableBackground="new 0 0 640 640"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
id="teabag"
|
||||||
|
style={{ fill: "#FFFFFF" }}
|
||||||
|
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#609926" }}
|
||||||
|
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
style={{ fill: "#609926" }}
|
||||||
|
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const DockerIcon = ({ className }: Props) => {
|
export const DockerIcon = ({ className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { IUpdateData } from "@dokploy/server/index";
|
import type { IUpdateData } from "@dokploy/server/index";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Download } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
|||||||
@@ -120,17 +120,6 @@ export const UserNav = () => {
|
|||||||
Docker
|
Docker
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.role === "owner" && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
router.push("/dashboard/settings");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
{_index + 1 < list.length && <BreadcrumbSeparator className="block" />}
|
{_index + 1 < list.length && (
|
||||||
|
<BreadcrumbSeparator className="block" />
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import { json } from "@codemirror/lang-json";
|
|||||||
import { yaml } from "@codemirror/lang-yaml";
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type Completion,
|
||||||
|
type CompletionContext,
|
||||||
|
type CompletionResult,
|
||||||
|
autocompletion,
|
||||||
|
} from "@codemirror/autocomplete";
|
||||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import {
|
|
||||||
autocompletion,
|
|
||||||
type CompletionContext,
|
|
||||||
type CompletionResult,
|
|
||||||
type Completion,
|
|
||||||
} from "@codemirror/autocomplete";
|
|
||||||
|
|
||||||
// Docker Compose completion options
|
// Docker Compose completion options
|
||||||
const dockerComposeServices = [
|
const dockerComposeServices = [
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type * as React from "react";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import type * as React from "react";
|
||||||
import { DayPicker } from "react-day-picker";
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
className={cn("text-left", className)}
|
className={cn("text-left", className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
value={props.value === undefined || props.value === "" ? "" : String(props.value)}
|
value={props.value === undefined ? undefined : String(props.value)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
@@ -60,21 +60,6 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={(e) => {
|
|
||||||
// If input is empty, make 0 when focus is lost
|
|
||||||
if (e.target.value === "") {
|
|
||||||
const syntheticEvent = {
|
|
||||||
...e,
|
|
||||||
target: {
|
|
||||||
...e.target,
|
|
||||||
value: "0",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
props.onChange?.(
|
|
||||||
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
31
apps/dokploy/drizzle/0079_bizarre_wendell_rand.sql
Normal file
31
apps/dokploy/drizzle/0079_bizarre_wendell_rand.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
ALTER TYPE "public"."sourceType" ADD VALUE 'gitea' BEFORE 'drop';--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."sourceTypeCompose" ADD VALUE 'gitea' BEFORE 'raw';--> statement-breakpoint
|
||||||
|
ALTER TYPE "public"."gitProviderType" ADD VALUE 'gitea';--> statement-breakpoint
|
||||||
|
CREATE TABLE "gitea" (
|
||||||
|
"giteaId" text PRIMARY KEY NOT NULL,
|
||||||
|
"giteaUrl" text DEFAULT 'https://gitea.com' NOT NULL,
|
||||||
|
"redirect_uri" text,
|
||||||
|
"client_id" text,
|
||||||
|
"client_secret" text,
|
||||||
|
"gitProviderId" text NOT NULL,
|
||||||
|
"gitea_username" text,
|
||||||
|
"access_token" text,
|
||||||
|
"refresh_token" text,
|
||||||
|
"expires_at" integer,
|
||||||
|
"scopes" text DEFAULT 'repo,repo:status,read:user,read:org',
|
||||||
|
"last_authenticated_at" integer
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "giteaProjectId" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "giteaBuildPath" text DEFAULT '/';--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "giteaId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ADD COLUMN "giteaId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "gitea" ADD CONSTRAINT "gitea_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD CONSTRAINT "application_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ADD CONSTRAINT "compose_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;
|
||||||
1
apps/dokploy/drizzle/0080_sleepy_sinister_six.sql
Normal file
1
apps/dokploy/drizzle/0080_sleepy_sinister_six.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" DROP COLUMN "giteaProjectId";
|
||||||
1
apps/dokploy/drizzle/0081_lovely_mentallo.sql
Normal file
1
apps/dokploy/drizzle/0081_lovely_mentallo.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "gitea" DROP COLUMN "gitea_username";
|
||||||
2
apps/dokploy/drizzle/0082_clean_mandarin.sql
Normal file
2
apps/dokploy/drizzle/0082_clean_mandarin.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "backup" ADD COLUMN "userId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;
|
||||||
1
apps/dokploy/drizzle/0083_parallel_stranger.sql
Normal file
1
apps/dokploy/drizzle/0083_parallel_stranger.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "public"."databaseType" ADD VALUE 'web-server';
|
||||||
1
apps/dokploy/drizzle/0084_thin_iron_lad.sql
Normal file
1
apps/dokploy/drizzle/0084_thin_iron_lad.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "https" boolean DEFAULT false NOT NULL;
|
||||||
5354
apps/dokploy/drizzle/meta/0079_snapshot.json
Normal file
5354
apps/dokploy/drizzle/meta/0079_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5348
apps/dokploy/drizzle/meta/0080_snapshot.json
Normal file
5348
apps/dokploy/drizzle/meta/0080_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5342
apps/dokploy/drizzle/meta/0081_snapshot.json
Normal file
5342
apps/dokploy/drizzle/meta/0081_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5361
apps/dokploy/drizzle/meta/0082_snapshot.json
Normal file
5361
apps/dokploy/drizzle/meta/0082_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5362
apps/dokploy/drizzle/meta/0083_snapshot.json
Normal file
5362
apps/dokploy/drizzle/meta/0083_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5369
apps/dokploy/drizzle/meta/0084_snapshot.json
Normal file
5369
apps/dokploy/drizzle/meta/0084_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -554,6 +554,48 @@
|
|||||||
"when": 1742112194375,
|
"when": 1742112194375,
|
||||||
"tag": "0078_uneven_omega_sentinel",
|
"tag": "0078_uneven_omega_sentinel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 79,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1742281690186,
|
||||||
|
"tag": "0079_bizarre_wendell_rand",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 80,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1743280866402,
|
||||||
|
"tag": "0080_sleepy_sinister_six",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 81,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1743281254393,
|
||||||
|
"tag": "0081_lovely_mentallo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 82,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1743287689974,
|
||||||
|
"tag": "0082_clean_mandarin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 83,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1743288371413,
|
||||||
|
"tag": "0083_parallel_stranger",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 84,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1743923992280,
|
||||||
|
"tag": "0084_thin_iron_lad",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
552
apps/dokploy/drizzle/meta/_journal.json.backup
Normal file
552
apps/dokploy/drizzle/meta/_journal.json.backup
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "pg",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1713262741218,
|
||||||
|
"tag": "0000_reflective_puck",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1713761637676,
|
||||||
|
"tag": "0001_striped_tattoo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1713763492341,
|
||||||
|
"tag": "0002_ambiguous_carlie_cooper",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1713947141424,
|
||||||
|
"tag": "0003_square_lightspeed",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1714004732716,
|
||||||
|
"tag": "0004_nice_tenebrous",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1715551130605,
|
||||||
|
"tag": "0005_cute_terror",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1715563165991,
|
||||||
|
"tag": "0006_oval_jimmy_woo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1715563497100,
|
||||||
|
"tag": "0007_cute_guardsmen",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1715564143641,
|
||||||
|
"tag": "0008_lazy_sage",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1715564774423,
|
||||||
|
"tag": "0009_majestic_spencer_smythe",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1715574037832,
|
||||||
|
"tag": "0010_lean_black_widow",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1715574230599,
|
||||||
|
"tag": "0011_petite_calypso",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1716015716708,
|
||||||
|
"tag": "0012_chubby_umar",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1716076179443,
|
||||||
|
"tag": "0013_blushing_starjammers",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1716715367982,
|
||||||
|
"tag": "0014_same_hammerhead",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1717564517104,
|
||||||
|
"tag": "0015_fearless_callisto",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1719109196484,
|
||||||
|
"tag": "0016_chunky_leopardon",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1719547174326,
|
||||||
|
"tag": "0017_minor_post",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1719928377858,
|
||||||
|
"tag": "0018_careful_killmonger",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721110706912,
|
||||||
|
"tag": "0019_heavy_freak",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721363861686,
|
||||||
|
"tag": "0020_fantastic_slapstick",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721370423752,
|
||||||
|
"tag": "0021_premium_sebastian_shaw",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 22,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721531163852,
|
||||||
|
"tag": "0022_warm_colonel_america",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 23,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721542782659,
|
||||||
|
"tag": "0023_icy_maverick",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 24,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721603595092,
|
||||||
|
"tag": "0024_dapper_supernaut",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721633853118,
|
||||||
|
"tag": "0025_lying_mephisto",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 26,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721979220929,
|
||||||
|
"tag": "0026_known_dormammu",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 27,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1722445099203,
|
||||||
|
"tag": "0027_red_lady_bullseye",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 28,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1722503439951,
|
||||||
|
"tag": "0028_jittery_eternity",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1722578386823,
|
||||||
|
"tag": "0029_colossal_zodiak",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 30,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1723608499147,
|
||||||
|
"tag": "0030_little_kabuki",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 31,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1723701656243,
|
||||||
|
"tag": "0031_steep_vulture",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 32,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1723705257806,
|
||||||
|
"tag": "0032_flashy_shadow_king",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 33,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1725250322137,
|
||||||
|
"tag": "0033_white_hawkeye",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 34,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1725256397019,
|
||||||
|
"tag": "0034_aspiring_secret_warriors",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 35,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1725429324584,
|
||||||
|
"tag": "0035_cool_gravity",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 36,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1725519351871,
|
||||||
|
"tag": "0036_tired_ronan",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 37,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1726988289562,
|
||||||
|
"tag": "0037_legal_namor",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 38,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1727942090102,
|
||||||
|
"tag": "0038_rapid_landau",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 39,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1728021127765,
|
||||||
|
"tag": "0039_many_tiger_shark",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 40,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1728780577084,
|
||||||
|
"tag": "0040_graceful_wolfsbane",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 41,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729667438853,
|
||||||
|
"tag": "0041_huge_bruce_banner",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 42,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729984439862,
|
||||||
|
"tag": "0042_fancy_havok",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 43,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731873965888,
|
||||||
|
"tag": "0043_closed_naoko",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 44,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731875539532,
|
||||||
|
"tag": "0044_sour_true_believers",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 45,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1732644181718,
|
||||||
|
"tag": "0045_smiling_blur",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 46,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1732851191048,
|
||||||
|
"tag": "0046_purple_sleeper",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 47,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1733599090582,
|
||||||
|
"tag": "0047_tidy_revanche",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 48,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1733599163710,
|
||||||
|
"tag": "0048_flat_expediter",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 49,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1733628762978,
|
||||||
|
"tag": "0049_dark_leopardon",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 50,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1733889104203,
|
||||||
|
"tag": "0050_nappy_wrecker",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 51,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1734241482851,
|
||||||
|
"tag": "0051_hard_gorgon",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 52,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1734809337308,
|
||||||
|
"tag": "0052_bumpy_luckman",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 53,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1735118844878,
|
||||||
|
"tag": "0053_broken_kulan_gath",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 54,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1736669421560,
|
||||||
|
"tag": "0054_nervous_spencer_smythe",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 55,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1736669623831,
|
||||||
|
"tag": "0055_next_serpent_society",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 56,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1736789918294,
|
||||||
|
"tag": "0056_majestic_skaar",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 57,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737306063563,
|
||||||
|
"tag": "0057_tricky_living_tribunal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 58,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737612903012,
|
||||||
|
"tag": "0058_brown_sharon_carter",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 59,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737615160768,
|
||||||
|
"tag": "0059_striped_bill_hollister",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 60,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737929896838,
|
||||||
|
"tag": "0060_disable-aggressive-cache",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 61,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1738481304953,
|
||||||
|
"tag": "0061_many_molten_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 62,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1738482795112,
|
||||||
|
"tag": "0062_slippery_white_tiger",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 63,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1738522845992,
|
||||||
|
"tag": "0063_panoramic_dreadnoughts",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 64,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1738564387043,
|
||||||
|
"tag": "0064_previous_agent_brand",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 65,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739087857244,
|
||||||
|
"tag": "0065_daily_zaladane",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 66,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1739426913392,
|
||||||
|
"tag": "0066_yielding_echo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 67,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1740892043121,
|
||||||
|
"tag": "0067_condemned_sugar_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 68,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1740897756774,
|
||||||
|
"tag": "0068_complex_rhino",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 69,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741152916611,
|
||||||
|
"tag": "0069_legal_bill_hollister",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741322697251,
|
||||||
|
"tag": "0070_useful_serpent_society",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 71,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741559743256,
|
||||||
|
"tag": "0071_flimsy_plazm",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 72,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741593124105,
|
||||||
|
"tag": "0072_low_redwing",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 73,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741645208694,
|
||||||
|
"tag": "0073_dark_tigra",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 74,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741673569715,
|
||||||
|
"tag": "0074_military_miss_america",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 75,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1742018928109,
|
||||||
|
"tag": "0075_wild_xorn",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 76,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1742237840762,
|
||||||
|
"tag": "0076_tough_iron_patriot",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 77,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1742238314349,
|
||||||
|
"tag": "0077_mature_tomorrow_man",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Sorted list based off of population of the country / speakers of the language.
|
||||||
|
*/
|
||||||
export const Languages = {
|
export const Languages = {
|
||||||
english: { code: "en", name: "English" },
|
english: { code: "en", name: "English" },
|
||||||
|
spanish: { code: "es", name: "Español" },
|
||||||
|
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
|
||||||
|
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
|
||||||
|
portuguese: { code: "pt-br", name: "Português" },
|
||||||
|
russian: { code: "ru", name: "Русский" },
|
||||||
|
japanese: { code: "ja", name: "日本語" },
|
||||||
|
german: { code: "de", name: "Deutsch" },
|
||||||
|
korean: { code: "ko", name: "한국어" },
|
||||||
|
french: { code: "fr", name: "Français" },
|
||||||
|
turkish: { code: "tr", name: "Türkçe" },
|
||||||
|
italian: { code: "it", name: "Italiano" },
|
||||||
polish: { code: "pl", name: "Polski" },
|
polish: { code: "pl", name: "Polski" },
|
||||||
ukrainian: { code: "uk", name: "Українська" },
|
ukrainian: { code: "uk", name: "Українська" },
|
||||||
russian: { code: "ru", name: "Русский" },
|
|
||||||
french: { code: "fr", name: "Français" },
|
|
||||||
german: { code: "de", name: "Deutsch" },
|
|
||||||
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
|
|
||||||
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
|
|
||||||
turkish: { code: "tr", name: "Türkçe" },
|
|
||||||
kazakh: { code: "kz", name: "Қазақ" },
|
|
||||||
persian: { code: "fa", name: "فارسی" },
|
persian: { code: "fa", name: "فارسی" },
|
||||||
korean: { code: "ko", name: "한국어" },
|
dutch: { code: "nl", name: "Nederlands" },
|
||||||
portuguese: { code: "pt-br", name: "Português" },
|
indonesian: { code: "id", name: "Bahasa Indonesia" },
|
||||||
italian: { code: "it", name: "Italiano" },
|
kazakh: { code: "kz", name: "Қазақ" },
|
||||||
japanese: { code: "ja", name: "日本語" },
|
|
||||||
spanish: { code: "es", name: "Español" },
|
|
||||||
norwegian: { code: "no", name: "Norsk" },
|
norwegian: { code: "no", name: "Norsk" },
|
||||||
azerbaijani: { code: "az", name: "Azərbaycan" },
|
azerbaijani: { code: "az", name: "Azərbaycan" },
|
||||||
indonesian: { code: "id", name: "Bahasa Indonesia" },
|
|
||||||
malayalam: { code: "ml", name: "മലയാളം" },
|
malayalam: { code: "ml", name: "മലയാളം" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// middleware.ts
|
|
||||||
import { verifyRequestOrigin } from "lucia";
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import type { NextRequest } from "next/server";
|
|
||||||
|
|
||||||
export async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
||||||
if (request.method === "GET") {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
const originHeader = request.headers.get("Origin");
|
|
||||||
const hostHeader = request.headers.get("Host");
|
|
||||||
|
|
||||||
if (
|
|
||||||
!originHeader ||
|
|
||||||
!hostHeader ||
|
|
||||||
!verifyRequestOrigin(originHeader, [hostHeader])
|
|
||||||
) {
|
|
||||||
return new NextResponse(null, {
|
|
||||||
status: 403,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
// Don't handle HMR requests for the dev server we rewrite to
|
|
||||||
"/settings",
|
|
||||||
"/dashboard/(.*)",
|
|
||||||
"/invitation",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.20.6",
|
"version": "v0.21.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
"@dokploy/trpc-openapi": "0.0.4",
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
|
||||||
"@octokit/auth-app": "^6.0.4",
|
"@octokit/auth-app": "^6.0.4",
|
||||||
"@octokit/webhooks": "^13.2.7",
|
"@octokit/webhooks": "^13.2.7",
|
||||||
"@radix-ui/react-accordion": "1.1.2",
|
"@radix-ui/react-accordion": "1.1.2",
|
||||||
@@ -93,7 +92,7 @@
|
|||||||
"adm-zip": "^0.5.14",
|
"adm-zip": "^0.5.14",
|
||||||
"ai": "^4.0.23",
|
"ai": "^4.0.23",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "1.2.4",
|
"better-auth": "1.2.6",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
"bullmq": "5.4.2",
|
||||||
@@ -113,11 +112,10 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lucia": "^3.0.1",
|
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"next": "^15.0.1",
|
"next": "^15.2.4",
|
||||||
"next-i18next": "^15.3.1",
|
"next-i18next": "^15.3.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-os-utils": "1.3.7",
|
"node-os-utils": "1.3.7",
|
||||||
@@ -152,7 +150,8 @@
|
|||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4",
|
||||||
"zod-form-data": "^2.0.2"
|
"zod-form-data": "^2.0.2",
|
||||||
|
"toml": "3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
|
|||||||
@@ -84,6 +84,33 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const provider = getProviderByHeader(req.headers);
|
||||||
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
|
if (provider === "github") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitlab") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitea") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
application.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
@@ -128,6 +155,27 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Watch Paths Not Match" });
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (sourceType === "gitea") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
application.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branchName || branchName !== application.giteaBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -280,6 +328,26 @@ export const extractBranchName = (headers: any, body: any) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProviderByHeader = (headers: any) => {
|
||||||
|
if (headers["x-github-event"]) {
|
||||||
|
return "github";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["x-gitea-event"]) {
|
||||||
|
return "gitea";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["x-gitlab-event"]) {
|
||||||
|
return "gitlab";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["x-event-key"]?.includes("repo:push")) {
|
||||||
|
return "bitbucket";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const extractCommitedPaths = async (
|
export const extractCommitedPaths = async (
|
||||||
body: any,
|
body: any,
|
||||||
bitbucketUsername: string | null,
|
bitbucketUsername: string | null,
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { eq } from "drizzle-orm";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import {
|
import {
|
||||||
extractBranchName,
|
extractBranchName,
|
||||||
extractCommitedPaths,
|
|
||||||
extractCommitMessage,
|
extractCommitMessage,
|
||||||
|
extractCommitedPaths,
|
||||||
extractHash,
|
extractHash,
|
||||||
|
getProviderByHeader,
|
||||||
} from "../[refreshToken]";
|
} from "../[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
@@ -91,12 +92,6 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (sourceType === "git") {
|
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
|
||||||
if (!branchName || branchName !== composeResult.customGitBranch) {
|
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitedPaths = await extractCommitedPaths(
|
const commitedPaths = await extractCommitedPaths(
|
||||||
req.body,
|
req.body,
|
||||||
@@ -104,6 +99,7 @@ export default async function handler(
|
|||||||
composeResult.bitbucket?.appPassword || "",
|
composeResult.bitbucket?.appPassword || "",
|
||||||
composeResult.bitbucketRepository || "",
|
composeResult.bitbucketRepository || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
commitedPaths,
|
commitedPaths,
|
||||||
@@ -113,6 +109,59 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Watch Paths Not Match" });
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (sourceType === "git") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
if (!branchName || branchName !== composeResult.customGitBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const provider = getProviderByHeader(req.headers);
|
||||||
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
|
if (provider === "github") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitlab") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitea") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeResult.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (sourceType === "gitea") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeResult.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branchName || branchName !== composeResult.giteaBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
41
apps/dokploy/pages/api/providers/gitea/authorize.ts
Normal file
41
apps/dokploy/pages/api/providers/gitea/authorize.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { findGitea, redirectWithError } from "./helper";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { giteaId } = req.query;
|
||||||
|
|
||||||
|
if (!giteaId || Array.isArray(giteaId)) {
|
||||||
|
return res.status(400).json({ error: "Invalid Gitea provider ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitea = await findGitea(giteaId as string);
|
||||||
|
if (!gitea || !gitea.clientId || !gitea.redirectUri) {
|
||||||
|
return redirectWithError(res, "Incomplete OAuth configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the Gitea authorization URL
|
||||||
|
const authorizationUrl = new URL(`${gitea.giteaUrl}/login/oauth/authorize`);
|
||||||
|
authorizationUrl.searchParams.append("client_id", gitea.clientId as string);
|
||||||
|
authorizationUrl.searchParams.append("response_type", "code");
|
||||||
|
authorizationUrl.searchParams.append(
|
||||||
|
"redirect_uri",
|
||||||
|
gitea.redirectUri as string,
|
||||||
|
);
|
||||||
|
authorizationUrl.searchParams.append("scope", "read:user repo");
|
||||||
|
authorizationUrl.searchParams.append("state", giteaId as string);
|
||||||
|
|
||||||
|
// Redirect user to Gitea authorization URL
|
||||||
|
return res.redirect(307, authorizationUrl.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initiating Gitea OAuth flow:", error);
|
||||||
|
return res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user