Skip to content

Declarative schedules #1226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4fa17b4
Added type (STATIC or DYNAMIC) to TaskSchedule. Defaults to dynamic
matt-aitken Jul 17, 2024
8db26ff
WIP with dev indexing of static schedules
matt-aitken Jul 17, 2024
deaae72
Added a code comment
matt-aitken Jul 17, 2024
54eccc6
First stab at deleting unused static schedules
matt-aitken Jul 17, 2024
7e9ec24
Dashboard changes for the static schedules
matt-aitken Jul 17, 2024
b87d4de
Generate the description. Upsert the instances when editing. Fix for …
matt-aitken Jul 17, 2024
c66c416
Don’t allow deleting of static schedules
matt-aitken Jul 17, 2024
5dafa8d
Don’t allow enabling/disabling of static schedules
matt-aitken Jul 17, 2024
cfbfe8c
Added filtering for schedule types
matt-aitken Jul 17, 2024
0fce2c8
Syncing of schedule for deployed tasks
matt-aitken Jul 17, 2024
8f93c3b
Static schedules are now created for each environment
matt-aitken Jul 17, 2024
ff6f994
Added a second static schedule for testing
matt-aitken Jul 17, 2024
a077d9e
Add the type to the schedule task run payload and the object you get …
matt-aitken Jul 18, 2024
329f09e
Changed static/dynamic to declarative/imperative
matt-aitken Jul 18, 2024
7ac4b83
Timezone example
matt-aitken Jul 18, 2024
260f7b5
Changeset
matt-aitken Jul 18, 2024
0c0f45e
Updated scheduled docs to include declarative
matt-aitken Jul 18, 2024
b63cb57
When you test a schedule it set the type to “IMPERATIVE”
matt-aitken Jul 18, 2024
1f3444e
Improved the tooltip
matt-aitken Jul 18, 2024
b52cd16
Fix for queue time continuing to rise when a run is canceled/expired etc
matt-aitken Jul 18, 2024
94b9047
Update the info panel on a selected declarative schedule
matt-aitken Jul 18, 2024
a780f8a
Check if there are no instances. This should never happen but log an …
matt-aitken Jul 18, 2024
7552e1c
Throw errors and push them through to the CLI dev command
matt-aitken Jul 18, 2024
2bc26c8
Fail deployments if creating the background tasks or schedules fails
matt-aitken Jul 18, 2024
5e9533e
Format the deployment error so it gets displayed
matt-aitken Jul 18, 2024
d74099e
Changed the maxed out schedules error message to remove bit about sup…
matt-aitken Jul 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/thirty-hotels-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---

Added declarative cron schedules
34 changes: 32 additions & 2 deletions apps/webapp/app/components/runs/v3/ScheduleFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { XMarkIcon } from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import { RuntimeEnvironment } from "@trigger.dev/database";
import { type RuntimeEnvironment } from "@trigger.dev/database";
import { useCallback } from "react";
import { z } from "zod";
import { Input } from "~/components/primitives/Input";
Expand All @@ -17,6 +17,7 @@ import {
SelectTrigger,
SelectValue,
} from "../../primitives/SimpleSelect";
import { ScheduleTypeCombo } from "./ScheduleType";

