mirror of
https://github.com/LukeHagar/dokploy.git
synced 2025-12-06 04:19:37 +00:00
Merge branch 'canary' into feature/stop-grace-period-2227
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { type Control, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
@@ -57,6 +57,7 @@ export const commonCronExpressions = [
|
||||
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
|
||||
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
||||
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
||||
{ label: "Custom", value: "custom" },
|
||||
];
|
||||
|
||||
const formSchema = z
|
||||
@@ -115,10 +116,91 @@ interface Props {
|
||||
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||
}
|
||||
|
||||
export const ScheduleFormField = ({
|
||||
name,
|
||||
formControl,
|
||||
}: {
|
||||
name: string;
|
||||
formControl: Control<any>;
|
||||
}) => {
|
||||
const [selectedOption, setSelectedOption] = useState("");
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={formControl}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Cron expression format: minute hour day month weekday</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
value={selectedOption}
|
||||
onValueChange={(value) => {
|
||||
setSelectedOption(value);
|
||||
field.onChange(value === "custom" ? "" : value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label}
|
||||
{expr.value !== "custom" && ` (${expr.value})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const commonExpression = commonCronExpressions.find(
|
||||
(expression) => expression.value === value,
|
||||
);
|
||||
if (commonExpression) {
|
||||
setSelectedOption(commonExpression.value);
|
||||
} else {
|
||||
setSelectedOption("custom");
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||
|
||||
const utils = api.useUtils();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -377,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
<ScheduleFormField
|
||||
name="cronExpression"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
{(scheduleTypeForm === "application" ||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -47,7 +41,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import type { CacheType } from "../domains/handle-domain";
|
||||
import { commonCronExpressions } from "../schedules/handle-schedules";
|
||||
import { ScheduleFormField } from "../schedules/handle-schedules";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -306,64 +300,9 @@ export const HandleVolumeBackups = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
<ScheduleFormField
|
||||
name="cronExpression"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -35,6 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
const form = useForm<AddComposeFile>({
|
||||
defaultValues: {
|
||||
@@ -53,6 +54,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.composeFile !== undefined) {
|
||||
setHasUnsavedChanges(composeFile !== data.composeFile);
|
||||
}
|
||||
}, [composeFile, data?.composeFile]);
|
||||
|
||||
const onSubmit = async (data: AddComposeFile) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.composeFile);
|
||||
if (!valid) {
|
||||
@@ -71,6 +78,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose config Updated");
|
||||
setHasUnsavedChanges(false);
|
||||
refetch();
|
||||
await utils.compose.getConvertedCompose.invalidate({
|
||||
composeId,
|
||||
@@ -99,6 +107,19 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col gap-4 ">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Compose File</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure your Docker Compose file for this service.
|
||||
{hasUnsavedChanges && (
|
||||
<span className="text-yellow-500 ml-2">
|
||||
(You have unsaved changes)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-save-compose-file"
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
CheckIcon,
|
||||
ChevronsUpDown,
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusIcon,
|
||||
RefreshCw,
|
||||
@@ -62,7 +61,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { commonCronExpressions } from "../../application/schedules/handle-schedules";
|
||||
import { ScheduleFormField } from "../../application/schedules/handle-schedules";
|
||||
|
||||
type CacheType = "cache" | "fetch";
|
||||
|
||||
@@ -579,66 +578,9 @@ export const HandleBackup = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="schedule"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScheduleFormField name="schedule" formControl={form.control} />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="prefix"
|
||||
|
||||
@@ -96,8 +96,30 @@ export const ShowProjects = () => {
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
case "services": {
|
||||
const aTotalServices = a.environments.length;
|
||||
const bTotalServices = b.environments.length;
|
||||
const aTotalServices = a.environments.reduce((total, env) => {
|
||||
return (
|
||||
total +
|
||||
(env.applications?.length || 0) +
|
||||
(env.mariadb?.length || 0) +
|
||||
(env.mongo?.length || 0) +
|
||||
(env.mysql?.length || 0) +
|
||||
(env.postgres?.length || 0) +
|
||||
(env.redis?.length || 0) +
|
||||
(env.compose?.length || 0)
|
||||
);
|
||||
}, 0);
|
||||
const bTotalServices = b.environments.reduce((total, env) => {
|
||||
return (
|
||||
total +
|
||||
(env.applications?.length || 0) +
|
||||
(env.mariadb?.length || 0) +
|
||||
(env.mongo?.length || 0) +
|
||||
(env.mysql?.length || 0) +
|
||||
(env.postgres?.length || 0) +
|
||||
(env.redis?.length || 0) +
|
||||
(env.compose?.length || 0)
|
||||
);
|
||||
}, 0);
|
||||
comparison = aTotalServices - bTotalServices;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -76,6 +77,9 @@ export const WebDomain = () => {
|
||||
resolver: zodResolver(addServerDomain),
|
||||
});
|
||||
const https = form.watch("https");
|
||||
const domain = form.watch("domain") || "";
|
||||
const host = data?.user?.host || "";
|
||||
const hasChanged = domain !== host;
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
@@ -119,6 +123,19 @@ export const WebDomain = () => {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-6 border-t">
|
||||
{/* Warning for GitHub webhook URL changes */}
|
||||
{hasChanged && (
|
||||
<AlertBlock type="warning">
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">⚠️ Important: URL Change Impact</p>
|
||||
<p>
|
||||
If you change the Dokploy Server URL make sure to update
|
||||
your Github Apps to keep the auto-deploy working and preview
|
||||
deployments working.
|
||||
</p>
|
||||
</div>
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -7,7 +7,14 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
status: "running" | "error" | "done" | "idle" | undefined | null;
|
||||
status:
|
||||
| "running"
|
||||
| "error"
|
||||
| "done"
|
||||
| "idle"
|
||||
| "cancelled"
|
||||
| undefined
|
||||
| null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -34,6 +41,14 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
className={cn("size-3.5 rounded-full bg-green-500", className)}
|
||||
/>
|
||||
)}
|
||||
{status === "cancelled" && (
|
||||
<div
|
||||
className={cn(
|
||||
"size-3.5 rounded-full bg-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{status === "running" && (
|
||||
<div
|
||||
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
|
||||
@@ -46,6 +61,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
{status === "error" && "Error"}
|
||||
{status === "done" && "Done"}
|
||||
{status === "running" && "Running"}
|
||||
{status === "cancelled" && "Cancelled"}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
1
apps/dokploy/drizzle/0113_complete_rafael_vega.sql
Normal file
1
apps/dokploy/drizzle/0113_complete_rafael_vega.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."deploymentStatus" ADD VALUE 'cancelled';
|
||||
6572
apps/dokploy/drizzle/meta/0113_snapshot.json
Normal file
6572
apps/dokploy/drizzle/meta/0113_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -792,6 +792,13 @@
|
||||
"when": 1758483520214,
|
||||
"tag": "0112_freezing_skrulls",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 113,
|
||||
"version": "7",
|
||||
"when": 1758960816504,
|
||||
"tag": "0113_complete_rafael_vega",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.25.3",
|
||||
"version": "v0.25.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -226,6 +226,7 @@ const Service = (
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="preview-deployments">
|
||||
Preview Deployments
|
||||
</TabsTrigger>
|
||||
@@ -233,7 +234,6 @@ const Service = (
|
||||
<TabsTrigger value="volume-backups">
|
||||
Volume Backups
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
initializeNetwork,
|
||||
initSchedules,
|
||||
initVolumeBackupsCronJobs,
|
||||
initCancelDeployments,
|
||||
sendDokployRestartNotifications,
|
||||
setupDirectories,
|
||||
} from "@dokploy/server";
|
||||
@@ -52,6 +53,7 @@ void app.prepare().then(async () => {
|
||||
await migration();
|
||||
await initCronJobs();
|
||||
await initSchedules();
|
||||
await initCancelDeployments();
|
||||
await initVolumeBackupsCronJobs();
|
||||
await sendDokployRestartNotifications();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user