mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 21:31:11 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| fe967239b4 | |||
| 1f28a21835 | |||
| 0114b371f5 | |||
| 66d6cb5710 | |||
| 5927c7c3c5 | |||
| 84afcf0de5 | |||
| e3527f7d69 | |||
| 39f4a35cc8 | |||
| 5d5913f39d | |||
| f04c8a36af | |||
| d5137d5d3a | |||
| 048c8ffc11 | |||
| b59597630c | |||
| 707463f973 | |||
| 4b3e0805a4 | |||
| 148c30f604 | |||
| 95f79f2afb | |||
| a067abd3e4 | |||
| 9359ee7a04 | |||
| fc7eff94b6 | |||
| ff3d444b89 | |||
| 530ad31aaa | |||
| a4e4d1c467 | |||
| 56d8defebe | |||
| 997e755b6f | |||
| 852011dde8 | |||
| d7ef201adb | |||
| 4eef65f1b7 | |||
| a7535c6862 | |||
| ff22404b3b | |||
| 17330ca71a | |||
| 2898a5e575 | |||
| fac8ea7a30 | |||
| 9a11d0db97 | |||
| cf28640188 | |||
| ea39b152f4 | |||
| 027406547e |
+1
-1
@@ -29,7 +29,7 @@ WORKDIR /app
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
|
||||
@@ -1006,7 +1006,7 @@ services:
|
||||
|
||||
volumes:
|
||||
db-config-testhash:
|
||||
`) as ComposeSpecification;
|
||||
`);
|
||||
|
||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
||||
@@ -1115,3 +1115,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
|
||||
});
|
||||
|
||||
const composeFileBackrest = `
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:v1.7.3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9898
|
||||
environment:
|
||||
- BACKREST_PORT=9898
|
||||
- BACKREST_DATA=/data
|
||||
- BACKREST_CONFIG=/config/config.json
|
||||
- XDG_CACHE_HOME=/cache
|
||||
- TZ=\${TZ}
|
||||
volumes:
|
||||
- backrest/data:/data
|
||||
- backrest/config:/config
|
||||
- backrest/cache:/cache
|
||||
- /:/userdata:ro
|
||||
|
||||
volumes:
|
||||
backrest:
|
||||
backrest-cache:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeBackrest = load(`
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:v1.7.3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9898
|
||||
environment:
|
||||
- BACKREST_PORT=9898
|
||||
- BACKREST_DATA=/data
|
||||
- BACKREST_CONFIG=/config/config.json
|
||||
- XDG_CACHE_HOME=/cache
|
||||
- TZ=\${TZ}
|
||||
volumes:
|
||||
- backrest-testhash/data:/data
|
||||
- backrest-testhash/config:/config
|
||||
- backrest-testhash/cache:/cache
|
||||
- /:/userdata:ro
|
||||
|
||||
volumes:
|
||||
backrest-testhash:
|
||||
backrest-cache-testhash:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Should handle volume paths with subdirectories correctly", () => {
|
||||
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerComposeBackrest);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,11 @@ if (typeof window === "undefined") {
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
herokuVersion: "",
|
||||
giteaBranch: "",
|
||||
giteaBuildPath: "",
|
||||
giteaId: "",
|
||||
giteaOwner: "",
|
||||
giteaRepository: "",
|
||||
cleanCache: false,
|
||||
watchPaths: [],
|
||||
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 { processTemplate } from "@dokploy/server/templates/processors";
|
||||
import type { Schema } from "@dokploy/server/templates";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("processTemplate", () => {
|
||||
// Mock schema for testing
|
||||
@@ -233,6 +233,49 @@ describe("processTemplate", () => {
|
||||
expect(base64Value.length).toBeGreaterThanOrEqual(42);
|
||||
expect(base64Value.length).toBeLessThanOrEqual(44);
|
||||
});
|
||||
|
||||
it("should handle boolean values in env vars when provided as an array", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
domains: [],
|
||||
env: [
|
||||
"ENABLE_USER_SIGN_UP=false",
|
||||
"DEBUG_MODE=true",
|
||||
"SOME_NUMBER=42",
|
||||
],
|
||||
mounts: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = processTemplate(template, mockSchema);
|
||||
expect(result.envs).toHaveLength(3);
|
||||
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||
});
|
||||
|
||||
it("should handle boolean values in env vars when provided as an object", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
domains: [],
|
||||
env: {
|
||||
ENABLE_USER_SIGN_UP: false,
|
||||
DEBUG_MODE: true,
|
||||
SOME_NUMBER: 42,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = processTemplate(template, mockSchema);
|
||||
expect(result.envs).toHaveLength(3);
|
||||
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
|
||||
expect(result.envs).toContain("DEBUG_MODE=true");
|
||||
expect(result.envs).toContain("SOME_NUMBER=42");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mounts processing", () => {
|
||||
|
||||
@@ -7,6 +7,11 @@ import { expect, test } from "vitest";
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
herokuVersion: "",
|
||||
giteaRepository: "",
|
||||
giteaOwner: "",
|
||||
giteaBranch: "",
|
||||
giteaBuildPath: "",
|
||||
giteaId: "",
|
||||
cleanCache: false,
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -32,7 +33,6 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
const ImportSchema = z.object({
|
||||
base64: z.string(),
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
enum BuildType {
|
||||
export enum BuildType {
|
||||
dockerfile = "dockerfile",
|
||||
heroku_buildpacks = "heroku_buildpacks",
|
||||
paketo_buildpacks = "paketo_buildpacks",
|
||||
@@ -29,9 +29,18 @@ enum BuildType {
|
||||
railpack = "railpack",
|
||||
}
|
||||
|
||||
const buildTypeDisplayMap: Record<BuildType, string> = {
|
||||
[BuildType.dockerfile]: "Dockerfile",
|
||||
[BuildType.railpack]: "Railpack",
|
||||
[BuildType.nixpacks]: "Nixpacks",
|
||||
[BuildType.heroku_buildpacks]: "Heroku Buildpacks",
|
||||
[BuildType.paketo_buildpacks]: "Paketo Buildpacks",
|
||||
[BuildType.static]: "Static",
|
||||
};
|
||||
|
||||
const mySchema = z.discriminatedUnion("buildType", [
|
||||
z.object({
|
||||
buildType: z.literal("dockerfile"),
|
||||
buildType: z.literal(BuildType.dockerfile),
|
||||
dockerfile: z
|
||||
.string({
|
||||
required_error: "Dockerfile path is required",
|
||||
@@ -42,39 +51,88 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
dockerBuildStage: z.string().nullable().default(""),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("heroku_buildpacks"),
|
||||
buildType: z.literal(BuildType.heroku_buildpacks),
|
||||
herokuVersion: z.string().nullable().default(""),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("paketo_buildpacks"),
|
||||
buildType: z.literal(BuildType.paketo_buildpacks),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("nixpacks"),
|
||||
buildType: z.literal(BuildType.nixpacks),
|
||||
publishDirectory: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("static"),
|
||||
buildType: z.literal(BuildType.static),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("railpack"),
|
||||
buildType: z.literal(BuildType.railpack),
|
||||
}),
|
||||
]);
|
||||
|
||||
type AddTemplate = z.infer<typeof mySchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
interface ApplicationData {
|
||||
buildType: BuildType;
|
||||
dockerfile?: string | null;
|
||||
dockerContextPath?: string | null;
|
||||
dockerBuildStage?: string | null;
|
||||
herokuVersion?: string | null;
|
||||
publishDirectory?: string | null;
|
||||
}
|
||||
|
||||
function isValidBuildType(value: string): value is BuildType {
|
||||
return Object.values(BuildType).includes(value as BuildType);
|
||||
}
|
||||
|
||||
const resetData = (data: ApplicationData): AddTemplate => {
|
||||
switch (data.buildType) {
|
||||
case BuildType.dockerfile:
|
||||
return {
|
||||
buildType: BuildType.dockerfile,
|
||||
dockerfile: data.dockerfile || "",
|
||||
dockerContextPath: data.dockerContextPath || "",
|
||||
dockerBuildStage: data.dockerBuildStage || "",
|
||||
};
|
||||
case BuildType.heroku_buildpacks:
|
||||
return {
|
||||
buildType: BuildType.heroku_buildpacks,
|
||||
herokuVersion: data.herokuVersion || "",
|
||||
};
|
||||
case BuildType.nixpacks:
|
||||
return {
|
||||
buildType: BuildType.nixpacks,
|
||||
publishDirectory: data.publishDirectory || undefined,
|
||||
};
|
||||
case BuildType.paketo_buildpacks:
|
||||
return {
|
||||
buildType: BuildType.paketo_buildpacks,
|
||||
};
|
||||
case BuildType.static:
|
||||
return {
|
||||
buildType: BuildType.static,
|
||||
};
|
||||
case BuildType.railpack:
|
||||
return {
|
||||
buildType: BuildType.railpack,
|
||||
};
|
||||
default:
|
||||
const buildType = data.buildType as BuildType;
|
||||
return {
|
||||
buildType,
|
||||
} as AddTemplate;
|
||||
}
|
||||
};
|
||||
|
||||
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveBuildType.useMutation();
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
{ applicationId },
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const form = useForm<AddTemplate>({
|
||||
@@ -85,46 +143,36 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
});
|
||||
|
||||
const buildType = form.watch("buildType");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data.buildType === "dockerfile") {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
...(data.buildType && {
|
||||
dockerfile: data.dockerfile || "",
|
||||
dockerContextPath: data.dockerContextPath || "",
|
||||
dockerBuildStage: data.dockerBuildStage || "",
|
||||
}),
|
||||
});
|
||||
} else if (data.buildType === "heroku_buildpacks") {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
...(data.buildType && {
|
||||
herokuVersion: data.herokuVersion || "",
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
publishDirectory: data.publishDirectory || undefined,
|
||||
});
|
||||
}
|
||||
const typedData: ApplicationData = {
|
||||
...data,
|
||||
buildType: isValidBuildType(data.buildType)
|
||||
? (data.buildType as BuildType)
|
||||
: BuildType.nixpacks, // fallback
|
||||
};
|
||||
|
||||
form.reset(resetData(typedData));
|
||||
}
|
||||
}, [form.formState.isSubmitSuccessful, form.reset, data, form]);
|
||||
}, [data, form]);
|
||||
|
||||
const onSubmit = async (data: AddTemplate) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
buildType: data.buildType,
|
||||
publishDirectory:
|
||||
data.buildType === "nixpacks" ? data.publishDirectory : null,
|
||||
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
|
||||
data.buildType === BuildType.nixpacks ? data.publishDirectory : null,
|
||||
dockerfile:
|
||||
data.buildType === BuildType.dockerfile ? data.dockerfile : null,
|
||||
dockerContextPath:
|
||||
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
||||
data.buildType === BuildType.dockerfile ? data.dockerContextPath : null,
|
||||
dockerBuildStage:
|
||||
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
|
||||
data.buildType === BuildType.dockerfile ? data.dockerBuildStage : null,
|
||||
herokuVersion:
|
||||
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
|
||||
data.buildType === BuildType.heroku_buildpacks
|
||||
? data.herokuVersion
|
||||
: null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Build type saved");
|
||||
@@ -160,193 +208,143 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="buildType"
|
||||
defaultValue={form.control._defaultValues.buildType}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Build Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="dockerfile" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Dockerfile
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="railpack" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Railpack{" "}
|
||||
<Badge className="ml-1 text-xs px-1">New</Badge>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="nixpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Nixpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="heroku_buildpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Heroku Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="paketo_buildpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Paketo Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="static" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Static</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Build Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
{Object.entries(buildTypeDisplayMap).map(
|
||||
([value, label]) => (
|
||||
<FormItem
|
||||
key={value}
|
||||
className="flex items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<RadioGroupItem value={value} />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{label}
|
||||
{value === BuildType.railpack && (
|
||||
<Badge className="ml-2 px-1 text-xs">New</Badge>
|
||||
)}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
),
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{buildType === "heroku_buildpacks" && (
|
||||
{buildType === BuildType.heroku_buildpacks && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="herokuVersion"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Heroku Version (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Heroku Version (Default: 24)"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Heroku Version (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Heroku Version (Default: 24)"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{buildType === "dockerfile" && (
|
||||
{buildType === BuildType.dockerfile && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerfile"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker File</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Path of your docker file"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerContextPath"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker Context Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
"Path of your docker context default: ."
|
||||
}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerBuildStage"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Docker Build Stage</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to target a specific stage in a
|
||||
Multi-stage Dockerfile. If empty, Docker defaults to
|
||||
build the last defined stage.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"E.g. production"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{buildType === "nixpacks" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="publishDirectory"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish Directory</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to serve a single directory via NGINX after
|
||||
the build phase. Useful if the final build assets
|
||||
should be served as a static site.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormLabel>Docker File</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Publish Directory"}
|
||||
placeholder="Path of your docker file"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerContextPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Docker Context Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Path of your docker context (default: .)"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerBuildStage"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Docker Build Stage</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to target a specific stage in a Multi-stage
|
||||
Dockerfile. If empty, Docker defaults to build the
|
||||
last defined stage.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="E.g. production"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{buildType === BuildType.nixpacks && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="publishDirectory"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish Directory</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to serve a single directory via NGINX after
|
||||
the build phase. Useful if the final build assets should
|
||||
be served as a static site.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Publish Directory"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full justify-end">
|
||||
|
||||
@@ -41,8 +41,8 @@ import { toast } from "sonner";
|
||||
import { domain } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Dices } from "lucide-react";
|
||||
import type z from "zod";
|
||||
import Link from "next/link";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Form } from "@/components/ui/form";
|
||||
import { Secrets } from "@/components/ui/secrets";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
env: z.string(),
|
||||
|
||||
+3
-3
@@ -1,4 +1,6 @@
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const BitbucketProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
|
||||
@@ -26,15 +26,15 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
|
||||
const GitProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
|
||||
+515
@@ -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>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -1,3 +1,5 @@
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -34,17 +36,15 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Link from "next/link";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
|
||||
const GithubProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
|
||||
+3
-3
@@ -1,4 +1,6 @@
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -35,17 +37,15 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Link from "next/link";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
|
||||
const GitlabProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
||||
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
@@ -18,7 +20,14 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||
|
||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
|
||||
type TabState =
|
||||
| "github"
|
||||
| "docker"
|
||||
| "git"
|
||||
| "drop"
|
||||
| "gitlab"
|
||||
| "bitbucket"
|
||||
| "gitea";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
@@ -29,6 +38,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
@@ -78,6 +88,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitea"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GiteaIcon className="size-4 text-current fill-current" />
|
||||
Gitea
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="docker"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
@@ -162,6 +179,26 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitea" className="w-full p-2">
|
||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||
<SaveGiteaProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using Gitea, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="docker" className="w-full p-2">
|
||||
<SaveDockerProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
|
||||
@@ -41,8 +41,8 @@ import {
|
||||
import { domainCompose } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||
import type z from "zod";
|
||||
import Link from "next/link";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domainCompose>;
|
||||
|
||||
|
||||
+3
-3
@@ -1,4 +1,6 @@
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const BitbucketProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -27,13 +28,12 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const GitProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
+483
@@ -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>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -39,12 +40,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const GithubProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
+3
-3
@@ -1,4 +1,6 @@
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
const GitlabProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
@@ -14,10 +15,11 @@ import { ComposeFileEditor } from "../compose-file-editor";
|
||||
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
|
||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
||||
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
@@ -27,9 +29,11 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
@@ -54,21 +58,21 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GithubIcon className="size-4 text-current fill-current" />
|
||||
Github
|
||||
GitHub
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitlab"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitlabIcon className="size-4 text-current fill-current" />
|
||||
Gitlab
|
||||
GitLab
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="bitbucket"
|
||||
@@ -77,7 +81,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value="gitea"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GiteaIcon className="size-4 text-current fill-current" /> Gitea
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
@@ -89,11 +98,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
value="raw"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<CodeIcon className="size-4 " />
|
||||
<CodeIcon className="size-4" />
|
||||
Raw
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{githubProviders && githubProviders?.length > 0 ? (
|
||||
<SaveGithubProviderCompose composeId={composeId} />
|
||||
@@ -154,6 +164,26 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitea" className="w-full p-2">
|
||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||
<SaveGiteaProviderCompose composeId={composeId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using Gitea, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProviderCompose composeId={composeId} />
|
||||
</TabsContent>
|
||||
|
||||
@@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
|
||||
|
||||
interface Props {
|
||||
databaseId: string;
|
||||
databaseType: "postgres" | "mariadb" | "mysql" | "mongo";
|
||||
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
database: "",
|
||||
database: databaseType === "web-server" ? "dokploy" : "",
|
||||
destinationId: "",
|
||||
enabled: true,
|
||||
prefix: "/",
|
||||
@@ -112,7 +112,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
? {
|
||||
mongoId: databaseId,
|
||||
}
|
||||
: undefined;
|
||||
: databaseType === "web-server"
|
||||
? {
|
||||
userId: databaseId,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await createBackup({
|
||||
destinationId: data.destinationId,
|
||||
@@ -236,7 +240,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Database</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"dokploy"} {...field} />
|
||||
<Input
|
||||
disabled={databaseType === "web-server"}
|
||||
placeholder={"dokploy"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -32,23 +35,20 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { debounce } from "lodash";
|
||||
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||
import { debounce } from "lodash";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 {
|
||||
databaseId: string;
|
||||
databaseType: Exclude<ServiceType, "application" | "redis">;
|
||||
serverId: string | null;
|
||||
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
const RestoreBackupSchema = z.object({
|
||||
@@ -91,7 +91,7 @@ export const RestoreBackup = ({
|
||||
defaultValues: {
|
||||
destinationId: "",
|
||||
backupFile: "",
|
||||
databaseName: "",
|
||||
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||
},
|
||||
resolver: zodResolver(RestoreBackupSchema),
|
||||
});
|
||||
@@ -340,7 +340,11 @@ export const RestoreBackup = ({
|
||||
<FormItem className="">
|
||||
<FormLabel>Database Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Enter database name" />
|
||||
<Input
|
||||
disabled={databaseType === "web-server"}
|
||||
{...field}
|
||||
placeholder="Enter database name"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -14,18 +14,18 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||
import { AddBackup } from "./add-backup";
|
||||
import { UpdateBackup } from "./update-backup";
|
||||
import { RestoreBackup } from "./restore-backup";
|
||||
import { useState } from "react";
|
||||
import { UpdateBackup } from "./update-backup";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: Exclude<ServiceType, "application" | "redis">;
|
||||
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
|
||||
}
|
||||
export const ShowBackups = ({ id, type }: Props) => {
|
||||
const [activeManualBackup, setActiveManualBackup] = useState<
|
||||
@@ -38,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
mariadb: () =>
|
||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||
"web-server": () => api.user.getBackups.useQuery(),
|
||||
};
|
||||
const { data } = api.destination.all.useQuery();
|
||||
const { data: postgres, refetch } = queryMap[type]
|
||||
@@ -49,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
||||
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
||||
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
||||
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
||||
@@ -64,7 +66,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="text-xl">Backups</CardTitle>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<Database className="size-6 text-muted-foreground" />
|
||||
Backups
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add backups to your database to save the data to a different
|
||||
provider.
|
||||
@@ -73,11 +78,17 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
|
||||
{postgres && postgres?.backups?.length > 0 && (
|
||||
<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
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
serverId={postgres.serverId}
|
||||
serverId={"serverId" in postgres ? postgres.serverId : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -115,7 +126,9 @@ export const ShowBackups = ({ id, type }: Props) => {
|
||||
<RestoreBackup
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
serverId={postgres.serverId}
|
||||
serverId={
|
||||
"serverId" in postgres ? postgres.serverId : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { useState } from "react";
|
||||
|
||||
const examples = [
|
||||
"Make a personal blog",
|
||||
@@ -23,7 +22,7 @@ const examples = [
|
||||
"Sendgrid service opensource analogue",
|
||||
];
|
||||
|
||||
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
||||
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
||||
// Get servers from the API
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
|
||||
@@ -25,13 +25,13 @@ import {
|
||||
import { type RouterOutputs, api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
ArrowDownUp,
|
||||
AlertCircle,
|
||||
InfoIcon,
|
||||
ArrowDownUp,
|
||||
Calendar as CalendarIcon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||
import { RequestsTable } from "./requests-table";
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
@@ -17,22 +25,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -7,13 +9,11 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Clock, ExternalLinkIcon, KeyIcon, Tag, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { AddApiKey } from "./add-api-key";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export const ShowApiKeys = () => {
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -12,7 +13,6 @@ import { ExternalLink, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { AddManager } from "./manager/add-manager";
|
||||
import { AddWorker } from "./workers/add-worker";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
|
||||
@@ -35,9 +35,9 @@ import { api } from "@/utils/api";
|
||||
import {
|
||||
Boxes,
|
||||
HelpCircle,
|
||||
Loader2,
|
||||
LockIcon,
|
||||
MoreHorizontal,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AddNode } from "./add-node";
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
type GiteaProviderResponse,
|
||||
getGiteaOAuthUrl,
|
||||
} from "@/utils/gitea-utils";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
giteaUrl: z.string().min(1, {
|
||||
message: "Gitea URL is required",
|
||||
}),
|
||||
clientId: z.string().min(1, {
|
||||
message: "Client ID is required",
|
||||
}),
|
||||
clientSecret: z.string().min(1, {
|
||||
message: "Client Secret is required",
|
||||
}),
|
||||
redirectUri: z.string().min(1, {
|
||||
message: "Redirect URI is required",
|
||||
}),
|
||||
organizationName: z.string().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const AddGiteaProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const urlObj = useUrl();
|
||||
const baseUrl =
|
||||
typeof urlObj === "string" ? urlObj : (urlObj as any)?.url || "";
|
||||
|
||||
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
|
||||
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
redirectUri: webhookUrl,
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
const giteaUrl = form.watch("giteaUrl");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
redirectUri: webhookUrl,
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
});
|
||||
}, [form, webhookUrl, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
try {
|
||||
// Send the form data to create the Gitea provider
|
||||
const result = (await mutateAsync({
|
||||
clientId: data.clientId,
|
||||
clientSecret: data.clientSecret,
|
||||
name: data.name,
|
||||
redirectUri: data.redirectUri,
|
||||
giteaUrl: data.giteaUrl,
|
||||
organizationName: data.organizationName,
|
||||
})) as unknown as GiteaProviderResponse;
|
||||
|
||||
// Check if we have a giteaId from the response
|
||||
if (!result || !result.giteaId) {
|
||||
toast.error("Failed to get Gitea ID from response");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate OAuth URL using the shared utility
|
||||
const authUrl = getGiteaOAuthUrl(
|
||||
result.giteaId,
|
||||
data.clientId,
|
||||
data.giteaUrl,
|
||||
baseUrl,
|
||||
);
|
||||
|
||||
// Open the Gitea OAuth URL
|
||||
if (authUrl !== "#") {
|
||||
window.open(authUrl, "_blank");
|
||||
} else {
|
||||
toast.error("Configuration Incomplete", {
|
||||
description: "Please fill in Client ID and Gitea URL first.",
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Gitea provider created successfully");
|
||||
setIsOpen(false);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(`Error configuring Gitea: ${error.message}`);
|
||||
} else {
|
||||
toast.error("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
|
||||
>
|
||||
<GiteaIcon />
|
||||
<span>Gitea</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Gitea Provider <GiteaIcon className="size-5" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-gitea"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-1"
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your Gitea account, you need to create a new
|
||||
application in your Gitea settings. Follow these steps:
|
||||
</p>
|
||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||
<li className="flex flex-row gap-2 items-center">
|
||||
Go to your Gitea settings{" "}
|
||||
<Link
|
||||
href={`${giteaUrl}/user/settings/applications`}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="w-fit text-primary size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
Navigate to Applications {"->"} Create new OAuth2
|
||||
Application
|
||||
</li>
|
||||
<li>
|
||||
Create a new application with the following details:
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
<li>Name: Dokploy</li>
|
||||
<li>
|
||||
Redirect URI:{" "}
|
||||
<span className="text-primary">{webhookUrl}</span>{" "}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
After creating, you'll receive an ID and Secret, copy them
|
||||
and paste them below.
|
||||
</li>
|
||||
</ol>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="giteaUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://gitea.com/" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="redirectUri"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Redirect URI</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Client ID" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Client Secret"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button isLoading={form.formState.isSubmitting}>
|
||||
Configure Gitea App
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
giteaUrl: z.string().min(1, "Gitea URL is required"),
|
||||
clientId: z.string().min(1, "Client ID is required"),
|
||||
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
giteaId: string;
|
||||
}
|
||||
|
||||
export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const {
|
||||
data: gitea,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = api.gitea.one.useQuery({ giteaId });
|
||||
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading: isTesting } =
|
||||
api.gitea.testConnection.useMutation();
|
||||
const url = useUrl();
|
||||
const utils = api.useUtils();
|
||||
|
||||
useEffect(() => {
|
||||
const { connected, error } = router.query;
|
||||
|
||||
if (!router.isReady) return;
|
||||
|
||||
if (connected) {
|
||||
toast.success("Successfully connected to Gitea", {
|
||||
description: "Your Gitea provider has been authorized.",
|
||||
id: "gitea-connection-success",
|
||||
});
|
||||
refetch();
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
toast.error("Gitea Connection Failed", {
|
||||
description: decodeURIComponent(error as string),
|
||||
id: "gitea-connection-error",
|
||||
});
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}
|
||||
}, [router.query, router.isReady, refetch]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (gitea) {
|
||||
form.reset({
|
||||
name: gitea.gitProvider?.name || "",
|
||||
giteaUrl: gitea.giteaUrl || "https://gitea.com",
|
||||
clientId: gitea.clientId || "",
|
||||
clientSecret: gitea.clientSecret || "",
|
||||
});
|
||||
}
|
||||
}, [gitea, form]);
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
await mutateAsync({
|
||||
giteaId: giteaId,
|
||||
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
|
||||
name: values.name,
|
||||
giteaUrl: values.giteaUrl,
|
||||
clientId: values.clientId,
|
||||
clientSecret: values.clientSecret,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Gitea provider updated successfully");
|
||||
await refetch();
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Gitea provider");
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
try {
|
||||
const result = await testConnection({ giteaId });
|
||||
toast.success("Gitea Connection Verified", {
|
||||
description: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
const formValues = form.getValues();
|
||||
const authUrl =
|
||||
error.authorizationUrl ||
|
||||
getGiteaOAuthUrl(
|
||||
giteaId,
|
||||
formValues.clientId,
|
||||
formValues.giteaUrl,
|
||||
typeof url === "string" ? url : (url as any).url || "",
|
||||
);
|
||||
|
||||
toast.error("Gitea Not Connected", {
|
||||
description:
|
||||
error.message || "Please complete the OAuth authorization process.",
|
||||
action:
|
||||
authUrl && authUrl !== "#"
|
||||
? {
|
||||
label: "Authorize Now",
|
||||
onClick: () => window.open(authUrl, "_blank"),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Button variant="ghost" size="icon" disabled>
|
||||
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Function to handle dialog open state
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10"
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Gitea Provider</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update your Gitea provider details.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="My Gitea"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="giteaUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://gitea.example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Client ID" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Client Secret"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
isLoading={isTesting}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const formValues = form.getValues();
|
||||
const authUrl = getGiteaOAuthUrl(
|
||||
giteaId,
|
||||
formValues.clientId,
|
||||
formValues.giteaUrl,
|
||||
typeof url === "string" ? url : (url as any).url || "",
|
||||
);
|
||||
if (authUrl !== "#") {
|
||||
window.open(authUrl, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connect to Gitea
|
||||
</Button>
|
||||
|
||||
<Button type="submit" isLoading={isUpdating}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
@@ -26,6 +27,8 @@ import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||
import { AddGiteaProvider } from "./gitea/add-gitea-provider";
|
||||
import { EditGiteaProvider } from "./gitea/edit-gitea-provider";
|
||||
import { AddGithubProvider } from "./github/add-github-provider";
|
||||
import { EditGithubProvider } from "./github/edit-github-provider";
|
||||
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
||||
@@ -36,19 +39,18 @@ export const ShowGitProviders = () => {
|
||||
const { mutateAsync, isLoading: isRemoving } =
|
||||
api.gitProvider.remove.useMutation();
|
||||
const url = useUrl();
|
||||
|
||||
const getGitlabUrl = (
|
||||
clientId: string,
|
||||
gitlabId: string,
|
||||
gitlabUrl: string,
|
||||
) => {
|
||||
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
||||
|
||||
const scope = "api read_user read_repository";
|
||||
|
||||
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
@@ -82,6 +84,7 @@ export const ShowGitProviders = () => {
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
<AddGiteaProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +100,7 @@ export const ShowGitProviders = () => {
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
<AddGiteaProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,13 +111,16 @@ export const ShowGitProviders = () => {
|
||||
const isGitlab = gitProvider.providerType === "gitlab";
|
||||
const isBitbucket =
|
||||
gitProvider.providerType === "bitbucket";
|
||||
const isGitea = gitProvider.providerType === "gitea";
|
||||
|
||||
const haveGithubRequirements =
|
||||
gitProvider.providerType === "github" &&
|
||||
isGithub &&
|
||||
gitProvider.github?.githubPrivateKey &&
|
||||
gitProvider.github?.githubAppId &&
|
||||
gitProvider.github?.githubInstallationId;
|
||||
|
||||
const haveGitlabRequirements =
|
||||
isGitlab &&
|
||||
gitProvider.gitlab?.accessToken &&
|
||||
gitProvider.gitlab?.refreshToken;
|
||||
|
||||
@@ -122,18 +129,19 @@ export const ShowGitProviders = () => {
|
||||
key={gitProvider.gitProviderId}
|
||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex flex-col items-center justify-between">
|
||||
<div className="flex gap-2 flex-row items-center">
|
||||
{gitProvider.providerType === "github" && (
|
||||
{isGithub && (
|
||||
<GithubIcon className="size-5" />
|
||||
)}
|
||||
{gitProvider.providerType === "gitlab" && (
|
||||
{isGitlab && (
|
||||
<GitlabIcon className="size-5" />
|
||||
)}
|
||||
{gitProvider.providerType === "bitbucket" && (
|
||||
{isBitbucket && (
|
||||
<BitbucketIcon className="size-5" />
|
||||
)}
|
||||
{isGitea && <GiteaIcon className="size-5" />}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium">
|
||||
{gitProvider.name}
|
||||
@@ -194,26 +202,33 @@ export const ShowGitProviders = () => {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={gitProvider.github.githubId}
|
||||
githubId={gitProvider.github?.githubId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={gitProvider.gitlab.gitlabId}
|
||||
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={
|
||||
gitProvider.bitbucket.bitbucketId
|
||||
gitProvider.bitbucket?.bitbucketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitea && (
|
||||
<EditGiteaProvider
|
||||
giteaId={gitProvider.gitea?.giteaId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description="Are you sure you want to delete this Git Provider?"
|
||||
@@ -238,7 +253,7 @@ export const ShowGitProviders = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
const _utils = api.useUtils();
|
||||
|
||||
return (
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -36,10 +36,12 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<LockKeyhole className="size-5" />
|
||||
<CardTitle className="text-xl">
|
||||
Setup Security Sugestions
|
||||
Setup Security Suggestions
|
||||
</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Check the security sugestions</CardDescription>
|
||||
<CardDescription>
|
||||
Check the security suggestions
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isRefreshing}
|
||||
@@ -120,36 +122,36 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
<div className="grid gap-2.5">
|
||||
<StatusRow
|
||||
label="Enabled"
|
||||
isEnabled={data?.ssh.enabled}
|
||||
isEnabled={data?.ssh?.enabled}
|
||||
description={
|
||||
data?.ssh.enabled
|
||||
data?.ssh?.enabled
|
||||
? "Enabled"
|
||||
: "Not Enabled (SSH should be enabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Key Auth"
|
||||
isEnabled={data?.ssh.keyAuth}
|
||||
isEnabled={data?.ssh?.keyAuth}
|
||||
description={
|
||||
data?.ssh.keyAuth
|
||||
data?.ssh?.keyAuth
|
||||
? "Enabled (Recommended)"
|
||||
: "Not Enabled (Key Authentication should be enabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Password Auth"
|
||||
isEnabled={data?.ssh.passwordAuth === "no"}
|
||||
isEnabled={data?.ssh?.passwordAuth === "no"}
|
||||
description={
|
||||
data?.ssh.passwordAuth === "no"
|
||||
data?.ssh?.passwordAuth === "no"
|
||||
? "Disabled (Recommended)"
|
||||
: "Enabled (Password Authentication should be disabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Use PAM"
|
||||
isEnabled={data?.ssh.usePam === "no"}
|
||||
isEnabled={data?.ssh?.usePam === "no"}
|
||||
description={
|
||||
data?.ssh.usePam === "no"
|
||||
data?.ssh?.usePam === "no"
|
||||
? "Disabled (Recommended for key-based auth)"
|
||||
: "Enabled (Should be disabled when using key-based auth)"
|
||||
}
|
||||
@@ -166,9 +168,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
<div className="grid gap-2.5">
|
||||
<StatusRow
|
||||
label="Installed"
|
||||
isEnabled={data?.fail2ban.installed}
|
||||
isEnabled={data?.fail2ban?.installed}
|
||||
description={
|
||||
data?.fail2ban.installed
|
||||
data?.fail2ban?.installed
|
||||
? "Installed (Recommended)"
|
||||
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
||||
}
|
||||
@@ -176,18 +178,18 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
|
||||
<StatusRow
|
||||
label="Enabled"
|
||||
isEnabled={data?.fail2ban.enabled}
|
||||
isEnabled={data?.fail2ban?.enabled}
|
||||
description={
|
||||
data?.fail2ban.enabled
|
||||
data?.fail2ban?.enabled
|
||||
? "Enabled (Recommended)"
|
||||
: "Not Enabled (Fail2Ban service should be enabled)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Active"
|
||||
isEnabled={data?.fail2ban.active}
|
||||
isEnabled={data?.fail2ban?.active}
|
||||
description={
|
||||
data?.fail2ban.active
|
||||
data?.fail2ban?.active
|
||||
? "Active (Recommended)"
|
||||
: "Not Active (Fail2Ban service should be running)"
|
||||
}
|
||||
@@ -195,9 +197,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
|
||||
<StatusRow
|
||||
label="SSH Protection"
|
||||
isEnabled={data?.fail2ban.sshEnabled === "true"}
|
||||
isEnabled={data?.fail2ban?.sshEnabled === "true"}
|
||||
description={
|
||||
data?.fail2ban.sshEnabled === "true"
|
||||
data?.fail2ban?.sshEnabled === "true"
|
||||
? "Enabled (Recommended)"
|
||||
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
|
||||
}
|
||||
@@ -205,11 +207,11 @@ export const SecurityAudit = ({ serverId }: Props) => {
|
||||
|
||||
<StatusRow
|
||||
label="SSH Mode"
|
||||
isEnabled={data?.fail2ban.sshMode === "aggressive"}
|
||||
isEnabled={data?.fail2ban?.sshMode === "aggressive"}
|
||||
description={
|
||||
data?.fail2ban.sshMode === "aggressive"
|
||||
data?.fail2ban?.sshMode === "aggressive"
|
||||
? "Aggressive Mode (Recommended)"
|
||||
: `Mode: ${data?.fail2ban.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
||||
: `Mode: ${data?.fail2ban?.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,7 @@ import { useTranslation } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
||||
import { TerminalModal } from "../web-server/terminal-modal";
|
||||
import { ShowServerActions } from "./actions/show-server-actions";
|
||||
import { HandleServers } from "./handle-servers";
|
||||
@@ -42,7 +43,6 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
|
||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
||||
|
||||
export const ShowServers = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -22,7 +23,6 @@ import dynamic from "next/dynamic";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { badgeStateColor } from "../../application/logs/show";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export const DockerLogsId = dynamic(
|
||||
() =>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { DatabaseIcon, AlertTriangle } from "lucide-react";
|
||||
import { AlertTriangle, DatabaseIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
|
||||
import { RebuildDatabase } from "./rebuild-database";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -238,6 +238,41 @@ export const BitbucketIcon = ({ className }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const GiteaIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
id="main_outline"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="5.67 143.05 628.65 387.55"
|
||||
enableBackground="new 0 0 640 640"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
id="teabag"
|
||||
style={{ fill: "#FFFFFF" }}
|
||||
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#609926" }}
|
||||
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#609926" }}
|
||||
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const DockerIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { api } from "@/utils/api";
|
||||
import type { IUpdateData } from "@dokploy/server/index";
|
||||
import { Download } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
||||
import { Button } from "../ui/button";
|
||||
import { Download } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
||||
@@ -3,18 +3,18 @@ import { json } from "@codemirror/lang-json";
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { StreamLanguage } from "@codemirror/language";
|
||||
|
||||
import {
|
||||
type Completion,
|
||||
type CompletionContext,
|
||||
type CompletionResult,
|
||||
autocompletion,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
autocompletion,
|
||||
type CompletionContext,
|
||||
type CompletionResult,
|
||||
type Completion,
|
||||
} from "@codemirror/autocomplete";
|
||||
|
||||
// Docker Compose completion options
|
||||
const dockerComposeServices = [
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" DROP COLUMN "giteaProjectId";
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "gitea" DROP COLUMN "gitea_username";
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."databaseType" ADD VALUE 'web-server';
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -554,6 +554,41 @@
|
||||
"when": 1742112194375,
|
||||
"tag": "0078_uneven_omega_sentinel",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.20.8",
|
||||
"version": "v0.21.1",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -150,7 +150,8 @@
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^3.23.4",
|
||||
"zod-form-data": "^2.0.2"
|
||||
"zod-form-data": "^2.0.2",
|
||||
"toml": "3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
|
||||
@@ -84,6 +84,33 @@ export default async function handler(
|
||||
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(
|
||||
application.watchPaths,
|
||||
normalizedCommits,
|
||||
);
|
||||
|
||||
if (!shouldDeployPaths) {
|
||||
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||
return;
|
||||
}
|
||||
} else if (sourceType === "gitlab") {
|
||||
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" });
|
||||
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 {
|
||||
@@ -280,6 +328,26 @@ export const extractBranchName = (headers: any, body: any) => {
|
||||
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 (
|
||||
body: any,
|
||||
bitbucketUsername: string | null,
|
||||
|
||||
@@ -8,9 +8,10 @@ import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import {
|
||||
extractBranchName,
|
||||
extractCommitedPaths,
|
||||
extractCommitMessage,
|
||||
extractCommitedPaths,
|
||||
extractHash,
|
||||
getProviderByHeader,
|
||||
} from "../[refreshToken]";
|
||||
|
||||
export default async function handler(
|
||||
@@ -91,12 +92,6 @@ export default async function handler(
|
||||
res.status(301).json({ message: "Branch Not Match" });
|
||||
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(
|
||||
req.body,
|
||||
@@ -104,6 +99,7 @@ export default async function handler(
|
||||
composeResult.bitbucket?.appPassword || "",
|
||||
composeResult.bitbucketRepository || "",
|
||||
);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
composeResult.watchPaths,
|
||||
commitedPaths,
|
||||
@@ -113,6 +109,59 @@ export default async function handler(
|
||||
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||
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 {
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -47,6 +56,13 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -64,8 +80,8 @@ import {
|
||||
Loader2,
|
||||
PlusIcon,
|
||||
Search,
|
||||
X,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
@@ -73,25 +89,10 @@ import type {
|
||||
} from "next";
|
||||
import Head from "next/head";
|
||||
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 superjson from "superjson";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -553,7 +554,7 @@ const Project = (
|
||||
</CardTitle>
|
||||
<CardDescription>{data?.description}</CardDescription>
|
||||
</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">
|
||||
<ProjectEnvironment projectId={projectId}>
|
||||
<Button variant="outline">Project Environment</Button>
|
||||
@@ -569,7 +570,7 @@ const Project = (
|
||||
className="w-[200px] space-y-2"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel className="text-sm font-normal ">
|
||||
<DropdownMenuLabel className="text-sm font-normal">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -593,7 +594,7 @@ const Project = (
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||
{isLoading ? (
|
||||
@@ -670,20 +671,27 @@ const Project = (
|
||||
</DialogAction>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||
type="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive"
|
||||
<>
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||
type="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DuplicateProject
|
||||
projectId={projectId}
|
||||
services={applications}
|
||||
selectedServiceIds={selectedServices}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<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 { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
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 { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||
|
||||
type TabState =
|
||||
| "projects"
|
||||
|
||||
@@ -8,11 +8,11 @@ import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres
|
||||
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
|
||||
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
||||
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 { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
|
||||
@@ -8,56 +8,21 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
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 { data: user } = api.user.get.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<WebDomain />
|
||||
<WebServer />
|
||||
{/* <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
|
||||
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 className="w-full flex flex-col gap-4">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<ShowBackups id={user?.userId ?? ""} type="web-server" />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { destinationRouter } from "./routers/destination";
|
||||
import { dockerRouter } from "./routers/docker";
|
||||
import { domainRouter } from "./routers/domain";
|
||||
import { gitProviderRouter } from "./routers/git-provider";
|
||||
import { giteaRouter } from "./routers/gitea";
|
||||
import { githubRouter } from "./routers/github";
|
||||
import { gitlabRouter } from "./routers/gitlab";
|
||||
import { mariadbRouter } from "./routers/mariadb";
|
||||
@@ -68,6 +69,7 @@ export const appRouter = createTRPCRouter({
|
||||
notification: notificationRouter,
|
||||
sshKey: sshRouter,
|
||||
gitProvider: gitProviderRouter,
|
||||
gitea: giteaRouter,
|
||||
bitbucket: bitbucketRouter,
|
||||
gitlab: gitlabRouter,
|
||||
github: githubRouter,
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
checkServiceAccess,
|
||||
} from "@dokploy/server/services/user";
|
||||
import {
|
||||
getProviderHeaders,
|
||||
type Model,
|
||||
getProviderHeaders,
|
||||
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
apiSaveDockerProvider,
|
||||
apiSaveEnvironmentVariables,
|
||||
apiSaveGitProvider,
|
||||
apiSaveGiteaProvider,
|
||||
apiSaveGithubProvider,
|
||||
apiSaveGitlabProvider,
|
||||
apiUpdateApplication,
|
||||
@@ -399,6 +400,31 @@ export const applicationRouter = createTRPCRouter({
|
||||
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;
|
||||
}),
|
||||
saveDockerProvider: protectedProcedure
|
||||
|
||||
@@ -25,25 +25,27 @@ import {
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
runPostgresBackup,
|
||||
runWebServerBackup,
|
||||
scheduleBackup,
|
||||
updateBackupById,
|
||||
} from "@dokploy/server";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import {
|
||||
restoreMariadbBackup,
|
||||
restoreMongoBackup,
|
||||
restoreMySqlBackup,
|
||||
restorePostgresBackup,
|
||||
restoreWebServerBackup,
|
||||
} from "@dokploy/server/utils/restore";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
|
||||
export const backupRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -85,9 +87,13 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Backup",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error creating the Backup",
|
||||
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
|
||||
.input(
|
||||
z.object({
|
||||
@@ -301,7 +314,13 @@ export const backupRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
databaseId: z.string(),
|
||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"web-server",
|
||||
]),
|
||||
databaseName: z.string().min(1),
|
||||
backupFile: 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;
|
||||
}),
|
||||
|
||||
@@ -9,23 +9,10 @@ import {
|
||||
apiUpdateCompose,
|
||||
compose as composeTable,
|
||||
} 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 { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
addDomainToCompose,
|
||||
@@ -55,6 +42,20 @@ import {
|
||||
stopCompose,
|
||||
updateCompose,
|
||||
} 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({
|
||||
create: protectedProcedure
|
||||
@@ -594,7 +595,7 @@ export const composeRouter = createTRPCRouter({
|
||||
serverIp = "127.0.0.1";
|
||||
}
|
||||
const templateData = JSON.parse(decodedData);
|
||||
const config = load(templateData.config) as CompleteTemplate;
|
||||
const config = parse(templateData.config) as CompleteTemplate;
|
||||
|
||||
if (!templateData.compose || !config) {
|
||||
throw new TRPCError({
|
||||
@@ -663,7 +664,8 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const templateData = JSON.parse(decodedData);
|
||||
const config = load(templateData.config) as CompleteTemplate;
|
||||
|
||||
const config = parse(templateData.config) as CompleteTemplate;
|
||||
|
||||
if (!templateData.compose || !config) {
|
||||
throw new TRPCError({
|
||||
@@ -678,7 +680,6 @@ export const composeRouter = createTRPCRouter({
|
||||
projectName: compose.appName,
|
||||
});
|
||||
|
||||
// Update compose file
|
||||
await updateCompose(input.composeId, {
|
||||
composeFile: templateData.compose,
|
||||
sourceType: "raw",
|
||||
@@ -686,7 +687,6 @@ export const composeRouter = createTRPCRouter({
|
||||
isolatedDeployment: true,
|
||||
});
|
||||
|
||||
// Create mounts
|
||||
if (processedTemplate.mounts && processedTemplate.mounts.length > 0) {
|
||||
for (const mount of processedTemplate.mounts) {
|
||||
await createMount({
|
||||
@@ -700,7 +700,6 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
// Create domains
|
||||
if (processedTemplate.domains && processedTemplate.domains.length > 0) {
|
||||
for (const domain of processedTemplate.domains) {
|
||||
await createDomain({
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
updateDestinationById,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
export const destinationRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
|
||||
@@ -12,6 +12,7 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
github: true,
|
||||
gitea: true,
|
||||
},
|
||||
orderBy: desc(gitProvider.createdAt),
|
||||
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiChangeMariaDBStatus,
|
||||
apiCreateMariaDB,
|
||||
apiDeployMariaDB,
|
||||
apiFindOneMariaDB,
|
||||
apiRebuildMariadb,
|
||||
apiResetMariadb,
|
||||
apiSaveEnvironmentVariablesMariaDB,
|
||||
apiSaveExternalPortMariaDB,
|
||||
apiUpdateMariaDB,
|
||||
apiRebuildMariadb,
|
||||
mariadb as mariadbTable,
|
||||
} from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
@@ -30,12 +31,11 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMariadbById,
|
||||
} from "@dokploy/server";
|
||||
import { rebuildDatabase } from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { rebuildDatabase } from "@dokploy/server";
|
||||
import { z } from "zod";
|
||||
export const mariadbRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateMariaDB)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiChangeMongoStatus,
|
||||
apiCreateMongo,
|
||||
@@ -30,12 +31,11 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMongoById,
|
||||
} from "@dokploy/server";
|
||||
import { rebuildDatabase } from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { rebuildDatabase } from "@dokploy/server";
|
||||
import { z } from "zod";
|
||||
export const mongoRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateMongo)
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { db } from "@/server/db";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
@@ -36,7 +37,6 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { z } from "zod";
|
||||
|
||||
export const mysqlRouter = createTRPCRouter({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiChangePostgresStatus,
|
||||
apiCreatePostgres,
|
||||
@@ -33,9 +34,8 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { z } from "zod";
|
||||
export const postgresRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreatePostgres)
|
||||
|
||||
@@ -14,21 +14,44 @@ import {
|
||||
projects,
|
||||
redis,
|
||||
} from "@/server/db/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
IS_CLOUD,
|
||||
addNewProject,
|
||||
checkProjectAccess,
|
||||
createApplication,
|
||||
createCompose,
|
||||
createMariadb,
|
||||
createMongo,
|
||||
createMysql,
|
||||
createPostgres,
|
||||
createProject,
|
||||
createRedis,
|
||||
deleteProject,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findMongoById,
|
||||
findMemberById,
|
||||
findRedisById,
|
||||
findProjectById,
|
||||
findUserById,
|
||||
updateProjectById,
|
||||
findPostgresById,
|
||||
findMariadbById,
|
||||
findMySqlById,
|
||||
createDomain,
|
||||
createPort,
|
||||
createMount,
|
||||
createRedirect,
|
||||
createPreviewDeployment,
|
||||
createBackup,
|
||||
createSecurity,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, sql } from "drizzle-orm";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
|
||||
export const projectRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateProject)
|
||||
@@ -266,7 +289,317 @@ export const projectRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
duplicate: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
sourceProjectId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
includeServices: z.boolean().default(true),
|
||||
selectedServices: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"mysql",
|
||||
"redis",
|
||||
"compose",
|
||||
]),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.rol === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
|
||||
// Get source project
|
||||
const sourceProject = await findProjectById(input.sourceProjectId);
|
||||
|
||||
if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
|
||||
// Create new project
|
||||
const newProject = await createProject(
|
||||
{
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
env: sourceProject.env,
|
||||
},
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (input.includeServices) {
|
||||
const servicesToDuplicate = input.selectedServices || [];
|
||||
|
||||
// Helper function to duplicate a service
|
||||
const duplicateService = async (id: string, type: string) => {
|
||||
switch (type) {
|
||||
case "application": {
|
||||
const {
|
||||
applicationId,
|
||||
domains,
|
||||
security,
|
||||
ports,
|
||||
registry,
|
||||
redirects,
|
||||
previewDeployments,
|
||||
mounts,
|
||||
...application
|
||||
} = await findApplicationById(id);
|
||||
|
||||
const newApplication = await createApplication({
|
||||
...application,
|
||||
projectId: newProject.projectId,
|
||||
});
|
||||
|
||||
for (const domain of domains) {
|
||||
const { domainId, ...rest } = domain;
|
||||
await createDomain({
|
||||
...rest,
|
||||
applicationId: newApplication.applicationId,
|
||||
domainType: "application",
|
||||
});
|
||||
}
|
||||
|
||||
for (const port of ports) {
|
||||
const { portId, ...rest } = port;
|
||||
await createPort({
|
||||
...rest,
|
||||
applicationId: newApplication.applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newApplication.applicationId,
|
||||
serviceType: "application",
|
||||
});
|
||||
}
|
||||
|
||||
for (const redirect of redirects) {
|
||||
const { redirectId, ...rest } = redirect;
|
||||
await createRedirect({
|
||||
...rest,
|
||||
applicationId: newApplication.applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
for (const secure of security) {
|
||||
const { securityId, ...rest } = secure;
|
||||
await createSecurity({
|
||||
...rest,
|
||||
applicationId: newApplication.applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
for (const previewDeployment of previewDeployments) {
|
||||
const { previewDeploymentId, ...rest } = previewDeployment;
|
||||
await createPreviewDeployment({
|
||||
...rest,
|
||||
applicationId: newApplication.applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "postgres": {
|
||||
const { postgresId, mounts, backups, ...postgres } =
|
||||
await findPostgresById(id);
|
||||
|
||||
const newPostgres = await createPostgres({
|
||||
...postgres,
|
||||
projectId: newProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newPostgres.postgresId,
|
||||
serviceType: "postgres",
|
||||
});
|
||||
}
|
||||
|
||||
for (const backup of backups) {
|
||||
const { backupId, ...rest } = backup;
|
||||
await createBackup({
|
||||
...rest,
|
||||
postgresId: newPostgres.postgresId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mariadb": {
|
||||
const { mariadbId, mounts, backups, ...mariadb } =
|
||||
await findMariadbById(id);
|
||||
const newMariadb = await createMariadb({
|
||||
...mariadb,
|
||||
projectId: newProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newMariadb.mariadbId,
|
||||
serviceType: "mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
for (const backup of backups) {
|
||||
const { backupId, ...rest } = backup;
|
||||
await createBackup({
|
||||
...rest,
|
||||
mariadbId: newMariadb.mariadbId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mongo": {
|
||||
const { mongoId, mounts, backups, ...mongo } =
|
||||
await findMongoById(id);
|
||||
const newMongo = await createMongo({
|
||||
...mongo,
|
||||
projectId: newProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newMongo.mongoId,
|
||||
serviceType: "mongo",
|
||||
});
|
||||
}
|
||||
|
||||
for (const backup of backups) {
|
||||
const { backupId, ...rest } = backup;
|
||||
await createBackup({
|
||||
...rest,
|
||||
mongoId: newMongo.mongoId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "mysql": {
|
||||
const { mysqlId, mounts, backups, ...mysql } =
|
||||
await findMySqlById(id);
|
||||
const newMysql = await createMysql({
|
||||
...mysql,
|
||||
projectId: newProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newMysql.mysqlId,
|
||||
serviceType: "mysql",
|
||||
});
|
||||
}
|
||||
|
||||
for (const backup of backups) {
|
||||
const { backupId, ...rest } = backup;
|
||||
await createBackup({
|
||||
...rest,
|
||||
mysqlId: newMysql.mysqlId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "redis": {
|
||||
const { redisId, mounts, ...redis } = await findRedisById(id);
|
||||
const newRedis = await createRedis({
|
||||
...redis,
|
||||
projectId: newProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newRedis.redisId,
|
||||
serviceType: "redis",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "compose": {
|
||||
const { composeId, mounts, domains, ...compose } =
|
||||
await findComposeById(id);
|
||||
const newCompose = await createCompose({
|
||||
...compose,
|
||||
projectId: newProject.projectId,
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newCompose.composeId,
|
||||
serviceType: "compose",
|
||||
});
|
||||
}
|
||||
|
||||
for (const domain of domains) {
|
||||
const { domainId, ...rest } = domain;
|
||||
await createDomain({
|
||||
...rest,
|
||||
composeId: newCompose.composeId,
|
||||
domainType: "compose",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Duplicate selected services
|
||||
|
||||
for (const service of servicesToDuplicate) {
|
||||
await duplicateService(service.id, service.type);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.user.rol === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
newProject.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
|
||||
return newProject;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error duplicating the project: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
function buildServiceFilter(
|
||||
fieldName: AnyPgColumn,
|
||||
accessedServices: string[],
|
||||
|
||||
@@ -4,16 +4,17 @@ import {
|
||||
apiCreateRedis,
|
||||
apiDeployRedis,
|
||||
apiFindOneRedis,
|
||||
apiRebuildRedis,
|
||||
apiResetRedis,
|
||||
apiSaveEnvironmentVariablesRedis,
|
||||
apiSaveExternalPortRedis,
|
||||
apiUpdateRedis,
|
||||
redis as redisTable,
|
||||
apiRebuildRedis,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
addNewService,
|
||||
@@ -31,11 +32,10 @@ import {
|
||||
stopServiceRemote,
|
||||
updateRedisById,
|
||||
} from "@dokploy/server";
|
||||
import { rebuildDatabase } from "@dokploy/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { z } from "zod";
|
||||
import { rebuildDatabase } from "@dokploy/server";
|
||||
export const redisRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateRedis)
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
findUserById,
|
||||
getDokployImage,
|
||||
getDokployImageTag,
|
||||
getLogCleanupStatus,
|
||||
getUpdateData,
|
||||
initializeTraefik,
|
||||
parseRawConfig,
|
||||
@@ -41,6 +42,8 @@ import {
|
||||
recreateDirectory,
|
||||
sendDockerCleanupNotifications,
|
||||
spawnAsync,
|
||||
startLogCleanup,
|
||||
stopLogCleanup,
|
||||
updateLetsEncryptEmail,
|
||||
updateServerById,
|
||||
updateServerTraefik,
|
||||
@@ -48,9 +51,6 @@ import {
|
||||
writeConfig,
|
||||
writeMainConfig,
|
||||
writeTraefikConfigInPath,
|
||||
startLogCleanup,
|
||||
stopLogCleanup,
|
||||
getLogCleanupStatus,
|
||||
} from "@dokploy/server";
|
||||
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
@@ -479,6 +479,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
"bitbucket",
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitea",
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createApiKey,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getUserByToken,
|
||||
removeUserById,
|
||||
updateUser,
|
||||
createApiKey,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
apiAssignPermissions,
|
||||
apiFindOneToken,
|
||||
apiUpdateUser,
|
||||
apikey,
|
||||
invitation,
|
||||
member,
|
||||
apikey,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { and, asc, eq, gt } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -91,6 +91,28 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
getBackups: adminProcedure.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: {
|
||||
with: {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
},
|
||||
},
|
||||
apiKeys: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return memberResult?.user;
|
||||
}),
|
||||
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
|
||||
@@ -46,7 +46,7 @@ void app.prepare().then(async () => {
|
||||
await initializeNetwork();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
initCronJobs();
|
||||
await initCronJobs();
|
||||
await migration();
|
||||
await sendDokployRestartNotifications();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// utils/gitea-utils.ts
|
||||
// This file contains client-safe utilities for Gitea integration
|
||||
|
||||
/**
|
||||
* Generates an OAuth URL for Gitea authorization
|
||||
*
|
||||
* @param giteaId The ID of the Gitea provider to be used as state
|
||||
* @param clientId The OAuth client ID
|
||||
* @param giteaUrl The base URL of the Gitea instance
|
||||
* @param baseUrl The base URL of the application for callback
|
||||
* @returns The complete OAuth authorization URL
|
||||
*/
|
||||
export const getGiteaOAuthUrl = (
|
||||
giteaId: string,
|
||||
clientId: string,
|
||||
giteaUrl: string,
|
||||
baseUrl: string,
|
||||
): string => {
|
||||
if (!clientId || !giteaUrl || !baseUrl) {
|
||||
// Return a marker that can be checked by the caller
|
||||
return "#";
|
||||
}
|
||||
|
||||
const redirectUri = `${baseUrl}/api/providers/gitea/callback`;
|
||||
const scopes = "repo repo:status read:user read:org";
|
||||
|
||||
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||
redirectUri,
|
||||
)}&response_type=code&scope=${encodeURIComponent(scopes)}&state=${encodeURIComponent(giteaId)}`;
|
||||
};
|
||||
|
||||
// Interfaces for Gitea API responses and components
|
||||
export interface Repository {
|
||||
name: string;
|
||||
url: string;
|
||||
id: number;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Branch {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GiteaProviderType {
|
||||
giteaId: string;
|
||||
gitProvider: {
|
||||
name: string;
|
||||
gitProviderId: string;
|
||||
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
|
||||
createdAt: string;
|
||||
organizationId: string;
|
||||
};
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GiteaProviderResponse {
|
||||
giteaId: string;
|
||||
clientId: string;
|
||||
giteaUrl: string;
|
||||
}
|
||||
|
||||
export interface GitProvider {
|
||||
gitProviderId: string;
|
||||
name: string;
|
||||
providerType: string;
|
||||
giteaId?: string;
|
||||
gitea?: {
|
||||
giteaId: string;
|
||||
giteaUrl: string;
|
||||
clientId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GiteaProvider {
|
||||
gitea?: {
|
||||
giteaId?: string;
|
||||
giteaUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
redirectUri?: string;
|
||||
organizationName?: string;
|
||||
};
|
||||
name?: string;
|
||||
gitProviderId?: string;
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"micromatch":"4.0.8",
|
||||
"micromatch": "4.0.8",
|
||||
"@ai-sdk/anthropic": "^1.0.6",
|
||||
"@ai-sdk/azure": "^1.0.15",
|
||||
"@ai-sdk/cohere": "^1.0.6",
|
||||
@@ -36,11 +36,11 @@
|
||||
"@ai-sdk/mistral": "^1.0.6",
|
||||
"@ai-sdk/openai": "^1.0.12",
|
||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
||||
"@better-auth/utils":"0.2.3",
|
||||
"@oslojs/encoding":"1.1.0",
|
||||
"@oslojs/crypto":"1.0.1",
|
||||
"drizzle-dbml-generator":"0.10.0",
|
||||
"better-auth":"1.2.4",
|
||||
"@better-auth/utils": "0.2.3",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"better-auth": "1.2.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
"@react-email/components": "^0.0.21",
|
||||
@@ -53,7 +53,7 @@
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"hi-base32": "^0.5.1",
|
||||
"js-yaml": "4.1.0",
|
||||
@@ -76,7 +76,8 @@
|
||||
"ws": "8.16.0",
|
||||
"zod": "^3.23.4",
|
||||
"ssh2": "1.15.0",
|
||||
"@octokit/rest": "^20.0.2"
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"toml": "3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/micromatch": "4.0.9",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { z } from "zod";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
@@ -33,6 +34,7 @@ export const sourceType = pgEnum("sourceType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"drop",
|
||||
]);
|
||||
|
||||
@@ -155,6 +157,11 @@ export const applications = pgTable("application", {
|
||||
gitlabBranch: text("gitlabBranch"),
|
||||
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
giteaBuildPath: text("giteaBuildPath").default("/"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
@@ -212,6 +219,9 @@ export const applications = pgTable("application", {
|
||||
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -249,6 +259,10 @@ export const applicationsRelations = relations(
|
||||
fields: [applications.gitlabId],
|
||||
references: [gitlab.gitlabId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [applications.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
bitbucket: one(bitbucket, {
|
||||
fields: [applications.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
@@ -379,7 +393,9 @@ const createSchema = createInsertSchema(applications, {
|
||||
customGitUrl: z.string().optional(),
|
||||
buildPath: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.enum(["github", "docker", "git"]).optional(),
|
||||
sourceType: z
|
||||
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
||||
.optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
@@ -483,6 +499,18 @@ export const apiSaveBitbucketProvider = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGiteaProvider = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
giteaBranch: true,
|
||||
giteaBuildPath: true,
|
||||
giteaOwner: true,
|
||||
giteaRepository: true,
|
||||
giteaId: true,
|
||||
watchPaths: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveDockerProvider = createSchema
|
||||
.pick({
|
||||
dockerImage: true,
|
||||
|
||||
@@ -15,12 +15,13 @@ import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
import { users_temp } from "./user";
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
]);
|
||||
|
||||
export const backups = pgTable("backup", {
|
||||
@@ -58,6 +59,7 @@ export const backups = pgTable("backup", {
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users_temp.id),
|
||||
});
|
||||
|
||||
export const backupsRelations = relations(backups, ({ one }) => ({
|
||||
@@ -81,6 +83,10 @@ export const backupsRelations = relations(backups, ({ one }) => ({
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [backups.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(backups, {
|
||||
@@ -91,11 +97,12 @@ const createSchema = createInsertSchema(backups, {
|
||||
database: z.string().min(1),
|
||||
schedule: z.string(),
|
||||
keepLatestCount: z.number().optional(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo"]),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
||||
postgresId: z.string().optional(),
|
||||
mariadbId: z.string().optional(),
|
||||
mysqlId: z.string().optional(),
|
||||
mongoId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateBackup = createSchema.pick({
|
||||
@@ -110,6 +117,7 @@ export const apiCreateBackup = createSchema.pick({
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
databaseType: true,
|
||||
userId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneBackup = createSchema
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
@@ -20,6 +21,7 @@ export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"raw",
|
||||
]);
|
||||
|
||||
@@ -55,6 +57,10 @@ export const compose = pgTable("compose", {
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
@@ -87,6 +93,9 @@ export const compose = pgTable("compose", {
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -116,6 +125,10 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
fields: [compose.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [compose.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [compose.serverId],
|
||||
references: [server.serverId],
|
||||
@@ -126,7 +139,7 @@ const createSchema = createInsertSchema(compose, {
|
||||
name: z.string().min(1),
|
||||
description: z.string(),
|
||||
env: z.string().optional(),
|
||||
composeFile: z.string().min(1),
|
||||
composeFile: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
customGitSSHKeyId: z.string().optional(),
|
||||
command: z.string().optional(),
|
||||
@@ -142,6 +155,7 @@ export const apiCreateCompose = createSchema.pick({
|
||||
composeType: true,
|
||||
appName: true,
|
||||
serverId: true,
|
||||
composeFile: true,
|
||||
});
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
|
||||
@@ -12,6 +13,7 @@ export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
]);
|
||||
|
||||
export const gitProvider = pgTable("git_provider", {
|
||||
@@ -42,6 +44,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [bitbucket.gitProviderId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [gitea.gitProviderId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { gitProvider } from "./git-provider";
|
||||
|
||||
export const gitea = pgTable("gitea", {
|
||||
giteaId: text("giteaId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(),
|
||||
redirectUri: text("redirect_uri"),
|
||||
clientId: text("client_id"),
|
||||
clientSecret: text("client_secret"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
expiresAt: integer("expires_at"),
|
||||
scopes: text("scopes").default("repo,repo:status,read:user,read:org"),
|
||||
lastAuthenticatedAt: integer("last_authenticated_at"),
|
||||
});
|
||||
|
||||
export const giteaProviderRelations = relations(gitea, ({ one }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [gitea.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitea);
|
||||
|
||||
export const apiCreateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneGitea = createSchema
|
||||
.extend({
|
||||
giteaId: z.string().min(1),
|
||||
})
|
||||
.pick({ giteaId: true });
|
||||
|
||||
export const apiGiteaTestConnection = createSchema
|
||||
.extend({
|
||||
organizationName: z.string().optional(),
|
||||
})
|
||||
.pick({ giteaId: true, organizationName: true });
|
||||
|
||||
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
|
||||
|
||||
export const apiFindGiteaBranches = z.object({
|
||||
owner: z.string().min(1),
|
||||
repositoryName: z.string().min(1),
|
||||
giteaId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaId: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
@@ -25,6 +25,7 @@ export * from "./git-provider";
|
||||
export * from "./bitbucket";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./gitea";
|
||||
export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
|
||||
@@ -52,6 +52,7 @@ const createSchema = createInsertSchema(projects, {
|
||||
export const apiCreateProject = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
env: true,
|
||||
});
|
||||
|
||||
export const apiFindOneProject = createSchema
|
||||
|
||||
@@ -46,6 +46,7 @@ enum gitProviderType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
}
|
||||
|
||||
enum mountType {
|
||||
@@ -98,6 +99,7 @@ enum sourceType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
drop
|
||||
}
|
||||
|
||||
@@ -106,6 +108,7 @@ enum sourceTypeCompose {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
raw
|
||||
}
|
||||
|
||||
@@ -206,6 +209,7 @@ table application {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -280,6 +284,9 @@ table compose {
|
||||
bitbucketRepository text
|
||||
bitbucketOwner text
|
||||
bitbucketBranch text
|
||||
giteaRepository text
|
||||
giteaOwner text
|
||||
giteaBranch text
|
||||
customGitUrl text
|
||||
customGitBranch text
|
||||
customGitSSHKeyId text
|
||||
@@ -294,6 +301,7 @@ table compose {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -389,6 +397,20 @@ table gitlab {
|
||||
gitProviderId text [not null]
|
||||
}
|
||||
|
||||
table gitea {
|
||||
giteaId text [pk, not null]
|
||||
giteaUrl text [not null, default: 'https://gitea.com']
|
||||
redirect_uri text
|
||||
client_id text [not null]
|
||||
client_secret text [not null]
|
||||
access_token text
|
||||
refresh_token text
|
||||
expires_at integer
|
||||
gitProviderId text [not null]
|
||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||
last_authenticated_at integer
|
||||
}
|
||||
|
||||
table gotify {
|
||||
gotifyId text [pk, not null]
|
||||
serverUrl text [not null]
|
||||
@@ -820,6 +842,8 @@ ref: github.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitea.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: git_provider.userId - user.id
|
||||
|
||||
ref: mariadb.projectId > project.projectId
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { account, organization, apikey } from "./account";
|
||||
import { account, apikey, organization } from "./account";
|
||||
import { projects } from "./project";
|
||||
import { certificateType } from "./shared";
|
||||
import { backups } from "./backups";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
@@ -124,6 +125,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
||||
organizations: many(organization),
|
||||
projects: many(projects),
|
||||
apiKeys: many(apikey),
|
||||
backups: many(backups),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(users_temp, {
|
||||
|
||||
@@ -28,6 +28,7 @@ export * from "./services/git-provider";
|
||||
export * from "./services/bitbucket";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/server";
|
||||
export * from "./services/application";
|
||||
export * from "./utils/databases/rebuild";
|
||||
@@ -47,6 +48,7 @@ export * from "./utils/backups/mongo";
|
||||
export * from "./utils/backups/mysql";
|
||||
export * from "./utils/backups/postgres";
|
||||
export * from "./utils/backups/utils";
|
||||
export * from "./utils/backups/web-server";
|
||||
export * from "./templates/processors";
|
||||
|
||||
export * from "./utils/notifications/build-error";
|
||||
@@ -90,6 +92,7 @@ export * from "./utils/providers/docker";
|
||||
export * from "./utils/providers/git";
|
||||
export * from "./utils/providers/github";
|
||||
export * from "./utils/providers/gitlab";
|
||||
export * from "./utils/providers/gitea";
|
||||
export * from "./utils/providers/raw";
|
||||
|
||||
export * from "./utils/servers/remote-docker";
|
||||
|
||||
@@ -2,16 +2,16 @@ import type { IncomingMessage } from "node:http";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { organization, twoFactor, apiKey } from "better-auth/plugins";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { apiKey, organization, twoFactor } from "better-auth/plugins";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { db } from "../db";
|
||||
import * as schema from "../db/schema";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
import { updateUser } from "../services/user";
|
||||
import { getUserByToken } from "../services/admin";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { updateUser } from "../services/user";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
|
||||
const { handler, api } = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { generateObject } from "ai";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findServerById } from "./server";
|
||||
import { findOrganizationById } from "./admin";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||
const aiSettings = await db.query.ai.findMany({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user