Skip to content

Commit c405ae7

Browse files
Schedule limits and timezone support (#1165)
* Added maximumScheduleInstancesLimit column to Org, default to 20 * Docs on the schedule limits and improved soft-limit communication * Added limit info to the schedules list page * Created a task that creates schedules, useful for testing * Make deduplicationKey required when creating/updating a schedule using the SDK * New schedule button shows an alert if you’re over the limit * Added timezone to the form and db * WIP on the timezone dropdown for the create/edit schedule form * Use the new filter search for timezones * Made the timezone dropdown faster by fixing the virtualization * The preview table is working and added a nice message about daylight savings * Created a page where you can view the full list of timezones The URL is included in the error message if you send an invalid time using the SDK * Creating tasks with the timezone * Added timezone support the the scheduler and the schedules list * Added timezone support to more of the schedules UI * The timezone comes through to scheduled runs with nice JSDocs * Allow setting the timezone from the SDK * Always have a timezone on a schedule * Updated jsdocs * Updated catalog example * Changed the column to be a string, not null. Added the timezone across the SDK * API endpoint for getting the timezones * Added an SDK function to get the list of timezones * Added timezones to the docs * Changeset: Added timezone support to schedules * Added support for testing timezone * Tidied up imports * Imports * Imports * Update limits.mdx * Fixed a couple type issues and use the already exported zodfetch --------- Co-authored-by: Eric Allam <[email protected]>
1 parent 3687fcb commit c405ae7

File tree

48 files changed

+1220
-106
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1220
-106
lines changed

.changeset/silver-doors-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Make deduplicationKey required when creating/updating a schedule

.changeset/tender-turkeys-compete.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Added timezone support to schedules

apps/webapp/app/components/primitives/DateTime.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ type DateTimeProps = {
66
timeZone?: string;
77
includeSeconds?: boolean;
88
includeTime?: boolean;
9+
showTimezone?: boolean;
910
};
1011

1112
export const DateTime = ({
1213
date,
1314
timeZone,
1415
includeSeconds = true,
1516
includeTime = true,
17+
showTimezone = false,
1618
}: DateTimeProps) => {
1719
const locales = useLocales();
1820

@@ -42,7 +44,12 @@ export const DateTime = ({
4244
);
4345
}, [locales, includeSeconds, realDate]);
4446

45-
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
47+
return (
48+
<Fragment>
49+
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
50+
{showTimezone ? ` (${timeZone ?? "UTC"})` : null}
51+
</Fragment>
52+
);
4653
};
4754