export const ScheduleListFilters = z.object({
page: z.coerce.number().default(1),
Expand All @@ -28,6 +29,7 @@ export const ScheduleListFilters = z.object({
.string()
.optional()
.transform((value) => (value ? value.split(",") : undefined)),
type: z.union([z.literal("declarative"), z.literal("imperative")]).optional(),
search: z.string().optional(),
});

Expand All @@ -48,7 +50,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
const navigate = useNavigate();
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const { environments, tasks, page, search } = ScheduleListFilters.parse(
const { environments, tasks, page, search, type } = ScheduleListFilters.parse(
Object.fromEntries(searchParams.entries())
);

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

const handleTypeChange = useCallback((value: string | typeof All) => {
handleFilterChange("type", value === "ALL" ? undefined : value);
}, []);

const handleSearchChange = useThrottle((value: string) => {
handleFilterChange("search", value.length === 0 ? undefined : value);
}, 300);
Expand All @@ -97,6 +103,30 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
defaultValue={search}
onChange={(e) => handleSearchChange(e.target.value)}
/>
<SelectGroup>
<Select name="type" value={type ?? "ALL"} onValueChange={handleTypeChange}>
<SelectTrigger size="minimal" width="full">
<SelectValue placeholder={"Select type"} className="ml-2 whitespace-nowrap p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph
variant="extra-small"
className="whitespace-nowrap pl-0.5 transition group-hover:text-text-bright"
>
All types
</Paragraph>
</SelectItem>
<SelectItem value={"declarative"}>
<ScheduleTypeCombo type="DECLARATIVE" className="text-xs text-text-dimmed" />
</SelectItem>
<SelectItem value={"imperative"}>
<ScheduleTypeCombo type="IMPERATIVE" className="text-xs text-text-dimmed" />
</SelectItem>
</SelectContent>
</Select>
</SelectGroup>

<SelectGroup>
<Select
name="environment"
Expand Down
29 changes: 29 additions & 0 deletions apps/webapp/app/components/runs/v3/ScheduleType.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ArchiveBoxIcon, ArrowsRightLeftIcon } from "@heroicons/react/20/solid";
import type { ScheduleType } from "@trigger.dev/database";
import { cn } from "~/utils/cn";

export function ScheduleTypeCombo({ type, className }: { type: ScheduleType; className?: string }) {
return (
<div className={cn("flex items-center space-x-1", className)}>
<ScheduleTypeIcon type={type} />
<span>{scheduleTypeName(type)}</span>
</div>
);
}

export function ScheduleTypeIcon({ type, className }: { type: ScheduleType; className?: string }) {
switch (type) {
case "IMPERATIVE":
return <ArrowsRightLeftIcon className={cn("size-4", className)} />;
case "DECLARATIVE":
return <ArchiveBoxIcon className={cn("size-4", className)} />;
}
}
export function scheduleTypeName(type: ScheduleType) {
switch (type) {
case "IMPERATIVE":
return "Imperative";
case "DECLARATIVE":
return "Declarative";
}
}
6 changes: 5 additions & 1 deletion apps/webapp/app/components/runs/v3/TaskRunsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,12 @@ export function TaskRunsTable({
formatDuration(new Date(run.createdAt), new Date(run.startedAt), {
style: "short",
})
) : (
) : run.isCancellable ? (
<LiveTimer startTime={new Date(run.createdAt)} />
) : (
formatDuration(new Date(run.createdAt), new Date(run.updatedAt), {
style: "short",
})
)}
</div>
</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class EditSchedulePresenter {
const schedule = await this.#prismaClient.taskSchedule.findFirst({
select: {
id: true,
type: true,
friendlyId: true,
generatorExpression: true,
externalId: true,
Expand Down
18 changes: 15 additions & 3 deletions apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Prisma, RuntimeEnvironmentType } from "@trigger.dev/database";
import { Prisma, RuntimeEnvironmentType, ScheduleType } from "@trigger.dev/database";
import { ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
import { sqlDatabaseSchema } from "~/db.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
Expand All @@ -16,6 +16,7 @@ const DEFAULT_PAGE_SIZE = 20;

export type ScheduleListItem = {
id: string;
type: ScheduleType;
friendlyId: string;
taskIdentifier: string;
deduplicationKey: string | null;
Expand Down Expand Up @@ -44,10 +45,17 @@ export class ScheduleListPresenter extends BasePresenter {
environments,
search,
page,
type,
pageSize = DEFAULT_PAGE_SIZE,
}: ScheduleListOptions) {
const hasFilters =
tasks !== undefined || environments !== undefined || (search !== undefined && search !== "");
type !== undefined ||
tasks !== undefined ||
environments !== undefined ||
(search !== undefined && search !== "");

const filterType =
type === "declarative" ? "DECLARATIVE" : type === "imperative" ? "IMPERATIVE" : undefined;

// Find the project scoped to the organization
const project = await this._replica.project.findFirstOrThrow({
Expand Down Expand Up @@ -105,6 +113,7 @@ export class ScheduleListPresenter extends BasePresenter {
environmentId: environments ? { in: environments } : undefined,
},
},
type: filterType,
AND: search
? {
OR: [
Expand Down Expand Up @@ -141,6 +150,7 @@ export class ScheduleListPresenter extends BasePresenter {
const rawSchedules = await this._replica.taskSchedule.findMany({
select: {
id: true,
type: true,
friendlyId: true,
taskIdentifier: true,
deduplicationKey: true,
Expand All @@ -166,6 +176,7 @@ export class ScheduleListPresenter extends BasePresenter {
},
}
: undefined,
type: filterType,
AND: search
? {
OR: [
Expand Down Expand Up @@ -215,11 +226,12 @@ export class ScheduleListPresenter extends BasePresenter {
ON t."scheduleId" = r."scheduleId" AND t."createdAt" = r."LatestRun";`
: [];

const schedules = rawSchedules.map((schedule) => {
const schedules: ScheduleListItem[] = rawSchedules.map((schedule) => {
const latestRun = latestRuns.find((r) => r.scheduleId === schedule.id);

return {
id: schedule.id,
type: schedule.type,
friendlyId: schedule.friendlyId,
taskIdentifier: schedule.taskIdentifier,
deduplicationKey: schedule.deduplicationKey,
Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/app/presenters/v3/ViewSchedulePresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class ViewSchedulePresenter {
const schedule = await this.#prismaClient.taskSchedule.findFirst({
select: {
id: true,
type: true,
friendlyId: true,
generatorExpression: true,
generatorDescription: true,
Expand Down Expand Up @@ -99,6 +100,7 @@ export class ViewSchedulePresenter {
public toJSONResponse(result: NonNullable<Awaited<ReturnType<ViewSchedulePresenter["call"]>>>) {
const response: ScheduleObject = {
id: result.schedule.friendlyId,
type: result.schedule.type,
task: result.schedule.taskIdentifier,
active: result.schedule.active,
nextRun: result.schedule.nextRuns[0],
Expand Down
Loading
Loading