Skip to content

Commit 8ba9987

Browse files
authored
Declarative schedules (#1226)
* Added type (STATIC or DYNAMIC) to TaskSchedule. Defaults to dynamic * WIP with dev indexing of static schedules * Added a code comment * First stab at deleting unused static schedules * Dashboard changes for the static schedules * Generate the description. Upsert the instances when editing. Fix for the friendlyId * Don’t allow deleting of static schedules * Don’t allow enabling/disabling of static schedules * Added filtering for schedule types * Syncing of schedule for deployed tasks * Static schedules are now created for each environment * Added a second static schedule for testing * Add the type to the schedule task run payload and the object you get back from the SDK * Changed static/dynamic to declarative/imperative * Timezone example * Changeset * Updated scheduled docs to include declarative * When you test a schedule it set the type to “IMPERATIVE” * Improved the tooltip * Fix for queue time continuing to rise when a run is canceled/expired etc * Update the info panel on a selected declarative schedule * Check if there are no instances. This should never happen but log an error if it does * Throw errors and push them through to the CLI dev command * Fail deployments if creating the background tasks or schedules fails * Format the deployment error so it gets displayed * Changed the maxed out schedules error message to remove bit about support
1 parent 7056ce5 commit 8ba9987

File tree

30 files changed

+789
-188
lines changed

30 files changed

+789
-188
lines changed

.changeset/thirty-hotels-raise.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 declarative cron schedules

apps/webapp/app/components/runs/v3/ScheduleFilters.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { XMarkIcon } from "@heroicons/react/20/solid";
22
import { useNavigate } from "@remix-run/react";
3-
import { RuntimeEnvironment } from "@trigger.dev/database";
3+
import { type RuntimeEnvironment } from "@trigger.dev/database";
44
import { useCallback } from "react";
55
import { z } from "zod";
66
import { Input } from "~/components/primitives/Input";
@@ -17,6 +17,7 @@ import {
1717
SelectTrigger,
1818
SelectValue,
1919
} from "../../primitives/SimpleSelect";
20+
import { ScheduleTypeCombo } from "./ScheduleType";
2021

2122
export const ScheduleListFilters = z.object({
2223
page: z.coerce.number().default(1),
@@ -28,6 +29,7 @@ export const ScheduleListFilters = z.object({
2829
.string()
2930
.optional()
3031
.transform((value) => (value ? value.split(",") : undefined)),
32+
type: z.union([z.literal("declarative"), z.literal("imperative")]).optional(),
3133
search: z.string().optional(),
3234
});
3335

@@ -48,7 +50,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
4850
const navigate = useNavigate();
4951
const location = useOptimisticLocation();
5052
const searchParams = new URLSearchParams(location.search);
51-
const { environments, tasks, page, search } = ScheduleListFilters.parse(
53+
const { environments, tasks, page, search, type } = ScheduleListFilters.parse(
5254
Object.fromEntries(searchParams.entries())
5355
);
5456

@@ -73,6 +75,10 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
7375
handleFilterChange("environments", value === "ALL" ? undefined : value);
7476
}, []);
7577

78+
const handleTypeChange = useCallback((value: string | typeof All) => {
79+
handleFilterChange("type", value === "ALL" ? undefined : value);
80+
}, []);
81+
7682
const handleSearchChange = useThrottle((value: string) => {
7783
handleFilterChange("search", value.length === 0 ? undefined : value);
7884
}, 300);
@@ -97,6 +103,30 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
97103
defaultValue={search}
98104
onChange={(e) => handleSearchChange(e.target.value)}
99105
/>
106+
<SelectGroup>
107+
<Select name="type" value={type ?? "ALL"} onValueChange={handleTypeChange}>
108+
<SelectTrigger size="minimal" width="full">
109+
<SelectValue placeholder={"Select type"} className="ml-2 whitespace-nowrap p-0" />
110+
</SelectTrigger>
111+
<SelectContent>
112+
<SelectItem value={"ALL"}>
113+
<Paragraph
114+
variant="extra-small"
115+
className="whitespace-nowrap pl-0.5 transition group-hover:text-text-bright"
116+
>
117+
All types
118+
</Paragraph>
119+
</SelectItem>
120+
<SelectItem value={"declarative"}>
121+
<ScheduleTypeCombo type="DECLARATIVE" className="text-xs text-text-dimmed" />
122+
</SelectItem>
123+
<SelectItem value={"imperative"}>
124+
<ScheduleTypeCombo type="IMPERATIVE" className="text-xs text-text-dimmed" />
125+
</SelectItem>
126+
</SelectContent>
127+
</Select>
128+
</SelectGroup>
129+
100130
<SelectGroup>
101131
<Select
102132
name="environment"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ArchiveBoxIcon, ArrowsRightLeftIcon } from "@heroicons/react/20/solid";
2+
import type { ScheduleType } from "@trigger.dev/database";
3+
import { cn } from "~/utils/cn";
4+
5+
export function ScheduleTypeCombo({ type, className }: { type: ScheduleType; className?: string }) {
6+
return (
7+
<div className={cn("flex items-center space-x-1", className)}>
8+
<ScheduleTypeIcon type={type} />
9+
<span>{scheduleTypeName(type)}</span>
10+
</div>
11+
);
12+
}
13+
14+
export function ScheduleTypeIcon({ type, className }: { type: ScheduleType; className?: string }) {
15+
switch (type) {
16+
case "IMPERATIVE":
17+
return <ArrowsRightLeftIcon className={cn("size-4", className)} />;
18+
case "DECLARATIVE":
19+
return <ArchiveBoxIcon className={cn("size-4", className)} />;
20+
}
21+
}
22+
export function scheduleTypeName(type: ScheduleType) {
23+
switch (type) {
24+
case "IMPERATIVE":
25+
return "Imperative";
26+
case "DECLARATIVE":
27+
return "Declarative";
28+
}
29+
}

apps/webapp/app/components/runs/v3/TaskRunsTable.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,12 @@ export function TaskRunsTable({
230230
formatDuration(new Date(run.createdAt), new Date(run.startedAt), {
231231
style: "short",
232232
})
233-
) : (
233+
) : run.isCancellable ? (
234234
<LiveTimer startTime={new Date(run.createdAt)} />
235+
) : (
236+
formatDuration(new Date(run.createdAt), new Date(run.updatedAt), {
237+
style: "short",
238+
})
235239
)}
236240
</div>
237241
</TableCell>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class EditSchedulePresenter {
8888
const schedule = await this.#prismaClient.taskSchedule.findFirst({
8989
select: {
9090
id: true,
91+
type: true,
9192
friendlyId: true,
9293
generatorExpression: true,
9394
externalId: true,

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Prisma, RuntimeEnvironmentType } from "@trigger.dev/database";
1+
import { Prisma, RuntimeEnvironmentType, ScheduleType } from "@trigger.dev/database";
22
import { ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
33
import { sqlDatabaseSchema } from "~/db.server";
44
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
@@ -16,6 +16,7 @@ const DEFAULT_PAGE_SIZE = 20;
1616

1717
export type ScheduleListItem = {
1818
id: string;
19+
type: ScheduleType;
1920
friendlyId: string;
2021
taskIdentifier: string;
2122
deduplicationKey: string | null;
@@ -44,10 +45,17 @@ export class ScheduleListPresenter extends BasePresenter {
4445
environments,
4546
search,
4647
page,
48+
type,
4749
pageSize = DEFAULT_PAGE_SIZE,
4850
}: ScheduleListOptions) {
4951
const hasFilters =
50-
tasks !== undefined || environments !== undefined || (search !== undefined && search !== "");
52+
type !== undefined ||
53+
tasks !== undefined ||
54+
environments !== undefined ||
55+
(search !== undefined && search !== "");
56+
57+
const filterType =
58+
type === "declarative" ? "DECLARATIVE" : type === "imperative" ? "IMPERATIVE" : undefined;
5159

5260
// Find the project scoped to the organization
5361
const project = await this._replica.project.findFirstOrThrow({
@@ -105,6 +113,7 @@ export class ScheduleListPresenter extends BasePresenter {
105113
environmentId: environments ? { in: environments } : undefined,
106114
},
107115
},
116+
type: filterType,
108117
AND: search
109118
? {
110119
OR: [
@@ -141,6 +150,7 @@ export class ScheduleListPresenter extends BasePresenter {
141150
const rawSchedules = await this._replica.taskSchedule.findMany({
142151
select: {
143152
id: true,
153+
type: true,
144154
friendlyId: true,
145155
taskIdentifier: true,
146156
deduplicationKey: true,
@@ -166,6 +176,7 @@ export class ScheduleListPresenter extends BasePresenter {
166176
},
167177
}
168178
: undefined,
179+
type: filterType,
169180
AND: search
170181
? {
171182
OR: [
@@ -215,11 +226,12 @@ export class ScheduleListPresenter extends BasePresenter {
215226
ON t."scheduleId" = r."scheduleId" AND t."createdAt" = r."LatestRun";`
216227
: [];
217228

218-
const schedules = rawSchedules.map((schedule) => {
229+
const schedules: ScheduleListItem[] = rawSchedules.map((schedule) => {
219230
const latestRun = latestRuns.find((r) => r.scheduleId === schedule.id);
220231

221232
return {
222233
id: schedule.id,
234+
type: schedule.type,
223235
friendlyId: schedule.friendlyId,
224236
taskIdentifier: schedule.taskIdentifier,
225237
deduplicationKey: schedule.deduplicationKey,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class ViewSchedulePresenter {
2121
const schedule = await this.#prismaClient.taskSchedule.findFirst({
2222
select: {
2323
id: true,
24+
type: true,
2425
friendlyId: true,
2526
generatorExpression: true,
2627
generatorDescription: true,
@@ -99,6 +100,7 @@ export class ViewSchedulePresenter {
99100
public toJSONResponse(result: NonNullable<Awaited<ReturnType<ViewSchedulePresenter["call"]>>>) {
100101
const response: ScheduleObject = {
101102
id: result.schedule.friendlyId,
103+
type: result.schedule.type,
102104
task: result.schedule.taskIdentifier,
103105
active: result.schedule.active,
104106
nextRun: result.schedule.nextRuns[0],

0 commit comments

Comments
 (0)