From cef0bd172d379cf74656a916bae3e42efe39d2bd Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Jul 2023 14:43:48 +0100 Subject: [PATCH] Scheduled Triggers tab --- .../ScheduledTriggersPresenter.server.ts | 83 +++++++++ .../route.tsx | 163 ++++++++++++++++++ .../schedules/nextScheduledEvent.server.ts | 2 +- 3 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 apps/webapp/app/presenters/ScheduledTriggersPresenter.server.ts create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx diff --git a/apps/webapp/app/presenters/ScheduledTriggersPresenter.server.ts b/apps/webapp/app/presenters/ScheduledTriggersPresenter.server.ts new file mode 100644 index 000000000..353c1b5ff --- /dev/null +++ b/apps/webapp/app/presenters/ScheduledTriggersPresenter.server.ts @@ -0,0 +1,83 @@ +import { User } from "@trigger.dev/database"; +import { ScheduleMetadataSchema } from "@trigger.dev/internal"; +import { PrismaClient, prisma } from "~/db.server"; +import { Organization } from "~/models/organization.server"; +import { Project } from "~/models/project.server"; +import { calculateNextScheduledEvent } from "~/services/schedules/nextScheduledEvent.server"; + +export class ScheduledTriggersPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ + userId, + projectSlug, + organizationSlug, + }: { + userId: User["id"]; + projectSlug: Project["slug"]; + organizationSlug: Organization["slug"]; + }) { + const scheduled = await this.#prismaClient.scheduleSource.findMany({ + select: { + id: true, + key: true, + active: true, + schedule: true, + lastEventTimestamp: true, + environment: { + select: { + type: true, + }, + }, + createdAt: true, + updatedAt: true, + metadata: true, + dynamicTrigger: true, + }, + where: { + environment: { + OR: [ + { + orgMember: null, + }, + { + orgMember: { + userId, + }, + }, + ], + organization: { + slug: organizationSlug, + members: { + some: { + userId, + }, + }, + }, + project: { + slug: projectSlug, + }, + }, + }, + }); + + return { + scheduled: scheduled.map((s) => { + const schedule = ScheduleMetadataSchema.parse(s.schedule); + const nextEventTimestamp = s.active + ? calculateNextScheduledEvent(schedule, s.lastEventTimestamp) + : undefined; + + return { + ...s, + schedule, + nextEventTimestamp, + }; + }), + }; + } +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx new file mode 100644 index 000000000..a45414da0 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx @@ -0,0 +1,163 @@ +import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; +import type { LoaderArgs } from "@remix-run/server-runtime"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { DateTime } from "~/components/primitives/DateTime"; +import { LabelValueStack } from "~/components/primitives/LabelValueStack"; +import { NamedIcon } from "~/components/primitives/NamedIcon"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { + Table, + TableBlankRow, + TableBody, + TableCell, + TableCellChevron, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { ScheduledTriggersPresenter } from "~/presenters/ScheduledTriggersPresenter.server"; +import { requireUser } from "~/services/session.server"; +import { cn } from "~/utils/cn"; +import { Handle } from "~/utils/handle"; +import { + ProjectParamSchema, + externalTriggerPath, + trimTrailingSlash, +} from "~/utils/pathBuilder"; + +export const loader = async ({ request, params }: LoaderArgs) => { + const user = await requireUser(request); + const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + + const presenter = new ScheduledTriggersPresenter(); + const data = await presenter.call({ + userId: user.id, + organizationSlug, + projectSlug: projectParam, + }); + + return typedjson(data); +}; + +export const handle: Handle = { + breadcrumb: (match) => ( + + ), + expandSidebar: true, +}; + +export default function Integrations() { + const { scheduled } = useTypedLoaderData(); + const organization = useOrganization(); + const project = useProject(); + + return ( + <> + + A Scheduled Trigger runs a Job on a repeated schedule. The schedule can + use a CRON expression or an interval. + + + + + ID + Schedule + Dynamic + Last run + Next run + Environment + Active + + + + {scheduled.length > 0 ? ( + scheduled.map((t) => { + return ( + + {t.key} + +
+ {t.schedule.type === "cron" ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ + {t.dynamicTrigger ? ( + + + {t.dynamicTrigger.slug} + + ) : ( + + )} + + + {t.lastEventTimestamp ? ( + + ) : ( + "–" + )} + + + {t.nextEventTimestamp ? ( + + ) : ( + "–" + )} + + + + + + + + {t.active ? ( + + ) : ( + + )} + +
+ ); + }) + ) : ( + + No Scheduled triggers + + )} +
+
+ + ); +} diff --git a/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts b/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts index e41466039..c71d274f5 100644 --- a/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts +++ b/apps/webapp/app/services/schedules/nextScheduledEvent.server.ts @@ -81,7 +81,7 @@ export class NextScheduledEventService { // this should always return a date in the future // if calculateNextStep returns a date in the past, call it again with the result as the previousTimestamp -function calculateNextScheduledEvent( +export function calculateNextScheduledEvent( schedule: ScheduleMetadata, previousTimestamp?: Date | null ): Date {