Merge branch 'canary' into 379-preview-deployment

This commit is contained in:
Mauricio Siu
2024-12-07 21:28:34 -06:00
parent 841b264257
commit 5058d9b47d
57 changed files with 933 additions and 754 deletions

View File

@@ -241,7 +241,7 @@ export function generate(schema: Schema): Template {
- Use the same name of the folder as the id of the template.
- The logo should be in the public folder.
- If you want to show a domain in the UI, please add the prefix \_HOST at the end of the variable name.
- If you want to show a domain in the UI, please add the `_HOST` suffix at the end of the variable name.
- Test first on a vps or a server to make sure the template works.
## Docs & Website

View File

@@ -35,7 +35,6 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle

View File

@@ -44,7 +44,6 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle

View File

@@ -26,6 +26,7 @@ if (typeof window === "undefined") {
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@@ -6,6 +6,7 @@ import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@@ -1,3 +1,4 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -18,7 +19,6 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -150,7 +150,7 @@ export const AddVolumes = ({
<DialogTrigger className="" asChild>
<Button>{children}</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Volumes / Mounts</DialogTitle>
</DialogHeader>
@@ -303,9 +303,12 @@ export const AddVolumes = ({
<FormLabel>Content</FormLabel>
<FormControl>
<FormControl>
<Textarea
placeholder="Any content"
className="h-64"
<CodeEditor
language="properties"
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>

View File

@@ -1,8 +1,8 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
@@ -19,7 +19,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil } from "lucide-react";
@@ -119,7 +118,7 @@ export const UpdateVolume = ({
} else if (typeForm === "file") {
form.reset({
content: data.content || "",
mountPath: "/",
mountPath: serviceType === "compose" ? "/" : data.mountPath,
filePath: data.filePath || "",
type: "file",
});
@@ -182,7 +181,7 @@ export const UpdateVolume = ({
<Pencil className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Update</DialogTitle>
<DialogDescription>Update the mount</DialogDescription>
@@ -247,9 +246,12 @@ export const UpdateVolume = ({
<FormLabel>Content</FormLabel>
<FormControl>
<FormControl>
<Textarea
placeholder="Any content"
className="h-64"
<CodeEditor
language="properties"
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>

View File

@@ -41,6 +41,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}),
z.object({
buildType: z.literal("heroku_buildpacks"),
herokuVersion: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("paketo_buildpacks"),
@@ -90,6 +91,13 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
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,
@@ -110,6 +118,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === "dockerfile" ? data.dockerContextPath : null,
dockerBuildStage:
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
herokuVersion:
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
})
.then(async () => {
toast.success("Build type saved");
@@ -200,6 +210,28 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
);
}}
/>
{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>
);
}}
/>
)}
{buildType === "dockerfile" && (
<>
<FormField

View File

@@ -2,9 +2,9 @@ import { ShowBuildChooseForm } from "@/components/dashboard/application/build/sh
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Toggle } from "@/components/ui/toggle";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { CheckCircle2, Terminal } from "lucide-react";
import { Terminal } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -39,27 +39,6 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
appName={data?.appName || ""}
/>
<Toggle
aria-label="Toggle italic"
pressed={data?.autoDeploy || false}
onPressedChange={async (enabled) => {
await update({
applicationId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
>
Autodeploy
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
</Toggle>
<RedbuildApplication applicationId={applicationId} />
{data?.applicationStatus === "idle" ? (
<StartApplication applicationId={applicationId} />
@@ -75,6 +54,27 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
applicationId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
/>
</div>
</CardContent>
</Card>
<ShowProviderForm applicationId={applicationId} />

View File

@@ -8,12 +8,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Toggle } from "@/components/ui/toggle";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { StartCompose } from "../start-compose";
import { DeployCompose } from "./deploy-compose";
import { RedbuildCompose } from "./rebuild-compose";
import { StopCompose } from "./stop-compose";
@@ -50,28 +51,11 @@ export const ComposeActions = ({ composeId }: Props) => {
return (
<div className="flex flex-row gap-4 w-full flex-wrap ">
<DeployCompose composeId={composeId} />
<Toggle
aria-label="Toggle italic"
pressed={data?.autoDeploy || false}
onPressedChange={async (enabled) => {
await update({
composeId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
>
Autodeploy {data?.autoDeploy && <CheckCircle2 className="size-4" />}
</Toggle>
<RedbuildCompose composeId={composeId} />
{data?.composeType === "docker-compose" && (
{data?.composeType === "docker-compose" &&
data?.composeStatus === "idle" ? (
<StartCompose composeId={composeId} />
) : (
<StopCompose composeId={composeId} />
)}
@@ -84,6 +68,27 @@ export const ComposeActions = ({ composeId }: Props) => {
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
composeId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
/>
</div>
{domains.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@@ -0,0 +1,65 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const StartCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.start.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Start
<CheckCircle2 className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to start the compose?
</AlertDialogTitle>
<AlertDialogDescription>
This will start the compose
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose started succesfully");
})
.catch(() => {
toast.error("Error to start the Compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,65 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Ban } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const StopCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Stop
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure to stop the compose?
</AlertDialogTitle>
<AlertDialogDescription>
This will stop the compose
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose stopped succesfully");
})
.catch(() => {
toast.error("Error to stop the Compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -1,13 +1,16 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import dynamic from "next/dynamic";
import { useState } from "react";
const Terminal = dynamic(
() => import("./docker-terminal").then((e) => e.DockerTerminal),
@@ -27,8 +30,27 @@ export const DockerTerminalModal = ({
containerId,
serverId,
}: Props) => {
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleCancel = () => {
setConfirmDialogOpen(false);
};
return (
<Dialog>
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
@@ -50,6 +72,24 @@ export const DockerTerminalModal = ({
containerId={containerId}
serverId={serverId || ""}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?
</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);

View File

@@ -49,8 +49,11 @@ export const columns: ColumnDef<LogEntry>[] = [
const log = row.original;
return (
<div className=" flex flex-col gap-2">
<div className="flex flex-row gap-3 ">
<div className="flex items-center flex-row gap-3 ">
{log.RequestMethod}{" "}
<div className="inline-flex items-center gap-2 bg-muted p-1 rounded">
<span>{log.RequestAddr}</span>
</div>
{log.RequestPath.length > 100
? `${log.RequestPath.slice(0, 82)}...`
: log.RequestPath}

View File

@@ -27,6 +27,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Languages } from "@/lib/languages";
import useLocale from "@/utils/hooks/use-locale";
import { useTranslation } from "next-i18next";
import { useTheme } from "next-themes";
@@ -37,12 +38,9 @@ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark", "system"], {
required_error: "Please select a theme.",
}),
language: z.enum(
["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
{
required_error: "Please select a language.",
},
),
language: z.nativeEnum(Languages, {
required_error: "Please select a language.",
}),
});
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
@@ -50,7 +48,7 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: "system",
language: "en",
language: Languages.English,
};
export function AppearanceForm() {
@@ -175,24 +173,15 @@ export function AppearanceForm() {
<SelectValue placeholder="No preset selected" />
</SelectTrigger>
<SelectContent>
{[
{ label: "English", value: "en" },
{ label: "Polski", value: "pl" },
{ label: "Русский", value: "ru" },
{ label: "Français", value: "fr" },
{ label: "Deutsch", value: "de" },
{ label: "繁體中文", value: "zh-Hant" },
{ label: "简体中文", value: "zh-Hans" },
{ label: "Türkçe", value: "tr" },
{
label: "Persian",
value: "fa",
},
].map((preset) => (
<SelectItem key={preset.label} value={preset.value}>
{preset.label}
</SelectItem>
))}
{Object.keys(Languages).map((preset) => {
const value =
Languages[preset as keyof typeof Languages];
return (
<SelectItem key={value} value={value}>
{preset}
</SelectItem>
);
})}
</SelectContent>
</Select>
</FormItem>

View File

@@ -667,7 +667,7 @@ export const AddNotification = () => {
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
Trigger the action when dokploy is restarted.
</FormDescription>
</div>
<FormControl>

View File

@@ -1,7 +1,9 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -49,14 +51,34 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
},
);
const [containerId, setContainerId] = useState<string | undefined>();
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleCancel = () => {
setConfirmDialogOpen(false);
};
useEffect(() => {
if (data && data?.length > 0) {
setContainerId(data[0]?.containerId);
}
}, [data]);
return (
<Dialog>
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
<DialogHeader>
@@ -96,6 +118,24 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
id="terminal"
containerId={containerId || "select-a-container"}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?
</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);

View File

@@ -1,3 +1,5 @@
"use client";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
@@ -9,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
@@ -17,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>

View File

@@ -1,60 +0,0 @@
ALTER TYPE "domainType" ADD VALUE 'preview';--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "preview_deployments" (
"previewDeploymentId" text PRIMARY KEY NOT NULL,
"branch" text NOT NULL,
"pullRequestId" text NOT NULL,
"pullRequestNumber" text NOT NULL,
"pullRequestURL" text NOT NULL,
"pullRequestTitle" text NOT NULL,
"pullRequestCommentId" text NOT NULL,
"appName" text NOT NULL,
"applicationId" text NOT NULL,
"domainId" text,
"deploymentId" text,
"createdAt" text NOT NULL,
"expiresAt" text,
CONSTRAINT "preview_deployments_appName_unique" UNIQUE("appName")
);
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewEnv" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewBuildArgs" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewWildcard" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewPort" integer DEFAULT 3000;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewHttps" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "certificateType" "certificateType" DEFAULT 'none' NOT NULL;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewLimit" integer DEFAULT 3;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "isPreviewDeploymentsActive" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "isPreviewDeployment" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "isPreviewDeployment" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_domainId_domain_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domain"("domainId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "domain" ADD CONSTRAINT "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "herokuVersion" text;

View File

@@ -1 +0,0 @@
ALTER TABLE "preview_deployments" ADD COLUMN "previewStatus" "applicationStatus" DEFAULT 'idle' NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ALTER COLUMN "herokuVersion" SET DEFAULT '24';

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "herokuVersion" text DEFAULT '24';

View File

@@ -1,3 +0,0 @@
ALTER TABLE "preview_deployments" DROP CONSTRAINT "preview_deployments_deploymentId_deployment_deploymentId_fk";
--> statement-breakpoint
ALTER TABLE "preview_deployments" DROP COLUMN IF EXISTS "deploymentId";

View File

@@ -1,5 +1,5 @@
{
"id": "053ad983-6299-4420-a551-645490689733",
"id": "16ee50a9-7cbc-46a4-af70-c3c85ed45466",
"prevId": "d70bcec5-e7af-4872-b2eb-f0a22ae2e3e8",
"version": "6",
"dialect": "postgresql",
@@ -38,67 +38,6 @@
"primaryKey": false,
"notNull": false
},
"previewEnv": {
"name": "previewEnv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"previewBuildArgs": {
"name": "previewBuildArgs",
"type": "text",
"primaryKey": false,
"notNull": false
},
"previewWildcard": {
"name": "previewWildcard",
"type": "text",
"primaryKey": false,
"notNull": false
},
"previewPort": {
"name": "previewPort",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 3000
},
"previewHttps": {
"name": "previewHttps",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"previewPath": {
"name": "previewPath",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'/'"
},
"certificateType": {
"name": "certificateType",
"type": "certificateType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'none'"
},
"previewLimit": {
"name": "previewLimit",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 3
},
"isPreviewDeploymentsActive": {
"name": "isPreviewDeploymentsActive",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"buildArgs": {
"name": "buildArgs",
"type": "text",
@@ -403,6 +342,12 @@
"notNull": true,
"default": "'nixpacks'"
},
"herokuVersion": {
"name": "herokuVersion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"publishDirectory": {
"name": "publishDirectory",
"type": "text",
@@ -1192,13 +1137,6 @@
"notNull": false,
"default": "'application'"
},
"isPreviewDeployment": {
"name": "isPreviewDeployment",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"uniqueConfigKey": {
"name": "uniqueConfigKey",
"type": "serial",
@@ -1223,12 +1161,6 @@
"primaryKey": false,
"notNull": false
},
"previewDeploymentId": {
"name": "previewDeploymentId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"certificateType": {
"name": "certificateType",
"type": "certificateType",
@@ -1265,19 +1197,6 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
"name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
"tableFrom": "domain",
"tableTo": "preview_deployments",
"columnsFrom": [
"previewDeploymentId"
],
"columnsTo": [
"previewDeploymentId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -2047,19 +1966,6 @@
"primaryKey": false,
"notNull": false
},
"isPreviewDeployment": {
"name": "isPreviewDeployment",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"previewDeploymentId": {
"name": "previewDeploymentId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
@@ -2107,19 +2013,6 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
"name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
"tableFrom": "deployment",
"tableTo": "preview_deployments",
"columnsFrom": [
"previewDeploymentId"
],
"columnsTo": [
"previewDeploymentId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -3927,142 +3820,6 @@
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.preview_deployments": {
"name": "preview_deployments",
"schema": "",
"columns": {
"previewDeploymentId": {
"name": "previewDeploymentId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"branch": {
"name": "branch",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestId": {
"name": "pullRequestId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestNumber": {
"name": "pullRequestNumber",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestURL": {
"name": "pullRequestURL",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestTitle": {
"name": "pullRequestTitle",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestCommentId": {
"name": "pullRequestCommentId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"appName": {
"name": "appName",
"type": "text",
"primaryKey": false,
"notNull": true
},
"applicationId": {
"name": "applicationId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"domainId": {
"name": "domainId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"deploymentId": {
"name": "deploymentId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"preview_deployments_applicationId_application_applicationId_fk": {
"name": "preview_deployments_applicationId_application_applicationId_fk",
"tableFrom": "preview_deployments",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"preview_deployments_domainId_domain_domainId_fk": {
"name": "preview_deployments_domainId_domain_domainId_fk",
"tableFrom": "preview_deployments",
"tableTo": "domain",
"columnsFrom": [
"domainId"
],
"columnsTo": [
"domainId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"preview_deployments_deploymentId_deployment_deploymentId_fk": {
"name": "preview_deployments_deploymentId_deployment_deploymentId_fk",
"tableFrom": "preview_deployments",
"tableTo": "deployment",
"columnsFrom": [
"deploymentId"
],
"columnsTo": [
"deploymentId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"preview_deployments_appName_unique": {
"name": "preview_deployments_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
}
}
}
},
"enums": {
@@ -4102,8 +3859,7 @@
"schema": "public",
"values": [
"compose",
"application",
"preview"
"application"
]
},
"public.databaseType": {

View File

@@ -1,6 +1,6 @@
{
"id": "20236ed8-104c-487b-bcdf-d08b69fbc80a",
"prevId": "053ad983-6299-4420-a551-645490689733",
"id": "928417c8-2e7b-43ba-bc19-44b4d70107f1",
"prevId": "16ee50a9-7cbc-46a4-af70-c3c85ed45466",
"version": "6",
"dialect": "postgresql",
"tables": {
@@ -38,67 +38,6 @@
"primaryKey": false,
"notNull": false
},
"previewEnv": {
"name": "previewEnv",
"type": "text",
"primaryKey": false,
"notNull": false
},
"previewBuildArgs": {
"name": "previewBuildArgs",
"type": "text",
"primaryKey": false,
"notNull": false
},
"previewWildcard": {
"name": "previewWildcard",
"type": "text",
"primaryKey": false,
"notNull": false
},
"previewPort": {
"name": "previewPort",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 3000
},
"previewHttps": {
"name": "previewHttps",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"previewPath": {
"name": "previewPath",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'/'"
},
"certificateType": {
"name": "certificateType",
"type": "certificateType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'none'"
},
"previewLimit": {
"name": "previewLimit",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 3
},
"isPreviewDeploymentsActive": {
"name": "isPreviewDeploymentsActive",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"buildArgs": {
"name": "buildArgs",
"type": "text",
@@ -403,6 +342,13 @@
"notNull": true,
"default": "'nixpacks'"
},
"herokuVersion": {
"name": "herokuVersion",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'24'"
},
"publishDirectory": {
"name": "publishDirectory",
"type": "text",
@@ -1192,13 +1138,6 @@
"notNull": false,
"default": "'application'"
},
"isPreviewDeployment": {
"name": "isPreviewDeployment",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"uniqueConfigKey": {
"name": "uniqueConfigKey",
"type": "serial",
@@ -1223,12 +1162,6 @@
"primaryKey": false,
"notNull": false
},
"previewDeploymentId": {
"name": "previewDeploymentId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"certificateType": {
"name": "certificateType",
"type": "certificateType",
@@ -1265,19 +1198,6 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
"name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
"tableFrom": "domain",
"tableTo": "preview_deployments",
"columnsFrom": [
"previewDeploymentId"
],
"columnsTo": [
"previewDeploymentId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -2047,19 +1967,6 @@
"primaryKey": false,
"notNull": false
},
"isPreviewDeployment": {
"name": "isPreviewDeployment",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"previewDeploymentId": {
"name": "previewDeploymentId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
@@ -2107,19 +2014,6 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
"name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
"tableFrom": "deployment",
"tableTo": "preview_deployments",
"columnsFrom": [
"previewDeploymentId"
],
"columnsTo": [
"previewDeploymentId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -3927,150 +3821,6 @@
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.preview_deployments": {
"name": "preview_deployments",
"schema": "",
"columns": {
"previewDeploymentId": {
"name": "previewDeploymentId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"branch": {
"name": "branch",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestId": {
"name": "pullRequestId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestNumber": {
"name": "pullRequestNumber",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestURL": {
"name": "pullRequestURL",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestTitle": {
"name": "pullRequestTitle",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pullRequestCommentId": {
"name": "pullRequestCommentId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"previewStatus": {
"name": "previewStatus",
"type": "applicationStatus",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'idle'"
},
"appName": {
"name": "appName",
"type": "text",
"primaryKey": false,
"notNull": true
},
"applicationId": {
"name": "applicationId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"domainId": {
"name": "domainId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"deploymentId": {
"name": "deploymentId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"preview_deployments_applicationId_application_applicationId_fk": {
"name": "preview_deployments_applicationId_application_applicationId_fk",
"tableFrom": "preview_deployments",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"preview_deployments_domainId_domain_domainId_fk": {
"name": "preview_deployments_domainId_domain_domainId_fk",
"tableFrom": "preview_deployments",
"tableTo": "domain",
"columnsFrom": [
"domainId"
],
"columnsTo": [
"domainId"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"preview_deployments_deploymentId_deployment_deploymentId_fk": {
"name": "preview_deployments_deploymentId_deployment_deploymentId_fk",
"tableFrom": "preview_deployments",
"tableTo": "deployment",
"columnsFrom": [
"deploymentId"
],
"columnsTo": [
"deploymentId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"preview_deployments_appName_unique": {
"name": "preview_deployments_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
}
}
}
},
"enums": {
@@ -4110,8 +3860,7 @@
"schema": "public",
"values": [
"compose",
"application",
"preview"
"application"
]
},
"public.databaseType": {

View File

@@ -1,6 +1,6 @@
{
"id": "71016fed-2c39-4d31-aa33-0e0aceb313ef",
"prevId": "20236ed8-104c-487b-bcdf-d08b69fbc80a",
"id": "82e1398d-0f5d-4290-9cb2-fd812f096b28",
"prevId": "71016fed-2c39-4d31-aa33-0e0aceb313ef",
"version": "6",
"dialect": "postgresql",
"tables": {
@@ -403,6 +403,13 @@
"notNull": true,
"default": "'nixpacks'"
},
"herokuVersion": {
"name": "herokuVersion",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'24'"
},
"publishDirectory": {
"name": "publishDirectory",
"type": "text",

View File

@@ -334,22 +334,22 @@
{
"idx": 47,
"version": "6",
"when": 1733089956329,
"tag": "0047_red_stephen_strange",
"when": 1733599090582,
"tag": "0047_tidy_revanche",
"breakpoints": true
},
{
"idx": 48,
"version": "6",
"when": 1733091544421,
"tag": "0048_clumsy_matthew_murdock",
"when": 1733599163710,
"tag": "0048_flat_expediter",
"breakpoints": true
},
{
"idx": 49,
"version": "6",
"when": 1733091820570,
"tag": "0049_useful_mole_man",
"when": 1733628466143,
"tag": "0049_overrated_sage",
"breakpoints": true
}
]

View File

@@ -0,0 +1,16 @@
export enum Languages {
English = "en",
Polish = "pl",
Russian = "ru",
French = "fr",
German = "de",
ChineseTraditional = "zh-Hant",
ChineseSimplified = "zh-Hans",
Turkish = "tr",
Kazakh = "kz",
Persian = "fa",
Korean = "ko",
Portuguese = "pt-br",
}
export type Language = keyof typeof Languages;

View File

@@ -1,10 +1,23 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "pl", "ru", "fr", "de", "tr", "zh-Hant", "zh-Hans", "fa"],
localeDetection: false,
},
fallbackLng: "en",
keySeparator: false,
i18n: {
defaultLocale: "en",
locales: [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"kz",
"zh-Hant",
"zh-Hans",
"fa",
"ko",
"pt-br",
],
localeDetection: false,
},
};

View File

@@ -1,6 +1,7 @@
import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { Languages } from "@/lib/languages";
import { api } from "@/utils/api";
import type { NextPage } from "next";
import { appWithTranslation } from "next-i18next";
@@ -71,17 +72,7 @@ export default api.withTRPC(
{
i18n: {
defaultLocale: "en",
locales: [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"zh-Hant",
"zh-Hans",
"fa",
],
locales: Object.values(Languages),
localeDetection: false,
},
fallbackLng: "en",

View File

@@ -77,6 +77,7 @@ export default async function handler(
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name;
const apps = await db.query.applications.findMany({
where: and(
@@ -84,6 +85,7 @@ export default async function handler(
eq(applications.autoDeploy, true),
eq(applications.branch, branchName),
eq(applications.repository, repository),
eq(applications.owner, owner),
),
});
@@ -118,6 +120,7 @@ export default async function handler(
eq(compose.autoDeploy, true),
eq(compose.branch, branchName),
eq(compose.repository, repository),
eq(compose.owner, owner),
),
});
@@ -160,6 +163,8 @@ export default async function handler(
}
} else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id;
console.log(githubBody);
if (githubBody?.action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "저장",
"settings.server.domain.title": "서버 도메인",
"settings.server.domain.description": "서버 애플리케이션에 도메인을 추가합니다.",
"settings.server.domain.form.domain": "도메인",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 이메일",
"settings.server.domain.form.certificate.label": "인증서",
"settings.server.domain.form.certificate.placeholder": "인증서 선택",
"settings.server.domain.form.certificateOptions.none": "없음",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (기본)",
"settings.server.webServer.title": "웹 서버",
"settings.server.webServer.description": "웹 서버를 재시작하거나 정리합니다.",
"settings.server.webServer.actions": "작업",
"settings.server.webServer.reload": "재시작",
"settings.server.webServer.watchLogs": "로그 보기",
"settings.server.webServer.updateServerIp": "서버 IP 갱신",
"settings.server.webServer.server.label": "서버",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "환경 변수 수정",
"settings.server.webServer.storage.label": "저장 공간",
"settings.server.webServer.storage.cleanUnusedImages": "사용하지 않는 이미지 정리",
"settings.server.webServer.storage.cleanUnusedVolumes": "사용하지 않는 볼륨 정리",
"settings.server.webServer.storage.cleanStoppedContainers": "정지된 컨테이너 정리",
"settings.server.webServer.storage.cleanDockerBuilder": "도커 빌더 & 시스템 정리",
"settings.server.webServer.storage.cleanMonitoring": "모니터링 데이터 정리",
"settings.server.webServer.storage.cleanAll": "전체 정리",
"settings.profile.title": "계정",
"settings.profile.description": "여기에서 프로필 세부 정보를 변경하세요.",
"settings.profile.email": "이메일",
"settings.profile.password": "비밀번호",
"settings.profile.avatar": "아바타",
"settings.appearance.title": "외관",
"settings.appearance.description": "대시보드의 테마를 사용자 설정합니다.",
"settings.appearance.theme": "테마",
"settings.appearance.themeDescription": "대시보드 테마 선택",
"settings.appearance.themes.light": "라이트",
"settings.appearance.themes.dark": "다크",
"settings.appearance.themes.system": "시스템",
"settings.appearance.language": "언어",
"settings.appearance.languageDescription": "대시보드에서 사용할 언어 선택"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,41 @@
{
"settings.common.save": "Сақтау",
"settings.server.domain.title": "Сервер домені",
"settings.server.domain.description": "Dokploy сервер қолданбасына домен енгізіңіз.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Эл. поштасы",
"settings.server.domain.form.certificate.label": "Сертификат",
"settings.server.domain.form.certificate.placeholder": "Сертификатты таңдаңыз",
"settings.server.domain.form.certificateOptions.none": "Жоқ",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Стандартты)",
"settings.server.webServer.title": "Веб-Сервер",
"settings.server.webServer.description": "Веб-серверді қайта жүктеу немесе тазалау.",
"settings.server.webServer.actions": "Әрекеттер",
"settings.server.webServer.reload": "Қайта жүктеу",
"settings.server.webServer.watchLogs": "Журналдарды қарау",
"settings.server.webServer.updateServerIp": "Сервердің IP жаңарту",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Env Өзгерту",
"settings.server.webServer.storage.label": "Диск кеңістігі",
"settings.server.webServer.storage.cleanUnusedImages": "Пайдаланылмаған образды тазалау",
"settings.server.webServer.storage.cleanUnusedVolumes": "Пайдаланылмаған томды тазалау",
"settings.server.webServer.storage.cleanStoppedContainers": "Тоқтатылған контейнерлерді тазалау",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & Системаны тазалау",
"settings.server.webServer.storage.cleanMonitoring": "Мониторингті тазалау",
"settings.server.webServer.storage.cleanAll": "Барлығын тазалау",
"settings.profile.title": "Аккаунт",
"settings.profile.description": "Профиль мәліметтерін осы жерден өзгертіңіз.",
"settings.profile.email": "Эл. пошта",
"settings.profile.password": "Құпия сөз",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Сыртқы түрі",
"settings.appearance.description": "Dokploy сыртқы келбетін өзгерту.",
"settings.appearance.theme": "Келбеті",
"settings.appearance.themeDescription": "Жүйе тақтасының келбетің таңдаңыз",
"settings.appearance.themes.light": "Жарық",
"settings.appearance.themes.dark": "Қараңғы",
"settings.appearance.themes.system": "Жүйелік",
"settings.appearance.language": "Тіл",
"settings.appearance.languageDescription": "Жүйе тақтасының тілің таңдаңыз"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "Salvar",
"settings.server.domain.title": "Domínio do Servidor",
"settings.server.domain.description": "Configure o domínio do servidor",
"settings.server.domain.form.domain": "Domínio",
"settings.server.domain.form.letsEncryptEmail": "Email do Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certificado",
"settings.server.domain.form.certificate.placeholder": "Selecione um Certificado",
"settings.server.domain.form.certificateOptions.none": "Nenhum",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Padrão)",
"settings.server.webServer.title": "Servidor web",
"settings.server.webServer.description": "Limpar e recarregar servidor web.",
"settings.server.webServer.actions": "Ações",
"settings.server.webServer.reload": "Recarregar",
"settings.server.webServer.watchLogs": "Ver logs",
"settings.server.webServer.updateServerIp": "Atualizar IP do Servidor",
"settings.server.webServer.server.label": "Servidor",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Alterar Env",
"settings.server.webServer.storage.label": "Armazenamento",
"settings.server.webServer.storage.cleanUnusedImages": "Limpar imagens não utilizadas",
"settings.server.webServer.storage.cleanUnusedVolumes": "Limpar volumes não utilizados",
"settings.server.webServer.storage.cleanStoppedContainers": "Limpar containers parados",
"settings.server.webServer.storage.cleanDockerBuilder": "Limpar Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Limpar Monitoramento",
"settings.server.webServer.storage.cleanAll": "Limpar Tudo",
"settings.profile.title": "Conta",
"settings.profile.description": "Altere os detalhes do seu perfil aqui.",
"settings.profile.email": "Email",
"settings.profile.password": "Senha",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Aparencia",
"settings.appearance.description": "Personalize o tema do seu dashboard.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Selecione um tema para o dashboard",
"settings.appearance.themes.light": "Claro",
"settings.appearance.themes.dark": "Escuro",
"settings.appearance.themes.system": "Automático",
"settings.appearance.language": "Linguagem",
"settings.appearance.languageDescription": "Selecione o idioma do dashboard"
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -296,6 +296,7 @@ export const applicationRouter = createTRPCRouter({
publishDirectory: input.publishDirectory,
dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion,
});
return true;

View File

@@ -48,6 +48,7 @@ import {
removeCompose,
removeComposeDirectory,
removeDeploymentsByComposeId,
startCompose,
stopCompose,
updateCompose,
} from "@dokploy/server";
@@ -309,6 +310,20 @@ export const composeRouter = createTRPCRouter({
}
await stopCompose(input.composeId);
return true;
}),
start: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
});
}
await startCompose(input.composeId);
return true;
}),
getDefaultCommand: protectedProcedure

View File

@@ -188,8 +188,9 @@ export const notificationRouter = createTRPCRouter({
.mutation(async ({ input }) => {
try {
await sendDiscordNotification(input, {
title: "Test Notification",
description: "Hi, From Dokploy 👋",
title: "> `🤚` - Test Notification",
description: "> Hi, From Dokploy 👋",
color: 0xf3f7f4,
});
return true;
} catch (error) {

View File

@@ -1,7 +1,7 @@
version: "3.8"
services:
pocketbase:
image: spectado/pocketbase:0.22.12
image: spectado/pocketbase:0.23.3
restart: unless-stopped
volumes:
- /etc/dokploy/templates/${HASH}/data:/pb_data

View File

@@ -380,7 +380,7 @@ export const templates: TemplateData[] = [
{
id: "umami",
name: "Umami",
version: "v2.12.1",
version: "v2.14.0",
description:
"Umami is a simple, fast, privacy-focused alternative to Google Analytics.",
logo: "umami.png",
@@ -972,4 +972,19 @@ export const templates: TemplateData[] = [
tags: ["event"],
load: () => import("./ontime/index").then((m) => m.generate),
},
{
id: "triggerdotdev",
name: "Trigger.dev",
version: "v3",
description:
"Trigger is a platform for building event-driven applications.",
logo: "triggerdotdev.svg",
links: {
github: "https://github.com/triggerdotdev/trigger.dev",
website: "https://trigger.dev/",
docs: "https://trigger.dev/docs",
},
tags: ["event-driven", "applications"],
load: () => import("./triggerdotdev/index").then((m) => m.generate),
},
];

View File

@@ -0,0 +1,107 @@
x-webapp-env: &webapp-env
LOGIN_ORIGIN: &trigger-url ${TRIGGER_PROTOCOL:-http}://${TRIGGER_DOMAIN:-localhost:3040}
APP_ORIGIN: *trigger-url
DEV_OTEL_EXPORTER_OTLP_ENDPOINT: &trigger-otel ${TRIGGER_PROTOCOL:-http}://${TRIGGER_DOMAIN:-localhost:3040}/otel
ELECTRIC_ORIGIN: http://electric:3000
x-worker-env: &worker-env
PLATFORM_HOST: webapp
PLATFORM_WS_PORT: 3030
SECURE_CONNECTION: "false"
OTEL_EXPORTER_OTLP_ENDPOINT: *trigger-otel
volumes:
postgres-data:
redis-data:
networks:
webapp:
services:
webapp:
image: ghcr.io/triggerdotdev/trigger.dev:${TRIGGER_IMAGE_TAG:-v3}
restart: ${RESTART_POLICY:-unless-stopped}
env_file:
- .env
environment:
<<: *webapp-env
ports:
- 3000
depends_on:
- postgres
- redis
networks:
- webapp
postgres:
image: postgres:${POSTGRES_IMAGE_TAG:-16}
restart: ${RESTART_POLICY:-unless-stopped}
volumes:
- postgres-data:/var/lib/postgresql/data/
env_file:
- .env
networks:
- webapp
ports:
- 5432
command:
- -c
- wal_level=logical
redis:
image: redis:${REDIS_IMAGE_TAG:-7}
restart: ${RESTART_POLICY:-unless-stopped}
volumes:
- redis-data:/data
networks:
- webapp
ports:
- 6379
docker-provider:
image: ghcr.io/triggerdotdev/provider/docker:${TRIGGER_IMAGE_TAG:-v3}
restart: ${RESTART_POLICY:-unless-stopped}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
user: root
networks:
- webapp
depends_on:
- webapp
ports:
- 9020
env_file:
- .env
environment:
<<: *worker-env
PLATFORM_SECRET: $PROVIDER_SECRET
coordinator:
image: ghcr.io/triggerdotdev/coordinator:${TRIGGER_IMAGE_TAG:-v3}
restart: ${RESTART_POLICY:-unless-stopped}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
user: root
networks:
- webapp
depends_on:
- webapp
ports:
- 9020
env_file:
- .env
environment:
<<: *worker-env
PLATFORM_SECRET: $COORDINATOR_SECRET
electric:
image: electricsql/electric:${ELECTRIC_IMAGE_TAG:-latest}
restart: ${RESTART_POLICY:-unless-stopped}
environment:
DATABASE_URL: ${DATABASE_URL}?sslmode=disable
networks:
- webapp
depends_on:
- postgres
ports:
- 3000

View File

@@ -0,0 +1,93 @@
import { Secrets } from "@/components/ui/secrets";
import {
type DomainSchema,
type Schema,
type Template,
generateBase64,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const triggerDomain = generateRandomDomain(schema);
const magicLinkSecret = generateBase64(16);
const sessionSecret = generateBase64(16);
const encryptionKey = generateBase64(32);
const providerSecret = generateBase64(32);
const coordinatorSecret = generateBase64(32);
const dbPassword = generateBase64(24);
const dbUser = "triggeruser";
const dbName = "triggerdb";
const domains: DomainSchema[] = [
{
host: triggerDomain,
port: 3000,
serviceName: "webapp",
},
];
const envs = [
"NODE_ENV=production",
"RUNTIME_PLATFORM=docker-compose",
"V3_ENABLED=true",
"# Domain configuration",
`TRIGGER_DOMAIN=${triggerDomain}`,
"TRIGGER_PROTOCOL=http",
"# Database configuration with secure credentials",
`POSTGRES_USER=${dbUser}`,
`POSTGRES_PASSWORD=${dbPassword}`,
`POSTGRES_DB=${dbName}`,
`DATABASE_URL=postgresql://${dbUser}:${dbPassword}@postgres:5432/${dbName}`,
"# Secrets",
`MAGIC_LINK_SECRET=${magicLinkSecret}`,
`SESSION_SECRET=${sessionSecret}`,
`ENCRYPTION_KEY=${encryptionKey}`,
`PROVIDER_SECRET=${providerSecret}`,
`COORDINATOR_SECRET=${coordinatorSecret}`,
"# TRIGGER_TELEMETRY_DISABLED=1",
"INTERNAL_OTEL_TRACE_DISABLED=1",
"INTERNAL_OTEL_TRACE_LOGGING_ENABLED=0",
"DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT=300",
"DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT=100",
"DIRECT_URL=${DATABASE_URL}",
"REDIS_HOST=redis",
"REDIS_PORT=6379",
"REDIS_TLS_DISABLED=true",
"# If this is set, emails that are not specified won't be able to log in",
'# WHITELISTED_EMAILS="authorized@yahoo.com|authorized@gmail.com"',
"# Accounts with these emails will become admins when signing up and get access to the admin panel",
'# ADMIN_EMAILS="admin@example.com|another-admin@example.com"',
"# If this is set, your users will be able to log in via GitHub",
"# AUTH_GITHUB_CLIENT_ID=",
"# AUTH_GITHUB_CLIENT_SECRET=",
"# E-mail settings",
"# Ensure the FROM_EMAIL matches what you setup with Resend.com",
"# If these are not set, emails will be printed to the console",
"# FROM_EMAIL=",
"# REPLY_TO_EMAIL=",
"# RESEND_API_KEY=",
"# Worker settings",
"HTTP_SERVER_PORT=9020",
"COORDINATOR_HOST=127.0.0.1",
"COORDINATOR_PORT=${HTTP_SERVER_PORT}",
"# REGISTRY_HOST=${DEPLOY_REGISTRY_HOST}",
"# REGISTRY_NAMESPACE=${DEPLOY_REGISTRY_NAMESPACE}",
];
return {
envs,
domains,
};
}

View File

@@ -1,6 +1,6 @@
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-v2.13.2
image: ghcr.io/umami-software/umami:postgresql-v2.14.0
restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]

View File

@@ -1,23 +1,10 @@
import type { Languages } from "@/lib/languages";
import Cookies from "js-cookie";
const SUPPORTED_LOCALES = [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"zh-Hant",
"zh-Hans",
"fa",
] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number];
export default function useLocale() {
const currentLocale = (Cookies.get("DOKPLOY_LOCALE") ?? "en") as Locale;
const currentLocale = (Cookies.get("DOKPLOY_LOCALE") ?? "en") as Languages;
const setLocale = (locale: Locale) => {
const setLocale = (locale: Languages) => {
Cookies.set("DOKPLOY_LOCALE", locale, { expires: 365 });
window.location.reload();
};

View File

@@ -5,11 +5,19 @@ export function getLocale(cookies: NextApiRequestCookies) {
return locale;
}
// libs/i18n.js
import { Languages } from "@/lib/languages";
import { serverSideTranslations as originalServerSideTranslations } from "next-i18next/serverSideTranslations";
import nextI18NextConfig from "../next-i18next.config.cjs";
export const serverSideTranslations = (
locale: string,
namespaces = ["common"],
) => originalServerSideTranslations(locale, namespaces, nextI18NextConfig);
) =>
originalServerSideTranslations(locale, namespaces, {
fallbackLng: "en",
keySeparator: false,
i18n: {
defaultLocale: "en",
locales: Object.values(Languages),
localeDetection: false,
},
});

View File

@@ -192,6 +192,7 @@ export const applications = pgTable("application", {
.notNull()
.default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
createdAt: text("createdAt")
.notNull()
@@ -384,6 +385,7 @@ const createSchema = createInsertSchema(applications, {
"nixpacks",
"static",
]),
herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(),
owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
@@ -432,6 +434,7 @@ export const apiSaveBuildType = createSchema
dockerfile: true,
dockerContextPath: true,
dockerBuildStage: true,
herokuVersion: true,
})
.required()
.merge(createSchema.pick({ publishDirectory: true }));

View File

@@ -468,6 +468,36 @@ export const removeCompose = async (compose: Compose) => {
return true;
};
export const startCompose = async (composeId: string) => {
const compose = await findComposeById(composeId);
try {
const { COMPOSE_PATH } = paths(!!compose.serverId);
if (compose.composeType === "docker-compose") {
if (compose.serverId) {
await execAsyncRemote(
compose.serverId,
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
);
} else {
await execAsync(`docker compose -p ${compose.appName} up -d`, {
cwd: join(COMPOSE_PATH, compose.appName, "code"),
});
}
}
await updateCompose(composeId, {
composeStatus: "done",
});
} catch (error) {
await updateCompose(composeId, {
composeStatus: "idle",
});
throw error;
}
return true;
};
export const stopCompose = async (composeId: string) => {
const compose = await findComposeById(composeId);
try {

View File

@@ -22,7 +22,7 @@ export const buildHeroku = async (
"--path",
buildAppDirectory,
"--builder",
"heroku/builder:24",
`heroku/builder:${application.herokuVersion || "24"}`,
];
for (const env of envVariables) {
@@ -58,7 +58,7 @@ export const getHerokuCommand = (
"--path",
buildAppDirectory,
"--builder",
"heroku/builder:24",
`heroku/builder:${application.herokuVersion || "24"}`,
];
for (const env of envVariables) {

View File

@@ -59,31 +59,46 @@ export const sendBuildErrorNotifications = async ({
if (discord) {
await sendDiscordNotification(discord, {
title: "⚠️ Build Failed",
color: 0xff0000,
title: "> `⚠️` - Build Failed",
color: 0xed4245,
fields: [
{
name: "Project",
name: "`🛠️`・Project",
value: projectName,
inline: true,
},
{
name: "Application",
name: "`⚙️`・Application",
value: applicationName,
inline: true,
},
{
name: "Type",
name: "`❔`・Type",
value: applicationType,
inline: true,
},
{
name: "Error",
value: errorMessage,
name: "`📅`・Date",
value: date.toLocaleDateString(),
inline: true,
},
{
name: "Build Link",
value: buildLink,
name: "`⌚`・Time",
value: date.toLocaleTimeString(),
inline: true,
},
{
name: "`❓`・Type",
value: "Failed",
inline: true,
},
{
name: "`⚠️`・Error Message",
value: `\`\`\`${errorMessage}\`\`\``,
},
{
name: "`🧷`・Build Link",
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),

View File

@@ -57,27 +57,42 @@ export const sendBuildSuccessNotifications = async ({
if (discord) {
await sendDiscordNotification(discord, {
title: " Build Success",
color: 0x00ff00,
title: "> `✅` - Build Success",
color: 0x57f287,
fields: [
{
name: "Project",
name: "`🛠️`・Project",
value: projectName,
inline: true,
},
{
name: "Application",
name: "`⚙️`・Application",
value: applicationName,
inline: true,
},
{
name: "Type",
name: "`❔`・Application Type",
value: applicationType,
inline: true,
},
{
name: "Build Link",
value: buildLink,
name: "`📅`・Date",
value: date.toLocaleDateString(),
inline: true,
},
{
name: "`⌚`・Time",
value: date.toLocaleTimeString(),
inline: true,
},
{
name: "`❓`・Type",
value: "Successful",
inline: true,
},
{
name: "`🧷`・Build Link",
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),

View File

@@ -64,39 +64,47 @@ export const sendDatabaseBackupNotifications = async ({
await sendDiscordNotification(discord, {
title:
type === "success"
? " Database Backup Successful"
: " Database Backup Failed",
color: type === "success" ? 0x00ff00 : 0xff0000,
? "> `✅` - Database Backup Successful"
: "> `❌` - Database Backup Failed",
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: "Project",
name: "`🛠️`・Project",
value: projectName,
inline: true,
},
{
name: "Application",
name: "`⚙️`・Application",
value: applicationName,
inline: true,
},
{
name: "Type",
name: "`❔`・Database",
value: databaseType,
inline: true,
},
{
name: "Time",
value: date.toLocaleString(),
name: "`📅`・Date",
value: date.toLocaleDateString(),
inline: true,
},
{
name: "Type",
value: type,
name: "`⌚`・Time",
value: date.toLocaleTimeString(),
inline: true,
},
{
name: "`❓`・Type",
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
inline: true,
},
...(type === "error" && errorMessage
? [
{
name: "Error Message",
value: errorMessage,
name: "`⚠️`・Error Message",
value: `\`\`\`${errorMessage}\`\`\``,
},
]
: []),

View File

@@ -45,12 +45,27 @@ export const sendDockerCleanupNotifications = async (
if (discord) {
await sendDiscordNotification(discord, {
title: " Docker Cleanup",
color: 0x00ff00,
title: "> `✅` - Docker Cleanup",
color: 0x57f287,
fields: [
{
name: "Message",
value: message,
name: "`📅`・Date",
value: date.toLocaleDateString(),
inline: true,
},
{
name: "`⌚`・Time",
value: date.toLocaleTimeString(),
inline: true,
},
{
name: "`❓`・Type",
value: "Successful",
inline: true,
},
{
name: "`📜`・Message",
value: `\`\`\`${message}\`\`\``,
},
],
timestamp: date.toISOString(),

View File

@@ -34,12 +34,22 @@ export const sendDokployRestartNotifications = async () => {
if (discord) {
await sendDiscordNotification(discord, {
title: " Dokploy Server Restarted",
color: 0x00ff00,
title: "> `✅` - Dokploy Server Restarted",
color: 0x57f287,
fields: [
{
name: "Time",
value: date.toLocaleString(),
name: "`📅`・Date",
value: date.toLocaleDateString(),
inline: true,
},
{
name: "`⌚`・Time",
value: date.toLocaleTimeString(),
inline: true,
},
{
name: "`❓`・Type",
value: "Successful",
inline: true,
},
],