4855
export function formatDateTime(

apps/webapp/app/components/primitives/Select.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
88
import { cn } from "~/utils/cn";
99
import { ShortcutKey } from "./ShortcutKey";
1010
import { ChevronDown } from "lucide-react";
11+
import { MatchSorterOptions, matchSorter } from "match-sorter";
1112

1213
const sizes = {
1314
small: {
@@ -75,7 +76,10 @@ export interface SelectProps<TValue extends string | string[], TItem>
7576
showHeading?: boolean;
7677
items?: TItem[] | Section<TItem>[];
7778
empty?: React.ReactNode;
78-
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean;
79+
filter?:
80+
| boolean
81+
| MatchSorterOptions<TItem>
82+
| ((item: ItemFromSection<TItem>, search: string, title?: string) => boolean);
7983
children:
8084
| React.ReactNode
8185
| ((
@@ -129,18 +133,44 @@ export function Select<TValue extends string | string[], TItem>({
129133
if (!items) return [];
130134
if (!searchValue || !filter) return items;
131135

136+
if (typeof filter === "function") {
137+
if (isSection(items)) {
138+
return items
139+
.map((section) => ({
140+
...section,
141+
items: section.items.filter((item) =>
142+
filter(item as ItemFromSection<TItem>, searchValue, section.title)
143+
),
144+
}))
145+
.filter((section) => section.items.length > 0);
146+
}
147+
148+
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
149+
}
150+
151+
if (typeof filter === "boolean" && filter) {
152+
if (isSection(items)) {
153+
return items
154+
.map((section) => ({
155+
...section,
156+
items: matchSorter(section.items, searchValue),
157+
}))
158+
.filter((section) => section.items.length > 0);
159+
}
160+
161+
return matchSorter(items, searchValue);
162+
}
163+
132164
if (isSection(items)) {
133165
return items
134166
.map((section) => ({
135167
...section,
136-
items: section.items.filter((item) =>
137-
filter(item as ItemFromSection<TItem>, searchValue, section.title)
138-
),
168+
items: matchSorter(section.items, searchValue, filter),
139169
}))
140170
.filter((section) => section.items.length > 0);
141171
}
142172

143-
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
173+
return matchSorter(items, searchValue, filter);
144174
}, [searchValue, items]);
145175

146176
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useVirtualizer } from "@tanstack/react-virtual";
2+
import { useRef } from "react";
3+
import { SelectItem } from "../primitives/Select";
4+
5+
export function TimezoneList({ timezones }: { timezones: string[] }) {
6+
const parentRef = useRef<HTMLDivElement>(null);
7+
8+
const rowVirtualizer = useVirtualizer({
9+
count: timezones.length,
10+
getScrollElement: () => parentRef.current,
11+
estimateSize: () => 28,
12+
});
13+
14+
return (
15+
<div
16+
ref={parentRef}
17+
className="max-h-[calc(min(480px,var(--popover-available-height))-2.35rem)] overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
18+
>
19+
<div
20+
style={{
21+
height: `${rowVirtualizer.getTotalSize()}px`,
22+
width: "100%",
23+
position: "relative",
24+
}}
25+
>
26+
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
27+
<TimezoneCell
28+
key={virtualItem.key}
29+
size={virtualItem.size}
30+
start={virtualItem.start}
31+
timezone={timezones[virtualItem.index]}
32+
/>
33+
))}
34+
</div>
35+
</div>
36+
);
37+
}
38+
39+
function TimezoneCell({
40+
timezone,
41+
size,
42+
start,
43+
}: {
44+
timezone: string;
45+
size: number;
46+
start: number;
47+
}) {
48+
return (
49+
<SelectItem
50+
value={timezone}
51+
style={{
52+
position: "absolute",
53+
top: 0,
54+
left: 0,
55+
width: "100%",
56+
height: `${size}px`,
57+
transform: `translateY(${start}px)`,
58+
}}
59+
>
60+
{timezone}
61+
</SelectItem>
62+
);
63+
}

apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RuntimeEnvironmentType } from "@trigger.dev/database";
22
import { PrismaClient, prisma } from "~/db.server";
33
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
4+
import { getTimezones } from "~/utils/timezones.server";
45

