mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bdaa81263 | ||
|
|
baf36b6fb6 | ||
|
|
d632e83799 | ||
|
|
6f52edd845 | ||
|
|
9d0f5bc8cd | ||
|
|
3dc558c260 | ||
|
|
180aa34140 | ||
|
|
ffc85b04a8 | ||
|
|
dbcfc702d4 | ||
|
|
7805efc738 | ||
|
|
3910e22412 | ||
|
|
2f16034cb0 | ||
|
|
d4925dd2b7 | ||
|
|
5aba6c79a0 | ||
|
|
84f5627471 | ||
|
|
4eaf8fee0f | ||
|
|
adee87b6da | ||
|
|
e5e987fcf9 | ||
|
|
d0a6373dcc | ||
|
|
8ed44066ad | ||
|
|
befe2193a7 | ||
|
|
f20c73cdee | ||
|
|
16bfc09202 | ||
|
|
d54a61b2a4 | ||
|
|
60c09a6434 | ||
|
|
5361e9074f | ||
|
|
13d4dea504 | ||
|
|
ffc2d593e4 | ||
|
|
297439a348 | ||
|
|
ff3e067866 | ||
|
|
f008a45bf2 | ||
|
|
50c8503cf9 | ||
|
|
930a03de60 | ||
|
|
2d3d86e823 | ||
|
|
7bab166e1b | ||
|
|
7a6e1dbc1b | ||
|
|
17a859d26d | ||
|
|
d793c6a2ec | ||
|
|
3adb9d54f4 | ||
|
|
7144adbf0c | ||
|
|
55328468d1 | ||
|
|
6968cb6930 | ||
|
|
a431e4c58e | ||
|
|
fe967239b4 | ||
|
|
c5b4b85470 | ||
|
|
b1ef9d25b1 | ||
|
|
74f7c51530 | ||
|
|
4ba2b9fe8d | ||
|
|
413eda50f4 | ||
|
|
9f09681708 | ||
|
|
8eb174812d | ||
|
|
be77f114eb | ||
|
|
ca42708035 | ||
|
|
8b03454a87 | ||
|
|
fa7f749f84 | ||
|
|
3daecd7d71 | ||
|
|
0666b5b292 | ||
|
|
b288ddd826 | ||
|
|
beadcf871a | ||
|
|
ee49dadf0b | ||
|
|
46de83a1de | ||
|
|
fee5024b7d | ||
|
|
1f28a21835 | ||
|
|
0114b371f5 | ||
|
|
66d6cb5710 | ||
|
|
5927c7c3c5 | ||
|
|
84afcf0de5 | ||
|
|
e3527f7d69 | ||
|
|
39f4a35cc8 | ||
|
|
e0433e9f7b | ||
|
|
d29ff881fc | ||
|
|
568c3a1d06 | ||
|
|
e9fd280fa2 | ||
|
|
5d5913f39d | ||
|
|
f04c8a36af | ||
|
|
d5137d5d3a | ||
|
|
9535fca28f | ||
|
|
dd62d603e0 | ||
|
|
8d227e2a2c | ||
|
|
048c8ffc11 | ||
|
|
b59597630c | ||
|
|
707463f973 | ||
|
|
4b3e0805a4 | ||
|
|
148c30f604 | ||
|
|
95f79f2afb | ||
|
|
a067abd3e4 | ||
|
|
9359ee7a04 | ||
|
|
fc7eff94b6 | ||
|
|
ff3d444b89 | ||
|
|
530ad31aaa | ||
|
|
68d0a48843 | ||
|
|
a4e4d1c467 | ||
|
|
56d8defebe | ||
|
|
997e755b6f | ||
|
|
852011dde8 | ||
|
|
d7ef201adb | ||
|
|
91183056f0 | ||
|
|
03bd4398d0 | ||
|
|
8c260eff72 | ||
|
|
4eef65f1b7 | ||
|
|
a7535c6862 | ||
|
|
bc78100613 | ||
|
|
ff22404b3b | ||
|
|
17330ca71a | ||
|
|
2898a5e575 | ||
|
|
fac8ea7a30 | ||
|
|
9a11d0db97 | ||
|
|
cf28640188 | ||
|
|
ea39b152f4 | ||
|
|
027406547e | ||
|
|
ac0922d742 |
2
.github/workflows/dokploy.yml
vendored
2
.github/workflows/dokploy.yml
vendored
@@ -2,7 +2,7 @@ name: Dokploy Docker Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, canary, "feat/better-auth-2"]
|
branches: [main, canary, "1061-custom-docker-service-hostname"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: dokploy/dokploy
|
IMAGE_NAME: dokploy/dokploy
|
||||||
|
|||||||
22
.github/workflows/format.yml
vendored
Normal file
22
.github/workflows/format.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: autofix.ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [canary]
|
||||||
|
pull_request:
|
||||||
|
branches: [canary]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup biomeJs
|
||||||
|
uses: biomejs/setup-biome@v2
|
||||||
|
|
||||||
|
- name: Run Biome formatter
|
||||||
|
run: biome format . --write
|
||||||
|
|
||||||
|
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
|
||||||
@@ -61,9 +61,9 @@ pnpm install
|
|||||||
cp apps/dokploy/.env.example apps/dokploy/.env
|
cp apps/dokploy/.env.example apps/dokploy/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Requirements
|
||||||
|
|
||||||
Is required to have **Docker** installed on your machine.
|
- [Docker](/GUIDES.md#docker)
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only the necessary files
|
# Copy only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
|
|||||||
49
GUIDES.md
Normal file
49
GUIDES.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
Here's how to install docker on different operating systems:
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
1. Visit [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop)
|
||||||
|
2. Download the Docker Desktop installer
|
||||||
|
3. Double-click the downloaded `.dmg` file
|
||||||
|
4. Drag Docker to your Applications folder
|
||||||
|
5. Open Docker Desktop from Applications
|
||||||
|
6. Follow the onboarding tutorial if desired
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
### Ubuntu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update package index
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
# Install prerequisites
|
||||||
|
sudo apt-get install \
|
||||||
|
apt-transport-https \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gnupg \
|
||||||
|
lsb-release
|
||||||
|
|
||||||
|
# Add Docker's official GPG key
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||||
|
|
||||||
|
# Set up stable repository
|
||||||
|
echo \
|
||||||
|
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
||||||
|
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
# Install Docker Engine
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install docker-ce docker-ce-cli containerd.io
|
||||||
|
```
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
1. Enable WSL2 if not already enabled
|
||||||
|
2. Visit [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop)
|
||||||
|
3. Download the installer
|
||||||
|
4. Run the installer and follow the prompts
|
||||||
|
5. Start Docker Desktop from the Start menu
|
||||||
@@ -1006,7 +1006,7 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-config-testhash:
|
db-config-testhash:
|
||||||
`) as ComposeSpecification;
|
`);
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
const composeData = load(composeFileComplex) as ComposeSpecification;
|
||||||
@@ -1115,3 +1115,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
|||||||
|
|
||||||
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
|
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const composeFileBackrest = `
|
||||||
|
services:
|
||||||
|
backrest:
|
||||||
|
image: garethgeorge/backrest:v1.7.3
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 9898
|
||||||
|
environment:
|
||||||
|
- BACKREST_PORT=9898
|
||||||
|
- BACKREST_DATA=/data
|
||||||
|
- BACKREST_CONFIG=/config/config.json
|
||||||
|
- XDG_CACHE_HOME=/cache
|
||||||
|
- TZ=\${TZ}
|
||||||
|
volumes:
|
||||||
|
- backrest/data:/data
|
||||||
|
- backrest/config:/config
|
||||||
|
- backrest/cache:/cache
|
||||||
|
- /:/userdata:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backrest:
|
||||||
|
backrest-cache:
|
||||||
|
`;
|
||||||
|
|
||||||
|
const expectedDockerComposeBackrest = load(`
|
||||||
|
services:
|
||||||
|
backrest:
|
||||||
|
image: garethgeorge/backrest:v1.7.3
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 9898
|
||||||
|
environment:
|
||||||
|
- BACKREST_PORT=9898
|
||||||
|
- BACKREST_DATA=/data
|
||||||
|
- BACKREST_CONFIG=/config/config.json
|
||||||
|
- XDG_CACHE_HOME=/cache
|
||||||
|
- TZ=\${TZ}
|
||||||
|
volumes:
|
||||||
|
- backrest-testhash/data:/data
|
||||||
|
- backrest-testhash/config:/config
|
||||||
|
- backrest-testhash/cache:/cache
|
||||||
|
- /:/userdata:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backrest-testhash:
|
||||||
|
backrest-cache-testhash:
|
||||||
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
|
test("Should handle volume paths with subdirectories correctly", () => {
|
||||||
|
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
||||||
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
|
|
||||||
|
expect(updatedComposeData).toEqual(expectedDockerComposeBackrest);
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ if (typeof window === "undefined") {
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
|
giteaBranch: "",
|
||||||
|
giteaBuildPath: "",
|
||||||
|
giteaId: "",
|
||||||
|
giteaOwner: "",
|
||||||
|
giteaRepository: "",
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import type { Schema } from "@dokploy/server/templates";
|
||||||
import type { CompleteTemplate } from "@dokploy/server/templates/processors";
|
import type { CompleteTemplate } from "@dokploy/server/templates/processors";
|
||||||
import { processTemplate } from "@dokploy/server/templates/processors";
|
import { processTemplate } from "@dokploy/server/templates/processors";
|
||||||
import type { Schema } from "@dokploy/server/templates";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("processTemplate", () => {
|
describe("processTemplate", () => {
|
||||||
// Mock schema for testing
|
// Mock schema for testing
|
||||||
@@ -233,6 +233,49 @@ describe("processTemplate", () => {
|
|||||||
expect(base64Value.length).toBeGreaterThanOrEqual(42);
|
expect(base64Value.length).toBeGreaterThanOrEqual(42);
|
||||||
expect(base64Value.length).toBeLessThanOrEqual(44);
|
expect(base64Value.length).toBeLessThanOrEqual(44);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle boolean values in env vars when provided as an array", () => {
|
||||||
|
const template: CompleteTemplate = {
|
||||||
|
metadata: {} as any,
|
||||||
|
variables: {},
|
||||||
|
config: {
|
||||||
|
domains: [],
|
||||||
|
env: [
|
||||||
|
"ENABLE_USER_SIGN_UP=false",
|
||||||
|
"DEBUG_MODE=true",
|
||||||
|
"SOME_NUMBER=42",
|
||||||
|
],
|
||||||
|
mounts: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = processTemplate(template, mockSchema);
|
||||||
|
expect(result.envs).toHaveLength(3);
|
||||||
|
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||||
|
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||||
|
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle boolean values in env vars when provided as an object", () => {
|
||||||
|
const template: CompleteTemplate = {
|
||||||
|
metadata: {} as any,
|
||||||
|
variables: {},
|
||||||
|
config: {
|
||||||
|
domains: [],
|
||||||
|
env: {
|
||||||
|
ENABLE_USER_SIGN_UP: false,
|
||||||
|
DEBUG_MODE: true,
|
||||||
|
SOME_NUMBER: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = processTemplate(template, mockSchema);
|
||||||
|
expect(result.envs).toHaveLength(3);
|
||||||
|
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||||
|
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||||
|
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("mounts processing", () => {
|
describe("mounts processing", () => {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { expect, test } from "vitest";
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
|
giteaRepository: "",
|
||||||
|
giteaOwner: "",
|
||||||
|
giteaBranch: "",
|
||||||
|
giteaBuildPath: "",
|
||||||
|
giteaId: "",
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const BitbucketProviderSchema = z.object({
|
const BitbucketProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -115,7 +115,11 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Username" autoComplete="username" {...field} />
|
<Input
|
||||||
|
placeholder="Username"
|
||||||
|
autoComplete="username"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -130,7 +134,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Password" autoComplete="one-time-code" {...field} type="password" />
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
|
|
||||||
const GitProviderSchema = z.object({
|
const GitProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -0,0 +1,515 @@
|
|||||||
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
interface GiteaRepository {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
id: number;
|
||||||
|
owner: {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GiteaBranch {
|
||||||
|
name: string;
|
||||||
|
commit: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const GiteaProviderSchema = z.object({
|
||||||
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
repository: z
|
||||||
|
.object({
|
||||||
|
repo: z.string().min(1, "Repo is required"),
|
||||||
|
owner: z.string().min(1, "Owner is required"),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
branch: z.string().min(1, "Branch is required"),
|
||||||
|
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading: isSavingGiteaProvider } =
|
||||||
|
api.application.saveGiteaProvider.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<GiteaProvider>({
|
||||||
|
defaultValues: {
|
||||||
|
buildPath: "/",
|
||||||
|
repository: {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
},
|
||||||
|
giteaId: "",
|
||||||
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(GiteaProviderSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const repository = form.watch("repository");
|
||||||
|
const giteaId = form.watch("giteaId");
|
||||||
|
|
||||||
|
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
|
||||||
|
{ giteaId },
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: repositories,
|
||||||
|
isLoading: isLoadingRepositories,
|
||||||
|
error,
|
||||||
|
} = api.gitea.getGiteaRepositories.useQuery(
|
||||||
|
{
|
||||||
|
giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: branches,
|
||||||
|
fetchStatus,
|
||||||
|
status,
|
||||||
|
} = api.gitea.getGiteaBranches.useQuery(
|
||||||
|
{
|
||||||
|
owner: repository?.owner,
|
||||||
|
repositoryName: repository?.repo,
|
||||||
|
giteaId: giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
branch: data.giteaBranch || "",
|
||||||
|
repository: {
|
||||||
|
repo: data.giteaRepository || "",
|
||||||
|
owner: data.giteaOwner || "",
|
||||||
|
},
|
||||||
|
buildPath: data.giteaBuildPath || "/",
|
||||||
|
giteaId: data.giteaId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
|
await mutateAsync({
|
||||||
|
giteaBranch: data.branch,
|
||||||
|
giteaRepository: data.repository.repo,
|
||||||
|
giteaOwner: data.repository.owner,
|
||||||
|
giteaBuildPath: data.buildPath,
|
||||||
|
giteaId: data.giteaId,
|
||||||
|
applicationId,
|
||||||
|
watchPaths: data.watchPaths,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Service Provider Saved");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error saving the Gitea provider");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4 py-3"
|
||||||
|
>
|
||||||
|
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<FormLabel>Gitea Account</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Gitea Account" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{giteaProviders?.map((giteaProvider) => (
|
||||||
|
<SelectItem
|
||||||
|
key={giteaProvider.giteaId}
|
||||||
|
value={giteaProvider.giteaId}
|
||||||
|
>
|
||||||
|
{giteaProvider.gitProvider.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="repository"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GiteaIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLoadingRepositories
|
||||||
|
? "Loading...."
|
||||||
|
: field.value.owner
|
||||||
|
? repositories?.find(
|
||||||
|
(repo: GiteaRepository) =>
|
||||||
|
repo.name === field.value.repo,
|
||||||
|
)?.name
|
||||||
|
: "Select repository"}
|
||||||
|
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search repository..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoadingRepositories && (
|
||||||
|
<span className="py-6 text-center text-sm">
|
||||||
|
Loading Repositories....
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandGroup>
|
||||||
|
{repositories && repositories.length === 0 && (
|
||||||
|
<CommandEmpty>
|
||||||
|
No repositories found.
|
||||||
|
</CommandEmpty>
|
||||||
|
)}
|
||||||
|
{repositories?.map((repo: GiteaRepository) => {
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
value={repo.name}
|
||||||
|
key={repo.url}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: repo.owner.username as string,
|
||||||
|
repo: repo.name,
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>{repo.name}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{repo.owner.username}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
repo.name === field.value.repo
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{form.formState.errors.repository && (
|
||||||
|
<p className={cn("text-sm font-medium text-destructive")}>
|
||||||
|
Repository is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="branch"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="block w-full">
|
||||||
|
<FormLabel>Branch</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
" w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{status === "loading" && fetchStatus === "fetching"
|
||||||
|
? "Loading...."
|
||||||
|
: field.value
|
||||||
|
? branches?.find(
|
||||||
|
(branch: GiteaBranch) =>
|
||||||
|
branch.name === field.value,
|
||||||
|
)?.name
|
||||||
|
: "Select branch"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search branch..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{status === "loading" && fetchStatus === "fetching" && (
|
||||||
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
Loading Branches....
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!repository?.owner && (
|
||||||
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
Select a repository
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandEmpty>No branch found.</CommandEmpty>
|
||||||
|
|
||||||
|
<CommandGroup>
|
||||||
|
{branches?.map((branch: GiteaBranch) => (
|
||||||
|
<CommandItem
|
||||||
|
value={branch.name}
|
||||||
|
key={branch.commit.id}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("branch", branch.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{branch.name}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
branch.name === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</Popover>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="buildPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Build Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="/" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path: string, index: number) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="size-3 cursor-pointer hover:text-destructive"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...field.value];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
field.onChange(newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...field.value, path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a path"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...field.value, path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
isLoading={isSavingGiteaProvider}
|
||||||
|
type="submit"
|
||||||
|
className="w-fit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -34,17 +36,15 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -35,17 +37,15 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
|
||||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
|
|
||||||
const GitlabProviderSchema = z.object({
|
const GitlabProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||||
|
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
||||||
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
DockerIcon,
|
DockerIcon,
|
||||||
GitIcon,
|
GitIcon,
|
||||||
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
@@ -18,7 +20,14 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
|||||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||||
|
|
||||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
|
type TabState =
|
||||||
|
| "github"
|
||||||
|
| "docker"
|
||||||
|
| "git"
|
||||||
|
| "drop"
|
||||||
|
| "gitlab"
|
||||||
|
| "bitbucket"
|
||||||
|
| "gitea";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -29,6 +38,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: bitbucketProviders } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||||
@@ -78,6 +88,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||||
Bitbucket
|
Bitbucket
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="gitea"
|
||||||
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
>
|
||||||
|
<GiteaIcon className="size-4 text-current fill-current" />
|
||||||
|
Gitea
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="docker"
|
value="docker"
|
||||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
@@ -162,6 +179,26 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="gitea" className="w-full p-2">
|
||||||
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
|
<SaveGiteaProvider applicationId={applicationId} />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
To deploy using Gitea, you need to configure your account
|
||||||
|
first. Please, go to{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/git-providers"
|
||||||
|
className="text-foreground"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Link>{" "}
|
||||||
|
to do so.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="docker" className="w-full p-2">
|
<TabsContent value="docker" className="w-full p-2">
|
||||||
<SaveDockerProvider applicationId={applicationId} />
|
<SaveDockerProvider applicationId={applicationId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ import {
|
|||||||
import { domainCompose } from "@/server/db/validations/domain";
|
import { domainCompose } from "@/server/db/validations/domain";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||||
import type z from "zod";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
type Domain = z.infer<typeof domainCompose>;
|
type Domain = z.infer<typeof domainCompose>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const BitbucketProviderSchema = z.object({
|
const BitbucketProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -27,13 +28,12 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const GitProviderSchema = z.object({
|
const GitProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -0,0 +1,483 @@
|
|||||||
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import type { Repository } from "@/utils/gitea-utils";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const GiteaProviderSchema = z.object({
|
||||||
|
composePath: z.string().min(1),
|
||||||
|
repository: z
|
||||||
|
.object({
|
||||||
|
repo: z.string().min(1, "Repo is required"),
|
||||||
|
owner: z.string().min(1, "Owner is required"),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
branch: z.string().min(1, "Branch is required"),
|
||||||
|
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||||
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||||
|
const { mutateAsync, isLoading: isSavingGiteaProvider } =
|
||||||
|
api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<GiteaProvider>({
|
||||||
|
defaultValues: {
|
||||||
|
composePath: "./docker-compose.yml",
|
||||||
|
repository: {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
},
|
||||||
|
giteaId: "",
|
||||||
|
branch: "",
|
||||||
|
watchPaths: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(GiteaProviderSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const repository = form.watch("repository");
|
||||||
|
const giteaId = form.watch("giteaId");
|
||||||
|
|
||||||
|
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
|
||||||
|
{ giteaId },
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: repositories,
|
||||||
|
isLoading: isLoadingRepositories,
|
||||||
|
error,
|
||||||
|
} = api.gitea.getGiteaRepositories.useQuery<Repository[]>(
|
||||||
|
{
|
||||||
|
giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: branches,
|
||||||
|
fetchStatus,
|
||||||
|
status,
|
||||||
|
} = api.gitea.getGiteaBranches.useQuery(
|
||||||
|
{
|
||||||
|
owner: repository?.owner,
|
||||||
|
repositoryName: repository?.repo,
|
||||||
|
giteaId: giteaId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
branch: data.giteaBranch || "",
|
||||||
|
repository: {
|
||||||
|
repo: data.giteaRepository || "",
|
||||||
|
owner: data.giteaOwner || "",
|
||||||
|
},
|
||||||
|
composePath: data.composePath || "./docker-compose.yml",
|
||||||
|
giteaId: data.giteaId || "",
|
||||||
|
watchPaths: data.watchPaths || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
|
await mutateAsync({
|
||||||
|
giteaBranch: data.branch,
|
||||||
|
giteaRepository: data.repository.repo,
|
||||||
|
giteaOwner: data.repository.owner,
|
||||||
|
composePath: data.composePath,
|
||||||
|
giteaId: data.giteaId,
|
||||||
|
composeId,
|
||||||
|
sourceType: "gitea",
|
||||||
|
composeStatus: "idle",
|
||||||
|
watchPaths: data.watchPaths,
|
||||||
|
} as any)
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Service Provider Saved");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error saving the Gitea provider");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4 py-3"
|
||||||
|
>
|
||||||
|
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<FormLabel>Gitea Account</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: "",
|
||||||
|
repo: "",
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a Gitea Account" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{giteaProviders?.map((giteaProvider) => (
|
||||||
|
<SelectItem
|
||||||
|
key={giteaProvider.giteaId}
|
||||||
|
value={giteaProvider.giteaId}
|
||||||
|
>
|
||||||
|
{giteaProvider.gitProvider.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="repository"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Repository</FormLabel>
|
||||||
|
{field.value.owner && field.value.repo && (
|
||||||
|
<Link
|
||||||
|
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
|
>
|
||||||
|
<GiteaIcon className="h-4 w-4" />
|
||||||
|
<span>View Repository</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLoadingRepositories
|
||||||
|
? "Loading...."
|
||||||
|
: field.value.owner
|
||||||
|
? repositories?.find(
|
||||||
|
(repo) => repo.name === field.value.repo,
|
||||||
|
)?.name
|
||||||
|
: "Select repository"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search repository..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoadingRepositories && (
|
||||||
|
<span className="py-6 text-center text-sm">
|
||||||
|
Loading Repositories....
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandGroup>
|
||||||
|
{repositories?.map((repo) => (
|
||||||
|
<CommandItem
|
||||||
|
key={repo.url}
|
||||||
|
value={repo.name}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("repository", {
|
||||||
|
owner: repo.owner.username,
|
||||||
|
repo: repo.name,
|
||||||
|
});
|
||||||
|
form.setValue("branch", "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>{repo.name}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{repo.owner.username}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
repo.name === field.value.repo
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{form.formState.errors.repository && (
|
||||||
|
<p className={cn("text-sm font-medium text-destructive")}>
|
||||||
|
Repository is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="branch"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="block w-full">
|
||||||
|
<FormLabel>Branch</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{status === "loading" && fetchStatus === "fetching"
|
||||||
|
? "Loading...."
|
||||||
|
: field.value
|
||||||
|
? branches?.find(
|
||||||
|
(branch) => branch.name === field.value,
|
||||||
|
)?.name
|
||||||
|
: "Select branch"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search branches..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<CommandEmpty>No branches found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<CommandGroup>
|
||||||
|
{branches?.map((branch) => (
|
||||||
|
<CommandItem
|
||||||
|
key={branch.name}
|
||||||
|
value={branch.name}
|
||||||
|
onSelect={() =>
|
||||||
|
form.setValue("branch", branch.name)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{branch.name}
|
||||||
|
</span>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
branch.name === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{form.formState.errors.branch && (
|
||||||
|
<p className={cn("text-sm font-medium text-destructive")}>
|
||||||
|
Branch is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="composePath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Compose Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="docker-compose.yml" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="watchPaths"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Add paths to watch for changes. When files in these
|
||||||
|
paths change, a new deployment will be triggered.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{field.value?.map((path, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{path}
|
||||||
|
<X
|
||||||
|
className="ml-1 size-3 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const newPaths = [...(field.value || [])];
|
||||||
|
newPaths.splice(index, 1);
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.currentTarget;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const newPaths = [...(field.value || []), value];
|
||||||
|
form.setValue("watchPaths", newPaths);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
'input[placeholder*="Enter a path"]',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const path = input.value.trim();
|
||||||
|
if (path) {
|
||||||
|
field.onChange([...(field.value || []), path]);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" isLoading={isSavingGiteaProvider}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -39,12 +40,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const GitlabProviderSchema = z.object({
|
const GitlabProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
GitIcon,
|
GitIcon,
|
||||||
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
@@ -14,10 +15,11 @@ import { ComposeFileEditor } from "../compose-file-editor";
|
|||||||
import { ShowConvertedCompose } from "../show-converted-compose";
|
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||||
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
||||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||||
|
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
|
||||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||||
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
||||||
|
|
||||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
|
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
@@ -27,9 +29,11 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: bitbucketProviders } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative w-full bg-transparent">
|
<Card className="group relative w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Enable Isolated Deployment ({data?.appName})</FormLabel>
|
<FormLabel>
|
||||||
|
Enable Isolated Deployment ({data?.appName})
|
||||||
|
</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enable isolated deployment to the compose file.
|
Enable isolated deployment to the compose file.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Preview your docker-compose file with added domains. Note: At least
|
||||||
|
one domain must be specified for this conversion to take effect.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseType: "postgres" | "mariadb" | "mysql" | "mongo";
|
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
database: "",
|
database: databaseType === "web-server" ? "dokploy" : "",
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
prefix: "/",
|
prefix: "/",
|
||||||
@@ -112,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>
|
||||||
@@ -286,16 +294,21 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Keep the latest</FormLabel>
|
<FormLabel>Keep the latest</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={"keeps all the backups if left empty"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Optional. If provided, only keeps the latest N backups in the cloud.
|
Optional. If provided, only keeps the latest N backups
|
||||||
|
in the cloud.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enabled"
|
name="enabled"
|
||||||
|
|||||||
@@ -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({
|
||||||
@@ -91,7 +91,7 @@ export const RestoreBackup = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
backupFile: "",
|
backupFile: "",
|
||||||
databaseName: "",
|
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(RestoreBackupSchema),
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
});
|
});
|
||||||
@@ -340,7 +340,11 @@ export const RestoreBackup = ({
|
|||||||
<FormItem className="">
|
<FormItem className="">
|
||||||
<FormLabel>Database Name</FormLabel>
|
<FormLabel>Database Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="Enter database name" />
|
<Input
|
||||||
|
disabled={databaseType === "web-server"}
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter database name"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { AddBackup } from "./add-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
import { UpdateBackup } from "./update-backup";
|
|
||||||
import { RestoreBackup } from "./restore-backup";
|
import { RestoreBackup } from "./restore-backup";
|
||||||
import { useState } from "react";
|
import { UpdateBackup } from "./update-backup";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: Exclude<ServiceType, "application" | "redis">;
|
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||||
}
|
}
|
||||||
export const ShowBackups = ({ id, type }: Props) => {
|
export const ShowBackups = ({ id, type }: Props) => {
|
||||||
const [activeManualBackup, setActiveManualBackup] = useState<
|
const [activeManualBackup, setActiveManualBackup] = useState<
|
||||||
@@ -38,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
mariadb: () =>
|
mariadb: () =>
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
"web-server": () => api.user.getBackups.useQuery(),
|
||||||
};
|
};
|
||||||
const { data } = api.destination.all.useQuery();
|
const { data } = api.destination.all.useQuery();
|
||||||
const { data: postgres, refetch } = queryMap[type]
|
const { data: postgres, refetch } = queryMap[type]
|
||||||
@@ -49,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
||||||
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
||||||
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
||||||
|
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
||||||
@@ -64,7 +66,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
|
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
|
<Database className="size-6 text-muted-foreground" />
|
||||||
|
Backups
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backups to your database to save the data to a different
|
Add backups to your database to save the data to a different
|
||||||
provider.
|
provider.
|
||||||
@@ -73,11 +78,17 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
|
|
||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
{type !== "web-server" && (
|
||||||
|
<AddBackup
|
||||||
|
databaseId={id}
|
||||||
|
databaseType={type}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
databaseId={id}
|
databaseId={id}
|
||||||
databaseType={type}
|
databaseType={type}
|
||||||
serverId={postgres.serverId}
|
serverId={"serverId" in postgres ? postgres.serverId : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -115,7 +126,9 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
databaseId={id}
|
databaseId={id}
|
||||||
databaseType={type}
|
databaseType={type}
|
||||||
serverId={postgres.serverId}
|
serverId={
|
||||||
|
"serverId" in postgres ? postgres.serverId : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
enabled: backup.enabled || false,
|
enabled: backup.enabled || false,
|
||||||
prefix: backup.prefix,
|
prefix: backup.prefix,
|
||||||
schedule: backup.schedule,
|
schedule: backup.schedule,
|
||||||
keepLatestCount: backup.keepLatestCount ? Number(backup.keepLatestCount) : undefined,
|
keepLatestCount: backup.keepLatestCount
|
||||||
|
? Number(backup.keepLatestCount)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, backup]);
|
}, [form, form.reset, backup]);
|
||||||
@@ -274,10 +276,15 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Keep the latest</FormLabel>
|
<FormLabel>Keep the latest</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={"keeps all the backups if left empty"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Optional. If provided, only keeps the latest N backups in the cloud.
|
Optional. If provided, only keeps the latest N backups
|
||||||
|
in the cloud.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -27,145 +27,149 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mariadbId,
|
mariadbId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-primary"
|
className="text-primary"
|
||||||
>
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to fix the database url connection.
|
to fix the database url connection.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid md:grid-cols-2 gap-4 ">
|
<div className="grid md:grid-cols-2 gap-4 ">
|
||||||
<div className="md:col-span-2 space-y-4">
|
<div className="md:col-span-2 space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="externalPort"
|
name="externalPort"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="3306"
|
placeholder="3306"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!data?.externalPort && (
|
{!!data?.externalPort && (
|
||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,144 +27,148 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mongoId,
|
mongoId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-primary"
|
className="text-primary"
|
||||||
>
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to fix the database url connection.
|
to fix the database url connection.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="externalPort"
|
name="externalPort"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="27017"
|
placeholder="27017"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!data?.externalPort && (
|
{!!data?.externalPort && (
|
||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,144 +27,148 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mysqlId,
|
mysqlId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
form,
|
form,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-primary"
|
className="text-primary"
|
||||||
>
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to fix the database url connection.
|
to fix the database url connection.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="externalPort"
|
name="externalPort"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="3306"
|
placeholder="3306"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!data?.externalPort && (
|
{!!data?.externalPort && (
|
||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<ToggleVisibilityInput disabled value={connectionUrl} />
|
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,146 +27,150 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.postgres.saveExternalPort.useMutation();
|
api.postgres.saveExternalPort.useMutation();
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
postgresId,
|
postgresId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-primary"
|
className="text-primary"
|
||||||
>
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to fix the database url connection.
|
to fix the database url connection.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="externalPort"
|
name="externalPort"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="5432"
|
placeholder="5432"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!data?.externalPort && (
|
{!!data?.externalPort && (
|
||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,58 +5,58 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>User</Label>
|
<Label>User</Label>
|
||||||
<Input disabled value={data?.databaseUser} />
|
<Input disabled value={data?.databaseUser} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Database Name</Label>
|
<Label>Database Name</Label>
|
||||||
<Input disabled value={data?.databaseName} />
|
<Input disabled value={data?.databaseName} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Port (Container)</Label>
|
<Label>Internal Port (Container)</Label>
|
||||||
<Input disabled value="5432" />
|
<Input disabled value="5432" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Host</Label>
|
<Label>Internal Host</Label>
|
||||||
<Input disabled value={data?.appName} />
|
<Input disabled value={data?.appName} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w
|
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w
|
||||||
|
|||||||
@@ -28,139 +28,139 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const updatePostgresSchema = z.object({
|
const updatePostgresSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
|
type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdatePostgres = ({ postgresId }: Props) => {
|
export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
api.postgres.update.useMutation();
|
api.postgres.update.useMutation();
|
||||||
const { data } = api.postgres.one.useQuery(
|
const { data } = api.postgres.one.useQuery(
|
||||||
{
|
{
|
||||||
postgresId,
|
postgresId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!postgresId,
|
enabled: !!postgresId,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const form = useForm<UpdatePostgres>({
|
const form = useForm<UpdatePostgres>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
description: data?.description ?? "",
|
description: data?.description ?? "",
|
||||||
name: data?.name ?? "",
|
name: data?.name ?? "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(updatePostgresSchema),
|
resolver: zodResolver(updatePostgresSchema),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
description: data.description ?? "",
|
description: data.description ?? "",
|
||||||
name: data.name,
|
name: data.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form, form.reset]);
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: UpdatePostgres) => {
|
const onSubmit = async (formData: UpdatePostgres) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Postgres updated successfully");
|
toast.success("Postgres updated successfully");
|
||||||
utils.postgres.one.invalidate({
|
utils.postgres.one.invalidate({
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating Postgres");
|
toast.error("Error updating Postgres");
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Postgres</DialogTitle>
|
<DialogTitle>Modify Postgres</DialogTitle>
|
||||||
<DialogDescription>Update the Postgres data</DialogDescription>
|
<DialogDescription>Update the Postgres data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
id="hook-form-update-postgres"
|
id="hook-form-update-postgres"
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-4 "
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Vandelay Industries" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Description about your project..."
|
placeholder="Description about your project..."
|
||||||
className="resize-none"
|
className="resize-none"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-update-postgres"
|
form="hook-form-update-postgres"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -186,7 +186,9 @@ export const ShowProjects = () => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">{domain.host}</span>
|
<span className="truncate">
|
||||||
|
{domain.host}
|
||||||
|
</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -222,7 +224,9 @@ export const ShowProjects = () => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">{domain.host}</span>
|
<span className="truncate">
|
||||||
|
{domain.host}
|
||||||
|
</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -27,138 +27,142 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
}, z
|
||||||
|
.number()
|
||||||
|
.gte(0, "Range must be 0 - 65535")
|
||||||
|
.lte(65535, "Range must be 0 - 65535")
|
||||||
|
.nullable()),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
redisId,
|
redisId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const _hostname = window.location.hostname;
|
const _hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
{!getIp && (
|
{!getIp && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-primary"
|
className="text-primary"
|
||||||
>
|
>
|
||||||
{data?.serverId
|
{data?.serverId
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to fix the database url connection.
|
to fix the database url connection.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="externalPort"
|
name="externalPort"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="6379"
|
placeholder="6379"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!data?.externalPort && (
|
{!!data?.externalPort && (
|
||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { api } from "@/utils/api";
|
|
||||||
import {
|
import {
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ import {
|
|||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
ArrowDownUp,
|
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
InfoIcon,
|
ArrowDownUp,
|
||||||
Calendar as CalendarIcon,
|
Calendar as CalendarIcon,
|
||||||
|
InfoIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||||
import { RequestsTable } from "./requests-table";
|
import { RequestsTable } from "./requests-table";
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogDescription,
|
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -17,22 +25,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
FormDescription,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -7,13 +9,11 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { Clock, ExternalLinkIcon, KeyIcon, Tag, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { AddApiKey } from "./add-api-key";
|
import { AddApiKey } from "./add-api-key";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
|
|
||||||
export const ShowApiKeys = () => {
|
export const ShowApiKeys = () => {
|
||||||
const { data, refetch } = api.user.get.useQuery();
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -48,6 +49,10 @@ export const AddNode = ({ serverId }: Props) => {
|
|||||||
Architecture
|
Architecture
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Make sure you use the same architecture as the node you are
|
||||||
|
adding.
|
||||||
|
</AlertBlock>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ import { api } from "@/utils/api";
|
|||||||
import {
|
import {
|
||||||
Boxes,
|
Boxes,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
Loader2,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Loader2,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddNode } from "./add-node";
|
import { AddNode } from "./add-node";
|
||||||
@@ -144,7 +144,7 @@ export const ShowNodes = ({ serverId }: Props) => {
|
|||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<ShowNodeData data={node} />
|
<ShowNodeData data={node} />
|
||||||
{node?.ManagerStatus?.Leader && (
|
{!node?.ManagerStatus?.Leader && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Node"
|
title="Delete Node"
|
||||||
description="Are you sure you want to delete this node from the cluster?"
|
description="Are you sure you want to delete this node from the cluster?"
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
type GiteaProviderResponse,
|
||||||
|
getGiteaOAuthUrl,
|
||||||
|
} from "@/utils/gitea-utils";
|
||||||
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
|
giteaUrl: z.string().min(1, {
|
||||||
|
message: "Gitea URL is required",
|
||||||
|
}),
|
||||||
|
clientId: z.string().min(1, {
|
||||||
|
message: "Client ID is required",
|
||||||
|
}),
|
||||||
|
clientSecret: z.string().min(1, {
|
||||||
|
message: "Client Secret is required",
|
||||||
|
}),
|
||||||
|
redirectUri: z.string().min(1, {
|
||||||
|
message: "Redirect URI is required",
|
||||||
|
}),
|
||||||
|
organizationName: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
|
export const AddGiteaProvider = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const urlObj = useUrl();
|
||||||
|
const baseUrl =
|
||||||
|
typeof urlObj === "string" ? urlObj : (urlObj as any)?.url || "";
|
||||||
|
|
||||||
|
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
|
||||||
|
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
redirectUri: webhookUrl,
|
||||||
|
name: "",
|
||||||
|
giteaUrl: "https://gitea.com",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(Schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const giteaUrl = form.watch("giteaUrl");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
redirectUri: webhookUrl,
|
||||||
|
name: "",
|
||||||
|
giteaUrl: "https://gitea.com",
|
||||||
|
});
|
||||||
|
}, [form, webhookUrl, isOpen]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
try {
|
||||||
|
// Send the form data to create the Gitea provider
|
||||||
|
const result = (await mutateAsync({
|
||||||
|
clientId: data.clientId,
|
||||||
|
clientSecret: data.clientSecret,
|
||||||
|
name: data.name,
|
||||||
|
redirectUri: data.redirectUri,
|
||||||
|
giteaUrl: data.giteaUrl,
|
||||||
|
organizationName: data.organizationName,
|
||||||
|
})) as unknown as GiteaProviderResponse;
|
||||||
|
|
||||||
|
// Check if we have a giteaId from the response
|
||||||
|
if (!result || !result.giteaId) {
|
||||||
|
toast.error("Failed to get Gitea ID from response");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate OAuth URL using the shared utility
|
||||||
|
const authUrl = getGiteaOAuthUrl(
|
||||||
|
result.giteaId,
|
||||||
|
data.clientId,
|
||||||
|
data.giteaUrl,
|
||||||
|
baseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open the Gitea OAuth URL
|
||||||
|
if (authUrl !== "#") {
|
||||||
|
window.open(authUrl, "_blank");
|
||||||
|
} else {
|
||||||
|
toast.error("Configuration Incomplete", {
|
||||||
|
description: "Please fill in Client ID and Gitea URL first.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Gitea provider created successfully");
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(`Error configuring Gitea: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
toast.error("An unknown error occurred.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
|
||||||
|
>
|
||||||
|
<GiteaIcon />
|
||||||
|
<span>Gitea</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
Gitea Provider <GiteaIcon className="size-5" />
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-add-gitea"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-1"
|
||||||
|
>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
To integrate your Gitea account, you need to create a new
|
||||||
|
application in your Gitea settings. Follow these steps:
|
||||||
|
</p>
|
||||||
|
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||||
|
<li className="flex flex-row gap-2 items-center">
|
||||||
|
Go to your Gitea settings{" "}
|
||||||
|
<Link
|
||||||
|
href={`${giteaUrl}/user/settings/applications`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-fit text-primary size-4" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Navigate to Applications {"->"} Create new OAuth2
|
||||||
|
Application
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Create a new application with the following details:
|
||||||
|
<ul className="list-disc list-inside ml-4">
|
||||||
|
<li>Name: Dokploy</li>
|
||||||
|
<li>
|
||||||
|
Redirect URI:{" "}
|
||||||
|
<span className="text-primary">{webhookUrl}</span>{" "}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
After creating, you'll receive an ID and Secret, copy them
|
||||||
|
and paste them below.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Random Name eg(my-personal-account)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Gitea URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://gitea.com/" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="redirectUri"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Redirect URI</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
placeholder="Random Name eg(my-personal-account)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Client ID" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Secret</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Client Secret"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button isLoading={form.formState.isSubmitting}>
|
||||||
|
Configure Gitea App
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
|
||||||
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
giteaUrl: z.string().min(1, "Gitea URL is required"),
|
||||||
|
clientId: z.string().min(1, "Client ID is required"),
|
||||||
|
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
giteaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const {
|
||||||
|
data: gitea,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = api.gitea.one.useQuery({ giteaId });
|
||||||
|
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
|
||||||
|
const { mutateAsync: testConnection, isLoading: isTesting } =
|
||||||
|
api.gitea.testConnection.useMutation();
|
||||||
|
const url = useUrl();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { connected, error } = router.query;
|
||||||
|
|
||||||
|
if (!router.isReady) return;
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
toast.success("Successfully connected to Gitea", {
|
||||||
|
description: "Your Gitea provider has been authorized.",
|
||||||
|
id: "gitea-connection-success",
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error("Gitea Connection Failed", {
|
||||||
|
description: decodeURIComponent(error as string),
|
||||||
|
id: "gitea-connection-error",
|
||||||
|
});
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [router.query, router.isReady, refetch]);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
giteaUrl: "https://gitea.com",
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gitea) {
|
||||||
|
form.reset({
|
||||||
|
name: gitea.gitProvider?.name || "",
|
||||||
|
giteaUrl: gitea.giteaUrl || "https://gitea.com",
|
||||||
|
clientId: gitea.clientId || "",
|
||||||
|
clientSecret: gitea.clientSecret || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [gitea, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
await mutateAsync({
|
||||||
|
giteaId: giteaId,
|
||||||
|
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
|
||||||
|
name: values.name,
|
||||||
|
giteaUrl: values.giteaUrl,
|
||||||
|
clientId: values.clientId,
|
||||||
|
clientSecret: values.clientSecret,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.gitProvider.getAll.invalidate();
|
||||||
|
toast.success("Gitea provider updated successfully");
|
||||||
|
await refetch();
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating Gitea provider");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
try {
|
||||||
|
const result = await testConnection({ giteaId });
|
||||||
|
toast.success("Gitea Connection Verified", {
|
||||||
|
description: result,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
const formValues = form.getValues();
|
||||||
|
const authUrl =
|
||||||
|
error.authorizationUrl ||
|
||||||
|
getGiteaOAuthUrl(
|
||||||
|
giteaId,
|
||||||
|
formValues.clientId,
|
||||||
|
formValues.giteaUrl,
|
||||||
|
typeof url === "string" ? url : (url as any).url || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.error("Gitea Not Connected", {
|
||||||
|
description:
|
||||||
|
error.message || "Please complete the OAuth authorization process.",
|
||||||
|
action:
|
||||||
|
authUrl && authUrl !== "#"
|
||||||
|
? {
|
||||||
|
label: "Authorize Now",
|
||||||
|
onClick: () => window.open(authUrl, "_blank"),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" size="icon" disabled>
|
||||||
|
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle dialog open state
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
setOpen(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Gitea Provider</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update your Gitea provider details.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="My Gitea"
|
||||||
|
{...field}
|
||||||
|
autoFocus={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="giteaUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Gitea URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://gitea.example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Client ID" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Secret</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Client Secret"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleTestConnection}
|
||||||
|
isLoading={isTesting}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const formValues = form.getValues();
|
||||||
|
const authUrl = getGiteaOAuthUrl(
|
||||||
|
giteaId,
|
||||||
|
formValues.clientId,
|
||||||
|
formValues.giteaUrl,
|
||||||
|
typeof url === "string" ? url : (url as any).url || "",
|
||||||
|
);
|
||||||
|
if (authUrl !== "#") {
|
||||||
|
window.open(authUrl, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Connect to Gitea
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isUpdating}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
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" />
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const _utils = api.useUtils();
|
|
||||||
return (
|
return (
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -36,10 +36,12 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LockKeyhole className="size-5" />
|
<LockKeyhole className="size-5" />
|
||||||
<CardTitle className="text-xl">
|
<CardTitle className="text-xl">
|
||||||
Setup Security Sugestions
|
Setup Security Suggestions
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>Check the security sugestions</CardDescription>
|
<CardDescription>
|
||||||
|
Check the security suggestions
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isRefreshing}
|
isLoading={isRefreshing}
|
||||||
@@ -120,36 +122,36 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
<div className="grid gap-2.5">
|
<div className="grid gap-2.5">
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
isEnabled={data?.ssh.enabled}
|
isEnabled={data?.ssh?.enabled}
|
||||||
description={
|
description={
|
||||||
data?.ssh.enabled
|
data?.ssh?.enabled
|
||||||
? "Enabled"
|
? "Enabled"
|
||||||
: "Not Enabled (SSH should be enabled)"
|
: "Not Enabled (SSH should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Key Auth"
|
label="Key Auth"
|
||||||
isEnabled={data?.ssh.keyAuth}
|
isEnabled={data?.ssh?.keyAuth}
|
||||||
description={
|
description={
|
||||||
data?.ssh.keyAuth
|
data?.ssh?.keyAuth
|
||||||
? "Enabled (Recommended)"
|
? "Enabled (Recommended)"
|
||||||
: "Not Enabled (Key Authentication should be enabled)"
|
: "Not Enabled (Key Authentication should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Password Auth"
|
label="Password Auth"
|
||||||
isEnabled={data?.ssh.passwordAuth === "no"}
|
isEnabled={data?.ssh?.passwordAuth === "no"}
|
||||||
description={
|
description={
|
||||||
data?.ssh.passwordAuth === "no"
|
data?.ssh?.passwordAuth === "no"
|
||||||
? "Disabled (Recommended)"
|
? "Disabled (Recommended)"
|
||||||
: "Enabled (Password Authentication should be disabled)"
|
: "Enabled (Password Authentication should be disabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Use PAM"
|
label="Use PAM"
|
||||||
isEnabled={data?.ssh.usePam === "no"}
|
isEnabled={data?.ssh?.usePam === "no"}
|
||||||
description={
|
description={
|
||||||
data?.ssh.usePam === "no"
|
data?.ssh?.usePam === "no"
|
||||||
? "Disabled (Recommended for key-based auth)"
|
? "Disabled (Recommended for key-based auth)"
|
||||||
: "Enabled (Should be disabled when using key-based auth)"
|
: "Enabled (Should be disabled when using key-based auth)"
|
||||||
}
|
}
|
||||||
@@ -166,9 +168,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
<div className="grid gap-2.5">
|
<div className="grid gap-2.5">
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Installed"
|
label="Installed"
|
||||||
isEnabled={data?.fail2ban.installed}
|
isEnabled={data?.fail2ban?.installed}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.installed
|
data?.fail2ban?.installed
|
||||||
? "Installed (Recommended)"
|
? "Installed (Recommended)"
|
||||||
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
||||||
}
|
}
|
||||||
@@ -176,18 +178,18 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
isEnabled={data?.fail2ban.enabled}
|
isEnabled={data?.fail2ban?.enabled}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.enabled
|
data?.fail2ban?.enabled
|
||||||
? "Enabled (Recommended)"
|
? "Enabled (Recommended)"
|
||||||
: "Not Enabled (Fail2Ban service should be enabled)"
|
: "Not Enabled (Fail2Ban service should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="Active"
|
label="Active"
|
||||||
isEnabled={data?.fail2ban.active}
|
isEnabled={data?.fail2ban?.active}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.active
|
data?.fail2ban?.active
|
||||||
? "Active (Recommended)"
|
? "Active (Recommended)"
|
||||||
: "Not Active (Fail2Ban service should be running)"
|
: "Not Active (Fail2Ban service should be running)"
|
||||||
}
|
}
|
||||||
@@ -195,9 +197,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="SSH Protection"
|
label="SSH Protection"
|
||||||
isEnabled={data?.fail2ban.sshEnabled === "true"}
|
isEnabled={data?.fail2ban?.sshEnabled === "true"}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.sshEnabled === "true"
|
data?.fail2ban?.sshEnabled === "true"
|
||||||
? "Enabled (Recommended)"
|
? "Enabled (Recommended)"
|
||||||
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
|
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
|
||||||
}
|
}
|
||||||
@@ -205,11 +207,11 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
<StatusRow
|
<StatusRow
|
||||||
label="SSH Mode"
|
label="SSH Mode"
|
||||||
isEnabled={data?.fail2ban.sshMode === "aggressive"}
|
isEnabled={data?.fail2ban?.sshMode === "aggressive"}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.sshMode === "aggressive"
|
data?.fail2ban?.sshMode === "aggressive"
|
||||||
? "Aggressive Mode (Recommended)"
|
? "Aggressive Mode (Recommended)"
|
||||||
: `Mode: ${data?.fail2ban.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
: `Mode: ${data?.fail2ban?.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { useTranslation } from "next-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
||||||
import { TerminalModal } from "../web-server/terminal-modal";
|
import { TerminalModal } from "../web-server/terminal-modal";
|
||||||
import { ShowServerActions } from "./actions/show-server-actions";
|
import { ShowServerActions } from "./actions/show-server-actions";
|
||||||
import { HandleServers } from "./handle-servers";
|
import { HandleServers } from "./handle-servers";
|
||||||
@@ -42,7 +43,6 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
|
|||||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|||||||
@@ -112,15 +112,17 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
toast.error("Error generating the SSH Key");
|
toast.error("Error generating the SSH Key");
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadKey = (
|
const downloadKey = (content: string, keyType: "private" | "public") => {
|
||||||
content: string,
|
|
||||||
defaultFilename: string,
|
|
||||||
keyType: "private" | "public",
|
|
||||||
) => {
|
|
||||||
const keyName = form.watch("name");
|
const keyName = form.watch("name");
|
||||||
|
const publicKey = form.watch("publicKey");
|
||||||
|
|
||||||
|
// Extract algorithm type from public key
|
||||||
|
const isEd25519 = publicKey.startsWith("ssh-ed25519");
|
||||||
|
const defaultName = isEd25519 ? "id_ed25519" : "id_rsa";
|
||||||
|
|
||||||
const filename = keyName
|
const filename = keyName
|
||||||
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}`
|
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultName}${keyType === "public" ? ".pub" : ""}`
|
||||||
: `${keyType}_${defaultFilename}`;
|
: `${defaultName}${keyType === "public" ? ".pub" : ""}`;
|
||||||
const blob = new Blob([content], { type: "text/plain" });
|
const blob = new Blob([content], { type: "text/plain" });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
@@ -273,7 +275,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="default"
|
size="default"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadKey(form.watch("privateKey"), "id_rsa", "private")
|
downloadKey(form.watch("privateKey"), "private")
|
||||||
}
|
}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -287,11 +289,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="default"
|
size="default"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadKey(
|
downloadKey(form.watch("publicKey"), "public")
|
||||||
form.watch("publicKey"),
|
|
||||||
"id_rsa.pub",
|
|
||||||
"public",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -161,7 +161,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
field.onChange(value === "" ? undefined : Number(value));
|
field.onChange(
|
||||||
|
value === ""
|
||||||
|
? undefined
|
||||||
|
: Number(value),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
className="w-full dark:bg-black"
|
||||||
@@ -189,7 +193,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
field.onChange(value === "" ? undefined : Number(value));
|
field.onChange(
|
||||||
|
value === ""
|
||||||
|
? undefined
|
||||||
|
: Number(value),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
className="w-full dark:bg-black"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
{_index + 1 < list.length && <BreadcrumbSeparator className="block" />}
|
{_index + 1 < list.length && (
|
||||||
|
<BreadcrumbSeparator className="block" />
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import { json } from "@codemirror/lang-json";
|
|||||||
import { yaml } from "@codemirror/lang-yaml";
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type Completion,
|
||||||
|
type CompletionContext,
|
||||||
|
type CompletionResult,
|
||||||
|
autocompletion,
|
||||||
|
} from "@codemirror/autocomplete";
|
||||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import {
|
|
||||||
autocompletion,
|
|
||||||
type CompletionContext,
|
|
||||||
type CompletionResult,
|
|
||||||
type Completion,
|
|
||||||
} from "@codemirror/autocomplete";
|
|
||||||
|
|
||||||
// Docker Compose completion options
|
// Docker Compose completion options
|
||||||
const dockerComposeServices = [
|
const dockerComposeServices = [
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type * as React from "react";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import type * as React from "react";
|
||||||
import { DayPicker } from "react-day-picker";
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
|||||||
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';
|
||||||
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
@@ -554,6 +554,41 @@
|
|||||||
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
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,32 +0,0 @@
|
|||||||
// middleware.ts
|
|
||||||
import { verifyRequestOrigin } from "lucia";
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import type { NextRequest } from "next/server";
|
|
||||||
|
|
||||||
export async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
||||||
if (request.method === "GET") {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
const originHeader = request.headers.get("Origin");
|
|
||||||
const hostHeader = request.headers.get("Host");
|
|
||||||
|
|
||||||
if (
|
|
||||||
!originHeader ||
|
|
||||||
!hostHeader ||
|
|
||||||
!verifyRequestOrigin(originHeader, [hostHeader])
|
|
||||||
) {
|
|
||||||
return new NextResponse(null, {
|
|
||||||
status: 403,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
// Don't handle HMR requests for the dev server we rewrite to
|
|
||||||
"/settings",
|
|
||||||
"/dashboard/(.*)",
|
|
||||||
"/invitation",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -5,23 +5,23 @@
|
|||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
transpilePackages: ["@dokploy/server"],
|
transpilePackages: ["@dokploy/server"],
|
||||||
/**
|
/**
|
||||||
* If you are using `appDir` then you must comment the below `i18n` config out.
|
* If you are using `appDir` then you must comment the below `i18n` config out.
|
||||||
*
|
*
|
||||||
* @see https://github.com/vercel/next.js/issues/41980
|
* @see https://github.com/vercel/next.js/issues/41980
|
||||||
*/
|
*/
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ["en"],
|
locales: ["en"],
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.20.7",
|
"version": "v0.21.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
"@dokploy/trpc-openapi": "0.0.4",
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
|
||||||
"@octokit/auth-app": "^6.0.4",
|
"@octokit/auth-app": "^6.0.4",
|
||||||
"@octokit/webhooks": "^13.2.7",
|
"@octokit/webhooks": "^13.2.7",
|
||||||
"@radix-ui/react-accordion": "1.1.2",
|
"@radix-ui/react-accordion": "1.1.2",
|
||||||
@@ -113,11 +112,10 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lucia": "^3.0.1",
|
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"next": "^15.0.1",
|
"next": "^15.2.4",
|
||||||
"next-i18next": "^15.3.1",
|
"next-i18next": "^15.3.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-os-utils": "1.3.7",
|
"node-os-utils": "1.3.7",
|
||||||
@@ -152,7 +150,8 @@
|
|||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4",
|
||||||
"zod-form-data": "^2.0.2"
|
"zod-form-data": "^2.0.2",
|
||||||
|
"toml": "3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
|
|||||||
@@ -84,6 +84,33 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const provider = getProviderByHeader(req.headers);
|
||||||
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
|
if (provider === "github") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitlab") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitea") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
application.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
@@ -128,6 +155,27 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Watch Paths Not Match" });
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (sourceType === "gitea") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
application.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branchName || branchName !== application.giteaBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -280,6 +328,26 @@ export const extractBranchName = (headers: any, body: any) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProviderByHeader = (headers: any) => {
|
||||||
|
if (headers["x-github-event"]) {
|
||||||
|
return "github";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["x-gitea-event"]) {
|
||||||
|
return "gitea";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["x-gitlab-event"]) {
|
||||||
|
return "gitlab";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["x-event-key"]?.includes("repo:push")) {
|
||||||
|
return "bitbucket";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const extractCommitedPaths = async (
|
export const extractCommitedPaths = async (
|
||||||
body: any,
|
body: any,
|
||||||
bitbucketUsername: string | null,
|
bitbucketUsername: string | null,
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { eq } from "drizzle-orm";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import {
|
import {
|
||||||
extractBranchName,
|
extractBranchName,
|
||||||
extractCommitedPaths,
|
|
||||||
extractCommitMessage,
|
extractCommitMessage,
|
||||||
|
extractCommitedPaths,
|
||||||
extractHash,
|
extractHash,
|
||||||
|
getProviderByHeader,
|
||||||
} from "../[refreshToken]";
|
} from "../[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
@@ -91,12 +92,6 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Branch Not Match" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (sourceType === "git") {
|
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
|
||||||
if (!branchName || branchName !== composeResult.customGitBranch) {
|
|
||||||
res.status(301).json({ message: "Branch Not Match" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitedPaths = await extractCommitedPaths(
|
const commitedPaths = await extractCommitedPaths(
|
||||||
req.body,
|
req.body,
|
||||||
@@ -104,6 +99,7 @@ export default async function handler(
|
|||||||
composeResult.bitbucket?.appPassword || "",
|
composeResult.bitbucket?.appPassword || "",
|
||||||
composeResult.bitbucketRepository || "",
|
composeResult.bitbucketRepository || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
commitedPaths,
|
commitedPaths,
|
||||||
@@ -113,6 +109,59 @@ export default async function handler(
|
|||||||
res.status(301).json({ message: "Watch Paths Not Match" });
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (sourceType === "git") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
if (!branchName || branchName !== composeResult.customGitBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const provider = getProviderByHeader(req.headers);
|
||||||
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
|
if (provider === "github") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitlab") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
} else if (provider === "gitea") {
|
||||||
|
normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeResult.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (sourceType === "gitea") {
|
||||||
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
|
const normalizedCommits = req.body?.commits?.flatMap(
|
||||||
|
(commit: any) => commit.modified,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldDeployPaths = shouldDeploy(
|
||||||
|
composeResult.watchPaths,
|
||||||
|
normalizedCommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldDeployPaths) {
|
||||||
|
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branchName || branchName !== composeResult.giteaBranch) {
|
||||||
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
41
apps/dokploy/pages/api/providers/gitea/authorize.ts
Normal file
41
apps/dokploy/pages/api/providers/gitea/authorize.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { findGitea, redirectWithError } from "./helper";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { giteaId } = req.query;
|
||||||
|
|
||||||
|
if (!giteaId || Array.isArray(giteaId)) {
|
||||||
|
return res.status(400).json({ error: "Invalid Gitea provider ID" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitea = await findGitea(giteaId as string);
|
||||||
|
if (!gitea || !gitea.clientId || !gitea.redirectUri) {
|
||||||
|
return redirectWithError(res, "Incomplete OAuth configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the Gitea authorization URL
|
||||||
|
const authorizationUrl = new URL(`${gitea.giteaUrl}/login/oauth/authorize`);
|
||||||
|
authorizationUrl.searchParams.append("client_id", gitea.clientId as string);
|
||||||
|
authorizationUrl.searchParams.append("response_type", "code");
|
||||||
|
authorizationUrl.searchParams.append(
|
||||||
|
"redirect_uri",
|
||||||
|
gitea.redirectUri as string,
|
||||||
|
);
|
||||||
|
authorizationUrl.searchParams.append("scope", "read:user repo");
|
||||||
|
authorizationUrl.searchParams.append("state", giteaId as string);
|
||||||
|
|
||||||
|
// Redirect user to Gitea authorization URL
|
||||||
|
return res.redirect(307, authorizationUrl.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initiating Gitea OAuth flow:", error);
|
||||||
|
return res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -553,7 +554,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 +570,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 +594,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 +671,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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
|||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
|
|||||||
@@ -8,56 +8,21 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
|
const { data: user } = api.user.get.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
<WebDomain />
|
<WebDomain />
|
||||||
<WebServer />
|
<WebServer />
|
||||||
{/* <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
<div className="w-full flex flex-col gap-4">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||||
<CardHeader className="">
|
<ShowBackups id={user?.userId ?? ""} type="web-server" />
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
</Card>
|
||||||
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
|
</div>
|
||||||
Paid Features
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Enable or disable paid features like monitoring
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-row gap-2 items-center">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
Enable Paid Features:
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
checked={data?.enablePaidFeatures}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
update({
|
|
||||||
enablePaidFeatures: !data?.enablePaidFeatures,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
`Paid features ${
|
|
||||||
data?.enablePaidFeatures ? "disabled" : "enabled"
|
|
||||||
} successfully`,
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating paid features");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
{data?.enablePaidFeatures && <SetupMonitoring />}
|
|
||||||
</div>
|
|
||||||
</Card> */}
|
|
||||||
|
|
||||||
{/* */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { authRouter } from "@/server/api/routers/auth";
|
|
||||||
import { createTRPCRouter } from "../api/trpc";
|
import { createTRPCRouter } from "../api/trpc";
|
||||||
import { adminRouter } from "./routers/admin";
|
import { adminRouter } from "./routers/admin";
|
||||||
import { aiRouter } from "./routers/ai";
|
import { aiRouter } from "./routers/ai";
|
||||||
@@ -13,6 +12,7 @@ import { destinationRouter } from "./routers/destination";
|
|||||||
import { dockerRouter } from "./routers/docker";
|
import { dockerRouter } from "./routers/docker";
|
||||||
import { domainRouter } from "./routers/domain";
|
import { domainRouter } from "./routers/domain";
|
||||||
import { gitProviderRouter } from "./routers/git-provider";
|
import { gitProviderRouter } from "./routers/git-provider";
|
||||||
|
import { giteaRouter } from "./routers/gitea";
|
||||||
import { githubRouter } from "./routers/github";
|
import { githubRouter } from "./routers/github";
|
||||||
import { gitlabRouter } from "./routers/gitlab";
|
import { gitlabRouter } from "./routers/gitlab";
|
||||||
import { mariadbRouter } from "./routers/mariadb";
|
import { mariadbRouter } from "./routers/mariadb";
|
||||||
@@ -44,7 +44,6 @@ import { userRouter } from "./routers/user";
|
|||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
admin: adminRouter,
|
admin: adminRouter,
|
||||||
docker: dockerRouter,
|
docker: dockerRouter,
|
||||||
auth: authRouter,
|
|
||||||
project: projectRouter,
|
project: projectRouter,
|
||||||
application: applicationRouter,
|
application: applicationRouter,
|
||||||
mysql: mysqlRouter,
|
mysql: mysqlRouter,
|
||||||
@@ -70,6 +69,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
notification: notificationRouter,
|
notification: notificationRouter,
|
||||||
sshKey: sshRouter,
|
sshKey: sshRouter,
|
||||||
gitProvider: gitProviderRouter,
|
gitProvider: gitProviderRouter,
|
||||||
|
gitea: giteaRouter,
|
||||||
bitbucket: bitbucketRouter,
|
bitbucket: bitbucketRouter,
|
||||||
gitlab: gitlabRouter,
|
gitlab: gitlabRouter,
|
||||||
github: githubRouter,
|
github: githubRouter,
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import {
|
|||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/user";
|
||||||
import {
|
import {
|
||||||
getProviderHeaders,
|
|
||||||
type Model,
|
type Model,
|
||||||
|
getProviderHeaders,
|
||||||
} from "@dokploy/server/utils/ai/select-ai-provider";
|
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
apiSaveDockerProvider,
|
apiSaveDockerProvider,
|
||||||
apiSaveEnvironmentVariables,
|
apiSaveEnvironmentVariables,
|
||||||
apiSaveGitProvider,
|
apiSaveGitProvider,
|
||||||
|
apiSaveGiteaProvider,
|
||||||
apiSaveGithubProvider,
|
apiSaveGithubProvider,
|
||||||
apiSaveGitlabProvider,
|
apiSaveGitlabProvider,
|
||||||
apiUpdateApplication,
|
apiUpdateApplication,
|
||||||
@@ -399,6 +400,31 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
watchPaths: input.watchPaths,
|
watchPaths: input.watchPaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
saveGiteaProvider: protectedProcedure
|
||||||
|
.input(apiSaveGiteaProvider)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
if (
|
||||||
|
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to save this gitea provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await updateApplication(input.applicationId, {
|
||||||
|
giteaRepository: input.giteaRepository,
|
||||||
|
giteaOwner: input.giteaOwner,
|
||||||
|
giteaBranch: input.giteaBranch,
|
||||||
|
giteaBuildPath: input.giteaBuildPath,
|
||||||
|
sourceType: "gitea",
|
||||||
|
applicationStatus: "idle",
|
||||||
|
giteaId: input.giteaId,
|
||||||
|
watchPaths: input.watchPaths,
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveDockerProvider: protectedProcedure
|
saveDockerProvider: protectedProcedure
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
import { createTRPCRouter } from "../trpc";
|
|
||||||
|
|
||||||
export const authRouter = createTRPCRouter({
|
|
||||||
// createAdmin: publicProcedure.mutation(async ({ input }) => {
|
|
||||||
// try {
|
|
||||||
// if (!IS_CLOUD) {
|
|
||||||
// const admin = await db.query.admins.findFirst({});
|
|
||||||
// if (admin) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message: "Admin already exists",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// const newAdmin = await createAdmin(input);
|
|
||||||
// if (IS_CLOUD) {
|
|
||||||
// await sendDiscordNotificationWelcome(newAdmin);
|
|
||||||
// await sendVerificationEmail(newAdmin.id);
|
|
||||||
// return {
|
|
||||||
// status: "success",
|
|
||||||
// type: "cloud",
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// // const session = await lucia.createSession(newAdmin.id || "", {});
|
|
||||||
// // ctx.res.appendHeader(
|
|
||||||
// // "Set-Cookie",
|
|
||||||
// // lucia.createSessionCookie(session.id).serialize(),
|
|
||||||
// // );
|
|
||||||
// return {
|
|
||||||
// status: "success",
|
|
||||||
// type: "selfhosted",
|
|
||||||
// };
|
|
||||||
// } catch (error) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// // @ts-ignore
|
|
||||||
// message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`,
|
|
||||||
// cause: error,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// createUser: publicProcedure.mutation(async ({ input }) => {
|
|
||||||
// try {
|
|
||||||
// const _token = await getUserByToken(input.token);
|
|
||||||
// // if (token.isExpired) {
|
|
||||||
// // throw new TRPCError({
|
|
||||||
// // code: "BAD_REQUEST",
|
|
||||||
// // message: "Invalid token",
|
|
||||||
// // });
|
|
||||||
// // }
|
|
||||||
// // const newUser = await createUser(input);
|
|
||||||
// // if (IS_CLOUD) {
|
|
||||||
// // await sendVerificationEmail(token.authId);
|
|
||||||
// // return true;
|
|
||||||
// // }
|
|
||||||
// // const session = await lucia.createSession(newUser?.authId || "", {});
|
|
||||||
// // ctx.res.appendHeader(
|
|
||||||
// // "Set-Cookie",
|
|
||||||
// // lucia.createSessionCookie(session.id).serialize(),
|
|
||||||
// // );
|
|
||||||
// return true;
|
|
||||||
// } catch (error) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message: "Error creating the user",
|
|
||||||
// cause: error,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// login: publicProcedure.mutation(async ({ input }) => {
|
|
||||||
// try {
|
|
||||||
// const auth = await findAuthByEmail(input.email);
|
|
||||||
// const correctPassword = bcrypt.compareSync(
|
|
||||||
// input.password,
|
|
||||||
// auth?.password || "",
|
|
||||||
// );
|
|
||||||
// if (!correctPassword) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message: "Credentials do not match",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// if (auth?.confirmationToken && IS_CLOUD) {
|
|
||||||
// await sendVerificationEmail(auth.id);
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message:
|
|
||||||
// "Email not confirmed, we have sent you a confirmation email please check your inbox.",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// if (auth?.is2FAEnabled) {
|
|
||||||
// return {
|
|
||||||
// is2FAEnabled: true,
|
|
||||||
// authId: auth.id,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// // const session = await lucia.createSession(auth?.id || "", {});
|
|
||||||
// // ctx.res.appendHeader(
|
|
||||||
// // "Set-Cookie",
|
|
||||||
// // lucia.createSessionCookie(session.id).serialize(),
|
|
||||||
// // );
|
|
||||||
// return {
|
|
||||||
// is2FAEnabled: false,
|
|
||||||
// authId: auth?.id,
|
|
||||||
// };
|
|
||||||
// } catch (error) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message: `Error: ${error instanceof Error ? error.message : "Login error"}`,
|
|
||||||
// cause: error,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// get: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
// const memberResult = await db.query.member.findFirst({
|
|
||||||
// where: and(
|
|
||||||
// eq(member.userId, ctx.user.id),
|
|
||||||
// eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
|
||||||
// ),
|
|
||||||
// with: {
|
|
||||||
// user: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// return memberResult;
|
|
||||||
// }),
|
|
||||||
// logout: protectedProcedure.mutation(async ({ ctx }) => {
|
|
||||||
// const { req } = ctx;
|
|
||||||
// const { session } = await validateRequest(req);
|
|
||||||
// if (!session) return false;
|
|
||||||
// // await lucia.invalidateSession(session.id);
|
|
||||||
// // res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
|
||||||
// return true;
|
|
||||||
// }),
|
|
||||||
// update: protectedProcedure.mutation(async ({ ctx, input }) => {
|
|
||||||
// const currentAuth = await findAuthByEmail(ctx.user.email);
|
|
||||||
// if (input.currentPassword || input.password) {
|
|
||||||
// const correctPassword = bcrypt.compareSync(
|
|
||||||
// input.currentPassword || "",
|
|
||||||
// currentAuth?.password || "",
|
|
||||||
// );
|
|
||||||
// if (!correctPassword) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message: "Current password is incorrect",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // const auth = await updateAuthById(ctx.user.authId, {
|
|
||||||
// // ...(input.email && { email: input.email.toLowerCase() }),
|
|
||||||
// // ...(input.password && {
|
|
||||||
// // password: bcrypt.hashSync(input.password, 10),
|
|
||||||
// // }),
|
|
||||||
// // ...(input.image && { image: input.image }),
|
|
||||||
// // });
|
|
||||||
// return auth;
|
|
||||||
// }),
|
|
||||||
// removeSelfAccount: protectedProcedure
|
|
||||||
// .input(
|
|
||||||
// z.object({
|
|
||||||
// password: z.string().min(1),
|
|
||||||
// }),
|
|
||||||
// )
|
|
||||||
// .mutation(async ({ ctx, input }) => {
|
|
||||||
// if (!IS_CLOUD) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "NOT_FOUND",
|
|
||||||
// message: "This feature is only available in the cloud version",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// const currentAuth = await findAuthByEmail(ctx.user.email);
|
|
||||||
// const correctPassword = bcrypt.compareSync(
|
|
||||||
// input.password,
|
|
||||||
// currentAuth?.password || "",
|
|
||||||
// );
|
|
||||||
// if (!correctPassword) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message: "Password is incorrect",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// const { req } = ctx;
|
|
||||||
// const { session } = await validateRequest(req);
|
|
||||||
// if (!session) return false;
|
|
||||||
// // await lucia.invalidateSession(session.id);
|
|
||||||
// // res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
|
||||||
// // if (ctx.user.rol === "owner") {
|
|
||||||
// // await removeAdminByAuthId(ctx.user.authId);
|
|
||||||
// // } else {
|
|
||||||
// // await removeUserByAuthId(ctx.user.authId);
|
|
||||||
// // }
|
|
||||||
// return true;
|
|
||||||
// }),
|
|
||||||
// generateToken: protectedProcedure.mutation(async ({ ctx }) => {
|
|
||||||
// const auth = await findUserById(ctx.user.id);
|
|
||||||
// console.log(auth);
|
|
||||||
// // if (auth.token) {
|
|
||||||
// // await luciaToken.invalidateSession(auth.token);
|
|
||||||
// // }
|
|
||||||
// // const session = await luciaToken.createSession(auth?.id || "", {
|
|
||||||
// // expiresIn: 60 * 60 * 24 * 30,
|
|
||||||
// // });
|
|
||||||
// // await updateUser(auth.id, {
|
|
||||||
// // token: session.id,
|
|
||||||
// // });
|
|
||||||
// return auth;
|
|
||||||
// }),
|
|
||||||
// verifyToken: protectedProcedure.mutation(async () => {
|
|
||||||
// return true;
|
|
||||||
// }),
|
|
||||||
// one: adminProcedure
|
|
||||||
// .input(z.object({ userId: z.string().min(1) }))
|
|
||||||
// .query(async ({ input }) => {
|
|
||||||
// // TODO: Check if the user is admin or member
|
|
||||||
// const user = await findUserById(input.userId);
|
|
||||||
// return user;
|
|
||||||
// }),
|
|
||||||
// sendResetPasswordEmail: publicProcedure
|
|
||||||
// .input(
|
|
||||||
// z.object({
|
|
||||||
// email: z.string().min(1).email(),
|
|
||||||
// }),
|
|
||||||
// )
|
|
||||||
// .mutation(async ({ input }) => {
|
|
||||||
// if (!IS_CLOUD) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "NOT_FOUND",
|
|
||||||
// message: "This feature is only available in the cloud version",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// const authR = await db.query.auth.findFirst({
|
|
||||||
// where: eq(auth.email, input.email),
|
|
||||||
// });
|
|
||||||
// if (!authR) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "NOT_FOUND",
|
|
||||||
// message: "User not found",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// const token = nanoid();
|
|
||||||
// await updateAuthById(authR.id, {
|
|
||||||
// resetPasswordToken: token,
|
|
||||||
// // Make resetPassword in 24 hours
|
|
||||||
// resetPasswordExpiresAt: new Date(
|
|
||||||
// new Date().getTime() + 24 * 60 * 60 * 1000,
|
|
||||||
// ).toISOString(),
|
|
||||||
// });
|
|
||||||
// await sendEmailNotification(
|
|
||||||
// {
|
|
||||||
// fromAddress: process.env.SMTP_FROM_ADDRESS!,
|
|
||||||
// toAddresses: [authR.email],
|
|
||||||
// smtpServer: process.env.SMTP_SERVER!,
|
|
||||||
// smtpPort: Number(process.env.SMTP_PORT),
|
|
||||||
// username: process.env.SMTP_USERNAME!,
|
|
||||||
// password: process.env.SMTP_PASSWORD!,
|
|
||||||
// },
|
|
||||||
// "Reset Password",
|
|
||||||
// `
|
|
||||||
// Reset your password by clicking the link below:
|
|
||||||
// The link will expire in 24 hours.
|
|
||||||
// <a href="${WEBSITE_URL}/reset-password?token=${token}">
|
|
||||||
// Reset Password
|
|
||||||
// </a>
|
|
||||||
// `,
|
|
||||||
// );
|
|
||||||
// }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// export const sendVerificationEmail = async (authId: string) => {
|
|
||||||
// const token = nanoid();
|
|
||||||
// const result = await updateAuthById(authId, {
|
|
||||||
// confirmationToken: token,
|
|
||||||
// confirmationExpiresAt: new Date(
|
|
||||||
// new Date().getTime() + 24 * 60 * 60 * 1000,
|
|
||||||
// ).toISOString(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!result) {
|
|
||||||
// throw new TRPCError({
|
|
||||||
// code: "BAD_REQUEST",
|
|
||||||
// message: "User not found",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// await sendEmailNotification(
|
|
||||||
// {
|
|
||||||
// fromAddress: process.env.SMTP_FROM_ADDRESS || "",
|
|
||||||
// toAddresses: [result?.email],
|
|
||||||
// smtpServer: process.env.SMTP_SERVER || "",
|
|
||||||
// smtpPort: Number(process.env.SMTP_PORT),
|
|
||||||
// username: process.env.SMTP_USERNAME || "",
|
|
||||||
// password: process.env.SMTP_PASSWORD || "",
|
|
||||||
// },
|
|
||||||
// "Confirm your email | Dokploy",
|
|
||||||
// `
|
|
||||||
// Welcome to Dokploy!
|
|
||||||
// Please confirm your email by clicking the link below:
|
|
||||||
// <a href="${WEBSITE_URL}/confirm-email?token=${result?.confirmationToken}">
|
|
||||||
// Confirm Email
|
|
||||||
// </a>
|
|
||||||
// `,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return true;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
|
|
||||||
// await sendDiscordNotification(
|
|
||||||
// {
|
|
||||||
// webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "New User Registered",
|
|
||||||
// color: 0x00ff00,
|
|
||||||
// fields: [
|
|
||||||
// {
|
|
||||||
// name: "Email",
|
|
||||||
// value: newAdmin.email,
|
|
||||||
// inline: true,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// timestamp: newAdmin.createdAt,
|
|
||||||
// footer: {
|
|
||||||
// text: "Dokploy User Registration Notification",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
@@ -25,25 +25,27 @@ import {
|
|||||||
runMongoBackup,
|
runMongoBackup,
|
||||||
runMySqlBackup,
|
runMySqlBackup,
|
||||||
runPostgresBackup,
|
runPostgresBackup,
|
||||||
|
runWebServerBackup,
|
||||||
scheduleBackup,
|
scheduleBackup,
|
||||||
updateBackupById,
|
updateBackupById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
import { z } from "zod";
|
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
||||||
import {
|
import {
|
||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
} from "@dokploy/server/utils/process/execAsync";
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
|
||||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
|
||||||
import {
|
import {
|
||||||
restoreMariadbBackup,
|
restoreMariadbBackup,
|
||||||
restoreMongoBackup,
|
restoreMongoBackup,
|
||||||
restoreMySqlBackup,
|
restoreMySqlBackup,
|
||||||
restorePostgresBackup,
|
restorePostgresBackup,
|
||||||
|
restoreWebServerBackup,
|
||||||
} from "@dokploy/server/utils/restore";
|
} from "@dokploy/server/utils/restore";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export const backupRouter = createTRPCRouter({
|
export const backupRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -85,9 +87,13 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error creating the Backup",
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error creating the Backup",
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,6 +233,13 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
manualBackupWebServer: protectedProcedure
|
||||||
|
.input(apiFindOneBackup)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const backup = await findBackupById(input.backupId);
|
||||||
|
await runWebServerBackup(backup);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
listBackupFiles: protectedProcedure
|
listBackupFiles: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -301,7 +314,13 @@ export const backupRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
databaseId: z.string(),
|
databaseId: z.string(),
|
||||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
|
databaseType: z.enum([
|
||||||
|
"postgres",
|
||||||
|
"mysql",
|
||||||
|
"mariadb",
|
||||||
|
"mongo",
|
||||||
|
"web-server",
|
||||||
|
]),
|
||||||
databaseName: z.string().min(1),
|
databaseName: z.string().min(1),
|
||||||
backupFile: z.string().min(1),
|
backupFile: z.string().min(1),
|
||||||
destinationId: z.string().min(1),
|
destinationId: z.string().min(1),
|
||||||
@@ -366,6 +385,13 @@ export const backupRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (input.databaseType === "web-server") {
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreWebServerBackup(destination, input.backupFile, (log) => {
|
||||||
|
emit.next(log);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
||||||
import {
|
import {
|
||||||
type DockerNode,
|
type DockerNode,
|
||||||
IS_CLOUD,
|
|
||||||
execAsync,
|
execAsync,
|
||||||
|
execAsyncRemote,
|
||||||
|
findServerById,
|
||||||
getRemoteDocker,
|
getRemoteDocker,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -16,10 +17,6 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const docker = await getRemoteDocker(input.serverId);
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
const workers: DockerNode[] = await docker.listNodes();
|
const workers: DockerNode[] = await docker.listNodes();
|
||||||
|
|
||||||
@@ -33,17 +30,17 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Functionality not available in cloud version",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await execAsync(
|
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
||||||
`docker node update --availability drain ${input.nodeId}`,
|
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
||||||
);
|
|
||||||
await execAsync(`docker node rm ${input.nodeId} --force`);
|
if (input.serverId) {
|
||||||
|
await execAsyncRemote(input.serverId, drainCommand);
|
||||||
|
await execAsyncRemote(input.serverId, removeCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(drainCommand);
|
||||||
|
await execAsync(removeCommand);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -60,20 +57,20 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
command: "",
|
|
||||||
version: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const docker = await getRemoteDocker(input.serverId);
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
const result = await docker.swarmInspect();
|
const result = await docker.swarmInspect();
|
||||||
const docker_version = await docker.version();
|
const docker_version = await docker.version();
|
||||||
|
|
||||||
|
let ip = await getPublicIpWithFallback();
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
ip = server?.ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: `docker swarm join --token ${
|
command: `docker swarm join --token ${
|
||||||
result.JoinTokens.Worker
|
result.JoinTokens.Worker
|
||||||
} ${await getPublicIpWithFallback()}:2377`,
|
} ${ip}:2377`,
|
||||||
version: docker_version.Version,
|
version: docker_version.Version,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -84,19 +81,19 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
command: "",
|
|
||||||
version: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const docker = await getRemoteDocker(input.serverId);
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
const result = await docker.swarmInspect();
|
const result = await docker.swarmInspect();
|
||||||
const docker_version = await docker.version();
|
const docker_version = await docker.version();
|
||||||
|
|
||||||
|
let ip = await getPublicIpWithFallback();
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
ip = server?.ipAddress;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
command: `docker swarm join --token ${
|
command: `docker swarm join --token ${
|
||||||
result.JoinTokens.Manager
|
result.JoinTokens.Manager
|
||||||
} ${await getPublicIpWithFallback()}:2377`,
|
} ${ip}:2377`,
|
||||||
version: docker_version.Version,
|
version: docker_version.Version,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -9,23 +9,10 @@ import {
|
|||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
compose as composeTable,
|
compose as composeTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import {
|
|
||||||
type CompleteTemplate,
|
|
||||||
fetchTemplateFiles,
|
|
||||||
fetchTemplatesList,
|
|
||||||
} from "@dokploy/server/templates/github";
|
|
||||||
import { processTemplate } from "@dokploy/server/templates/processors";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { dump, load } from "js-yaml";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
|
||||||
import { z } from "zod";
|
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
|
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
addDomainToCompose,
|
addDomainToCompose,
|
||||||
@@ -55,6 +42,20 @@ import {
|
|||||||
stopCompose,
|
stopCompose,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
type CompleteTemplate,
|
||||||
|
fetchTemplateFiles,
|
||||||
|
fetchTemplatesList,
|
||||||
|
} from "@dokploy/server/templates/github";
|
||||||
|
import { processTemplate } from "@dokploy/server/templates/processors";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { dump } from "js-yaml";
|
||||||
|
import { parse } from "toml";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
export const composeRouter = createTRPCRouter({
|
export const composeRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -594,7 +595,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
serverIp = "127.0.0.1";
|
serverIp = "127.0.0.1";
|
||||||
}
|
}
|
||||||
const templateData = JSON.parse(decodedData);
|
const templateData = JSON.parse(decodedData);
|
||||||
const config = load(templateData.config) as CompleteTemplate;
|
const config = parse(templateData.config) as CompleteTemplate;
|
||||||
|
|
||||||
if (!templateData.compose || !config) {
|
if (!templateData.compose || !config) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -663,7 +664,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const templateData = JSON.parse(decodedData);
|
const templateData = JSON.parse(decodedData);
|
||||||
const config = load(templateData.config) as CompleteTemplate;
|
|
||||||
|
const config = parse(templateData.config) as CompleteTemplate;
|
||||||
|
|
||||||
if (!templateData.compose || !config) {
|
if (!templateData.compose || !config) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -678,7 +680,6 @@ export const composeRouter = createTRPCRouter({
|
|||||||
projectName: compose.appName,
|
projectName: compose.appName,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update compose file
|
|
||||||
await updateCompose(input.composeId, {
|
await updateCompose(input.composeId, {
|
||||||
composeFile: templateData.compose,
|
composeFile: templateData.compose,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
@@ -686,7 +687,6 @@ export const composeRouter = createTRPCRouter({
|
|||||||
isolatedDeployment: true,
|
isolatedDeployment: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create mounts
|
|
||||||
if (processedTemplate.mounts && processedTemplate.mounts.length > 0) {
|
if (processedTemplate.mounts && processedTemplate.mounts.length > 0) {
|
||||||
for (const mount of processedTemplate.mounts) {
|
for (const mount of processedTemplate.mounts) {
|
||||||
await createMount({
|
await createMount({
|
||||||
@@ -700,7 +700,6 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create domains
|
|
||||||
if (processedTemplate.domains && processedTemplate.domains.length > 0) {
|
if (processedTemplate.domains && processedTemplate.domains.length > 0) {
|
||||||
for (const domain of processedTemplate.domains) {
|
for (const domain of processedTemplate.domains) {
|
||||||
await createDomain({
|
await createDomain({
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
updateDestinationById,
|
updateDestinationById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, desc } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const destinationRouter = createTRPCRouter({
|
export const destinationRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const gitProviderRouter = createTRPCRouter({
|
|||||||
gitlab: true,
|
gitlab: true,
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
github: true,
|
github: true,
|
||||||
|
gitea: true,
|
||||||
},
|
},
|
||||||
orderBy: desc(gitProvider.createdAt),
|
orderBy: desc(gitProvider.createdAt),
|
||||||
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
|||||||
245
apps/dokploy/server/api/routers/gitea.ts
Normal file
245
apps/dokploy/server/api/routers/gitea.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
|
import {
|
||||||
|
apiCreateGitea,
|
||||||
|
apiFindGiteaBranches,
|
||||||
|
apiFindOneGitea,
|
||||||
|
apiGiteaTestConnection,
|
||||||
|
apiUpdateGitea,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import {
|
||||||
|
createGitea,
|
||||||
|
findGiteaById,
|
||||||
|
getGiteaBranches,
|
||||||
|
getGiteaRepositories,
|
||||||
|
haveGiteaRequirements,
|
||||||
|
testGiteaConnection,
|
||||||
|
updateGitProvider,
|
||||||
|
updateGitea,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
export const giteaRouter = createTRPCRouter({
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(apiCreateGitea)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
return await createGitea(input, ctx.session.activeOrganizationId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error creating this Gitea provider",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
one: protectedProcedure
|
||||||
|
.input(apiFindOneGitea)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const giteaProvider = await findGiteaById(input.giteaId);
|
||||||
|
if (
|
||||||
|
giteaProvider.gitProvider.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this Gitea provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return giteaProvider;
|
||||||
|
}),
|
||||||
|
|
||||||
|
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
|
||||||
|
let result = await db.query.gitea.findMany({
|
||||||
|
with: {
|
||||||
|
gitProvider: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
result = result.filter(
|
||||||
|
(provider) =>
|
||||||
|
provider.gitProvider.organizationId ===
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered = result
|
||||||
|
.filter((provider) => haveGiteaRequirements(provider))
|
||||||
|
.map((provider) => {
|
||||||
|
return {
|
||||||
|
giteaId: provider.giteaId,
|
||||||
|
gitProvider: {
|
||||||
|
...provider.gitProvider,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getGiteaRepositories: protectedProcedure
|
||||||
|
.input(apiFindOneGitea)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { giteaId } = input;
|
||||||
|
|
||||||
|
if (!giteaId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Gitea provider ID is required.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const giteaProvider = await findGiteaById(giteaId);
|
||||||
|
if (
|
||||||
|
giteaProvider.gitProvider.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this Gitea provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const repositories = await getGiteaRepositories(giteaId);
|
||||||
|
return repositories;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Gitea repositories:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getGiteaBranches: protectedProcedure
|
||||||
|
.input(apiFindGiteaBranches)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { giteaId, owner, repositoryName } = input;
|
||||||
|
|
||||||
|
if (!giteaId || !owner || !repositoryName) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message:
|
||||||
|
"Gitea provider ID, owner, and repository name are required.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const giteaProvider = await findGiteaById(giteaId);
|
||||||
|
if (
|
||||||
|
giteaProvider.gitProvider.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this Gitea provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await getGiteaBranches({
|
||||||
|
giteaId,
|
||||||
|
owner,
|
||||||
|
repo: repositoryName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Gitea branches:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
testConnection: protectedProcedure
|
||||||
|
.input(apiGiteaTestConnection)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const giteaId = input.giteaId ?? "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const giteaProvider = await findGiteaById(giteaId);
|
||||||
|
if (
|
||||||
|
giteaProvider.gitProvider.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this Gitea provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await testGiteaConnection({
|
||||||
|
giteaId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `Found ${result} repositories`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gitea connection test error:", error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
update: protectedProcedure
|
||||||
|
.input(apiUpdateGitea)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const giteaProvider = await findGiteaById(input.giteaId);
|
||||||
|
if (
|
||||||
|
giteaProvider.gitProvider.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this Gitea provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.name) {
|
||||||
|
await updateGitProvider(input.gitProviderId, {
|
||||||
|
name: input.name,
|
||||||
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateGitea(input.giteaId, {
|
||||||
|
...input,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await updateGitea(input.giteaId, {
|
||||||
|
...input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
|
||||||
|
getGiteaUrl: protectedProcedure
|
||||||
|
.input(apiFindOneGitea)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { giteaId } = input;
|
||||||
|
|
||||||
|
if (!giteaId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Gitea provider ID is required.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const giteaProvider = await findGiteaById(giteaId);
|
||||||
|
if (
|
||||||
|
giteaProvider.gitProvider.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not allowed to access this Gitea provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the base URL of the Gitea instance
|
||||||
|
return giteaProvider.giteaUrl;
|
||||||
|
}),
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user