mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
feat(schedules): add timezone support for scheduled jobs and update database schema
- Introduced a new `commonTimezones` array for timezone selection in the UI. - Updated the schedule form to include an optional timezone field. - Modified the database schema to add a `timezone` column to the `schedule` table. - Enhanced the scheduling logic to utilize the specified timezone, defaulting to UTC if not provided.
This commit is contained in:
@@ -60,6 +60,30 @@ export const commonCronExpressions = [
|
||||
{ label: "Custom", value: "custom" },
|
||||
];
|
||||
|
||||
export const commonTimezones = [
|
||||
{ label: "UTC (Coordinated Universal Time)", value: "UTC" },
|
||||
{ label: "America/New_York (Eastern Time)", value: "America/New_York" },
|
||||
{ label: "America/Chicago (Central Time)", value: "America/Chicago" },
|
||||
{ label: "America/Denver (Mountain Time)", value: "America/Denver" },
|
||||
{ label: "America/Los_Angeles (Pacific Time)", value: "America/Los_Angeles" },
|
||||
{
|
||||
label: "America/Mexico_City (Central Mexico)",
|
||||
value: "America/Mexico_City",
|
||||
},
|
||||
{ label: "America/Sao_Paulo (Brasilia Time)", value: "America/Sao_Paulo" },
|
||||
{ label: "Europe/London (Greenwich Mean Time)", value: "Europe/London" },
|
||||
{ label: "Europe/Paris (Central European Time)", value: "Europe/Paris" },
|
||||
{ label: "Europe/Berlin (Central European Time)", value: "Europe/Berlin" },
|
||||
{ label: "Asia/Tokyo (Japan Standard Time)", value: "Asia/Tokyo" },
|
||||
{ label: "Asia/Shanghai (China Standard Time)", value: "Asia/Shanghai" },
|
||||
{ label: "Asia/Dubai (Gulf Standard Time)", value: "Asia/Dubai" },
|
||||
{ label: "Asia/Kolkata (India Standard Time)", value: "Asia/Kolkata" },
|
||||
{
|
||||
label: "Australia/Sydney (Australian Eastern Time)",
|
||||
value: "Australia/Sydney",
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
@@ -75,6 +99,7 @@ const formSchema = z
|
||||
"dokploy-server",
|
||||
]),
|
||||
script: z.string(),
|
||||
timezone: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.scheduleType === "compose" && !data.serviceName) {
|
||||
@@ -213,6 +238,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
serviceName: "",
|
||||
scheduleType: scheduleType || "application",
|
||||
script: "",
|
||||
timezone: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -251,6 +277,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
serviceName: schedule.serviceName || "",
|
||||
scheduleType: schedule.scheduleType,
|
||||
script: schedule.script || "",
|
||||
timezone: schedule.timezone || undefined,
|
||||
});
|
||||
}
|
||||
}, [form, schedule, scheduleId]);
|
||||
@@ -464,6 +491,54 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timezone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Timezone
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Select a timezone for the schedule. If not
|
||||
specified, UTC will be used.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="UTC (default)" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonTimezones.map((tz) => (
|
||||
<SelectItem key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Optional: Choose a timezone for the schedule execution time
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{(scheduleTypeForm === "application" ||
|
||||
scheduleTypeForm === "compose") && (
|
||||
<>
|
||||
|
||||
1
apps/dokploy/drizzle/0130_perpetual_screwball.sql
Normal file
1
apps/dokploy/drizzle/0130_perpetual_screwball.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "schedule" ADD COLUMN "timezone" text;
|
||||
6921
apps/dokploy/drizzle/meta/0130_snapshot.json
Normal file
6921
apps/dokploy/drizzle/meta/0130_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -911,6 +911,13 @@
|
||||
"when": 1765136384035,
|
||||
"tag": "0129_pale_roughhouse",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 130,
|
||||
"version": "7",
|
||||
"when": 1765167657813,
|
||||
"tag": "0130_perpetual_screwball",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export const scheduleRouter = createTRPCRouter({
|
||||
scheduleId: newSchedule.scheduleId,
|
||||
type: "schedule",
|
||||
cronSchedule: newSchedule.cronExpression,
|
||||
timezone: newSchedule.timezone,
|
||||
});
|
||||
} else {
|
||||
scheduleJob(newSchedule);
|
||||
@@ -49,6 +50,7 @@ export const scheduleRouter = createTRPCRouter({
|
||||
scheduleId: updatedSchedule.scheduleId,
|
||||
type: "schedule",
|
||||
cronSchedule: updatedSchedule.cronExpression,
|
||||
timezone: updatedSchedule.timezone,
|
||||
});
|
||||
} else {
|
||||
await removeJob({
|
||||
|
||||
@@ -19,6 +19,7 @@ type QueueJob =
|
||||
type: "schedule";
|
||||
cronSchedule: string;
|
||||
scheduleId: string;
|
||||
timezone?: string | null;
|
||||
}
|
||||
| {
|
||||
type: "volume-backup";
|
||||
|
||||
@@ -40,6 +40,7 @@ export const scheduleJob = (job: QueueJob) => {
|
||||
jobQueue.add(job.scheduleId, job, {
|
||||
repeat: {
|
||||
pattern: job.cronSchedule,
|
||||
tz: job.timezone || "UTC",
|
||||
},
|
||||
});
|
||||
} else if (job.type === "volume-backup") {
|
||||
|
||||
@@ -15,6 +15,7 @@ export const jobQueueSchema = z.discriminatedUnion("type", [
|
||||
cronSchedule: z.string(),
|
||||
type: z.literal("schedule"),
|
||||
scheduleId: z.string(),
|
||||
timezone: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
cronSchedule: z.string(),
|
||||
|
||||
@@ -49,6 +49,7 @@ export const schedules = pgTable("schedule", {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
timezone: text("timezone"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
@@ -14,11 +14,21 @@ import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const scheduleJob = (schedule: Schedule) => {
|
||||
const { cronExpression, scheduleId } = schedule;
|
||||
const { cronExpression, scheduleId, timezone } = schedule;
|
||||
|
||||
scheduleJobNode(scheduleId, cronExpression, async () => {
|
||||
await runCommand(scheduleId);
|
||||
});
|
||||
// Use timezone from schedule, default to UTC if not specified
|
||||
const tz = timezone || "UTC";
|
||||
|
||||
scheduleJobNode(
|
||||
scheduleId,
|
||||
{
|
||||
tz,
|
||||
rule: cronExpression,
|
||||
},
|
||||
async () => {
|
||||
await runCommand(scheduleId);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const removeScheduleJob = (scheduleId: string) => {
|
||||
|
||||
Reference in New Issue
Block a user