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 <eallam@icloud.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@trigger.dev/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Make deduplicationKey required when creating/updating a schedule
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@trigger.dev/sdk": patch
|
||||||
|
"@trigger.dev/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added timezone support to schedules
|
||||||
@@ -6,6 +6,7 @@ type DateTimeProps = {
|
|||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
includeSeconds?: boolean;
|
includeSeconds?: boolean;
|
||||||
includeTime?: boolean;
|
includeTime?: boolean;
|
||||||
|
showTimezone?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateTime = ({
|
export const DateTime = ({
|
||||||
@@ -13,6 +14,7 @@ export const DateTime = ({
|
|||||||
timeZone,
|
timeZone,
|
||||||
includeSeconds = true,
|
includeSeconds = true,
|
||||||
includeTime = true,
|
includeTime = true,
|
||||||
|
showTimezone = false,
|
||||||
}: DateTimeProps) => {
|
}: DateTimeProps) => {
|
||||||
const locales = useLocales();
|
const locales = useLocales();
|
||||||
|
|
||||||
@@ -42,7 +44,12 @@ export const DateTime = ({
|
|||||||
);
|
);
|
||||||
}, [locales, includeSeconds, realDate]);
|
}, [locales, includeSeconds, realDate]);
|
||||||
|
|
||||||
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
|
||||||
|
{showTimezone ? ` (${timeZone ?? "UTC"})` : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatDateTime(
|
export function formatDateTime(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
|||||||
import { cn } from "~/utils/cn";
|
import { cn } from "~/utils/cn";
|
||||||
import { ShortcutKey } from "./ShortcutKey";
|
import { ShortcutKey } from "./ShortcutKey";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import { MatchSorterOptions, matchSorter } from "match-sorter";
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
small: {
|
small: {
|
||||||
@@ -75,7 +76,10 @@ export interface SelectProps<TValue extends string | string[], TItem>
|
|||||||
showHeading?: boolean;
|
showHeading?: boolean;
|
||||||
items?: TItem[] | Section<TItem>[];
|
items?: TItem[] | Section<TItem>[];
|
||||||
empty?: React.ReactNode;
|
empty?: React.ReactNode;
|
||||||
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean;
|
filter?:
|
||||||
|
| boolean
|
||||||
|
| MatchSorterOptions<TItem>
|
||||||
|
| ((item: ItemFromSection<TItem>, search: string, title?: string) => boolean);
|
||||||
children:
|
children:
|
||||||
| React.ReactNode
|
| React.ReactNode
|
||||||
| ((
|
| ((
|
||||||
@@ -129,18 +133,44 @@ export function Select<TValue extends string | string[], TItem>({
|
|||||||
if (!items) return [];
|
if (!items) return [];
|
||||||
if (!searchValue || !filter) return items;
|
if (!searchValue || !filter) return items;
|
||||||
|
|
||||||
|
if (typeof filter === "function") {
|
||||||
|
if (isSection(items)) {
|
||||||
|
return items
|
||||||
|
.map((section) => ({
|
||||||
|
...section,
|
||||||
|
items: section.items.filter((item) =>
|
||||||
|
filter(item as ItemFromSection<TItem>, searchValue, section.title)
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter((section) => section.items.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof filter === "boolean" && filter) {
|
||||||
|
if (isSection(items)) {
|
||||||
|
return items
|
||||||
|
.map((section) => ({
|
||||||
|
...section,
|
||||||
|
items: matchSorter(section.items, searchValue),
|
||||||
|
}))
|
||||||
|
.filter((section) => section.items.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchSorter(items, searchValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (isSection(items)) {
|
if (isSection(items)) {
|
||||||
return items
|
return items
|
||||||
.map((section) => ({
|
.map((section) => ({
|
||||||
...section,
|
...section,
|
||||||
items: section.items.filter((item) =>
|
items: matchSorter(section.items, searchValue, filter),
|
||||||
filter(item as ItemFromSection<TItem>, searchValue, section.title)
|
|
||||||
),
|
|
||||||
}))
|
}))
|
||||||
.filter((section) => section.items.length > 0);
|
.filter((section) => section.items.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
|
return matchSorter(items, searchValue, filter);
|
||||||
}, [searchValue, items]);
|
}, [searchValue, items]);
|
||||||
|
|
||||||
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
|
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { SelectItem } from "../primitives/Select";
|
||||||
|
|
||||||
|
export function TimezoneList({ timezones }: { timezones: string[] }) {
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: timezones.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 28,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={parentRef}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||||
|
width: "100%",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
|
||||||
|
<TimezoneCell
|
||||||
|
key={virtualItem.key}
|
||||||
|
size={virtualItem.size}
|
||||||
|
start={virtualItem.start}
|
||||||
|
timezone={timezones[virtualItem.index]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimezoneCell({
|
||||||
|
timezone,
|
||||||
|
size,
|
||||||
|
start,
|
||||||
|
}: {
|
||||||
|
timezone: string;
|
||||||
|
size: number;
|
||||||
|
start: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={timezone}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: `${size}px`,
|
||||||
|
transform: `translateY(${start}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timezone}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||||
import { PrismaClient, prisma } from "~/db.server";
|
import { PrismaClient, prisma } from "~/db.server";
|
||||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||||
|
import { getTimezones } from "~/utils/timezones.server";
|
||||||
|
|
||||||
type EditScheduleOptions = {
|
type EditScheduleOptions = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -74,6 +75,7 @@ export class EditSchedulePresenter {
|
|||||||
return {
|
return {
|
||||||
possibleTasks: possibleTasks.map((task) => task.slug),
|
possibleTasks: possibleTasks.map((task) => task.slug),
|
||||||
possibleEnvironments,
|
possibleEnvironments,
|
||||||
|
possibleTimezones: getTimezones(),
|
||||||
schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments),
|
schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,7 @@ export class EditSchedulePresenter {
|
|||||||
externalId: true,
|
externalId: true,
|
||||||
deduplicationKey: true,
|
deduplicationKey: true,
|
||||||
userProvidedDeduplicationKey: true,
|
userProvidedDeduplicationKey: true,
|
||||||
|
timezone: true,
|
||||||
taskIdentifier: true,
|
taskIdentifier: true,
|
||||||
instances: {
|
instances: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type ScheduleListItem = {
|
|||||||
userProvidedDeduplicationKey: boolean;
|
userProvidedDeduplicationKey: boolean;
|
||||||
cron: string;
|
cron: string;
|
||||||
cronDescription: string;
|
cronDescription: string;
|
||||||
|
timezone: string;
|
||||||
externalId: string | null;
|
externalId: string | null;
|
||||||
nextRun: Date;
|
nextRun: Date;
|
||||||
lastRun: Date | undefined;
|
lastRun: Date | undefined;
|
||||||
@@ -36,7 +37,6 @@ export type ScheduleList = Awaited<ReturnType<ScheduleListPresenter["call"]>>;
|
|||||||
export type ScheduleListAppliedFilters = ScheduleList["filters"];
|
export type ScheduleListAppliedFilters = ScheduleList["filters"];
|
||||||
|
|
||||||
export class ScheduleListPresenter extends BasePresenter {
|
export class ScheduleListPresenter extends BasePresenter {
|
||||||
|
|
||||||
public async call({
|
public async call({
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -71,12 +71,23 @@ export class ScheduleListPresenter extends BasePresenter {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
organization: {
|
||||||
|
select: {
|
||||||
|
maximumSchedulesLimit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
id: projectId,
|
id: projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const schedulesCount = await this._prisma.taskSchedule.count({
|
||||||
|
where: {
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
//get all possible scheduled tasks
|
//get all possible scheduled tasks
|
||||||
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
|
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
|
||||||
distinct: ["slug"],
|
distinct: ["slug"],
|
||||||
@@ -140,6 +151,7 @@ export class ScheduleListPresenter extends BasePresenter {
|
|||||||
userProvidedDeduplicationKey: true,
|
userProvidedDeduplicationKey: true,
|
||||||
generatorExpression: true,
|
generatorExpression: true,
|
||||||
generatorDescription: true,
|
generatorDescription: true,
|
||||||
|
timezone: true,
|
||||||
externalId: true,
|
externalId: true,
|
||||||
instances: {
|
instances: {
|
||||||
select: {
|
select: {
|
||||||
@@ -218,10 +230,11 @@ export class ScheduleListPresenter extends BasePresenter {
|
|||||||
userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
|
userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
|
||||||
cron: schedule.generatorExpression,
|
cron: schedule.generatorExpression,
|
||||||
cronDescription: schedule.generatorDescription,
|
cronDescription: schedule.generatorDescription,
|
||||||
|
timezone: schedule.timezone,
|
||||||
active: schedule.active,
|
active: schedule.active,
|
||||||
externalId: schedule.externalId,
|
externalId: schedule.externalId,
|
||||||
lastRun: latestRun?.createdAt,
|
lastRun: latestRun?.createdAt,
|
||||||
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression),
|
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression, schedule.timezone),
|
||||||
environments: schedule.instances.map((instance) => {
|
environments: schedule.instances.map((instance) => {
|
||||||
const environment = project.environments.find((env) => env.id === instance.environmentId);
|
const environment = project.environments.find((env) => env.id === instance.environmentId);
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
@@ -245,6 +258,10 @@ export class ScheduleListPresenter extends BasePresenter {
|
|||||||
return displayableEnvironment(environment, userId);
|
return displayableEnvironment(environment, userId);
|
||||||
}),
|
}),
|
||||||
hasFilters,
|
hasFilters,
|
||||||
|
limits: {
|
||||||
|
used: schedulesCount,
|
||||||
|
limit: project.organization.maximumSchedulesLimit,
|
||||||
|
},
|
||||||
filters: {
|
filters: {
|
||||||
tasks,
|
tasks,
|
||||||
environments,
|
environments,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TaskTriggerSource,
|
TaskTriggerSource,
|
||||||
} from "@trigger.dev/database";
|
} from "@trigger.dev/database";
|
||||||
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
|
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
|
||||||
|
import { getTimezones } from "~/utils/timezones.server";
|
||||||
import { getUsername } from "~/utils/username";
|
import { getUsername } from "~/utils/username";
|
||||||
|
|
||||||
type TestTaskOptions = {
|
type TestTaskOptions = {
|
||||||
@@ -37,6 +38,7 @@ export type TestTask =
|
|||||||
| {
|
| {
|
||||||
triggerSource: "SCHEDULED";
|
triggerSource: "SCHEDULED";
|
||||||
task: Task;
|
task: Task;
|
||||||
|
possibleTimezones: string[];
|
||||||
runs: ScheduledRun[];
|
runs: ScheduledRun[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
lastTimestamp?: Date;
|
lastTimestamp?: Date;
|
||||||
externalId?: string;
|
externalId?: string;
|
||||||
|
timezone: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,9 +171,11 @@ export class TestTaskPresenter {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
case "SCHEDULED":
|
case "SCHEDULED":
|
||||||
|
const possibleTimezones = getTimezones();
|
||||||
return {
|
return {
|
||||||
triggerSource: "SCHEDULED",
|
triggerSource: "SCHEDULED",
|
||||||
task: taskWithEnvironment,
|
task: taskWithEnvironment,
|
||||||
|
possibleTimezones,
|
||||||
runs: (
|
runs: (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
latestRuns.map(async (r) => {
|
latestRuns.map(async (r) => {
|
||||||
@@ -195,6 +200,9 @@ export class TestTaskPresenter {
|
|||||||
|
|
||||||
async function getScheduleTaskRunPayload(run: RawRun) {
|
async function getScheduleTaskRunPayload(run: RawRun) {
|
||||||
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
|
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
|
||||||
|
if (!payload.timezone) {
|
||||||
|
payload.timezone = "UTC";
|
||||||
|
}
|
||||||
const parsed = ScheduledTaskPayload.safeParse(payload);
|
const parsed = ScheduledTaskPayload.safeParse(payload);
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { ScheduleObject } from "@trigger.dev/core/v3";
|
||||||
import { PrismaClient, prisma } from "~/db.server";
|
import { PrismaClient, prisma } from "~/db.server";
|
||||||
|
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||||
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
|
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
|
||||||
import { RunListPresenter } from "./RunListPresenter.server";
|
import { RunListPresenter } from "./RunListPresenter.server";
|
||||||
import { ScheduleObject } from "@trigger.dev/core/v3";
|
|
||||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
|
||||||
|
|
||||||
type ViewScheduleOptions = {
|
type ViewScheduleOptions = {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@@ -24,6 +24,7 @@ export class ViewSchedulePresenter {
|
|||||||
friendlyId: true,
|
friendlyId: true,
|
||||||
generatorExpression: true,
|
generatorExpression: true,
|
||||||
generatorDescription: true,
|
generatorDescription: true,
|
||||||
|
timezone: true,
|
||||||
externalId: true,
|
externalId: true,
|
||||||
deduplicationKey: true,
|
deduplicationKey: true,
|
||||||
userProvidedDeduplicationKey: true,
|
userProvidedDeduplicationKey: true,
|
||||||
@@ -68,7 +69,7 @@ export class ViewSchedulePresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextRuns = schedule.active
|
const nextRuns = schedule.active
|
||||||
? nextScheduledTimestamps(schedule.generatorExpression, new Date(), 5)
|
? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const runPresenter = new RunListPresenter(this.#prismaClient);
|
const runPresenter = new RunListPresenter(this.#prismaClient);
|
||||||
@@ -82,6 +83,7 @@ export class ViewSchedulePresenter {
|
|||||||
return {
|
return {
|
||||||
schedule: {
|
schedule: {
|
||||||
...schedule,
|
...schedule,
|
||||||
|
timezone: schedule.timezone,
|
||||||
cron: schedule.generatorExpression,
|
cron: schedule.generatorExpression,
|
||||||
cronDescription: schedule.generatorDescription,
|
cronDescription: schedule.generatorDescription,
|
||||||
nextRuns,
|
nextRuns,
|
||||||
@@ -105,6 +107,7 @@ export class ViewSchedulePresenter {
|
|||||||
expression: result.schedule.cron,
|
expression: result.schedule.cron,
|
||||||
description: result.schedule.cronDescription,
|
description: result.schedule.cronDescription,
|
||||||
},
|
},
|
||||||
|
timezone: result.schedule.timezone,
|
||||||
externalId: result.schedule.externalId ?? undefined,
|
externalId: result.schedule.externalId ?? undefined,
|
||||||
deduplicationKey: result.schedule.userProvidedDeduplicationKey
|
deduplicationKey: result.schedule.userProvidedDeduplicationKey
|
||||||
? result.schedule.deduplicationKey ?? undefined
|
? result.schedule.deduplicationKey ?? undefined
|
||||||
|
|||||||
+9
-5
@@ -185,7 +185,8 @@ export default function Page() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const organization = useOrganization();
|
const organization = useOrganization();
|
||||||
const project = useProject();
|
const project = useProject();
|
||||||
const user = useUser();
|
|
||||||
|
const isUtc = schedule.timezone === "UTC";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
|
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
|
||||||
@@ -210,6 +211,7 @@ export default function Page() {
|
|||||||
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
|
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
</Property>
|
</Property>
|
||||||
|
<Property label="Timezone">{schedule.timezone}</Property>
|
||||||
<Property label="Environments">
|
<Property label="Environments">
|
||||||
<EnvironmentLabels size="small" environments={schedule.environments} />
|
<EnvironmentLabels size="small" environments={schedule.environments} />
|
||||||
</Property>
|
</Property>
|
||||||
@@ -245,19 +247,21 @@ export default function Page() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
|
||||||
<TableHeaderCell>UTC</TableHeaderCell>
|
<TableHeaderCell>UTC</TableHeaderCell>
|
||||||
<TableHeaderCell>Local time</TableHeaderCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{schedule.nextRuns.map((run, index) => (
|
{schedule.nextRuns.map((run, index) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
|
{!isUtc && (
|
||||||
|
<TableCell>
|
||||||
|
<DateTime date={run} timeZone={schedule.timezone} />
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DateTime date={run} timeZone="UTC" />
|
<DateTime date={run} timeZone="UTC" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<DateTime date={run} />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
+2
-1
@@ -21,7 +21,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
|
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
|
||||||
useTypedLoaderData<typeof loader>();
|
useTypedLoaderData<typeof loader>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,6 +29,7 @@ export default function Page() {
|
|||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
possibleTasks={possibleTasks}
|
possibleTasks={possibleTasks}
|
||||||
possibleEnvironments={possibleEnvironments}
|
possibleEnvironments={possibleEnvironments}
|
||||||
|
possibleTimezones={possibleTimezones}
|
||||||
showGenerateField={showGenerateField}
|
showGenerateField={showGenerateField}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
|
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
|
||||||
useTypedLoaderData<typeof loader>();
|
useTypedLoaderData<typeof loader>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,6 +29,7 @@ export default function Page() {
|
|||||||
possibleTasks={possibleTasks}
|
possibleTasks={possibleTasks}
|
||||||
possibleEnvironments={possibleEnvironments}
|
possibleEnvironments={possibleEnvironments}
|
||||||
showGenerateField={showGenerateField}
|
showGenerateField={showGenerateField}
|
||||||
|
possibleTimezones={possibleTimezones}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-22
@@ -4,12 +4,21 @@ import { Outlet, useLocation, useParams } from "@remix-run/react";
|
|||||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||||
|
import { Feedback } from "~/components/Feedback";
|
||||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||||
import { InlineCode } from "~/components/code/InlineCode";
|
import { InlineCode } from "~/components/code/InlineCode";
|
||||||
import { EnvironmentLabel, EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||||
import { LinkButton } from "~/components/primitives/Buttons";
|
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||||
import { DateTime } from "~/components/primitives/DateTime";
|
import { DateTime } from "~/components/primitives/DateTime";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/primitives/Dialog";
|
||||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||||
@@ -78,6 +87,7 @@ export default function Page() {
|
|||||||
possibleEnvironments,
|
possibleEnvironments,
|
||||||
hasFilters,
|
hasFilters,
|
||||||
filters,
|
filters,
|
||||||
|
limits,
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
} = useTypedLoaderData<typeof loader>();
|
} = useTypedLoaderData<typeof loader>();
|
||||||
@@ -107,15 +117,43 @@ export default function Page() {
|
|||||||
</PropertyTable>
|
</PropertyTable>
|
||||||
</AdminDebugTooltip>
|
</AdminDebugTooltip>
|
||||||
|
|
||||||
<LinkButton
|
{limits.used >= limits.limit ? (
|
||||||
LeadingIcon={PlusIcon}
|
<Dialog>
|
||||||
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
|
<DialogTrigger asChild>
|
||||||
variant="primary/small"
|
<Button
|
||||||
shortcut={{ key: "n" }}
|
LeadingIcon={PlusIcon}
|
||||||
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
variant="primary/small"
|
||||||
>
|
shortcut={{ key: "n" }}
|
||||||
New schedule
|
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||||
</LinkButton>
|
>
|
||||||
|
New schedule
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>You've exceeded your limit</DialogHeader>
|
||||||
|
<DialogDescription>
|
||||||
|
You've used {limits.used}/{limits.limit} of your schedules. You can request more
|
||||||
|
schedules.
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter>
|
||||||
|
<Feedback
|
||||||
|
button={<Button variant="primary/medium">Request more</Button>}
|
||||||
|
defaultValue="help"
|
||||||
|
/>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : (
|
||||||
|
<LinkButton
|
||||||
|
LeadingIcon={PlusIcon}
|
||||||
|
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
|
||||||
|
variant="primary/small"
|
||||||
|
shortcut={{ key: "n" }}
|
||||||
|
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||||
|
>
|
||||||
|
New schedule
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
</PageAccessories>
|
</PageAccessories>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
<PageBody scrollable={false}>
|
<PageBody scrollable={false}>
|
||||||
@@ -142,7 +180,21 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SchedulesTable schedules={schedules} hasFilters={hasFilters} />
|
<SchedulesTable schedules={schedules} hasFilters={hasFilters} />
|
||||||
<div className="mt-2 justify-end">
|
<div className="mt-2 justify-between">
|
||||||
|
<Paragraph variant="extra-small" className="mt-3">
|
||||||
|
<span className={limits.used >= limits.limit ? "text-warning" : ""}>
|
||||||
|
You've used {limits.used}/{limits.limit} of your schedules.
|
||||||
|
</span>{" "}
|
||||||
|
<Feedback
|
||||||
|
button={
|
||||||
|
<button className=" text-secondary transition hover:text-indigo-400">
|
||||||
|
Request more
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
defaultValue="help"
|
||||||
|
/>
|
||||||
|
.
|
||||||
|
</Paragraph>
|
||||||
<PaginationControls currentPage={currentPage} totalPages={totalPages} />
|
<PaginationControls currentPage={currentPage} totalPages={totalPages} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,12 +288,13 @@ function SchedulesTable({
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>ID</TableHeaderCell>
|
<TableHeaderCell>ID</TableHeaderCell>
|
||||||
<TableHeaderCell>Task ID</TableHeaderCell>
|
<TableHeaderCell>Task ID</TableHeaderCell>
|
||||||
|
<TableHeaderCell>External ID</TableHeaderCell>
|
||||||
<TableHeaderCell>CRON</TableHeaderCell>
|
<TableHeaderCell>CRON</TableHeaderCell>
|
||||||
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
|
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
|
||||||
<TableHeaderCell>External ID</TableHeaderCell>
|
<TableHeaderCell>Timezone</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Next run</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Last run</TableHeaderCell>
|
||||||
<TableHeaderCell>Deduplication key</TableHeaderCell>
|
<TableHeaderCell>Deduplication key</TableHeaderCell>
|
||||||
<TableHeaderCell>Next run (UTC)</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Last run (UTC)</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Environments</TableHeaderCell>
|
<TableHeaderCell>Environments</TableHeaderCell>
|
||||||
<TableHeaderCell>Enabled</TableHeaderCell>
|
<TableHeaderCell>Enabled</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -262,6 +315,9 @@ function SchedulesTable({
|
|||||||
<TableCell to={path} className={cellClass}>
|
<TableCell to={path} className={cellClass}>
|
||||||
{schedule.taskIdentifier}
|
{schedule.taskIdentifier}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell to={path} className={cellClass}>
|
||||||
|
{schedule.externalId ? schedule.externalId : "–"}
|
||||||
|
</TableCell>
|
||||||
<TableCell to={path} className={cellClass}>
|
<TableCell to={path} className={cellClass}>
|
||||||
{schedule.cron}
|
{schedule.cron}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -269,17 +325,21 @@ function SchedulesTable({
|
|||||||
{schedule.cronDescription}
|
{schedule.cronDescription}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell to={path} className={cellClass}>
|
<TableCell to={path} className={cellClass}>
|
||||||
{schedule.externalId ? schedule.externalId : "–"}
|
{schedule.timezone}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell to={path} className={cellClass}>
|
||||||
|
<DateTime date={schedule.nextRun} timeZone={schedule.timezone} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell to={path} className={cellClass}>
|
||||||
|
{schedule.lastRun ? (
|
||||||
|
<DateTime date={schedule.lastRun} timeZone={schedule.timezone} />
|
||||||
|
) : (
|
||||||
|
"–"
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell to={path} className={cellClass}>
|
<TableCell to={path} className={cellClass}>
|
||||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell to={path} className={cellClass}>
|
|
||||||
<DateTime date={schedule.nextRun} timeZone="utc" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell to={path} className={cellClass}>
|
|
||||||
{schedule.lastRun ? <DateTime date={schedule.lastRun} timeZone="utc" /> : "–"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell to={path} className={cellClass}>
|
<TableCell to={path} className={cellClass}>
|
||||||
<EnvironmentLabels environments={schedule.environments} size="small" />
|
<EnvironmentLabels environments={schedule.environments} size="small" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
+53
-3
@@ -26,8 +26,10 @@ import {
|
|||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "~/components/primitives/Resizable";
|
} from "~/components/primitives/Resizable";
|
||||||
|
import { Select } from "~/components/primitives/Select";
|
||||||
import { TextLink } from "~/components/primitives/TextLink";
|
import { TextLink } from "~/components/primitives/TextLink";
|
||||||
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
|
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
|
||||||
|
import { TimezoneList } from "~/components/scheduled/timezones";
|
||||||
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||||
import {
|
import {
|
||||||
ScheduledRun,
|
ScheduledRun,
|
||||||
@@ -95,7 +97,13 @@ export default function Page() {
|
|||||||
return <StandardTaskForm task={result.task} runs={result.runs} />;
|
return <StandardTaskForm task={result.task} runs={result.runs} />;
|
||||||
}
|
}
|
||||||
case "SCHEDULED": {
|
case "SCHEDULED": {
|
||||||
return <ScheduledTaskForm task={result.task} runs={result.runs} />;
|
return (
|
||||||
|
<ScheduledTaskForm
|
||||||
|
task={result.task}
|
||||||
|
runs={result.runs}
|
||||||
|
possibleTimezones={result.possibleTimezones}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,12 +223,21 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: ScheduledRun[] }) {
|
function ScheduledTaskForm({
|
||||||
|
task,
|
||||||
|
runs,
|
||||||
|
possibleTimezones,
|
||||||
|
}: {
|
||||||
|
task: TestTask["task"];
|
||||||
|
runs: ScheduledRun[];
|
||||||
|
possibleTimezones: string[];
|
||||||
|
}) {
|
||||||
const lastSubmission = useActionData();
|
const lastSubmission = useActionData();
|
||||||
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id);
|
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id);
|
||||||
const [timestampValue, setTimestampValue] = useState<Date | undefined>();
|
const [timestampValue, setTimestampValue] = useState<Date | undefined>();
|
||||||
const [lastTimestampValue, setLastTimestampValue] = useState<Date | undefined>();
|
const [lastTimestampValue, setLastTimestampValue] = useState<Date | undefined>();
|
||||||
const [externalIdValue, setExternalIdValue] = useState<string | undefined>();
|
const [externalIdValue, setExternalIdValue] = useState<string | undefined>();
|
||||||
|
const [timezoneValue, setTimezoneValue] = useState<string>("UTC");
|
||||||
|
|
||||||
//set initial values
|
//set initial values
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -233,11 +250,20 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
|
|||||||
setTimestampValue(initialRun.payload.timestamp);
|
setTimestampValue(initialRun.payload.timestamp);
|
||||||
setLastTimestampValue(initialRun.payload.lastTimestamp);
|
setLastTimestampValue(initialRun.payload.lastTimestamp);
|
||||||
setExternalIdValue(initialRun.payload.externalId);
|
setExternalIdValue(initialRun.payload.externalId);
|
||||||
|
setTimezoneValue(initialRun.payload.timezone);
|
||||||
}, [selectedCodeSampleId]);
|
}, [selectedCodeSampleId]);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
form,
|
form,
|
||||||
{ timestamp, lastTimestamp, externalId, triggerSource, taskIdentifier, environmentId },
|
{
|
||||||
|
timestamp,
|
||||||
|
lastTimestamp,
|
||||||
|
externalId,
|
||||||
|
triggerSource,
|
||||||
|
taskIdentifier,
|
||||||
|
environmentId,
|
||||||
|
timezone,
|
||||||
|
},
|
||||||
] = useForm({
|
] = useForm({
|
||||||
id: "test-task-scheduled",
|
id: "test-task-scheduled",
|
||||||
// TODO: type this
|
// TODO: type this
|
||||||
@@ -314,6 +340,30 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
|
|||||||
</Hint>
|
</Hint>
|
||||||
<FormError id={lastTimestamp.errorId}>{lastTimestamp.error}</FormError>
|
<FormError id={lastTimestamp.errorId}>{lastTimestamp.error}</FormError>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<Label htmlFor={timezone.id}>Timezone</Label>
|
||||||
|
<Select
|
||||||
|
{...conform.select(timezone)}
|
||||||
|
placeholder="Select a timezone"
|
||||||
|
defaultValue={timezoneValue}
|
||||||
|
value={timezoneValue}
|
||||||
|
setValue={(e) => {
|
||||||
|
if (Array.isArray(e)) return;
|
||||||
|
setTimezoneValue(e);
|
||||||
|
}}
|
||||||
|
items={possibleTimezones}
|
||||||
|
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
|
||||||
|
dropdownIcon
|
||||||
|
variant="tertiary/medium"
|
||||||
|
>
|
||||||
|
{(matches) => <TimezoneList timezones={matches} />}
|
||||||
|
</Select>
|
||||||
|
<Hint>
|
||||||
|
The Timestamp and Last timestamp are in UTC so this just changes the timezone
|
||||||
|
string that comes through in the payload.
|
||||||
|
</Hint>
|
||||||
|
<FormError id={timezone.errorId}>{timezone.error}</FormError>
|
||||||
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Label required={false} htmlFor={externalId.id}>
|
<Label required={false} htmlFor={externalId.id}>
|
||||||
External ID
|
External ID
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Link } from "@remix-run/react";
|
||||||
|
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||||
|
import { LogoIcon } from "~/components/LogoIcon";
|
||||||
|
import { Header1 } from "~/components/primitives/Headers";
|
||||||
|
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||||
|
import { getTimezones } from "~/utils/timezones.server";
|
||||||
|
|
||||||
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
|
return typedjson({
|
||||||
|
timezones: getTimezones(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { timezones } = useTypedLoaderData<typeof loader>();
|
||||||
|
return (
|
||||||
|
<div className="grid grid-rows-[2.5rem,1fr]">
|
||||||
|
<div className="flex items-center border-b border-b-grid-dimmed px-3">
|
||||||
|
<Link to="/">
|
||||||
|
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto p-8 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||||
|
<Header1 spacing>Supported timezones</Header1>
|
||||||
|
<Paragraph spacing>We support these timezones when creating a schedule.</Paragraph>
|
||||||
|
<ul className="">
|
||||||
|
{timezones.map((timezone) => (
|
||||||
|
<li key={timezone}>{timezone}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -79,9 +79,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
|||||||
friendlyId: parsedParams.data.scheduleId,
|
friendlyId: parsedParams.data.scheduleId,
|
||||||
taskIdentifier: body.data.task,
|
taskIdentifier: body.data.task,
|
||||||
cron: body.data.cron,
|
cron: body.data.cron,
|
||||||
|
timezone: body.data.timezone,
|
||||||
environments: [authenticationResult.environment.id],
|
environments: [authenticationResult.environment.id],
|
||||||
externalId: body.data.externalId,
|
externalId: body.data.externalId,
|
||||||
deduplicationKey: body.data.deduplicationKey,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
||||||
@@ -95,6 +95,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
|||||||
expression: schedule.cron,
|
expression: schedule.cron,
|
||||||
description: schedule.cronDescription,
|
description: schedule.cronDescription,
|
||||||
},
|
},
|
||||||
|
timezone: schedule.timezone,
|
||||||
externalId: schedule.externalId ?? undefined,
|
externalId: schedule.externalId ?? undefined,
|
||||||
deduplicationKey: schedule.deduplicationKey,
|
deduplicationKey: schedule.deduplicationKey,
|
||||||
environments: schedule.environments,
|
environments: schedule.environments,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
environments: [authenticationResult.environment.id],
|
environments: [authenticationResult.environment.id],
|
||||||
externalId: body.data.externalId,
|
externalId: body.data.externalId,
|
||||||
deduplicationKey: body.data.deduplicationKey,
|
deduplicationKey: body.data.deduplicationKey,
|
||||||
|
timezone: body.data.timezone,
|
||||||
};
|
};
|
||||||
|
|
||||||
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
||||||
@@ -56,6 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
expression: schedule.cron,
|
expression: schedule.cron,
|
||||||
description: schedule.cronDescription,
|
description: schedule.cronDescription,
|
||||||
},
|
},
|
||||||
|
timezone: schedule.timezone,
|
||||||
externalId: schedule.externalId ?? undefined,
|
externalId: schedule.externalId ?? undefined,
|
||||||
deduplicationKey: schedule.deduplicationKey,
|
deduplicationKey: schedule.deduplicationKey,
|
||||||
environments: schedule.environments,
|
environments: schedule.environments,
|
||||||
@@ -111,6 +113,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
expression: schedule.cron,
|
expression: schedule.cron,
|
||||||
description: schedule.cronDescription,
|
description: schedule.cronDescription,
|
||||||
},
|
},
|
||||||
|
timezone: schedule.timezone,
|
||||||
deduplicationKey: schedule.userProvidedDeduplicationKey
|
deduplicationKey: schedule.userProvidedDeduplicationKey
|
||||||
? schedule.deduplicationKey
|
? schedule.deduplicationKey
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { json } from "@remix-run/server-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { apiCors } from "~/utils/apiCors";
|
||||||
|
import { getTimezones } from "~/utils/timezones.server";
|
||||||
|
|
||||||
|
const SearchParamsSchema = z.object({
|
||||||
|
excludeUtc: z.preprocess((value) => value === "true", z.boolean()).default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
if (request.method.toUpperCase() === "OPTIONS") {
|
||||||
|
return apiCors(request, json({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawSearchParams = new URL(request.url).searchParams;
|
||||||
|
const params = SearchParamsSchema.safeParse(Object.fromEntries(rawSearchParams.entries()));
|
||||||
|
|
||||||
|
if (!params.success) {
|
||||||
|
return apiCors(
|
||||||
|
request,
|
||||||
|
json({ error: "Invalid request parameters", issues: params.error.issues }, { status: 400 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timezones = getTimezones(!params.data.excludeUtc);
|
||||||
|
return apiCors(request, json({ timezones }));
|
||||||
|
}
|
||||||
+53
-15
@@ -3,9 +3,10 @@ import { parse } from "@conform-to/zod";
|
|||||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||||
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
||||||
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { parseExpression } from "cron-parser";
|
import { parseExpression } from "cron-parser";
|
||||||
import cronstrue from "cronstrue";
|
import cronstrue from "cronstrue";
|
||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
environmentTextClassName,
|
environmentTextClassName,
|
||||||
environmentTitle,
|
environmentTitle,
|
||||||
@@ -42,6 +43,7 @@ import { ProjectParamSchema, docsPath, v3SchedulesPath } from "~/utils/pathBuild
|
|||||||
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
|
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
|
||||||
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
|
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
|
||||||
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
|
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
|
||||||
|
import { TimezoneList } from "~/components/scheduled/timezones";
|
||||||
|
|
||||||
const cronFormat = `* * * * *
|
const cronFormat = `* * * * *
|
||||||
┬ ┬ ┬ ┬ ┬
|
┬ ┬ ┬ ┬ ┬
|
||||||
@@ -117,9 +119,12 @@ export function UpsertScheduleForm({
|
|||||||
schedule,
|
schedule,
|
||||||
possibleTasks,
|
possibleTasks,
|
||||||
possibleEnvironments,
|
possibleEnvironments,
|
||||||
|
possibleTimezones,
|
||||||
showGenerateField,
|
showGenerateField,
|
||||||
}: EditableScheduleElements & { showGenerateField: boolean }) {
|
}: EditableScheduleElements & { showGenerateField: boolean }) {
|
||||||
const lastSubmission = useActionData();
|
const lastSubmission = useActionData();
|
||||||
|
const [selectedTimezone, setSelectedTimezone] = useState<string>(schedule?.timezone ?? "UTC");
|
||||||
|
const isUtc = selectedTimezone === "UTC";
|
||||||
const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? "");
|
const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? "");
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const isLoading = navigation.state !== "idle";
|
const isLoading = navigation.state !== "idle";
|
||||||
@@ -127,18 +132,20 @@ export function UpsertScheduleForm({
|
|||||||
const project = useProject();
|
const project = useProject();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [form, { taskIdentifier, cron, externalId, environments, deduplicationKey }] = useForm({
|
const [form, { taskIdentifier, cron, timezone, externalId, environments, deduplicationKey }] =
|
||||||
id: "create-schedule",
|
useForm({
|
||||||
// TODO: type this
|
id: "create-schedule",
|
||||||
lastSubmission: lastSubmission as any,
|
// TODO: type this
|
||||||
shouldRevalidate: "onSubmit",
|
lastSubmission: lastSubmission as any,
|
||||||
onValidate({ formData }) {
|
shouldRevalidate: "onSubmit",
|
||||||
return parse(formData, { schema: UpsertSchedule });
|
onValidate({ formData }) {
|
||||||
},
|
return parse(formData, { schema: UpsertSchedule });
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
let cronPatternResult: CronPatternResult | undefined = undefined;
|
let cronPatternResult: CronPatternResult | undefined = undefined;
|
||||||
let nextRuns: Date[] | undefined = undefined;
|
let nextRuns: Date[] | undefined = undefined;
|
||||||
|
|
||||||
if (cronPattern !== "") {
|
if (cronPattern !== "") {
|
||||||
const result = CronPattern.safeParse(cronPattern);
|
const result = CronPattern.safeParse(cronPattern);
|
||||||
|
|
||||||
@@ -149,7 +156,10 @@ export function UpsertScheduleForm({
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const expression = parseExpression(cronPattern, { utc: true });
|
const expression = parseExpression(
|
||||||
|
cronPattern,
|
||||||
|
isUtc ? { utc: true } : { tz: selectedTimezone }
|
||||||
|
);
|
||||||
cronPatternResult = {
|
cronPatternResult = {
|
||||||
isValid: true,
|
isValid: true,
|
||||||
description: cronstrue.toString(cronPattern),
|
description: cronstrue.toString(cronPattern),
|
||||||
@@ -195,6 +205,7 @@ export function UpsertScheduleForm({
|
|||||||
items={possibleTasks}
|
items={possibleTasks}
|
||||||
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
|
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
|
||||||
dropdownIcon
|
dropdownIcon
|
||||||
|
variant="tertiary/medium"
|
||||||
>
|
>
|
||||||
{(matches) => (
|
{(matches) => (
|
||||||
<>
|
<>
|
||||||
@@ -241,25 +252,52 @@ export function UpsertScheduleForm({
|
|||||||
<ValidCronMessage isValid={false} message={cronPatternResult.error} />
|
<ValidCronMessage isValid={false} message={cronPatternResult.error} />
|
||||||
)}
|
)}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<Label htmlFor={timezone.id}>Timezone</Label>
|
||||||
|
<Select
|
||||||
|
{...conform.select(timezone)}
|
||||||
|
placeholder="Select a timezone"
|
||||||
|
defaultValue={selectedTimezone}
|
||||||
|
value={selectedTimezone}
|
||||||
|
setValue={(e) => {
|
||||||
|
if (Array.isArray(e)) return;
|
||||||
|
setSelectedTimezone(e);
|
||||||
|
}}
|
||||||
|
items={possibleTimezones}
|
||||||
|
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
|
||||||
|
dropdownIcon
|
||||||
|
variant="tertiary/medium"
|
||||||
|
>
|
||||||
|
{(matches) => <TimezoneList timezones={matches} />}
|
||||||
|
</Select>
|
||||||
|
<Hint>
|
||||||
|
{isUtc
|
||||||
|
? "UTC will not change with daylight savings time."
|
||||||
|
: "This will automatically adjust for daylight savings time."}
|
||||||
|
</Hint>
|
||||||
|
<FormError id={timezone.errorId}>{timezone.error}</FormError>
|
||||||
|
</InputGroup>
|
||||||
{nextRuns !== undefined && (
|
{nextRuns !== undefined && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Header3>Next 5 runs</Header3>
|
<Header3>Next 5 runs</Header3>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
{!isUtc && <TableHeaderCell>{selectedTimezone}</TableHeaderCell>}
|
||||||
<TableHeaderCell>UTC</TableHeaderCell>
|
<TableHeaderCell>UTC</TableHeaderCell>
|
||||||
<TableHeaderCell>Local time</TableHeaderCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{nextRuns.map((run, index) => (
|
{nextRuns.map((run, index) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
|
{!isUtc && (
|
||||||
|
<TableCell>
|
||||||
|
<DateTime date={run} timeZone={selectedTimezone} />
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DateTime date={run} timeZone="UTC" />
|
<DateTime date={run} timeZone="UTC" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<DateTime date={run} />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
|
|||||||
/^\/api\/v1\/http-endpoints\/[^\/]+\/env\/[^\/]+\/[^\/]+$/, // /api/v1/http-endpoints/$httpEndpointId/env/$envType/$shortcode
|
/^\/api\/v1\/http-endpoints\/[^\/]+\/env\/[^\/]+\/[^\/]+$/, // /api/v1/http-endpoints/$httpEndpointId/env/$envType/$shortcode
|
||||||
/^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id
|
/^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id
|
||||||
/^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier
|
/^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier
|
||||||
|
"/api/v1/timezones",
|
||||||
],
|
],
|
||||||
log: {
|
log: {
|
||||||
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
|
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function getTimezones(includeUtc = true) {
|
||||||
|
const possibleTimezones = Intl.supportedValuesOf("timeZone").sort();
|
||||||
|
if (includeUtc) {
|
||||||
|
possibleTimezones.unshift("UTC");
|
||||||
|
}
|
||||||
|
return possibleTimezones;
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ export const UpsertSchedule = z.object({
|
|||||||
),
|
),
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
deduplicationKey: z.string().optional(),
|
deduplicationKey: z.string().optional(),
|
||||||
|
timezone: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpsertSchedule = z.infer<typeof UpsertSchedule>;
|
export type UpsertSchedule = z.infer<typeof UpsertSchedule>;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class RegisterNextTaskScheduleInstanceService extends BaseService {
|
|||||||
|
|
||||||
const nextScheduledTimestamp = calculateNextScheduledTimestamp(
|
const nextScheduledTimestamp = calculateNextScheduledTimestamp(
|
||||||
instance.taskSchedule.generatorExpression,
|
instance.taskSchedule.generatorExpression,
|
||||||
|
instance.taskSchedule.timezone,
|
||||||
instance.lastScheduledTimestamp ?? new Date()
|
instance.lastScheduledTimestamp ?? new Date()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export class TestTaskService extends BaseService {
|
|||||||
scheduleId: "sched_1234",
|
scheduleId: "sched_1234",
|
||||||
timestamp: data.timestamp,
|
timestamp: data.timestamp,
|
||||||
lastTimestamp: data.lastTimestamp,
|
lastTimestamp: data.lastTimestamp,
|
||||||
|
timezone: data.timezone,
|
||||||
externalId: data.externalId,
|
externalId: data.externalId,
|
||||||
upcoming: [],
|
upcoming: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -96,8 +96,10 @@ export class TriggerScheduledTaskService extends BaseService {
|
|||||||
timestamp: instance.nextScheduledTimestamp,
|
timestamp: instance.nextScheduledTimestamp,
|
||||||
lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
|
lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
|
||||||
externalId: instance.taskSchedule.externalId ?? undefined,
|
externalId: instance.taskSchedule.externalId ?? undefined,
|
||||||
|
timezone: instance.taskSchedule.timezone,
|
||||||
upcoming: nextScheduledTimestamps(
|
upcoming: nextScheduledTimestamps(
|
||||||
instance.taskSchedule.generatorExpression,
|
instance.taskSchedule.generatorExpression,
|
||||||
|
instance.taskSchedule.timezone,
|
||||||
instance.nextScheduledTimestamp!,
|
instance.nextScheduledTimestamp!,
|
||||||
10
|
10
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { BaseService, ServiceValidationError } from "./baseService.server";
|
|||||||
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
|
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
|
||||||
import cronstrue from "cronstrue";
|
import cronstrue from "cronstrue";
|
||||||
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
|
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
|
||||||
|
import { getTimezones } from "~/utils/timezones.server";
|
||||||
|
import { env } from "~/env.server";
|
||||||
|
|
||||||
export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
|
export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
|
||||||
|
|
||||||
@@ -62,6 +64,48 @@ export class UpsertTaskScheduleService extends BaseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//if creating a schedule, check they're under the limits
|
||||||
|
if (!schedule.friendlyId) {
|
||||||
|
//check they're within their limit
|
||||||
|
const limits = await this._prisma.organization.findFirst({
|
||||||
|
select: {
|
||||||
|
maximumSchedulesLimit: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
projects: {
|
||||||
|
some: {
|
||||||
|
id: projectId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!limits) {
|
||||||
|
throw new ServiceValidationError("Organization not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedulesCount = await this._prisma.taskSchedule.count({
|
||||||
|
where: {
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (schedulesCount >= limits.maximumSchedulesLimit) {
|
||||||
|
throw new ServiceValidationError(
|
||||||
|
`You have created ${schedulesCount}/${limits.maximumSchedulesLimit} schedules so you'll need to increase your limits or delete some schedules. Increase your limits by contacting support.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedule.timezone) {
|
||||||
|
const possibleTimezones = getTimezones();
|
||||||
|
if (!possibleTimezones.includes(schedule.timezone)) {
|
||||||
|
throw new ServiceValidationError(
|
||||||
|
`Invalid IANA timezone: "${schedule.timezone}". View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await $transaction(this._prisma, async (tx) => {
|
const result = await $transaction(this._prisma, async (tx) => {
|
||||||
const deduplicationKey =
|
const deduplicationKey =
|
||||||
typeof schedule.deduplicationKey === "string" && schedule.deduplicationKey !== ""
|
typeof schedule.deduplicationKey === "string" && schedule.deduplicationKey !== ""
|
||||||
@@ -115,6 +159,7 @@ export class UpsertTaskScheduleService extends BaseService {
|
|||||||
options.deduplicationKey !== undefined && options.deduplicationKey !== "",
|
options.deduplicationKey !== undefined && options.deduplicationKey !== "",
|
||||||
generatorExpression: options.cron,
|
generatorExpression: options.cron,
|
||||||
generatorDescription: cronstrue.toString(options.cron),
|
generatorDescription: cronstrue.toString(options.cron),
|
||||||
|
timezone: options.timezone ?? "UTC",
|
||||||
externalId: options.externalId ? options.externalId : undefined,
|
externalId: options.externalId ? options.externalId : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -164,6 +209,7 @@ export class UpsertTaskScheduleService extends BaseService {
|
|||||||
data: {
|
data: {
|
||||||
generatorExpression: options.cron,
|
generatorExpression: options.cron,
|
||||||
generatorDescription: cronstrue.toString(options.cron),
|
generatorDescription: cronstrue.toString(options.cron),
|
||||||
|
timezone: options.timezone ?? "UTC",
|
||||||
externalId: options.externalId ? options.externalId : null,
|
externalId: options.externalId ? options.externalId : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -280,7 +326,11 @@ export class UpsertTaskScheduleService extends BaseService {
|
|||||||
: undefined,
|
: undefined,
|
||||||
cron: taskSchedule.generatorExpression,
|
cron: taskSchedule.generatorExpression,
|
||||||
cronDescription: taskSchedule.generatorDescription,
|
cronDescription: taskSchedule.generatorDescription,
|
||||||
nextRun: calculateNextScheduledTimestamp(taskSchedule.generatorExpression),
|
timezone: taskSchedule.timezone,
|
||||||
|
nextRun: calculateNextScheduledTimestamp(
|
||||||
|
taskSchedule.generatorExpression,
|
||||||
|
taskSchedule.timezone
|
||||||
|
),
|
||||||
environments: instances.map((instance) => ({
|
environments: instances.map((instance) => ({
|
||||||
id: instance.environment.id,
|
id: instance.environment.id,
|
||||||
shortcode: instance.environment.shortcode,
|
shortcode: instance.environment.shortcode,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const TestTaskData = z
|
|||||||
(val) => (val === "" ? undefined : val),
|
(val) => (val === "" ? undefined : val),
|
||||||
z.coerce.date().optional()
|
z.coerce.date().optional()
|
||||||
),
|
),
|
||||||
|
timezone: z.string(),
|
||||||
externalId: z.preprocess((val) => (val === "" ? undefined : val), z.string().optional()),
|
externalId: z.preprocess((val) => (val === "" ? undefined : val), z.string().optional()),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -2,21 +2,23 @@ import { parseExpression } from "cron-parser";
|
|||||||
|
|
||||||
export function calculateNextScheduledTimestamp(
|
export function calculateNextScheduledTimestamp(
|
||||||
schedule: string,
|
schedule: string,
|
||||||
|
timezone: string | null,
|
||||||
lastScheduledTimestamp: Date = new Date()
|
lastScheduledTimestamp: Date = new Date()
|
||||||
) {
|
) {
|
||||||
let nextStep = calculateNextStep(schedule, lastScheduledTimestamp);
|
let nextStep = calculateNextStep(schedule, timezone, lastScheduledTimestamp);
|
||||||
|
|
||||||
while (nextStep.getTime() < Date.now()) {
|
while (nextStep.getTime() < Date.now()) {
|
||||||
nextStep = calculateNextStep(schedule, nextStep);
|
nextStep = calculateNextStep(schedule, timezone, nextStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextStep;
|
return nextStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateNextStep(schedule: string, currentDate: Date) {
|
function calculateNextStep(schedule: string, timezone: string | null, currentDate: Date) {
|
||||||
return parseExpression(schedule, {
|
return parseExpression(schedule, {
|
||||||
currentDate,
|
currentDate,
|
||||||
utc: true,
|
utc: timezone === null,
|
||||||
|
tz: timezone ?? undefined,
|
||||||
})
|
})
|
||||||
.next()
|
.next()
|
||||||
.toDate();
|
.toDate();
|
||||||
@@ -24,6 +26,7 @@ function calculateNextStep(schedule: string, currentDate: Date) {
|
|||||||
|
|
||||||
export function nextScheduledTimestamps(
|
export function nextScheduledTimestamps(
|
||||||
cron: string,
|
cron: string,
|
||||||
|
timezone: string | null,
|
||||||
lastScheduledTimestamp: Date,
|
lastScheduledTimestamp: Date,
|
||||||
count: number = 1
|
count: number = 1
|
||||||
) {
|
) {
|
||||||
@@ -31,7 +34,11 @@ export function nextScheduledTimestamps(
|
|||||||
let nextScheduledTimestamp = lastScheduledTimestamp;
|
let nextScheduledTimestamp = lastScheduledTimestamp;
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
nextScheduledTimestamp = calculateNextScheduledTimestamp(cron, nextScheduledTimestamp);
|
nextScheduledTimestamp = calculateNextScheduledTimestamp(
|
||||||
|
cron,
|
||||||
|
timezone,
|
||||||
|
nextScheduledTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
result.push(nextScheduledTimestamp);
|
result.push(nextScheduledTimestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Intl {
|
||||||
|
type Key = "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit";
|
||||||
|
|
||||||
|
function supportedValuesOf(input: Key): string[];
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"exclude": ["./cypress", "./cypress.config.ts"],
|
"exclude": ["./cypress", "./cypress.config.ts"],
|
||||||
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["remix.env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["vitest/globals"],
|
"types": ["vitest/globals"],
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<Accordion title="How to increase these limits?">
|
||||||
|
These are soft-limits and can be increased. Before we introduce paid plans in July you can request
|
||||||
|
more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact).
|
||||||
|
If you increase these defaults you may have to subscribe to a paid plan when we introduce them.
|
||||||
|
For more details on the v3 Cloud pricing see the [pricing
|
||||||
|
details](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
|
||||||
|
</Accordion>
|
||||||
+2
-1
@@ -190,7 +190,8 @@
|
|||||||
"v3/management/schedules/update",
|
"v3/management/schedules/update",
|
||||||
"v3/management/schedules/delete",
|
"v3/management/schedules/delete",
|
||||||
"v3/management/schedules/deactivate",
|
"v3/management/schedules/deactivate",
|
||||||
"v3/management/schedules/activate"
|
"v3/management/schedules/activate",
|
||||||
|
"v3/management/schedules/timezones"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+506
-16
@@ -5,7 +5,7 @@ info:
|
|||||||
description: "The REST API lets you trigger and manage runs on Trigger.dev. You
|
description: "The REST API lets you trigger and manage runs on Trigger.dev. You
|
||||||
can trigger a run, get the status of a run, and get the results of a run. "
|
can trigger a run, get the status of a run, and get the results of a run. "
|
||||||
version: 2024-04
|
version: 2024-04
|
||||||
license:
|
license:
|
||||||
name: Apache 2.0
|
name: Apache 2.0
|
||||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
servers:
|
servers:
|
||||||
@@ -48,6 +48,8 @@ paths:
|
|||||||
const schedule = await schedules.create({
|
const schedule = await schedules.create({
|
||||||
task: 'my-task',
|
task: 'my-task',
|
||||||
cron: '0 0 * * *'
|
cron: '0 0 * * *'
|
||||||
|
deduplicationKey: 'my-schedule',
|
||||||
|
timezone: 'America/New_York'
|
||||||
});
|
});
|
||||||
|
|
||||||
get:
|
get:
|
||||||
@@ -138,7 +140,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
"$ref": "#/components/schemas/CreateScheduleOptions"
|
"$ref": "#/components/schemas/UpdateScheduleOptions"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Schedule updated successfully
|
description: Schedule updated successfully
|
||||||
@@ -265,6 +267,34 @@ paths:
|
|||||||
|
|
||||||
const schedule = await schedules.activate(scheduleId);
|
const schedule = await schedules.activate(scheduleId);
|
||||||
|
|
||||||
|
"/api/v1/timezones":
|
||||||
|
get:
|
||||||
|
operationId: get_timezones_v1
|
||||||
|
summary: Get all supported timezones
|
||||||
|
description: Get all supported timezones that schedule tasks support.
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: excludeUtc
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
description: Defaults to false. Whether to include UTC in the results or not.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
"$ref": "#/components/schemas/GetTimezonesResult"
|
||||||
|
tags:
|
||||||
|
- schedules
|
||||||
|
x-codeSamples:
|
||||||
|
- lang: typescript
|
||||||
|
source: |-
|
||||||
|
import { schedules } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
const { timezones } = await schedules.timezones();
|
||||||
|
|
||||||
"/api/v1/runs/{runId}/replay":
|
"/api/v1/runs/{runId}/replay":
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/runId"
|
- $ref: "#/components/parameters/runId"
|
||||||
@@ -401,7 +431,7 @@ paths:
|
|||||||
operationId: retrieve_run_v1
|
operationId: retrieve_run_v1
|
||||||
summary: Retrieve a run
|
summary: Retrieve a run
|
||||||
description: |
|
description: |
|
||||||
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
|
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successful request
|
description: Successful request
|
||||||
@@ -619,8 +649,6 @@ paths:
|
|||||||
for (const run of response.data) {
|
for (const run of response.data) {
|
||||||
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
|
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"/api/v1/projects/{projectRef}/envvars/{env}":
|
"/api/v1/projects/{projectRef}/envvars/{env}":
|
||||||
parameters:
|
parameters:
|
||||||
@@ -792,7 +820,7 @@ paths:
|
|||||||
format: binary
|
format: binary
|
||||||
override:
|
override:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- variables
|
- variables
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
@@ -851,7 +879,7 @@ paths:
|
|||||||
source: |-
|
source: |-
|
||||||
import { envvars } from "@trigger.dev/sdk/v3";
|
import { envvars } from "@trigger.dev/sdk/v3";
|
||||||
import { createReadStream } from "node:fs";
|
import { createReadStream } from "node:fs";
|
||||||
|
|
||||||
// Import variables in dotenv format from a file
|
// Import variables in dotenv format from a file
|
||||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||||
variables: createReadStream(".env"),
|
variables: createReadStream(".env"),
|
||||||
@@ -861,7 +889,7 @@ paths:
|
|||||||
label: Import variables from a response
|
label: Import variables from a response
|
||||||
source: |-
|
source: |-
|
||||||
import { envvars } from "@trigger.dev/sdk/v3";
|
import { envvars } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
// Import variables in dotenv format from a response
|
// Import variables in dotenv format from a response
|
||||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||||
variables: await fetch("https://example.com/.env"),
|
variables: await fetch("https://example.com/.env"),
|
||||||
@@ -871,7 +899,7 @@ paths:
|
|||||||
label: Import variables from a Buffer
|
label: Import variables from a Buffer
|
||||||
source: |-
|
source: |-
|
||||||
import { envvars } from "@trigger.dev/sdk/v3";
|
import { envvars } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
// Import variables in dotenv format from a buffer
|
// Import variables in dotenv format from a buffer
|
||||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||||
variables: Buffer.from("SLACK_API_KEY=slack_1234"),
|
variables: Buffer.from("SLACK_API_KEY=slack_1234"),
|
||||||
@@ -881,14 +909,13 @@ paths:
|
|||||||
label: Import variables from a File
|
label: Import variables from a File
|
||||||
source: |-
|
source: |-
|
||||||
import { envvars } from "@trigger.dev/sdk/v3";
|
import { envvars } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
// Import variables in dotenv format from a file
|
// Import variables in dotenv format from a file
|
||||||
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
|
||||||
variables: new File(["SLACK_API_KEY=slack_1234"], ".env"),
|
variables: new File(["SLACK_API_KEY=slack_1234"], ".env"),
|
||||||
override: false
|
override: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
"/api/v1/projects/{projectRef}/envvars/{env}/{name}":
|
"/api/v1/projects/{projectRef}/envvars/{env}/{name}":
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/projectRef"
|
- $ref: "#/components/parameters/projectRef"
|
||||||
@@ -951,7 +978,7 @@ paths:
|
|||||||
console.log(`Value: ${variable.value}`);
|
console.log(`Value: ${variable.value}`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
operationId: delete_project_envvar_v1
|
operationId: delete_project_envvar_v1
|
||||||
summary: Delete environment variable
|
summary: Delete environment variable
|
||||||
@@ -1068,7 +1095,6 @@ paths:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
@@ -1471,11 +1497,11 @@ components:
|
|||||||
payload:
|
payload:
|
||||||
type: object
|
type: object
|
||||||
description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key
|
description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key
|
||||||
example: {"foo": "bar"}
|
example: { "foo": "bar" }
|
||||||
output:
|
output:
|
||||||
type: object
|
type: object
|
||||||
description: The output of the run. Will be omitted if the request was made with a Public API key
|
description: The output of the run. Will be omitted if the request was made with a Public API key
|
||||||
example: {"foo": "bar"}
|
example: { "foo": "bar" }
|
||||||
idempotencyKey:
|
idempotencyKey:
|
||||||
type: string
|
type: string
|
||||||
description: The idempotency key used to prevent creating duplicate runs, if provided
|
description: The idempotency key used to prevent creating duplicate runs, if provided
|
||||||
@@ -1580,6 +1606,27 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
externalId:
|
externalId:
|
||||||
type: string
|
type: string
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
|
example: "America/New_York"
|
||||||
|
description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
|
||||||
|
required:
|
||||||
|
- task
|
||||||
|
- cron
|
||||||
|
- deduplicationKey
|
||||||
|
UpdateScheduleOptions:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
task:
|
||||||
|
type: string
|
||||||
|
cron:
|
||||||
|
type: string
|
||||||
|
externalId:
|
||||||
|
type: string
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
|
example: "America/New_York"
|
||||||
|
description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
|
||||||
required:
|
required:
|
||||||
- task
|
- task
|
||||||
- cron
|
- cron
|
||||||
@@ -1623,6 +1670,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: The description of the generator in plain english
|
description: The description of the generator in plain english
|
||||||
example: Every day at midnight
|
example: Every day at midnight
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
|
example: "America/New_York"
|
||||||
|
description: Defaults to UTC. In IANA format, if set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
|
||||||
nextRun:
|
nextRun:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -1648,6 +1699,445 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
count:
|
count:
|
||||||
type: integer
|
type: integer
|
||||||
|
GetTimezonesResult:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
timezones:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
example:
|
||||||
|
[
|
||||||
|
"UTC",
|
||||||
|
"Africa/Abidjan",
|
||||||
|
"Africa/Accra",
|
||||||
|
"Africa/Addis_Ababa",
|
||||||
|
"Africa/Algiers",
|
||||||
|
"Africa/Asmera",
|
||||||
|
"Africa/Bamako",
|
||||||
|
"Africa/Bangui",
|
||||||
|
"Africa/Banjul",
|
||||||
|
"Africa/Bissau",
|
||||||
|
"Africa/Blantyre",
|
||||||
|
"Africa/Brazzaville",
|
||||||
|
"Africa/Bujumbura",
|
||||||
|
"Africa/Cairo",
|
||||||
|
"Africa/Casablanca",
|
||||||
|
"Africa/Ceuta",
|
||||||
|
"Africa/Conakry",
|
||||||
|
"Africa/Dakar",
|
||||||
|
"Africa/Dar_es_Salaam",
|
||||||
|
"Africa/Djibouti",
|
||||||
|
"Africa/Douala",
|
||||||
|
"Africa/El_Aaiun",
|
||||||
|
"Africa/Freetown",
|
||||||
|
"Africa/Gaborone",
|
||||||
|
"Africa/Harare",
|
||||||
|
"Africa/Johannesburg",
|
||||||
|
"Africa/Juba",
|
||||||
|
"Africa/Kampala",
|
||||||
|
"Africa/Khartoum",
|
||||||
|
"Africa/Kigali",
|
||||||
|
"Africa/Kinshasa",
|
||||||
|
"Africa/Lagos",
|
||||||
|
"Africa/Libreville",
|
||||||
|
"Africa/Lome",
|
||||||
|
"Africa/Luanda",
|
||||||
|
"Africa/Lubumbashi",
|
||||||
|
"Africa/Lusaka",
|
||||||
|
"Africa/Malabo",
|
||||||
|
"Africa/Maputo",
|
||||||
|
"Africa/Maseru",
|
||||||
|
"Africa/Mbabane",
|
||||||
|
"Africa/Mogadishu",
|
||||||
|
"Africa/Monrovia",
|
||||||
|
"Africa/Nairobi",
|
||||||
|
"Africa/Ndjamena",
|
||||||
|
"Africa/Niamey",
|
||||||
|
"Africa/Nouakchott",
|
||||||
|
"Africa/Ouagadougou",
|
||||||
|
"Africa/Porto-Novo",
|
||||||
|
"Africa/Sao_Tome",
|
||||||
|
"Africa/Tripoli",
|
||||||
|
"Africa/Tunis",
|
||||||
|
"Africa/Windhoek",
|
||||||
|
"America/Adak",
|
||||||
|
"America/Anchorage",
|
||||||
|
"America/Anguilla",
|
||||||
|
"America/Antigua",
|
||||||
|
"America/Araguaina",
|
||||||
|
"America/Argentina/La_Rioja",
|
||||||
|
"America/Argentina/Rio_Gallegos",
|
||||||
|
"America/Argentina/Salta",
|
||||||
|
"America/Argentina/San_Juan",
|
||||||
|
"America/Argentina/San_Luis",
|
||||||
|
"America/Argentina/Tucuman",
|
||||||
|
"America/Argentina/Ushuaia",
|
||||||
|
"America/Aruba",
|
||||||
|
"America/Asuncion",
|
||||||
|
"America/Bahia",
|
||||||
|
"America/Bahia_Banderas",
|
||||||
|
"America/Barbados",
|
||||||
|
"America/Belem",
|
||||||
|
"America/Belize",
|
||||||
|
"America/Blanc-Sablon",
|
||||||
|
"America/Boa_Vista",
|
||||||
|
"America/Bogota",
|
||||||
|
"America/Boise",
|
||||||
|
"America/Buenos_Aires",
|
||||||
|
"America/Cambridge_Bay",
|
||||||
|
"America/Campo_Grande",
|
||||||
|
"America/Cancun",
|
||||||
|
"America/Caracas",
|
||||||
|
"America/Catamarca",
|
||||||
|
"America/Cayenne",
|
||||||
|
"America/Cayman",
|
||||||
|
"America/Chicago",
|
||||||
|
"America/Chihuahua",
|
||||||
|
"America/Ciudad_Juarez",
|
||||||
|
"America/Coral_Harbour",
|
||||||
|
"America/Cordoba",
|
||||||
|
"America/Costa_Rica",
|
||||||
|
"America/Creston",
|
||||||
|
"America/Cuiaba",
|
||||||
|
"America/Curacao",
|
||||||
|
"America/Danmarkshavn",
|
||||||
|
"America/Dawson",
|
||||||
|
"America/Dawson_Creek",
|
||||||
|
"America/Denver",
|
||||||
|
"America/Detroit",
|
||||||
|
"America/Dominica",
|
||||||
|
"America/Edmonton",
|
||||||
|
"America/Eirunepe",
|
||||||
|
"America/El_Salvador",
|
||||||
|
"America/Fort_Nelson",
|
||||||
|
"America/Fortaleza",
|
||||||
|
"America/Glace_Bay",
|
||||||
|
"America/Godthab",
|
||||||
|
"America/Goose_Bay",
|
||||||
|
"America/Grand_Turk",
|
||||||
|
"America/Grenada",
|
||||||
|
"America/Guadeloupe",
|
||||||
|
"America/Guatemala",
|
||||||
|
"America/Guayaquil",
|
||||||
|
"America/Guyana",
|
||||||
|
"America/Halifax",
|
||||||
|
"America/Havana",
|
||||||
|
"America/Hermosillo",
|
||||||
|
"America/Indiana/Knox",
|
||||||
|
"America/Indiana/Marengo",
|
||||||
|
"America/Indiana/Petersburg",
|
||||||
|
"America/Indiana/Tell_City",
|
||||||
|
"America/Indiana/Vevay",
|
||||||
|
"America/Indiana/Vincennes",
|
||||||
|
"America/Indiana/Winamac",
|
||||||
|
"America/Indianapolis",
|
||||||
|
"America/Inuvik",
|
||||||
|
"America/Iqaluit",
|
||||||
|
"America/Jamaica",
|
||||||
|
"America/Jujuy",
|
||||||
|
"America/Juneau",
|
||||||
|
"America/Kentucky/Monticello",
|
||||||
|
"America/Kralendijk",
|
||||||
|
"America/La_Paz",
|
||||||
|
"America/Lima",
|
||||||
|
"America/Los_Angeles",
|
||||||
|
"America/Louisville",
|
||||||
|
"America/Lower_Princes",
|
||||||
|
"America/Maceio",
|
||||||
|
"America/Managua",
|
||||||
|
"America/Manaus",
|
||||||
|
"America/Marigot",
|
||||||
|
"America/Martinique",
|
||||||
|
"America/Matamoros",
|
||||||
|
"America/Mazatlan",
|
||||||
|
"America/Mendoza",
|
||||||
|
"America/Menominee",
|
||||||
|
"America/Merida",
|
||||||
|
"America/Metlakatla",
|
||||||
|
"America/Mexico_City",
|
||||||
|
"America/Miquelon",
|
||||||
|
"America/Moncton",
|
||||||
|
"America/Monterrey",
|
||||||
|
"America/Montevideo",
|
||||||
|
"America/Montserrat",
|
||||||
|
"America/Nassau",
|
||||||
|
"America/New_York",
|
||||||
|
"America/Nipigon",
|
||||||
|
"America/Nome",
|
||||||
|
"America/Noronha",
|
||||||
|
"America/North_Dakota/Beulah",
|
||||||
|
"America/North_Dakota/Center",
|
||||||
|
"America/North_Dakota/New_Salem",
|
||||||
|
"America/Ojinaga",
|
||||||
|
"America/Panama",
|
||||||
|
"America/Pangnirtung",
|
||||||
|
"America/Paramaribo",
|
||||||
|
"America/Phoenix",
|
||||||
|
"America/Port-au-Prince",
|
||||||
|
"America/Port_of_Spain",
|
||||||
|
"America/Porto_Velho",
|
||||||
|
"America/Puerto_Rico",
|
||||||
|
"America/Punta_Arenas",
|
||||||
|
"America/Rainy_River",
|
||||||
|
"America/Rankin_Inlet",
|
||||||
|
"America/Recife",
|
||||||
|
"America/Regina",
|
||||||
|
"America/Resolute",
|
||||||
|
"America/Rio_Branco",
|
||||||
|
"America/Santa_Isabel",
|
||||||
|
"America/Santarem",
|
||||||
|
"America/Santiago",
|
||||||
|
"America/Santo_Domingo",
|
||||||
|
"America/Sao_Paulo",
|
||||||
|
"America/Scoresbysund",
|
||||||
|
"America/Sitka",
|
||||||
|
"America/St_Barthelemy",
|
||||||
|
"America/St_Johns",
|
||||||
|
"America/St_Kitts",
|
||||||
|
"America/St_Lucia",
|
||||||
|
"America/St_Thomas",
|
||||||
|
"America/St_Vincent",
|
||||||
|
"America/Swift_Current",
|
||||||
|
"America/Tegucigalpa",
|
||||||
|
"America/Thule",
|
||||||
|
"America/Thunder_Bay",
|
||||||
|
"America/Tijuana",
|
||||||
|
"America/Toronto",
|
||||||
|
"America/Tortola",
|
||||||
|
"America/Vancouver",
|
||||||
|
"America/Whitehorse",
|
||||||
|
"America/Winnipeg",
|
||||||
|
"America/Yakutat",
|
||||||
|
"America/Yellowknife",
|
||||||
|
"Antarctica/Casey",
|
||||||
|
"Antarctica/Davis",
|
||||||
|
"Antarctica/DumontDUrville",
|
||||||
|
"Antarctica/Macquarie",
|
||||||
|
"Antarctica/Mawson",
|
||||||
|
"Antarctica/McMurdo",
|
||||||
|
"Antarctica/Palmer",
|
||||||
|
"Antarctica/Rothera",
|
||||||
|
"Antarctica/Syowa",
|
||||||
|
"Antarctica/Troll",
|
||||||
|
"Antarctica/Vostok",
|
||||||
|
"Arctic/Longyearbyen",
|
||||||
|
"Asia/Aden",
|
||||||
|
"Asia/Almaty",
|
||||||
|
"Asia/Amman",
|
||||||
|
"Asia/Anadyr",
|
||||||
|
"Asia/Aqtau",
|
||||||
|
"Asia/Aqtobe",
|
||||||
|
"Asia/Ashgabat",
|
||||||
|
"Asia/Atyrau",
|
||||||
|
"Asia/Baghdad",
|
||||||
|
"Asia/Bahrain",
|
||||||
|
"Asia/Baku",
|
||||||
|
"Asia/Bangkok",
|
||||||
|
"Asia/Barnaul",
|
||||||
|
"Asia/Beirut",
|
||||||
|
"Asia/Bishkek",
|
||||||
|
"Asia/Brunei",
|
||||||
|
"Asia/Calcutta",
|
||||||
|
"Asia/Chita",
|
||||||
|
"Asia/Choibalsan",
|
||||||
|
"Asia/Colombo",
|
||||||
|
"Asia/Damascus",
|
||||||
|
"Asia/Dhaka",
|
||||||
|
"Asia/Dili",
|
||||||
|
"Asia/Dubai",
|
||||||
|
"Asia/Dushanbe",
|
||||||
|
"Asia/Famagusta",
|
||||||
|
"Asia/Gaza",
|
||||||
|
"Asia/Hebron",
|
||||||
|
"Asia/Hong_Kong",
|
||||||
|
"Asia/Hovd",
|
||||||
|
"Asia/Irkutsk",
|
||||||
|
"Asia/Jakarta",
|
||||||
|
"Asia/Jayapura",
|
||||||
|
"Asia/Jerusalem",
|
||||||
|
"Asia/Kabul",
|
||||||
|
"Asia/Kamchatka",
|
||||||
|
"Asia/Karachi",
|
||||||
|
"Asia/Katmandu",
|
||||||
|
"Asia/Khandyga",
|
||||||
|
"Asia/Krasnoyarsk",
|
||||||
|
"Asia/Kuala_Lumpur",
|
||||||
|
"Asia/Kuching",
|
||||||
|
"Asia/Kuwait",
|
||||||
|
"Asia/Macau",
|
||||||
|
"Asia/Magadan",
|
||||||
|
"Asia/Makassar",
|
||||||
|
"Asia/Manila",
|
||||||
|
"Asia/Muscat",
|
||||||
|
"Asia/Nicosia",
|
||||||
|
"Asia/Novokuznetsk",
|
||||||
|
"Asia/Novosibirsk",
|
||||||
|
"Asia/Omsk",
|
||||||
|
"Asia/Oral",
|
||||||
|
"Asia/Phnom_Penh",
|
||||||
|
"Asia/Pontianak",
|
||||||
|
"Asia/Pyongyang",
|
||||||
|
"Asia/Qatar",
|
||||||
|
"Asia/Qostanay",
|
||||||
|
"Asia/Qyzylorda",
|
||||||
|
"Asia/Rangoon",
|
||||||
|
"Asia/Riyadh",
|
||||||
|
"Asia/Saigon",
|
||||||
|
"Asia/Sakhalin",
|
||||||
|
"Asia/Samarkand",
|
||||||
|
"Asia/Seoul",
|
||||||
|
"Asia/Shanghai",
|
||||||
|
"Asia/Singapore",
|
||||||
|
"Asia/Srednekolymsk",
|
||||||
|
"Asia/Taipei",
|
||||||
|
"Asia/Tashkent",
|
||||||
|
"Asia/Tbilisi",
|
||||||
|
"Asia/Tehran",
|
||||||
|
"Asia/Thimphu",
|
||||||
|
"Asia/Tokyo",
|
||||||
|
"Asia/Tomsk",
|
||||||
|
"Asia/Ulaanbaatar",
|
||||||
|
"Asia/Urumqi",
|
||||||
|
"Asia/Ust-Nera",
|
||||||
|
"Asia/Vientiane",
|
||||||
|
"Asia/Vladivostok",
|
||||||
|
"Asia/Yakutsk",
|
||||||
|
"Asia/Yekaterinburg",
|
||||||
|
"Asia/Yerevan",
|
||||||
|
"Atlantic/Azores",
|
||||||
|
"Atlantic/Bermuda",
|
||||||
|
"Atlantic/Canary",
|
||||||
|
"Atlantic/Cape_Verde",
|
||||||
|
"Atlantic/Faeroe",
|
||||||
|
"Atlantic/Madeira",
|
||||||
|
"Atlantic/Reykjavik",
|
||||||
|
"Atlantic/South_Georgia",
|
||||||
|
"Atlantic/St_Helena",
|
||||||
|
"Atlantic/Stanley",
|
||||||
|
"Australia/Adelaide",
|
||||||
|
"Australia/Brisbane",
|
||||||
|
"Australia/Broken_Hill",
|
||||||
|
"Australia/Currie",
|
||||||
|
"Australia/Darwin",
|
||||||
|
"Australia/Eucla",
|
||||||
|
"Australia/Hobart",
|
||||||
|
"Australia/Lindeman",
|
||||||
|
"Australia/Lord_Howe",
|
||||||
|
"Australia/Melbourne",
|
||||||
|
"Australia/Perth",
|
||||||
|
"Australia/Sydney",
|
||||||
|
"Europe/Amsterdam",
|
||||||
|
"Europe/Andorra",
|
||||||
|
"Europe/Astrakhan",
|
||||||
|
"Europe/Athens",
|
||||||
|
"Europe/Belgrade",
|
||||||
|
"Europe/Berlin",
|
||||||
|
"Europe/Bratislava",
|
||||||
|
"Europe/Brussels",
|
||||||
|
"Europe/Bucharest",
|
||||||
|
"Europe/Budapest",
|
||||||
|
"Europe/Busingen",
|
||||||
|
"Europe/Chisinau",
|
||||||
|
"Europe/Copenhagen",
|
||||||
|
"Europe/Dublin",
|
||||||
|
"Europe/Gibraltar",
|
||||||
|
"Europe/Guernsey",
|
||||||
|
"Europe/Helsinki",
|
||||||
|
"Europe/Isle_of_Man",
|
||||||
|
"Europe/Istanbul",
|
||||||
|
"Europe/Jersey",
|
||||||
|
"Europe/Kaliningrad",
|
||||||
|
"Europe/Kiev",
|
||||||
|
"Europe/Kirov",
|
||||||
|
"Europe/Lisbon",
|
||||||
|
"Europe/Ljubljana",
|
||||||
|
"Europe/London",
|
||||||
|
"Europe/Luxembourg",
|
||||||
|
"Europe/Madrid",
|
||||||
|
"Europe/Malta",
|
||||||
|
"Europe/Mariehamn",
|
||||||
|
"Europe/Minsk",
|
||||||
|
"Europe/Monaco",
|
||||||
|
"Europe/Moscow",
|
||||||
|
"Europe/Oslo",
|
||||||
|
"Europe/Paris",
|
||||||
|
"Europe/Podgorica",
|
||||||
|
"Europe/Prague",
|
||||||
|
"Europe/Riga",
|
||||||
|
"Europe/Rome",
|
||||||
|
"Europe/Samara",
|
||||||
|
"Europe/San_Marino",
|
||||||
|
"Europe/Sarajevo",
|
||||||
|
"Europe/Saratov",
|
||||||
|
"Europe/Simferopol",
|
||||||
|
"Europe/Skopje",
|
||||||
|
"Europe/Sofia",
|
||||||
|
"Europe/Stockholm",
|
||||||
|
"Europe/Tallinn",
|
||||||
|
"Europe/Tirane",
|
||||||
|
"Europe/Ulyanovsk",
|
||||||
|
"Europe/Uzhgorod",
|
||||||
|
"Europe/Vaduz",
|
||||||
|
"Europe/Vatican",
|
||||||
|
"Europe/Vienna",
|
||||||
|
"Europe/Vilnius",
|
||||||
|
"Europe/Volgograd",
|
||||||
|
"Europe/Warsaw",
|
||||||
|
"Europe/Zagreb",
|
||||||
|
"Europe/Zaporozhye",
|
||||||
|
"Europe/Zurich",
|
||||||
|
"Indian/Antananarivo",
|
||||||
|
"Indian/Chagos",
|
||||||
|
"Indian/Christmas",
|
||||||
|
"Indian/Cocos",
|
||||||
|
"Indian/Comoro",
|
||||||
|
"Indian/Kerguelen",
|
||||||
|
"Indian/Mahe",
|
||||||
|
"Indian/Maldives",
|
||||||
|
"Indian/Mauritius",
|
||||||
|
"Indian/Mayotte",
|
||||||
|
"Indian/Reunion",
|
||||||
|
"Pacific/Apia",
|
||||||
|
"Pacific/Auckland",
|
||||||
|
"Pacific/Bougainville",
|
||||||
|
"Pacific/Chatham",
|
||||||
|
"Pacific/Easter",
|
||||||
|
"Pacific/Efate",
|
||||||
|
"Pacific/Enderbury",
|
||||||
|
"Pacific/Fakaofo",
|
||||||
|
"Pacific/Fiji",
|
||||||
|
"Pacific/Funafuti",
|
||||||
|
"Pacific/Galapagos",
|
||||||
|
"Pacific/Gambier",
|
||||||
|
"Pacific/Guadalcanal",
|
||||||
|
"Pacific/Guam",
|
||||||
|
"Pacific/Honolulu",
|
||||||
|
"Pacific/Johnston",
|
||||||
|
"Pacific/Kiritimati",
|
||||||
|
"Pacific/Kosrae",
|
||||||
|
"Pacific/Kwajalein",
|
||||||
|
"Pacific/Majuro",
|
||||||
|
"Pacific/Marquesas",
|
||||||
|
"Pacific/Midway",
|
||||||
|
"Pacific/Nauru",
|
||||||
|
"Pacific/Niue",
|
||||||
|
"Pacific/Norfolk",
|
||||||
|
"Pacific/Noumea",
|
||||||
|
"Pacific/Pago_Pago",
|
||||||
|
"Pacific/Palau",
|
||||||
|
"Pacific/Pitcairn",
|
||||||
|
"Pacific/Ponape",
|
||||||
|
"Pacific/Port_Moresby",
|
||||||
|
"Pacific/Rarotonga",
|
||||||
|
"Pacific/Saipan",
|
||||||
|
"Pacific/Tahiti",
|
||||||
|
"Pacific/Tarawa",
|
||||||
|
"Pacific/Tongatapu",
|
||||||
|
"Pacific/Truk",
|
||||||
|
"Pacific/Wake",
|
||||||
|
"Pacific/Wallis",
|
||||||
|
]
|
||||||
ScheduleEnvironment:
|
ScheduleEnvironment:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1659,7 +2149,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
SerializedError:
|
SerializedError:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- message
|
- message
|
||||||
properties:
|
properties:
|
||||||
message:
|
message:
|
||||||
|
|||||||
+16
-2
@@ -7,13 +7,13 @@ description: "There are some hard and soft limits in v3 that you might hit."
|
|||||||
|
|
||||||
These are the default limits on a free account.
|
These are the default limits on a free account.
|
||||||
|
|
||||||
Before we introduce paid plans in July you can request more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact). If you increase these defaults you may have to subscribe to a paid plan when we introduce them. For more details on the v3 Cloud pricing see the [pricing page](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
|
|
||||||
|
|
||||||
| Limit | Details |
|
| Limit | Details |
|
||||||
| ------------ | ------------------ |
|
| ------------ | ------------------ |
|
||||||
| Organization | 10 concurrent runs |
|
| Organization | 10 concurrent runs |
|
||||||
| Environment | 5 concurrent runs |
|
| Environment | 5 concurrent runs |
|
||||||
|
|
||||||
|
<Snippet file="v3/soft-limit.mdx" />
|
||||||
|
|
||||||
## Rate limits
|
## Rate limits
|
||||||
|
|
||||||
| Limit | Details |
|
| Limit | Details |
|
||||||
@@ -23,3 +23,17 @@ Before we introduce paid plans in July you can request more [on Discord](https:/
|
|||||||
Generally speaking each SDK call is an API call.
|
Generally speaking each SDK call is an API call.
|
||||||
|
|
||||||
The most common cause of hitting the API rate limit is if you're calling `trigger()` on a task in a loop, instead of doing this use `batchTrigger()` which will trigger multiple tasks in a single API call. You can have up to 100 tasks in a single batch trigger call.
|
The most common cause of hitting the API rate limit is if you're calling `trigger()` on a task in a loop, instead of doing this use `batchTrigger()` which will trigger multiple tasks in a single API call. You can have up to 100 tasks in a single batch trigger call.
|
||||||
|
|
||||||
|
## Schedules
|
||||||
|
|
||||||
|
| Limit | Details |
|
||||||
|
| --------- | ------------- |
|
||||||
|
| Schedules | 5 per project |
|
||||||
|
|
||||||
|
When attaching schedules to tasks we strongly recommend you add them in our dashboard if they're "static". That way you can control them easily per environment.
|
||||||
|
|
||||||
|
If you add them dynamically using code make sure you add a `deduplicationKey` so you don't add the same schedule to a task multiple times. If you don't your task will get triggered multiple times, it will cost you more, and you will hit the limit.
|
||||||
|
|
||||||
|
If you're creating schedules for your user you will definitely need to request more schedules from us.
|
||||||
|
|
||||||
|
<Snippet file="v3/soft-limit.mdx" />
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get timezones"
|
||||||
|
openapi: "v3-openapi GET /api/v1/timezones"
|
||||||
|
---
|
||||||
+42
-11
@@ -29,6 +29,16 @@ export const firstScheduledTask = schedules.task({
|
|||||||
//this can be undefined if it's never been run
|
//this can be undefined if it's never been run
|
||||||
console.log(payload.lastTimestamp); //is a Date object or undefined
|
console.log(payload.lastTimestamp); //is a Date object or undefined
|
||||||
|
|
||||||
|
//the timezone the schedule was registered with, defaults to "UTC"
|
||||||
|
//this is in IANA format, e.g. "America/New_York"
|
||||||
|
//See the full list here: https://cloud.trigger.dev/timezones
|
||||||
|
console.log(payload.timezone); //is a string
|
||||||
|
|
||||||
|
//If you want to output the time in the user's timezone do this:
|
||||||
|
const formatted = payload.timestamp.toLocaleString("en-US", {
|
||||||
|
timeZone: payload.timezone,
|
||||||
|
});
|
||||||
|
|
||||||
//the schedule id (you can have many schedules for the same task)
|
//the schedule id (you can have many schedules for the same task)
|
||||||
//using this you can remove the schedule, update it, etc
|
//using this you can remove the schedule, update it, etc
|
||||||
console.log(payload.scheduleId); //is a string
|
console.log(payload.scheduleId); //is a string
|
||||||
@@ -46,8 +56,9 @@ export const firstScheduledTask = schedules.task({
|
|||||||
|
|
||||||
You can see from the comments that the payload has several useful properties:
|
You can see from the comments that the payload has several useful properties:
|
||||||
|
|
||||||
- `timestamp` - the time the task was scheduled to run
|
- `timestamp` - the time the task was scheduled to run, as a UTC date.
|
||||||
- `lastTimestamp` - the time the task was last run
|
- `lastTimestamp` - the time the task was last run, as a UTC date.
|
||||||
|
- `timezone` - the timezone the schedule was registered with, defaults to "UTC". In IANA format, e.g. "America/New_York".
|
||||||
- `scheduleId` - the id of the schedule that triggered the task
|
- `scheduleId` - the id of the schedule that triggered the task
|
||||||
- `externalId` - the external id you (optionally) provided when creating the schedule
|
- `externalId` - the external id you (optionally) provided when creating the schedule
|
||||||
- `upcoming` - the next 5 times the task is scheduled to run
|
- `upcoming` - the next 5 times the task is scheduled to run
|
||||||
@@ -103,6 +114,7 @@ These are the options when creating a schedule:
|
|||||||
| ----------------- | --------------------------------------------------------------------------------------------- |
|
| ----------------- | --------------------------------------------------------------------------------------------- |
|
||||||
| Task | The id of the task you want to attach to. |
|
| Task | The id of the task you want to attach to. |
|
||||||
| CRON pattern | The schedule in CRON format. |
|
| CRON pattern | The schedule in CRON format. |
|
||||||
|
| Timezone | The timezone the schedule will run in. Defaults to "UTC" |
|
||||||
| External id | An optional external id, usually you'd use a userId. |
|
| External id | An optional external id, usually you'd use a userId. |
|
||||||
| Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. |
|
| Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. |
|
||||||
| Environments | The environments this schedule will run in. |
|
| Environments | The environments this schedule will run in. |
|
||||||
@@ -121,6 +133,8 @@ const createdSchedule = await schedules.create({
|
|||||||
task: firstScheduledTask.id,
|
task: firstScheduledTask.id,
|
||||||
//The schedule in CRON format.
|
//The schedule in CRON format.
|
||||||
cron: "0 0 * * *",
|
cron: "0 0 * * *",
|
||||||
|
//this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
|
||||||
|
deduplicationKey: "my-deduplication-key",
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -136,17 +150,21 @@ const createdSchedule = await schedules.create({
|
|||||||
task: firstScheduledTask.id,
|
task: firstScheduledTask.id,
|
||||||
//The schedule in CRON format.
|
//The schedule in CRON format.
|
||||||
cron: "0 0 * * *",
|
cron: "0 0 * * *",
|
||||||
|
// Optional, it defaults to "UTC". In IANA format, e.g. "America/New_York".
|
||||||
|
// In this case, the task will run at midnight every day in New York time.
|
||||||
|
// If you specify a timezone it will automatically work with daylight saving time.
|
||||||
|
timezone: "America/New_York",
|
||||||
//Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
|
//Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
|
||||||
//This allows you to have per-user CRON tasks.
|
//This allows you to have per-user CRON tasks.
|
||||||
externalId: "user_123456",
|
externalId: "user_123456",
|
||||||
//(Optional) You can only create one schedule with this key.
|
//You can only create one schedule with this key.
|
||||||
//If you use it twice, the second call will update the schedule.
|
//If you use it twice, the second call will update the schedule.
|
||||||
//This is useful if you don't want to create duplicate schedules for a user.
|
//This is useful because you don't want to create duplicate schedules for a user.
|
||||||
deduplicationKey: "user_123456-todo_reminder",
|
deduplicationKey: "user_123456-todo_reminder",
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
See [the SDK reference](/v3/management-create-schedule) for full details.
|
See [the SDK reference](/v3/management/schedules/create) for full details.
|
||||||
|
|
||||||
### Dynamic schedules (or multi-tenant schedules)
|
### Dynamic schedules (or multi-tenant schedules)
|
||||||
|
|
||||||
@@ -189,6 +207,8 @@ export async function POST(request: Request) {
|
|||||||
task: reminderTask.id,
|
task: reminderTask.id,
|
||||||
//8am every day
|
//8am every day
|
||||||
cron: "0 8 * * *",
|
cron: "0 8 * * *",
|
||||||
|
//the user's timezone
|
||||||
|
timezone: data.timezone,
|
||||||
//the user id
|
//the user id
|
||||||
externalId: data.userId,
|
externalId: data.userId,
|
||||||
//this makes it impossible to have two reminder schedules for the same user
|
//this makes it impossible to have two reminder schedules for the same user
|
||||||
@@ -228,7 +248,7 @@ You can test a scheduled task in the dashboard. Note that the `scheduleId` will
|
|||||||
const retrievedSchedule = await schedules.retrieve(scheduleId);
|
const retrievedSchedule = await schedules.retrieve(scheduleId);
|
||||||
```
|
```
|
||||||
|
|
||||||
See [the SDK reference](/v3/management-retrieve-schedule) for full details.
|
See [the SDK reference](/v3/management/schedules/retrieve) for full details.
|
||||||
|
|
||||||
### Listing schedules
|
### Listing schedules
|
||||||
|
|
||||||
@@ -236,7 +256,7 @@ See [the SDK reference](/v3/management-retrieve-schedule) for full details.
|
|||||||
const allSchedules = await schedules.list();
|
const allSchedules = await schedules.list();
|
||||||
```
|
```
|
||||||
|
|
||||||
See [the SDK reference](/v3/management-list-schedules) for full details.
|
See [the SDK reference](/v3/management/schedules/list) for full details.
|
||||||
|
|
||||||
### Updating a schedule
|
### Updating a schedule
|
||||||
|
|
||||||
@@ -245,10 +265,11 @@ const updatedSchedule = await schedules.update(scheduleId, {
|
|||||||
task: firstScheduledTask.id,
|
task: firstScheduledTask.id,
|
||||||
cron: "0 0 1 * *",
|
cron: "0 0 1 * *",
|
||||||
externalId: "ext_1234444",
|
externalId: "ext_1234444",
|
||||||
|
deduplicationKey: "my-deduplication-key",
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
See [the SDK reference](/v3/management-update-schedule) for full details.
|
See [the SDK reference](/v3/management/schedules/update) for full details.
|
||||||
|
|
||||||
### Deactivating a schedule
|
### Deactivating a schedule
|
||||||
|
|
||||||
@@ -256,7 +277,7 @@ See [the SDK reference](/v3/management-update-schedule) for full details.
|
|||||||
const deactivatedSchedule = await schedules.deactivate(scheduleId);
|
const deactivatedSchedule = await schedules.deactivate(scheduleId);
|
||||||
```
|
```
|
||||||
|
|
||||||
See [the SDK reference](/v3/management-deactivate-schedule) for full details.
|
See [the SDK reference](/v3/management/schedules/deactivate) for full details.
|
||||||
|
|
||||||
### Activating a schedule
|
### Activating a schedule
|
||||||
|
|
||||||
@@ -264,7 +285,7 @@ See [the SDK reference](/v3/management-deactivate-schedule) for full details.
|
|||||||
const activatedSchedule = await schedules.activate(scheduleId);
|
const activatedSchedule = await schedules.activate(scheduleId);
|
||||||
```
|
```
|
||||||
|
|
||||||
See [the SDK reference](/v3/management-activate-schedule) for full details.
|
See [the SDK reference](/v3/management/schedules/activate) for full details.
|
||||||
|
|
||||||
### Deleting a schedule
|
### Deleting a schedule
|
||||||
|
|
||||||
@@ -272,4 +293,14 @@ See [the SDK reference](/v3/management-activate-schedule) for full details.
|
|||||||
const deletedSchedule = await schedules.del(scheduleId);
|
const deletedSchedule = await schedules.del(scheduleId);
|
||||||
```
|
```
|
||||||
|
|
||||||
See [the SDK reference](/v3/management-delete-schedule) for full details.
|
See [the SDK reference](/v3/management/schedules/delete) for full details.
|
||||||
|
|
||||||
|
### Getting possible timezones
|
||||||
|
|
||||||
|
You might want to show a dropdown menu in your UI so your users can select their timezone. You can get a list of all possible timezones using the SDK:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const timezones = await schedules.timezones();
|
||||||
|
```
|
||||||
|
|
||||||
|
See [the SDK reference](/v3/management/schedules/timezones) for full details.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * from "./apiClient";
|
export * from "./apiClient";
|
||||||
export * from "./apiClient/types";
|
export * from "./apiClient/types";
|
||||||
export * from "./apiClient/pagination";
|
export * from "./apiClient/pagination";
|
||||||
export type { ApiPromise } from "./apiClient/core";
|
export type { ApiPromise, OffsetLimitPagePromise, CursorPagePromise } from "./apiClient/core";
|
||||||
export * from "./apiClient/errors";
|
export * from "./apiClient/errors";
|
||||||
export * from "./clock-api";
|
export * from "./clock-api";
|
||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
|
|||||||
@@ -222,15 +222,26 @@ export const ScheduledTaskPayload = z.object({
|
|||||||
You can use this to remove the schedule, update it, etc */
|
You can use this to remove the schedule, update it, etc */
|
||||||
scheduleId: z.string(),
|
scheduleId: z.string(),
|
||||||
/** When the task was scheduled to run.
|
/** When the task was scheduled to run.
|
||||||
* Note this will be slightly different from `new Date()` because it takes a few ms to run the task. */
|
* Note this will be slightly different from `new Date()` because it takes a few ms to run the task.
|
||||||
|
*
|
||||||
|
* This date is UTC. To output it as a string with a timezone you would do this:
|
||||||
|
* ```ts
|
||||||
|
* const formatted = payload.timestamp.toLocaleString("en-US", {
|
||||||
|
timeZone: payload.timezone,
|
||||||
|
});
|
||||||
|
``` */
|
||||||
timestamp: z.date(),
|
timestamp: z.date(),
|
||||||
/** When the task was last run (it has been).
|
/** When the task was last run (it has been).
|
||||||
This can be undefined if it's never been run */
|
This can be undefined if it's never been run. This date is UTC. */
|
||||||
lastTimestamp: z.date().optional(),
|
lastTimestamp: z.date().optional(),
|
||||||
/** You can optionally provide an external id when creating the schedule.
|
/** You can optionally provide an external id when creating the schedule.
|
||||||
Usually you would use a userId or some other unique identifier.
|
Usually you would use a userId or some other unique identifier.
|
||||||
This defaults to undefined if you didn't provide one. */
|
This defaults to undefined if you didn't provide one. */
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
|
/** The IANA timezone the schedule is set to. The default is UTC.
|
||||||
|
* You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
|
||||||
|
*/
|
||||||
|
timezone: z.string(),
|
||||||
/** The next 5 dates this task is scheduled to run */
|
/** The next 5 dates this task is scheduled to run */
|
||||||
upcoming: z.array(z.date()),
|
upcoming: z.array(z.date()),
|
||||||
});
|
});
|
||||||
@@ -257,20 +268,31 @@ export const CreateScheduleOptions = z.object({
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
cron: z.string(),
|
cron: z.string(),
|
||||||
/** (Optional) You can only create one schedule with this key. If you use it twice, the second call will update the schedule.
|
/** You can only create one schedule with this key. If you use it twice, the second call will update the schedule.
|
||||||
*
|
*
|
||||||
* This is useful if you don't want to create duplicate schedules for a user. */
|
* This is required to prevent you from creating duplicate schedules. */
|
||||||
deduplicationKey: z.string().optional(),
|
deduplicationKey: z.string(),
|
||||||
/** Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
|
/** Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
|
||||||
*
|
*
|
||||||
* This allows you to have per-user CRON tasks.
|
* This allows you to have per-user CRON tasks.
|
||||||
*/
|
*/
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
|
/** Optionally, you can specify a timezone in the IANA format. If unset it will use UTC.
|
||||||
|
* If specified then the CRON will be evaluated in that timezone and will respect daylight savings.
|
||||||
|
*
|
||||||
|
* If you set the CRON to `0 0 * * *` and the timezone to `America/New_York` then the task will run at midnight in New York time, no matter whether it's daylight savings or not.
|
||||||
|
*
|
||||||
|
* You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
|
||||||
|
*
|
||||||
|
* @example "America/New_York", "Europe/London", "Asia/Tokyo", "Africa/Cairo"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
timezone: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateScheduleOptions = z.infer<typeof CreateScheduleOptions>;
|
export type CreateScheduleOptions = z.infer<typeof CreateScheduleOptions>;
|
||||||
|
|
||||||
export const UpdateScheduleOptions = CreateScheduleOptions;
|
export const UpdateScheduleOptions = CreateScheduleOptions.omit({ deduplicationKey: true });
|
||||||
|
|
||||||
export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>;
|
export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>;
|
||||||
|
|
||||||
@@ -289,6 +311,7 @@ export const ScheduleObject = z.object({
|
|||||||
deduplicationKey: z.string().nullish(),
|
deduplicationKey: z.string().nullish(),
|
||||||
externalId: z.string().nullish(),
|
externalId: z.string().nullish(),
|
||||||
generator: ScheduleGenerator,
|
generator: ScheduleGenerator,
|
||||||
|
timezone: z.string(),
|
||||||
nextRun: z.coerce.date().nullish(),
|
nextRun: z.coerce.date().nullish(),
|
||||||
environments: z.array(
|
environments: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -325,6 +348,12 @@ export const ListScheduleOptions = z.object({
|
|||||||
|
|
||||||
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
|
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
|
||||||
|
|
||||||
|
export const TimezonesResult = z.object({
|
||||||
|
timezones: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TimezonesResult = z.infer<typeof TimezonesResult>;
|
||||||
|
|
||||||
export const RunStatus = z.enum([
|
export const RunStatus = z.enum([
|
||||||
/// Task hasn't been deployed yet but is waiting to be executed
|
/// Task hasn't been deployed yet but is waiting to be executed
|
||||||
"WAITING_FOR_DEPLOY",
|
"WAITING_FOR_DEPLOY",
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Organization" ADD COLUMN "maximumScheduleInstancesLimit" INTEGER NOT NULL DEFAULT 20;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `maximumScheduleInstancesLimit` on the `Organization` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Organization" DROP COLUMN "maximumScheduleInstancesLimit",
|
||||||
|
ADD COLUMN "maximumSchedulesLimit" INTEGER NOT NULL DEFAULT 5;
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TaskSchedule" ADD COLUMN "timezone" TEXT;
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `timezone` on the `TaskSchedule` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TaskSchedule" DROP COLUMN "timezone";
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TaskSchedule" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'UTC';
|
||||||
@@ -111,6 +111,7 @@ model Organization {
|
|||||||
|
|
||||||
maximumExecutionTimePerRunInMs Int @default(900000) // 15 minutes
|
maximumExecutionTimePerRunInMs Int @default(900000) // 15 minutes
|
||||||
maximumConcurrencyLimit Int @default(10)
|
maximumConcurrencyLimit Int @default(10)
|
||||||
|
maximumSchedulesLimit Int @default(5)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -2208,8 +2209,12 @@ model TaskSchedule {
|
|||||||
generatorExpression String
|
generatorExpression String
|
||||||
generatorDescription String @default("")
|
generatorDescription String @default("")
|
||||||
generatorType ScheduleGeneratorType @default(CRON)
|
generatorType ScheduleGeneratorType @default(CRON)
|
||||||
|
|
||||||
|
/// These are IANA format string, or the default "UTC". E.g. "America/New_York"
|
||||||
|
timezone String @default("UTC")
|
||||||
|
|
||||||
///Can be provided by the user then accessed inside a run
|
///Can be provided by the user then accessed inside a run
|
||||||
externalId String?
|
externalId String?
|
||||||
|
|
||||||
///Instances of the schedule that are active
|
///Instances of the schedule that are active
|
||||||
instances TaskScheduleInstance[]
|
instances TaskScheduleInstance[]
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
ReplayRunResponse,
|
ReplayRunResponse,
|
||||||
RetrieveRunResponse,
|
RetrieveRunResponse,
|
||||||
apiClientManager,
|
apiClientManager,
|
||||||
|
CursorPagePromise,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
import type { ListProjectRunsQueryParams, ListRunsQueryParams } from "@trigger.dev/core/v3";
|
import type { ListProjectRunsQueryParams, ListRunsQueryParams } from "@trigger.dev/core/v3";
|
||||||
import { apiClientMissingError } from "./shared";
|
import { apiClientMissingError } from "./shared";
|
||||||
import { CursorPagePromise } from "@trigger.dev/core/v3/apiClient/core";
|
|
||||||
|
|
||||||
export type RetrieveRunResult = RetrieveRunResponse;
|
export type RetrieveRunResult = RetrieveRunResponse;
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import {
|
|||||||
ApiPromise,
|
ApiPromise,
|
||||||
DeletedScheduleObject,
|
DeletedScheduleObject,
|
||||||
InitOutput,
|
InitOutput,
|
||||||
|
OffsetLimitPagePromise,
|
||||||
ScheduleObject,
|
ScheduleObject,
|
||||||
|
TimezonesResult,
|
||||||
apiClientManager,
|
apiClientManager,
|
||||||
taskCatalog,
|
taskCatalog,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
|
import { zodfetch } from "@trigger.dev/core/v3/zodfetch";
|
||||||
import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared";
|
import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared";
|
||||||
import * as SchedulesAPI from "./api";
|
import * as SchedulesAPI from "./api";
|
||||||
import { OffsetLimitPagePromise } from "@trigger.dev/core/v3/apiClient/core";
|
|
||||||
|
|
||||||
export function task<TOutput, TInitOutput extends InitOutput>(
|
export function task<TOutput, TInitOutput extends InitOutput>(
|
||||||
params: TaskOptions<SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput>
|
params: TaskOptions<SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput>
|
||||||
@@ -27,6 +29,7 @@ export function task<TOutput, TInitOutput extends InitOutput>(
|
|||||||
* @param options
|
* @param options
|
||||||
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
|
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
|
||||||
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
|
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
|
||||||
|
* @param options.timezone - An optional timezone for the schedule in the IANA format (e.g. `America/Los_Angeles`). Defaults to "UTC".
|
||||||
* @param options.externalId - An optional external identifier for the schedule
|
* @param options.externalId - An optional external identifier for the schedule
|
||||||
* @param options.deduplicationKey - An optional deduplication key for the schedule
|
* @param options.deduplicationKey - An optional deduplication key for the schedule
|
||||||
* @returns The created schedule
|
* @returns The created schedule
|
||||||
@@ -62,6 +65,7 @@ export function retrieve(scheduleId: string): ApiPromise<ScheduleObject> {
|
|||||||
* @param options - The updated schedule options
|
* @param options - The updated schedule options
|
||||||
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
|
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
|
||||||
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
|
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
|
||||||
|
* @param options.timezone - An optional timezone for the schedule in the IANA format (e.g. `America/Los_Angeles`). Defaults to "UTC".
|
||||||
* @param options.externalId - An optional external identifier for the schedule
|
* @param options.externalId - An optional external identifier for the schedule
|
||||||
* @returns The updated schedule
|
* @returns The updated schedule
|
||||||
*/
|
*/
|
||||||
@@ -138,3 +142,26 @@ export function list(
|
|||||||
|
|
||||||
return apiClient.listSchedules(options);
|
return apiClient.listSchedules(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the possible timezones we support
|
||||||
|
* @param excludeUtc - By default "UTC" is included and is first. If true, "UTC" will be excluded.
|
||||||
|
*/
|
||||||
|
export function timezones(options?: { excludeUtc?: boolean }) {
|
||||||
|
const baseUrl = apiClientManager.baseURL;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw apiClientMissingError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodfetch(
|
||||||
|
TimezonesResult,
|
||||||
|
`${baseUrl}/api/v1/timezones${options?.excludeUtc === true ? "?excludeUtc=true" : ""}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,8 +273,8 @@ async function doTriggerUnfriendlyTaskId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// doRuns().catch(console.error);
|
// doRuns().catch(console.error);
|
||||||
// doListRuns().catch(console.error);
|
doListRuns().catch(console.error);
|
||||||
// doScheduleLists().catch(console.error);
|
// doScheduleLists().catch(console.error);
|
||||||
// doSchedules().catch(console.error);
|
// doSchedules().catch(console.error);
|
||||||
// doEnvVars().catch(console.error);
|
// doEnvVars().catch(console.error);
|
||||||
doTriggerUnfriendlyTaskId().catch(console.error);
|
// doTriggerUnfriendlyTaskId().catch(console.error);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { logger, schedules } from "@trigger.dev/sdk/v3";
|
import { logger, schedules, task } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
export const firstScheduledTask = schedules.task({
|
export const firstScheduledTask = schedules.task({
|
||||||
id: "first-scheduled-task",
|
id: "first-scheduled-task",
|
||||||
@@ -6,6 +6,50 @@ export const firstScheduledTask = schedules.task({
|
|||||||
const distanceInMs =
|
const distanceInMs =
|
||||||
payload.timestamp.getTime() - (payload.lastTimestamp ?? new Date()).getTime();
|
payload.timestamp.getTime() - (payload.lastTimestamp ?? new Date()).getTime();
|
||||||
|
|
||||||
|
logger.log(payload.timezone);
|
||||||
|
|
||||||
logger.log("First scheduled tasks", { payload, distanceInMs });
|
logger.log("First scheduled tasks", { payload, distanceInMs });
|
||||||
|
|
||||||
|
const formatted = payload.timestamp.toLocaleString("en-US", {
|
||||||
|
timeZone: payload.timezone,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log(formatted);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const manageSchedules = task({
|
||||||
|
id: "manage-schedules",
|
||||||
|
run: async (payload) => {
|
||||||
|
const createdSchedule = await schedules.create({
|
||||||
|
//The id of the scheduled task you want to attach to.
|
||||||
|
task: firstScheduledTask.id,
|
||||||
|
//The schedule in CRON format.
|
||||||
|
cron: "* * * * *",
|
||||||
|
deduplicationKey: `create-schedule-1718277290717`,
|
||||||
|
timezone: "Asia/Tokyo",
|
||||||
|
});
|
||||||
|
logger.log("Created schedule", createdSchedule);
|
||||||
|
|
||||||
|
const editedSchedule = await schedules.update(createdSchedule.id, {
|
||||||
|
//The id of the scheduled task you want to attach to.
|
||||||
|
task: firstScheduledTask.id,
|
||||||
|
//The schedule in CRON format.
|
||||||
|
cron: "* * * * *",
|
||||||
|
timezone: "Europe/Athens",
|
||||||
|
});
|
||||||
|
logger.log("Edited schedule", editedSchedule);
|
||||||
|
|
||||||
|
const sched = await schedules.retrieve(createdSchedule.id);
|
||||||
|
logger.log("Retrieved schedule", sched);
|
||||||
|
|
||||||
|
const allSchedules = await schedules.list();
|
||||||
|
logger.log("All schedules", { allSchedules });
|
||||||
|
|
||||||
|
const { timezones } = await schedules.timezones();
|
||||||
|
logger.log("Timezones", { timezones });
|
||||||
|
|
||||||
|
const withoutUtc = await schedules.timezones({ excludeUtc: true });
|
||||||
|
logger.log("Timezones without UTC", { withoutUtc });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user