mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
217 Commits
feat/backu
...
1730-pg_re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eef874ecd4 | ||
|
|
d6daa5677a | ||
|
|
dc03ba73b3 | ||
|
|
5c2159f7b2 | ||
|
|
ffcdbcf046 | ||
|
|
c0b35efaca | ||
|
|
22dee88e51 | ||
|
|
1645f7e932 | ||
|
|
b4aeb6577e | ||
|
|
fdd330ca19 | ||
|
|
33de620893 | ||
|
|
6518407c0c | ||
|
|
6f47999a2e | ||
|
|
fe69d5d405 | ||
|
|
a6880fd38c | ||
|
|
5d25de13dd | ||
|
|
5611dcccfd | ||
|
|
e2a1882fe3 | ||
|
|
ceb16ae9f7 | ||
|
|
1911b5b674 | ||
|
|
6b818bbb51 | ||
|
|
79796185d6 | ||
|
|
461d7c530a | ||
|
|
ade4b8dd1b | ||
|
|
f49a67f8df | ||
|
|
c3986d7a08 | ||
|
|
0bf4e5560c | ||
|
|
79d55d8d34 | ||
|
|
d4c6e5b048 | ||
|
|
cd4eed3507 | ||
|
|
a650bd16fb | ||
|
|
4e5b5f219e | ||
|
|
dfda934726 | ||
|
|
e6d0b7b4ee | ||
|
|
d0dbc1837f | ||
|
|
2b5af1897f | ||
|
|
11b9cee73d | ||
|
|
bc17991580 | ||
|
|
8d28a50a17 | ||
|
|
08bbeceeba | ||
|
|
b7bf09bf21 | ||
|
|
546c6ade82 | ||
|
|
db2e3691a5 | ||
|
|
a6dca144a8 | ||
|
|
43a17e7e75 | ||
|
|
da60c4f3a8 | ||
|
|
e14f2780af | ||
|
|
33ab87f3db | ||
|
|
571d73a5b6 | ||
|
|
a630909612 | ||
|
|
8eaa006f0f | ||
|
|
8e8bc3e71e | ||
|
|
f4cd617107 | ||
|
|
48cfe66a6b | ||
|
|
bdc10cacef | ||
|
|
8fbad8a26e | ||
|
|
0f36bcb04e | ||
|
|
f4054453b4 | ||
|
|
dbd36fc024 | ||
|
|
850d06a32c | ||
|
|
dfd3dc180d | ||
|
|
3d42bfc81b | ||
|
|
764f8ec993 | ||
|
|
d2eaa4b40b | ||
|
|
7d7f2b4b1f | ||
|
|
8e97c63faa | ||
|
|
74ec8f4594 | ||
|
|
76c0bff13a | ||
|
|
9b5cd0f5fe | ||
|
|
efee798880 | ||
|
|
1c470b8ba7 | ||
|
|
692864ced1 | ||
|
|
9ca61476d2 | ||
|
|
773a610be1 | ||
|
|
37f9e073f0 | ||
|
|
d335a9515d | ||
|
|
7a5a3de43d | ||
|
|
ef7918a33a | ||
|
|
ee6ad07c0a | ||
|
|
48fe26b204 | ||
|
|
3ede89fe8a | ||
|
|
fa698d173e | ||
|
|
1279fac137 | ||
|
|
0e1f0b42ee | ||
|
|
05f43ad06b | ||
|
|
af4511040f | ||
|
|
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 | ||
|
|
1bbbdfba60 | ||
|
|
031d0ce315 | ||
|
|
131a1acbbe | ||
|
|
9a839de022 | ||
|
|
b9de05015f | ||
|
|
e176def5b6 | ||
|
|
94c947e288 | ||
|
|
116e33ce37 | ||
|
|
0bdaa81263 | ||
|
|
baf36b6fb6 | ||
|
|
d632e83799 | ||
|
|
6f52edd845 | ||
|
|
e9b92d2641 | ||
|
|
9d0f5bc8cd | ||
|
|
3dc558c260 | ||
|
|
180aa34140 | ||
|
|
96e9799afb | ||
|
|
3e07be38df | ||
|
|
ffc85b04a8 | ||
|
|
dbcfc702d4 | ||
|
|
67e85cabcb | ||
|
|
7805efc738 | ||
|
|
3910e22412 | ||
|
|
2f16034cb0 | ||
|
|
d4925dd2b7 | ||
|
|
5aba6c79a0 | ||
|
|
84f5627471 | ||
|
|
4eaf8fee0f | ||
|
|
adee87b6da | ||
|
|
e5e987fcf9 | ||
|
|
d0a6373dcc | ||
|
|
8ed44066ad | ||
|
|
befe2193a7 | ||
|
|
f20c73cdee | ||
|
|
64a77decfd | ||
|
|
16bfc09202 | ||
|
|
d54a61b2a4 | ||
|
|
60c09a6434 | ||
|
|
5361e9074f | ||
|
|
13d4dea504 | ||
|
|
ffc2d593e4 | ||
|
|
297439a348 | ||
|
|
ff3e067866 | ||
|
|
f008a45bf2 | ||
|
|
50c8503cf9 | ||
|
|
930a03de60 | ||
|
|
2d3d86e823 | ||
|
|
7bab166e1b | ||
|
|
7a6e1dbc1b | ||
|
|
17a859d26d | ||
|
|
d793c6a2ec | ||
|
|
3adb9d54f4 | ||
|
|
7144adbf0c | ||
|
|
55328468d1 | ||
|
|
fe967239b4 | ||
|
|
1f28a21835 | ||
|
|
0114b371f5 | ||
|
|
66d6cb5710 | ||
|
|
5927c7c3c5 | ||
|
|
84afcf0de5 | ||
|
|
e3527f7d69 | ||
|
|
cc5a3e6873 | ||
|
|
39f4a35cc8 | ||
|
|
5d5913f39d | ||
|
|
f04c8a36af | ||
|
|
d5137d5d3a | ||
|
|
048c8ffc11 | ||
|
|
b59597630c | ||
|
|
707463f973 | ||
|
|
4b3e0805a4 | ||
|
|
148c30f604 | ||
|
|
95f79f2afb | ||
|
|
a067abd3e4 | ||
|
|
9359ee7a04 | ||
|
|
fc7eff94b6 | ||
|
|
ff3d444b89 | ||
|
|
530ad31aaa | ||
|
|
a4e4d1c467 | ||
|
|
56d8defebe | ||
|
|
997e755b6f | ||
|
|
852011dde8 | ||
|
|
d7ef201adb | ||
|
|
b1d1763988 | ||
|
|
4eef65f1b7 | ||
|
|
a7535c6862 | ||
|
|
b5d199057d | ||
|
|
ff22404b3b | ||
|
|
bfb6baf572 | ||
|
|
17330ca71a | ||
|
|
2898a5e575 | ||
|
|
fac8ea7a30 | ||
|
|
9a11d0db97 | ||
|
|
371c6317aa | ||
|
|
1f81794904 | ||
|
|
d5d3831d54 | ||
|
|
cf28640188 | ||
|
|
856399550a | ||
|
|
ea39b152f4 | ||
|
|
027406547e | ||
|
|
86b8b0987b | ||
|
|
0dac1fefe6 | ||
|
|
633ba899e0 |
@@ -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
|
||||||
@@ -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
|
||||||
@@ -145,11 +147,9 @@ curl -sSL https://railpack.com/install.sh | sh
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Buildpacks
|
# Install Buildpacks
|
||||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Pull Request
|
## Pull Request
|
||||||
|
|
||||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||||
@@ -167,7 +167,6 @@ Thank you for your contribution!
|
|||||||
|
|
||||||
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
|
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
|
||||||
|
|
||||||
|
|
||||||
### Recommendations
|
### Recommendations
|
||||||
|
|
||||||
- Use the same name of the folder as the id of the template.
|
- Use the same name of the folder as the id of the template.
|
||||||
|
|||||||
@@ -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,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
|||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|
||||||
ARG NIXPACKS_VERSION=1.29.1
|
ARG NIXPACKS_VERSION=1.35.0
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||||
&& chmod +x install.sh \
|
&& chmod +x install.sh \
|
||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
|
|||||||
@@ -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,8 +27,14 @@ 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: [],
|
||||||
|
enableSubmodules: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -51,6 +51,35 @@ describe("processTemplate", () => {
|
|||||||
expect(result.domains).toHaveLength(0);
|
expect(result.domains).toHaveLength(0);
|
||||||
expect(result.mounts).toHaveLength(0);
|
expect(result.mounts).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow creation of real jwt secret", () => {
|
||||||
|
const template: CompleteTemplate = {
|
||||||
|
metadata: {} as any,
|
||||||
|
variables: {
|
||||||
|
jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ",
|
||||||
|
anon_payload: JSON.stringify({
|
||||||
|
role: "tester",
|
||||||
|
iss: "dockploy",
|
||||||
|
iat: "${timestamps:2025-01-01T00:00:00Z}",
|
||||||
|
exp: "${timestamps:2030-01-01T00:00:00Z}",
|
||||||
|
}),
|
||||||
|
anon_key: "${jwt:jwt_secret:anon_payload}",
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
domains: [],
|
||||||
|
env: {
|
||||||
|
ANON_KEY: "${anon_key}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = processTemplate(template, mockSchema);
|
||||||
|
expect(result.envs).toHaveLength(1);
|
||||||
|
expect(result.envs).toContain(
|
||||||
|
"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY",
|
||||||
|
);
|
||||||
|
expect(result.mounts).toHaveLength(0);
|
||||||
|
expect(result.domains).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("domains processing", () => {
|
describe("domains processing", () => {
|
||||||
@@ -233,6 +262,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", () => {
|
||||||
|
|||||||
232
apps/dokploy/__test__/templates/helpers.template.test.ts
Normal file
232
apps/dokploy/__test__/templates/helpers.template.test.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import type { Schema } from "@dokploy/server/templates";
|
||||||
|
import { processValue } from "@dokploy/server/templates/processors";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("helpers functions", () => {
|
||||||
|
// Mock schema for testing
|
||||||
|
const mockSchema: Schema = {
|
||||||
|
projectName: "test",
|
||||||
|
serverIp: "127.0.0.1",
|
||||||
|
};
|
||||||
|
// some helpers to test jwt
|
||||||
|
type JWTParts = [string, string, string];
|
||||||
|
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
|
||||||
|
const jwtBase64Decode = (str: string) => {
|
||||||
|
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
||||||
|
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
|
||||||
|
return JSON.parse(decoded);
|
||||||
|
};
|
||||||
|
const jwtCheckHeader = (jwtHeader: string) => {
|
||||||
|
const decodedHeader = jwtBase64Decode(jwtHeader);
|
||||||
|
expect(decodedHeader).toHaveProperty("alg");
|
||||||
|
expect(decodedHeader).toHaveProperty("typ");
|
||||||
|
expect(decodedHeader.alg).toEqual("HS256");
|
||||||
|
expect(decodedHeader.typ).toEqual("JWT");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("${domain}", () => {
|
||||||
|
it("should generate a random domain", () => {
|
||||||
|
const domain = processValue("${domain}", {}, mockSchema);
|
||||||
|
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
domain.endsWith(
|
||||||
|
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${base64}", () => {
|
||||||
|
it("should generate a base64 string", () => {
|
||||||
|
const base64 = processValue("${base64}", {}, mockSchema);
|
||||||
|
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
|
||||||
|
});
|
||||||
|
it.each([
|
||||||
|
[4, 8],
|
||||||
|
[8, 12],
|
||||||
|
[16, 24],
|
||||||
|
[32, 44],
|
||||||
|
[64, 88],
|
||||||
|
[128, 172],
|
||||||
|
])(
|
||||||
|
"should generate a base64 string from parameter %d bytes length",
|
||||||
|
(length, finalLength) => {
|
||||||
|
const base64 = processValue(`\${base64:${length}}`, {}, mockSchema);
|
||||||
|
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
|
||||||
|
expect(base64.length).toBe(finalLength);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${password}", () => {
|
||||||
|
it("should generate a password string", () => {
|
||||||
|
const password = processValue("${password}", {}, mockSchema);
|
||||||
|
expect(password).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
});
|
||||||
|
it.each([6, 8, 12, 16, 32])(
|
||||||
|
"should generate a password string respecting parameter %d length",
|
||||||
|
(length) => {
|
||||||
|
const password = processValue(`\${password:${length}}`, {}, mockSchema);
|
||||||
|
expect(password).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
expect(password.length).toBe(length);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${hash}", () => {
|
||||||
|
it("should generate a hash string", () => {
|
||||||
|
const hash = processValue("${hash}", {}, mockSchema);
|
||||||
|
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
});
|
||||||
|
it.each([6, 8, 12, 16, 32])(
|
||||||
|
"should generate a hash string respecting parameter %d length",
|
||||||
|
(length) => {
|
||||||
|
const hash = processValue(`\${hash:${length}}`, {}, mockSchema);
|
||||||
|
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
expect(hash.length).toBe(length);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${uuid}", () => {
|
||||||
|
it("should generate a UUID string", () => {
|
||||||
|
const uuid = processValue("${uuid}", {}, mockSchema);
|
||||||
|
expect(uuid).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${timestamp}", () => {
|
||||||
|
it("should generate a timestamp string in milliseconds", () => {
|
||||||
|
const timestamp = processValue("${timestamp}", {}, mockSchema);
|
||||||
|
const nowLength = Math.floor(Date.now()).toString().length;
|
||||||
|
expect(timestamp).toMatch(/^\d+$/);
|
||||||
|
expect(timestamp.length).toBe(nowLength);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${timestampms}", () => {
|
||||||
|
it("should generate a timestamp string in milliseconds", () => {
|
||||||
|
const timestamp = processValue("${timestampms}", {}, mockSchema);
|
||||||
|
const nowLength = Date.now().toString().length;
|
||||||
|
expect(timestamp).toMatch(/^\d+$/);
|
||||||
|
expect(timestamp.length).toBe(nowLength);
|
||||||
|
});
|
||||||
|
it("should generate a timestamp string in milliseconds from parameter", () => {
|
||||||
|
const timestamp = processValue(
|
||||||
|
"${timestampms:2025-01-01}",
|
||||||
|
{},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(timestamp).toEqual("1735689600000");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${timestamps}", () => {
|
||||||
|
it("should generate a timestamp string in seconds", () => {
|
||||||
|
const timestamps = processValue("${timestamps}", {}, mockSchema);
|
||||||
|
const nowLength = Math.floor(Date.now() / 1000).toString().length;
|
||||||
|
expect(timestamps).toMatch(/^\d+$/);
|
||||||
|
expect(timestamps.length).toBe(nowLength);
|
||||||
|
});
|
||||||
|
it("should generate a timestamp string in seconds from parameter", () => {
|
||||||
|
const timestamps = processValue(
|
||||||
|
"${timestamps:2025-01-01}",
|
||||||
|
{},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(timestamps).toEqual("1735689600");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${randomPort}", () => {
|
||||||
|
it("should generate a random port string", () => {
|
||||||
|
const randomPort = processValue("${randomPort}", {}, mockSchema);
|
||||||
|
expect(randomPort).toMatch(/^\d+$/);
|
||||||
|
expect(Number(randomPort)).toBeLessThan(65536);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${username}", () => {
|
||||||
|
it("should generate a username string", () => {
|
||||||
|
const username = processValue("${username}", {}, mockSchema);
|
||||||
|
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${email}", () => {
|
||||||
|
it("should generate an email string", () => {
|
||||||
|
const email = processValue("${email}", {}, mockSchema);
|
||||||
|
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("${jwt}", () => {
|
||||||
|
it("should generate a JWT string", () => {
|
||||||
|
const jwt = processValue("${jwt}", {}, mockSchema);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
expect(decodedPayload).toHaveProperty("iat");
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.iss).toEqual("dokploy");
|
||||||
|
});
|
||||||
|
it.each([6, 8, 12, 16, 32])(
|
||||||
|
"should generate a random hex string from parameter %d byte length",
|
||||||
|
(length) => {
|
||||||
|
const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema);
|
||||||
|
expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/);
|
||||||
|
expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length
|
||||||
|
expect(jwt.length).toBeLessThanOrEqual(length * 2);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
describe("${jwt:secret}", () => {
|
||||||
|
it("should generate a JWT string respecting parameter secret from variable", () => {
|
||||||
|
const jwt = processValue(
|
||||||
|
"${jwt:secret}",
|
||||||
|
{ secret: "mysecret" },
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
expect(decodedPayload).toHaveProperty("iat");
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.iss).toEqual("dokploy");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("${jwt:secret:payload}", () => {
|
||||||
|
it("should generate a JWT string respecting parameters secret and payload from variables", () => {
|
||||||
|
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||||
|
const expiry = iat + 3600;
|
||||||
|
const jwt = processValue(
|
||||||
|
"${jwt:secret:payload}",
|
||||||
|
{
|
||||||
|
secret: "mysecret",
|
||||||
|
payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`,
|
||||||
|
},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
expect(decodedPayload).toHaveProperty("iat");
|
||||||
|
expect(decodedPayload.iat).toEqual(iat);
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload.iss).toEqual("test-issuer");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.exp).toEqual(expiry);
|
||||||
|
expect(decodedPayload).toHaveProperty("customprop");
|
||||||
|
expect(decodedPayload.customprop).toEqual("customvalue");
|
||||||
|
expect(jwt).toEqual(
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,10 +7,16 @@ 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: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
enableSubmodules: false,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
|||||||
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/");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [form.formState.isSubmitSuccessful, form.reset, data, form]);
|
}, [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,193 +208,143 @@ 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>
|
<RadioGroup
|
||||||
<RadioGroup
|
onValueChange={field.onChange}
|
||||||
onValueChange={field.onChange}
|
value={field.value}
|
||||||
value={field.value}
|
className="flex flex-col space-y-1"
|
||||||
className="flex flex-col space-y-1"
|
>
|
||||||
>
|
{Object.entries(buildTypeDisplayMap).map(
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
([value, label]) => (
|
||||||
<FormControl>
|
<FormItem
|
||||||
<RadioGroupItem value="dockerfile" />
|
key={value}
|
||||||
</FormControl>
|
className="flex items-center space-x-3 space-y-0"
|
||||||
<FormLabel className="font-normal">
|
>
|
||||||
Dockerfile
|
<FormControl>
|
||||||
</FormLabel>
|
<RadioGroupItem value={value} />
|
||||||
</FormItem>
|
</FormControl>
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
<FormLabel className="font-normal">
|
||||||
<FormControl>
|
{label}
|
||||||
<RadioGroupItem value="railpack" />
|
{value === BuildType.railpack && (
|
||||||
</FormControl>
|
<Badge className="ml-2 px-1 text-xs">New</Badge>
|
||||||
<FormLabel className="font-normal">
|
)}
|
||||||
Railpack{" "}
|
</FormLabel>
|
||||||
<Badge className="ml-1 text-xs px-1">New</Badge>
|
</FormItem>
|
||||||
</FormLabel>
|
),
|
||||||
</FormItem>
|
)}
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
</RadioGroup>
|
||||||
<FormControl>
|
</FormControl>
|
||||||
<RadioGroupItem value="nixpacks" />
|
<FormMessage />
|
||||||
</FormControl>
|
</FormItem>
|
||||||
<FormLabel className="font-normal">
|
)}
|
||||||
Nixpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="heroku_buildpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Heroku Buildpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="paketo_buildpacks" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
Paketo Buildpacks
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value="static" />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">Static</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{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 />
|
||||||
|
</FormItem>
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{buildType === "dockerfile" && (
|
{buildType === BuildType.dockerfile && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerfile"
|
name="dockerfile"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Docker File</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"Path of your docker file"}
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="dockerContextPath"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Docker Context Path</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={
|
|
||||||
"Path of your docker context default: ."
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="dockerBuildStage"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Docker Build Stage</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allows you to target a specific stage in a
|
|
||||||
Multi-stage Dockerfile. If empty, Docker defaults to
|
|
||||||
build the last defined stage.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"E.g. production"}
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{buildType === "nixpacks" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="publishDirectory"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="space-y-0.5">
|
<FormLabel>Docker File</FormLabel>
|
||||||
<FormLabel>Publish Directory</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Allows you to serve a single directory via NGINX after
|
|
||||||
the build phase. Useful if the final build assets
|
|
||||||
should be served as a static site.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"Publish Directory"}
|
placeholder="Path of your docker file"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
)}
|
||||||
}}
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerContextPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Docker Context Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Path of your docker context (default: .)"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerBuildStage"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Docker Build Stage</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allows you to target a specific stage in a Multi-stage
|
||||||
|
Dockerfile. If empty, Docker defaults to build the
|
||||||
|
last defined stage.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="E.g. production"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{buildType === BuildType.nixpacks && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="publishDirectory"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Publish Directory</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allows you to serve a single directory via NGINX after
|
||||||
|
the build phase. Useful if the final build assets should
|
||||||
|
be served as a static site.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Publish Directory"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex w-full justify-end">
|
<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,
|
||||||
@@ -29,6 +31,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -39,13 +42,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("/"),
|
||||||
@@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||||
@@ -84,6 +86,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
bitbucketId: "",
|
bitbucketId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(BitbucketProviderSchema),
|
resolver: zodResolver(BitbucketProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -130,6 +133,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.bitbucketBuildPath || "/",
|
buildPath: data.bitbucketBuildPath || "/",
|
||||||
bitbucketId: data.bitbucketId || "",
|
bitbucketId: data.bitbucketId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -143,6 +147,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
bitbucketId: data.bitbucketId,
|
bitbucketId: data.bitbucketId,
|
||||||
applicationId,
|
applicationId,
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -467,6 +472,21 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -23,18 +23,19 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
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 { 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("/"),
|
||||||
@@ -44,6 +45,7 @@ const GitProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch required"),
|
branch: z.string().min(1, "Branch required"),
|
||||||
sshKey: z.string().optional(),
|
sshKey: z.string().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||||
@@ -67,6 +69,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
repositoryURL: "",
|
repositoryURL: "",
|
||||||
sshKey: undefined,
|
sshKey: undefined,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitProviderSchema),
|
resolver: zodResolver(GitProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -79,6 +82,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.customGitBuildPath || "/",
|
buildPath: data.customGitBuildPath || "/",
|
||||||
repositoryURL: data.customGitUrl || "",
|
repositoryURL: data.customGitUrl || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -91,6 +95,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
||||||
applicationId,
|
applicationId,
|
||||||
watchPaths: values.watchPaths || [],
|
watchPaths: values.watchPaths || [],
|
||||||
|
enableSubmodules: values.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Git Provider Saved");
|
toast.success("Git Provider Saved");
|
||||||
@@ -294,6 +299,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
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 { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { 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([]),
|
||||||
|
enableSubmodules: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
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: [],
|
||||||
|
enableSubmodules: false,
|
||||||
|
},
|
||||||
|
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 || [],
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [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,
|
||||||
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
|
})
|
||||||
|
.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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</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,
|
||||||
@@ -28,23 +30,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
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("/"),
|
||||||
@@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
githubId: z.string().min(1, "Github Provider is required"),
|
githubId: z.string().min(1, "Github Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||||
@@ -81,6 +83,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
githubId: "",
|
githubId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GithubProviderSchema),
|
resolver: zodResolver(GithubProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -124,6 +127,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.buildPath || "/",
|
buildPath: data.buildPath || "/",
|
||||||
githubId: data.githubId || "",
|
githubId: data.githubId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -137,6 +141,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.buildPath,
|
buildPath: data.buildPath,
|
||||||
githubId: data.githubId,
|
githubId: data.githubId,
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -458,6 +463,22 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -29,23 +31,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
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("/"),
|
||||||
@@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||||
@@ -86,6 +88,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
gitlabId: "",
|
gitlabId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitlabProviderSchema),
|
resolver: zodResolver(GitlabProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -135,6 +138,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
buildPath: data.gitlabBuildPath || "/",
|
buildPath: data.gitlabBuildPath || "/",
|
||||||
gitlabId: data.gitlabId || "",
|
gitlabId: data.gitlabId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -150,6 +154,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
gitlabProjectId: data.repository.id,
|
gitlabProjectId: data.repository.id,
|
||||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -483,6 +488,21 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -55,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
setSab(e as TabState);
|
setSab(e as TabState);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="github"
|
value="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>;
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
toast.error("Error updating the Compose config");
|
toast.error("Error updating the Compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex flex-col gap-4 ">
|
<div className="w-full flex flex-col gap-4 ">
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -29,6 +31,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -39,13 +42,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),
|
||||||
@@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||||
@@ -84,6 +86,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
bitbucketId: "",
|
bitbucketId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(BitbucketProviderSchema),
|
resolver: zodResolver(BitbucketProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -130,6 +133,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
bitbucketId: data.bitbucketId || "",
|
bitbucketId: data.bitbucketId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -145,6 +149,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
sourceType: "bitbucket",
|
sourceType: "bitbucket",
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
watchPaths: data.watchPaths,
|
watchPaths: data.watchPaths,
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -469,6 +474,21 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -27,13 +29,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),
|
||||||
@@ -43,6 +44,7 @@ const GitProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch required"),
|
branch: z.string().min(1, "Branch required"),
|
||||||
sshKey: z.string().optional(),
|
sshKey: z.string().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||||
@@ -65,6 +67,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
composePath: "./docker-compose.yml",
|
composePath: "./docker-compose.yml",
|
||||||
sshKey: undefined,
|
sshKey: undefined,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitProviderSchema),
|
resolver: zodResolver(GitProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -77,6 +80,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
repositoryURL: data.customGitUrl || "",
|
repositoryURL: data.customGitUrl || "",
|
||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -91,6 +95,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
composePath: values.composePath,
|
composePath: values.composePath,
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
watchPaths: values.watchPaths || [],
|
watchPaths: values.watchPaths || [],
|
||||||
|
enableSubmodules: values.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Git Provider Saved");
|
toast.success("Git Provider Saved");
|
||||||
@@ -295,6 +300,21 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
|
|||||||
@@ -0,0 +1,503 @@
|
|||||||
|
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 { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import 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(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
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: [],
|
||||||
|
enableSubmodules: false,
|
||||||
|
},
|
||||||
|
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 || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [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,
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
|
} 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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</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 {
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -39,12 +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 { 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),
|
||||||
@@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
githubId: z.string().min(1, "Github Provider is required"),
|
githubId: z.string().min(1, "Github Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||||
@@ -82,6 +84,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
githubId: "",
|
githubId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GithubProviderSchema),
|
resolver: zodResolver(GithubProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -125,6 +128,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
githubId: data.githubId || "",
|
githubId: data.githubId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -140,6 +144,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
sourceType: "github",
|
sourceType: "github",
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
watchPaths: data.watchPaths,
|
watchPaths: data.watchPaths,
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -460,6 +465,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -29,6 +31,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -39,13 +42,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),
|
||||||
@@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
|
|||||||
branch: z.string().min(1, "Branch is required"),
|
branch: z.string().min(1, "Branch is required"),
|
||||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
enableSubmodules: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||||
@@ -87,6 +89,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
gitlabId: "",
|
gitlabId: "",
|
||||||
branch: "",
|
branch: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
enableSubmodules: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(GitlabProviderSchema),
|
resolver: zodResolver(GitlabProviderSchema),
|
||||||
});
|
});
|
||||||
@@ -136,6 +139,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
composePath: data.composePath,
|
composePath: data.composePath,
|
||||||
gitlabId: data.gitlabId || "",
|
gitlabId: data.gitlabId || "",
|
||||||
watchPaths: data.watchPaths || [],
|
watchPaths: data.watchPaths || [],
|
||||||
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
@@ -153,6 +157,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
sourceType: "gitlab",
|
sourceType: "gitlab",
|
||||||
composeStatus: "idle",
|
composeStatus: "idle",
|
||||||
watchPaths: data.watchPaths,
|
watchPaths: data.watchPaths,
|
||||||
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provided Saved");
|
||||||
@@ -485,6 +490,21 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableSubmodules"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -54,21 +58,21 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
setSab(e as TabState);
|
setSab(e as TabState);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full 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"
|
||||||
@@ -89,11 +98,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
value="raw"
|
value="raw"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
>
|
>
|
||||||
<CodeIcon className="size-4 " />
|
<CodeIcon className="size-4" />
|
||||||
Raw
|
Raw
|
||||||
</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>
|
||||||
|
|||||||
@@ -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,7 +112,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
? {
|
? {
|
||||||
mongoId: databaseId,
|
mongoId: databaseId,
|
||||||
}
|
}
|
||||||
: undefined;
|
: databaseType === "web-server"
|
||||||
|
? {
|
||||||
|
userId: databaseId,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await createBackup({
|
await createBackup({
|
||||||
destinationId: data.destinationId,
|
destinationId: data.destinationId,
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
@@ -77,6 +77,14 @@ const RestoreBackupSchema = z.object({
|
|||||||
|
|
||||||
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
|
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const RestoreBackup = ({
|
export const RestoreBackup = ({
|
||||||
databaseId,
|
databaseId,
|
||||||
databaseType,
|
databaseType,
|
||||||
@@ -84,6 +92,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 +100,7 @@ export const RestoreBackup = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
backupFile: "",
|
backupFile: "",
|
||||||
databaseName: "",
|
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(RestoreBackupSchema),
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
});
|
});
|
||||||
@@ -99,13 +108,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);
|
||||||
|
}, 350);
|
||||||
|
|
||||||
|
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 ?? "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -265,7 +279,7 @@ export const RestoreBackup = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Popover>
|
<Popover modal>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
@@ -284,7 +298,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 ? (
|
||||||
@@ -301,26 +316,51 @@ export const RestoreBackup = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea className="h-64">
|
<ScrollArea className="h-64">
|
||||||
<CommandGroup>
|
<CommandGroup className="w-96">
|
||||||
{files.map((file) => (
|
{files?.map((file) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={file}
|
value={file.Path}
|
||||||
key={file}
|
key={file.Path}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("backupFile", file);
|
form.setValue("backupFile", file.Path);
|
||||||
|
if (file.IsDir) {
|
||||||
|
setSearch(`${file.Path}/`);
|
||||||
|
setDebouncedSearchTerm(`${file.Path}/`);
|
||||||
|
} else {
|
||||||
|
setSearch(file.Path);
|
||||||
|
setDebouncedSearchTerm(file.Path);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full flex-col gap-1">
|
||||||
<span>{file}</span>
|
<div className="flex w-full justify-between">
|
||||||
|
<span className="font-medium">
|
||||||
|
{file.Path}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
file.Path === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
Size: {formatBytes(file.Size)}
|
||||||
|
</span>
|
||||||
|
{file.IsDir && (
|
||||||
|
<span className="text-blue-500">
|
||||||
|
Directory
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{file.Hashes?.MD5 && (
|
||||||
|
<span>MD5: {file.Hashes.MD5}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"ml-auto h-4 w-4",
|
|
||||||
file === field.value
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
@@ -340,7 +380,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>
|
||||||
|
|||||||
@@ -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">
|
||||||
<Link
|
{template?.links?.github && (
|
||||||
href={template.links.github}
|
|
||||||
target="_blank"
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
<GithubIcon className="size-5" />
|
|
||||||
</Link>
|
|
||||||
{template.links.website && (
|
|
||||||
<Link
|
<Link
|
||||||
href={template.links.website}
|
href={template?.links?.github}
|
||||||
|
target="_blank"
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<GithubIcon className="size-5" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{template?.links?.website && (
|
||||||
|
<Link
|
||||||
|
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,9 +31,14 @@ 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
|
||||||
message: "Name is required",
|
.string()
|
||||||
}),
|
.min(1, {
|
||||||
|
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 &&
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const AiForm = () => {
|
|||||||
key={config.aiId}
|
key={config.aiId}
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{config.name}
|
{config.name}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const ShowCertificates = () => {
|
|||||||
key={certificate.certificateId}
|
key={certificate.certificateId}
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-2 flex-col">
|
<div className="flex gap-2 flex-col">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
|
|||||||
@@ -13,53 +13,65 @@ export const extractExpirationDate = (certData: string): Date | null => {
|
|||||||
bytes[i] = binaryStr.charCodeAt(i);
|
bytes[i] = binaryStr.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateFound = 0;
|
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18
|
||||||
|
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
|
||||||
|
let dateFound = false;
|
||||||
for (let i = 0; i < bytes.length - 2; i++) {
|
for (let i = 0; i < bytes.length - 2; i++) {
|
||||||
if (bytes[i] === 0x17 || bytes[i] === 0x18) {
|
// Look for sequence containing validity period (0x30)
|
||||||
const dateType = bytes[i];
|
if (bytes[i] === 0x30) {
|
||||||
const dateLength = bytes[i + 1];
|
// Check next bytes for UTCTime or GeneralizedTime
|
||||||
if (typeof dateLength === "undefined") continue;
|
let j = i + 1;
|
||||||
|
while (j < bytes.length - 2) {
|
||||||
|
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
|
||||||
|
const dateType = bytes[j];
|
||||||
|
const dateLength = bytes[j + 1];
|
||||||
|
if (typeof dateLength === "undefined") break;
|
||||||
|
|
||||||
if (dateFound === 0) {
|
if (!dateFound) {
|
||||||
dateFound++;
|
// Skip "not before" date
|
||||||
i += dateLength + 1;
|
dateFound = true;
|
||||||
continue;
|
j += dateLength + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found "not after" date
|
||||||
|
let dateStr = "";
|
||||||
|
for (let k = 0; k < dateLength; k++) {
|
||||||
|
const charCode = bytes[j + 2 + k];
|
||||||
|
if (typeof charCode === "undefined") continue;
|
||||||
|
dateStr += String.fromCharCode(charCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateType === 0x17) {
|
||||||
|
// UTCTime (YYMMDDhhmmssZ)
|
||||||
|
const year = Number.parseInt(dateStr.slice(0, 2));
|
||||||
|
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
|
||||||
|
return new Date(
|
||||||
|
Date.UTC(
|
||||||
|
fullYear,
|
||||||
|
Number.parseInt(dateStr.slice(2, 4)) - 1,
|
||||||
|
Number.parseInt(dateStr.slice(4, 6)),
|
||||||
|
Number.parseInt(dateStr.slice(6, 8)),
|
||||||
|
Number.parseInt(dateStr.slice(8, 10)),
|
||||||
|
Number.parseInt(dateStr.slice(10, 12)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneralizedTime (YYYYMMDDhhmmssZ)
|
||||||
|
return new Date(
|
||||||
|
Date.UTC(
|
||||||
|
Number.parseInt(dateStr.slice(0, 4)),
|
||||||
|
Number.parseInt(dateStr.slice(4, 6)) - 1,
|
||||||
|
Number.parseInt(dateStr.slice(6, 8)),
|
||||||
|
Number.parseInt(dateStr.slice(8, 10)),
|
||||||
|
Number.parseInt(dateStr.slice(10, 12)),
|
||||||
|
Number.parseInt(dateStr.slice(12, 14)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
j++;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dateStr = "";
|
|
||||||
for (let j = 0; j < dateLength; j++) {
|
|
||||||
const charCode = bytes[i + 2 + j];
|
|
||||||
if (typeof charCode === "undefined") continue;
|
|
||||||
dateStr += String.fromCharCode(charCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateType === 0x17) {
|
|
||||||
// UTCTime (YYMMDDhhmmssZ)
|
|
||||||
const year = Number.parseInt(dateStr.slice(0, 2));
|
|
||||||
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
|
|
||||||
return new Date(
|
|
||||||
Date.UTC(
|
|
||||||
fullYear,
|
|
||||||
Number.parseInt(dateStr.slice(2, 4)) - 1,
|
|
||||||
Number.parseInt(dateStr.slice(4, 6)),
|
|
||||||
Number.parseInt(dateStr.slice(6, 8)),
|
|
||||||
Number.parseInt(dateStr.slice(8, 10)),
|
|
||||||
Number.parseInt(dateStr.slice(10, 12)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GeneralizedTime (YYYYMMDDhhmmssZ)
|
|
||||||
return new Date(
|
|
||||||
Date.UTC(
|
|
||||||
Number.parseInt(dateStr.slice(0, 4)),
|
|
||||||
Number.parseInt(dateStr.slice(4, 6)) - 1,
|
|
||||||
Number.parseInt(dateStr.slice(6, 8)),
|
|
||||||
Number.parseInt(dateStr.slice(8, 10)),
|
|
||||||
Number.parseInt(dateStr.slice(10, 12)),
|
|
||||||
Number.parseInt(dateStr.slice(12, 14)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -12,7 +13,6 @@ import { ExternalLink, PlusIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AddManager } from "./manager/add-manager";
|
import { AddManager } from "./manager/add-manager";
|
||||||
import { AddWorker } from "./workers/add-worker";
|
import { AddWorker } from "./workers/add-worker";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const ShowRegistry = () => {
|
|||||||
key={registry.registryId}
|
key={registry.registryId}
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-2 flex-col">
|
<div className="flex gap-2 flex-col">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const ShowDestinations = () => {
|
|||||||
key={destination.destinationId}
|
key={destination.destinationId}
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{index + 1}. {destination.name}
|
{index + 1}. {destination.name}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -122,18 +129,19 @@ export const ShowGitProviders = () => {
|
|||||||
key={gitProvider.gitProviderId}
|
key={gitProvider.gitProviderId}
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<div className="flex flex-col items-center justify-between">
|
<div className="flex 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?"
|
||||||
@@ -238,7 +253,7 @@ export const ShowGitProviders = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-red-500/10 "
|
className="group hover:bg-red-500/10"
|
||||||
isLoading={isRemoving}
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const ShowNotifications = () => {
|
|||||||
key={notification.notificationId}
|
key={notification.notificationId}
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<span className="text-sm flex flex-row items-center gap-4">
|
<span className="text-sm flex flex-row items-center gap-4">
|
||||||
{notification.notificationType === "slack" && (
|
{notification.notificationType === "slack" && (
|
||||||
<div className="flex items-center justify-center rounded-lg">
|
<div className="flex items-center justify-center rounded-lg">
|
||||||
|
|||||||
@@ -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,36 +307,33 @@ export const Enable2FA = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<div className="flex flex-col justify-center items-center">
|
||||||
control={pinForm.control}
|
<FormLabel>Verification Code</FormLabel>
|
||||||
name="pin"
|
<InputOTP
|
||||||
render={({ field }) => (
|
maxLength={6}
|
||||||
<FormItem className="flex flex-col justify-center items-center">
|
value={otpValue}
|
||||||
<FormLabel>Verification Code</FormLabel>
|
onChange={setOtpValue}
|
||||||
<FormControl>
|
autoComplete="off"
|
||||||
<InputOTP maxLength={6} {...field}>
|
>
|
||||||
<InputOTPGroup>
|
<InputOTPGroup>
|
||||||
<InputOTPSlot index={0} />
|
<InputOTPSlot index={0} />
|
||||||
<InputOTPSlot index={1} />
|
<InputOTPSlot index={1} />
|
||||||
<InputOTPSlot index={2} />
|
<InputOTPSlot index={2} />
|
||||||
<InputOTPSlot index={3} />
|
<InputOTPSlot index={3} />
|
||||||
<InputOTPSlot index={4} />
|
<InputOTPSlot index={4} />
|
||||||
<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>
|
</div>
|
||||||
<FormMessage />
|
|
||||||
</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 || "",
|
{
|
||||||
password: "",
|
email: data?.user?.email || "",
|
||||||
image: data?.user?.image || "",
|
password: form.getValues("password") || "",
|
||||||
currentPassword: "",
|
image: data?.user?.image || "",
|
||||||
});
|
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");
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const ShowDestinations = () => {
|
|||||||
key={sshKey.sshKeyId}
|
key={sshKey.sshKeyId}
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -155,44 +169,67 @@ export const WebDomain = () => {
|
|||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="certificateType"
|
name="https"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm w-full col-span-2">
|
||||||
<FormItem className="md:col-span-2">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>
|
<FormLabel>HTTPS</FormLabel>
|
||||||
{t("settings.server.domain.form.certificate.label")}
|
<FormDescription>
|
||||||
</FormLabel>
|
Automatically provision SSL Certificate.
|
||||||
<Select
|
</FormDescription>
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"settings.server.domain.form.certificate.placeholder",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={"none"}>
|
|
||||||
{t(
|
|
||||||
"settings.server.domain.form.certificateOptions.none",
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={"letsencrypt"}>
|
|
||||||
{t(
|
|
||||||
"settings.server.domain.form.certificateOptions.letsencrypt",
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</div>
|
||||||
);
|
<FormControl>
|
||||||
}}
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
{https && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="certificateType"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.certificate.label")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"settings.server.domain.form.certificate.placeholder",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"none"}>
|
||||||
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.none",
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt",
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -144,17 +133,6 @@ export const UserNav = () => {
|
|||||||
Servers
|
Servers
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.role === "owner" && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
router.push("/dashboard/settings");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
|
|||||||
@@ -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 = [
|
||||||
@@ -26,15 +26,20 @@ const dockerComposeServices = [
|
|||||||
{ label: "secrets", type: "keyword", info: "Define secrets" },
|
{ label: "secrets", type: "keyword", info: "Define secrets" },
|
||||||
].map((opt) => ({
|
].map((opt) => ({
|
||||||
...opt,
|
...opt,
|
||||||
apply: (view: EditorView, completion: Completion) => {
|
apply: (
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
) => {
|
||||||
const insert = `${completion.label}:`;
|
const insert = `${completion.label}:`;
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: view.state.selection.main.from,
|
from,
|
||||||
to: view.state.selection.main.to,
|
to,
|
||||||
insert,
|
insert,
|
||||||
},
|
},
|
||||||
selection: { anchor: view.state.selection.main.from + insert.length },
|
selection: { anchor: from + insert.length },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -74,15 +79,20 @@ const dockerComposeServiceOptions = [
|
|||||||
{ label: "networks", type: "keyword", info: "Networks to join" },
|
{ label: "networks", type: "keyword", info: "Networks to join" },
|
||||||
].map((opt) => ({
|
].map((opt) => ({
|
||||||
...opt,
|
...opt,
|
||||||
apply: (view: EditorView, completion: Completion) => {
|
apply: (
|
||||||
|
view: EditorView,
|
||||||
|
completion: Completion,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
) => {
|
||||||
const insert = `${completion.label}: `;
|
const insert = `${completion.label}: `;
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: view.state.selection.main.from,
|
from,
|
||||||
to: view.state.selection.main.to,
|
to,
|
||||||
insert,
|
insert,
|
||||||
},
|
},
|
||||||
selection: { anchor: view.state.selection.main.from + insert.length },
|
selection: { anchor: from + insert.length },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -99,6 +109,7 @@ function dockerComposeComplete(
|
|||||||
const line = context.state.doc.lineAt(context.pos);
|
const line = context.state.doc.lineAt(context.pos);
|
||||||
const indentation = /^\s*/.exec(line.text)?.[0].length || 0;
|
const indentation = /^\s*/.exec(line.text)?.[0].length || 0;
|
||||||
|
|
||||||
|
// If we're at the root level
|
||||||
if (indentation === 0) {
|
if (indentation === 0) {
|
||||||
return {
|
return {
|
||||||
from: word.from,
|
from: word.from,
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
2
apps/dokploy/drizzle/0085_equal_captain_stacy.sql
Normal file
2
apps/dokploy/drizzle/0085_equal_captain_stacy.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "enableSubmodules" boolean DEFAULT false;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ADD COLUMN "enableSubmodules" boolean DEFAULT false;
|
||||||
2
apps/dokploy/drizzle/0086_rainy_gertrude_yorkes.sql
Normal file
2
apps/dokploy/drizzle/0086_rainy_gertrude_yorkes.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "application" ALTER COLUMN "enableSubmodules" SET NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "compose" ALTER COLUMN "enableSubmodules" SET 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
5383
apps/dokploy/drizzle/meta/0085_snapshot.json
Normal file
5383
apps/dokploy/drizzle/meta/0085_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5383
apps/dokploy/drizzle/meta/0086_snapshot.json
Normal file
5383
apps/dokploy/drizzle/meta/0086_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -554,6 +554,62 @@
|
|||||||
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 85,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1745705609181,
|
||||||
|
"tag": "0085_equal_captain_stacy",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 86,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1745706676004,
|
||||||
|
"tag": "0086_rainy_gertrude_yorkes",
|
||||||
|
"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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.20.8",
|
"version": "v0.21.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -92,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",
|
||||||
@@ -150,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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
93
apps/dokploy/pages/api/providers/gitea/callback.ts
Normal file
93
apps/dokploy/pages/api/providers/gitea/callback.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { updateGitea } from "@dokploy/server";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { type Gitea, findGitea, redirectWithError } from "./helper";
|
||||||
|
|
||||||
|
// Helper to parse the state parameter
|
||||||
|
const parseState = (state: string): string | null => {
|
||||||
|
try {
|
||||||
|
const stateObj =
|
||||||
|
state.startsWith("{") && state.endsWith("}") ? JSON.parse(state) : {};
|
||||||
|
return stateObj.giteaId || state || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to fetch access token from Gitea
|
||||||
|
const fetchAccessToken = async (gitea: Gitea, code: string) => {
|
||||||
|
const response = await fetch(`${gitea.giteaUrl}/login/oauth/access_token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: gitea.clientId as string,
|
||||||
|
client_secret: gitea.clientSecret as string,
|
||||||
|
code,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
redirect_uri: gitea.redirectUri || "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
return response.ok
|
||||||
|
? JSON.parse(responseText)
|
||||||
|
: { error: "Token exchange failed", responseText };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
const { code, state } = req.query;
|
||||||
|
|
||||||
|
if (!code || Array.isArray(code) || !state || Array.isArray(state)) {
|
||||||
|
return redirectWithError(
|
||||||
|
res,
|
||||||
|
"Invalid authorization code or state parameter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const giteaId = parseState(state as string);
|
||||||
|
if (!giteaId) return redirectWithError(res, "Invalid state format");
|
||||||
|
|
||||||
|
const gitea = await findGitea(giteaId);
|
||||||
|
if (!gitea) return redirectWithError(res, "Failed to find Gitea provider");
|
||||||
|
|
||||||
|
// Fetch the access token from Gitea
|
||||||
|
const result = await fetchAccessToken(gitea, code as string);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error("Token exchange failed:", result);
|
||||||
|
return redirectWithError(res, result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.access_token) {
|
||||||
|
console.error("Missing access token:", result);
|
||||||
|
return redirectWithError(res, "No access token received");
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresAt = result.expires_in
|
||||||
|
? Math.floor(Date.now() / 1000) + result.expires_in
|
||||||
|
: null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateGitea(gitea.giteaId, {
|
||||||
|
accessToken: result.access_token,
|
||||||
|
refreshToken: result.refresh_token,
|
||||||
|
expiresAt,
|
||||||
|
...(result.organizationName
|
||||||
|
? { organizationName: result.organizationName }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.redirect(
|
||||||
|
307,
|
||||||
|
"/dashboard/settings/git-providers?connected=true",
|
||||||
|
);
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error("Failed to update Gitea provider:", updateError);
|
||||||
|
return redirectWithError(res, "Failed to store access token");
|
||||||
|
}
|
||||||
|
}
|
||||||
39
apps/dokploy/pages/api/providers/gitea/helper.ts
Normal file
39
apps/dokploy/pages/api/providers/gitea/helper.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { findGiteaById } from "@dokploy/server";
|
||||||
|
import type { NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export interface Gitea {
|
||||||
|
giteaId: string;
|
||||||
|
gitProviderId: string;
|
||||||
|
redirectUri: string | null;
|
||||||
|
accessToken: string | null;
|
||||||
|
refreshToken: string | null;
|
||||||
|
expiresAt: number | null;
|
||||||
|
giteaUrl: string;
|
||||||
|
clientId: string | null;
|
||||||
|
clientSecret: string | null;
|
||||||
|
organizationName?: string;
|
||||||
|
gitProvider: {
|
||||||
|
name: string;
|
||||||
|
gitProviderId: string;
|
||||||
|
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
|
||||||
|
createdAt: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findGitea = async (giteaId: string): Promise<Gitea | null> => {
|
||||||
|
try {
|
||||||
|
const gitea = await findGiteaById(giteaId);
|
||||||
|
return gitea;
|
||||||
|
} catch (findError) {
|
||||||
|
console.error("Error finding Gitea provider:", findError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redirectWithError = (res: NextApiResponse, error: string) => {
|
||||||
|
return res.redirect(
|
||||||
|
307,
|
||||||
|
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -34,6 +34,15 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -47,6 +56,13 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -64,8 +80,8 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
X,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
@@ -73,25 +89,10 @@ import type {
|
|||||||
} from "next";
|
} from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useMemo, useState, useEffect } from "react";
|
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import {
|
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -313,31 +314,43 @@ const Project = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applicationActions = {
|
const applicationActions = {
|
||||||
|
start: api.application.start.useMutation(),
|
||||||
|
stop: api.application.stop.useMutation(),
|
||||||
move: api.application.move.useMutation(),
|
move: api.application.move.useMutation(),
|
||||||
delete: api.application.delete.useMutation(),
|
delete: api.application.delete.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const postgresActions = {
|
const postgresActions = {
|
||||||
|
start: api.postgres.start.useMutation(),
|
||||||
|
stop: api.postgres.stop.useMutation(),
|
||||||
move: api.postgres.move.useMutation(),
|
move: api.postgres.move.useMutation(),
|
||||||
delete: api.postgres.remove.useMutation(),
|
delete: api.postgres.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mysqlActions = {
|
const mysqlActions = {
|
||||||
|
start: api.mysql.start.useMutation(),
|
||||||
|
stop: api.mysql.stop.useMutation(),
|
||||||
move: api.mysql.move.useMutation(),
|
move: api.mysql.move.useMutation(),
|
||||||
delete: api.mysql.remove.useMutation(),
|
delete: api.mysql.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mariadbActions = {
|
const mariadbActions = {
|
||||||
|
start: api.mariadb.start.useMutation(),
|
||||||
|
stop: api.mariadb.stop.useMutation(),
|
||||||
move: api.mariadb.move.useMutation(),
|
move: api.mariadb.move.useMutation(),
|
||||||
delete: api.mariadb.remove.useMutation(),
|
delete: api.mariadb.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const redisActions = {
|
const redisActions = {
|
||||||
|
start: api.redis.start.useMutation(),
|
||||||
|
stop: api.redis.stop.useMutation(),
|
||||||
move: api.redis.move.useMutation(),
|
move: api.redis.move.useMutation(),
|
||||||
delete: api.redis.remove.useMutation(),
|
delete: api.redis.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mongoActions = {
|
const mongoActions = {
|
||||||
|
start: api.mongo.start.useMutation(),
|
||||||
|
stop: api.mongo.stop.useMutation(),
|
||||||
move: api.mongo.move.useMutation(),
|
move: api.mongo.move.useMutation(),
|
||||||
delete: api.mongo.remove.useMutation(),
|
delete: api.mongo.remove.useMutation(),
|
||||||
};
|
};
|
||||||
@@ -347,7 +360,34 @@ const Project = (
|
|||||||
setIsBulkActionLoading(true);
|
setIsBulkActionLoading(true);
|
||||||
for (const serviceId of selectedServices) {
|
for (const serviceId of selectedServices) {
|
||||||
try {
|
try {
|
||||||
await composeActions.start.mutateAsync({ composeId: serviceId });
|
const service = filteredServices.find((s) => s.id === serviceId);
|
||||||
|
if (!service) continue;
|
||||||
|
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
await applicationActions.start.mutateAsync({
|
||||||
|
applicationId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compose":
|
||||||
|
await composeActions.start.mutateAsync({ composeId: serviceId });
|
||||||
|
break;
|
||||||
|
case "postgres":
|
||||||
|
await postgresActions.start.mutateAsync({ postgresId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
await mysqlActions.start.mutateAsync({ mysqlId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
await mariadbActions.start.mutateAsync({ mariadbId: serviceId });
|
||||||
|
break;
|
||||||
|
case "redis":
|
||||||
|
await redisActions.start.mutateAsync({ redisId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
||||||
|
break;
|
||||||
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(`Error starting service ${serviceId}`);
|
toast.error(`Error starting service ${serviceId}`);
|
||||||
@@ -367,7 +407,34 @@ const Project = (
|
|||||||
setIsBulkActionLoading(true);
|
setIsBulkActionLoading(true);
|
||||||
for (const serviceId of selectedServices) {
|
for (const serviceId of selectedServices) {
|
||||||
try {
|
try {
|
||||||
await composeActions.stop.mutateAsync({ composeId: serviceId });
|
const service = filteredServices.find((s) => s.id === serviceId);
|
||||||
|
if (!service) continue;
|
||||||
|
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
await applicationActions.stop.mutateAsync({
|
||||||
|
applicationId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compose":
|
||||||
|
await composeActions.stop.mutateAsync({ composeId: serviceId });
|
||||||
|
break;
|
||||||
|
case "postgres":
|
||||||
|
await postgresActions.stop.mutateAsync({ postgresId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
await mysqlActions.stop.mutateAsync({ mysqlId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
await mariadbActions.stop.mutateAsync({ mariadbId: serviceId });
|
||||||
|
break;
|
||||||
|
case "redis":
|
||||||
|
await redisActions.stop.mutateAsync({ redisId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
||||||
|
break;
|
||||||
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(`Error stopping service ${serviceId}`);
|
toast.error(`Error stopping service ${serviceId}`);
|
||||||
@@ -553,7 +620,7 @@ const Project = (
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>{data?.description}</CardDescription>
|
<CardDescription>{data?.description}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.role === "owner" || auth?.canCreateServices) && (
|
<div className="flex flex-row gap-4 flex-wrap justify-between items-center">
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<ProjectEnvironment projectId={projectId}>
|
<ProjectEnvironment projectId={projectId}>
|
||||||
<Button variant="outline">Project Environment</Button>
|
<Button variant="outline">Project Environment</Button>
|
||||||
@@ -569,7 +636,7 @@ const Project = (
|
|||||||
className="w-[200px] space-y-2"
|
className="w-[200px] space-y-2"
|
||||||
align="end"
|
align="end"
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="text-sm font-normal ">
|
<DropdownMenuLabel className="text-sm font-normal">
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -593,7 +660,7 @@ const Project = (
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -670,20 +737,27 @@ const Project = (
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
{(auth?.role === "owner" ||
|
{(auth?.role === "owner" ||
|
||||||
auth?.canDeleteServices) && (
|
auth?.canDeleteServices) && (
|
||||||
<DialogAction
|
<>
|
||||||
title="Delete Services"
|
<DialogAction
|
||||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
title="Delete Services"
|
||||||
type="destructive"
|
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||||
onClick={handleBulkDelete}
|
type="destructive"
|
||||||
>
|
onClick={handleBulkDelete}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-start text-destructive"
|
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Button
|
||||||
Delete
|
variant="ghost"
|
||||||
</Button>
|
className="w-full justify-start text-destructive"
|
||||||
</DialogAction>
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DuplicateProject
|
||||||
|
projectId={projectId}
|
||||||
|
services={applications}
|
||||||
|
selectedServiceIds={selectedServices}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ const Service = (
|
|||||||
router.push(newPath);
|
router.push(newPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",
|
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||||
@@ -47,7 +48,6 @@ import { useRouter } from "next/router";
|
|||||||
import { type ReactElement, useEffect, useState } from "react";
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -212,15 +212,15 @@ const Service = (
|
|||||||
router.push(newPath);
|
router.push(newPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
isCloud && data?.serverId
|
isCloud && data?.serverId
|
||||||
? "md:grid-cols-7"
|
? "lg:grid-cols-7"
|
||||||
: data?.serverId
|
: data?.serverId
|
||||||
? "md:grid-cols-6"
|
? "lg:grid-cols-6"
|
||||||
: "md:grid-cols-7",
|
: "lg:grid-cols-7",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const Mariadb = (
|
|||||||
router.push(newPath, undefined, { shallow: true });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ const Mongo = (
|
|||||||
router.push(newPath, undefined, { shallow: true });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ const MySql = (
|
|||||||
router.push(newPath, undefined, { shallow: true });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ",
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ",
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres
|
|||||||
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
|
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
|
||||||
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
||||||
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
||||||
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -182,7 +182,7 @@ const Postgresql = (
|
|||||||
router.push(newPath, undefined, { shallow: true });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const Redis = (
|
|||||||
router.push(newPath, undefined, { shallow: true });
|
router.push(newPath, undefined, { shallow: true });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user