56
type EditScheduleOptions = {
67
userId: string;
@@ -74,6 +75,7 @@ export class EditSchedulePresenter {
7475
return {
7576
possibleTasks: possibleTasks.map((task) => task.slug),
7677
possibleEnvironments,
78+
possibleTimezones: getTimezones(),
7779
schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments),
7880
};
7981
}
@@ -91,6 +93,7 @@ export class EditSchedulePresenter {
9193
externalId: true,
9294
deduplicationKey: true,
9395
userProvidedDeduplicationKey: true,
96+
timezone: true,
9497
taskIdentifier: true,
9598
instances: {
9699
select: {

apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type ScheduleListItem = {
2222
userProvidedDeduplicationKey: boolean;
2323
cron: string;
2424
cronDescription: string;
25+
timezone: string;
2526
externalId: string | null;
2627
nextRun: Date;
2728
lastRun: Date | undefined;
@@ -36,7 +37,6 @@ export type ScheduleList = Awaited<ReturnType<ScheduleListPresenter["call"]>>;
3637
export type ScheduleListAppliedFilters = ScheduleList["filters"];
3738

3839
export class ScheduleListPresenter extends BasePresenter {
39-
4040
public async call({
4141
userId,
4242
projectId,
@@ -71,12 +71,23 @@ export class ScheduleListPresenter extends BasePresenter {
7171
},
7272
},
7373
},
74+
organization: {
75+
select: {
76+
maximumSchedulesLimit: true,
77+
},
78+
},
7479
},
7580
where: {
7681
id: projectId,
7782
},
7883
});
7984

85+
const schedulesCount = await this._prisma.taskSchedule.count({
86+
where: {
87+
projectId,
88+
},
89+
});
90+
8091
//get all possible scheduled tasks
8192
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
8293
distinct: ["slug"],
@@ -140,6 +151,7 @@ export class ScheduleListPresenter extends BasePresenter {
140151
userProvidedDeduplicationKey: true,
141152
generatorExpression: true,
142153
generatorDescription: true,
154+
timezone: true,
143155
externalId: true,
144156
instances: {
145157
select: {
@@ -218,10 +230,11 @@ export class ScheduleListPresenter extends BasePresenter {
218230
userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
219231
cron: schedule.generatorExpression,
220232
cronDescription: schedule.generatorDescription,
233+
timezone: schedule.timezone,
221234
active: schedule.active,
222235
externalId: schedule.externalId,
223236
lastRun: latestRun?.createdAt,
224-
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression),
237+
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression, schedule.timezone),
225238
environments: schedule.instances.map((instance) => {
226239
const environment = project.environments.find((env) => env.id === instance.environmentId);
227240
if (!environment) {
@@ -245,6 +258,10 @@ export class ScheduleListPresenter extends BasePresenter {
245258
return displayableEnvironment(environment, userId);
246259
}),
247260
hasFilters,
261+
limits: {
262+
used: schedulesCount,
263+
limit: project.organization.maximumSchedulesLimit,
264+
},
248265
filters: {
249266
tasks,
250267
environments,

apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
TaskTriggerSource,
77
} from "@trigger.dev/database";
88
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
9+
import { getTimezones } from "~/utils/timezones.server";
910
import { getUsername } from "~/utils/username";
1011

1112
type TestTaskOptions = {
@@ -37,6 +38,7 @@ export type TestTask =
3738
| {
3839
triggerSource: "SCHEDULED";
3940
task: Task;
41+
possibleTimezones: string[];
4042
runs: ScheduledRun[];
4143
};
4244

@@ -61,6 +63,7 @@ export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
6163
timestamp: Date;
6264
lastTimestamp?: Date;
6365
externalId?: string;
66+
timezone: string;
6467
};
6568
};
6669

@@ -168,9 +171,11 @@ export class TestTaskPresenter {
168171
),
169172
};
170173
case "SCHEDULED":
174+
const possibleTimezones = getTimezones();
171175
return {
172176
triggerSource: "SCHEDULED",
173177
task: taskWithEnvironment,
178+
possibleTimezones,
174179
runs: (
175180
await Promise.all(
176181
latestRuns.map(async (r) => {
@@ -195,6 +200,9 @@ export class TestTaskPresenter {
195200

196201
async function getScheduleTaskRunPayload(run: RawRun) {
197202
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
203+
if (!payload.timezone) {
204+
payload.timezone = "UTC";
205+
}
198206
const parsed = ScheduledTaskPayload.safeParse(payload);
199207
return parsed;
200208
}

apps/webapp/app/presenters/v3/ViewSchedulePresenter.server.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { ScheduleObject } from "@trigger.dev/core/v3";
12
import { PrismaClient, prisma } from "~/db.server";
3+
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
24
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
35
import { RunListPresenter } from "./RunListPresenter.server";
4-
import { ScheduleObject } from "@trigger.dev/core/v3";
5-
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
66

77
type ViewScheduleOptions = {
88
userId?: string;
@@ -24,6 +24,7 @@ export class ViewSchedulePresenter {
2424
friendlyId: true,
2525
generatorExpression: true,
2626
generatorDescription: true,
27+
timezone: true,
2728
externalId: true,
2829
deduplicationKey: true,
2930
userProvidedDeduplicationKey: true,
@@ -68,7 +69,7 @@ export class ViewSchedulePresenter {
6869
}
6970

7071
const nextRuns = schedule.active
71-
? nextScheduledTimestamps(schedule.generatorExpression, new Date(), 5)
72+
? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
7273
: [];
7374

7475
const runPresenter = new RunListPresenter(this.#prismaClient);
@@ -82,6 +83,7 @@ export class ViewSchedulePresenter {
8283
return {
8384
schedule: {
8485
...schedule,
86+
timezone: schedule.timezone,
8587
cron: schedule.generatorExpression,
8688
cronDescription: schedule.generatorDescription,
8789
nextRuns,
@@ -105,6 +107,7 @@ export class ViewSchedulePresenter {
105107
expression: result.schedule.cron,
106108
description: result.schedule.cronDescription,
107109
},
110+
timezone: result.schedule.timezone,
108111
externalId: result.schedule.externalId ?? undefined,
109112
deduplicationKey: result.schedule.userProvidedDeduplicationKey
110113
? result.schedule.deduplicationKey ?? undefined

0 commit comments

Comments
 (0)