diff --git a/.changeset/spotty-walls-flow.md b/.changeset/spotty-walls-flow.md new file mode 100644 index 000000000..5122f128c --- /dev/null +++ b/.changeset/spotty-walls-flow.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sdk": patch +--- + +Automatically pickup on the TRIGGER_WSS_URL for the wss endpoint diff --git a/apps/webapp/app/components/workflows/workflowList.tsx b/apps/webapp/app/components/workflows/workflowList.tsx new file mode 100644 index 000000000..e60e5f534 --- /dev/null +++ b/apps/webapp/app/components/workflows/workflowList.tsx @@ -0,0 +1,174 @@ +import { + ExclamationTriangleIcon, + ChevronRightIcon, +} from "@heroicons/react/24/outline"; +import { Link } from "@remix-run/react"; +import { Body } from "~/components/primitives/text/Body"; +import classNames from "classnames"; +import { WorkflowListItem } from "~/models/workflowListPresenter.server"; +import { formatDateTime } from "~/utils"; +import { ApiLogoIcon } from "../code/ApiLogoIcon"; +import { List } from "../layout/List"; +import { Header2, Header3 } from "../primitives/text/Headers"; +import { runStatusLabel } from "../runs/runStatus"; +import { TriggerTypeIcon } from "../triggers/TriggerIcons"; + +export function WorkflowList({ + workflows, + currentOrganizationSlug, +}: { + workflows: WorkflowListItem[]; + currentOrganizationSlug: string; +}) { + return ( + + {workflows.map((workflow) => { + return ( +
  • + + {workflow.lastRun === undefined && ( +
    + New +
    + )} + +
    +
    +
    + {workflow.status === "CREATED" && ( + + )} +
    + +
    +
    + + {workflow.title} + +
    + + + {workflow.trigger.title} + +
    +
    + {workflow.trigger.properties && + workflow.trigger.properties.map((property) => ( + + ))} +
    +
    +
    +
    +
    +
    +
    + + Last run: {lastRunDescription(workflow.lastRun)} + + + {workflow.slug} + +
    +
    + {workflow.integrations.source && ( + + )} + {workflow.integrations.services.map((service) => { + if (service === undefined) { + return null; + } + return ( + + ); + })} +
    +
    +
    +
    + +
  • + ); + })} +
    + ); +} + +function lastRunDescription(lastRun: WorkflowListItem["lastRun"]) { + if (lastRun === null || lastRun === undefined) { + return "Never"; + } + + if (lastRun.status === "SUCCESS") { + if (lastRun.finishedAt) { + return formatDateTime(lastRun.finishedAt); + } else { + return "Unknown"; + } + } + + return runStatusLabel(lastRun.status); +} + +function PillLabel({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function WorkflowProperty({ + label, + content, +}: { + label: string; + content: string; +}) { + return ( +
    + + {label} + + + {content} + +
    + ); +} + +const workflowDisabled = "opacity-30"; diff --git a/apps/webapp/app/models/workflowListPresenter.server.ts b/apps/webapp/app/models/workflowListPresenter.server.ts index 29f602e65..d2a72f622 100644 --- a/apps/webapp/app/models/workflowListPresenter.server.ts +++ b/apps/webapp/app/models/workflowListPresenter.server.ts @@ -1,18 +1,8 @@ -import type { SchedulerSource, InternalSource } from ".prisma/client"; -import { - ScheduleSourceSchema, - SlackInteractionSourceSchema, -} from "@trigger.dev/common-schemas"; -import cronstrue from "cronstrue"; -import type { DisplayProperties } from "@trigger.dev/integration-sdk"; -import * as github from "@trigger.dev/github/internal"; import invariant from "tiny-invariant"; -import { triggerLabel } from "~/components/triggers/triggerLabel"; import type { PrismaClient } from "~/db.server"; import { prisma } from "~/db.server"; -import { getIntegrationMetadata, getIntegrations } from "./integrations.server"; +import { WorkflowsPresenter } from "~/presenters/workflowsPresenter.server"; import { getRuntimeEnvironment } from "./runtimeEnvironment.server"; -import type { ExternalSource, Workflow } from "./workflow.server"; export type WorkflowListItem = Awaited< ReturnType @@ -38,250 +28,11 @@ export class WorkflowListPresenter { }); invariant(runtimeEnvironment, "Runtime environment not found"); - const workflows = await getWorkflows( - this.#prismaClient, - organizationSlug, + const workflowsPresenter = new WorkflowsPresenter(); + + return workflowsPresenter.data( + { organization: { slug: organizationSlug }, isArchived: false }, runtimeEnvironment.id ); - const integrations = getIntegrations(true); - - return workflows.map((workflow) => { - const lastRun = - workflow.runs[0] === undefined - ? undefined - : { - finishedAt: workflow.runs[0].finishedAt, - status: workflow.runs[0].status, - }; - - return { - id: workflow.id, - title: workflow.title, - slug: workflow.slug, - status: workflow.status, - trigger: triggerProperties( - workflow, - workflow.externalSource ?? undefined, - workflow.schedulerSources[0] ?? undefined, - workflow.internalSources[0] ?? undefined - ), - integrations: { - source: workflow.service - ? getIntegrationMetadata(integrations, workflow.service) - : undefined, - services: workflow.externalServices.map((service) => - getIntegrationMetadata(integrations, service.service) - ), - }, - lastRun, - }; - }); - } -} - -function getWorkflows( - prismaClient: PrismaClient, - organizationSlug: string, - environmentId: string -) { - return prismaClient.workflow.findMany({ - where: { organization: { slug: organizationSlug }, isArchived: false }, - include: { - externalServices: { - select: { - service: true, - }, - }, - externalSource: { - select: { - service: true, - source: true, - }, - }, - schedulerSources: { - select: { - schedule: true, - }, - where: { - environmentId, - }, - orderBy: { createdAt: "desc" }, - take: 1, - }, - internalSources: { - select: { - source: true, - type: true, - }, - where: { - environmentId, - }, - orderBy: { createdAt: "desc" }, - take: 1, - }, - runs: { - select: { - finishedAt: true, - status: true, - }, - take: 1, - orderBy: { finishedAt: { sort: "desc", nulls: "last" } }, - }, - }, - orderBy: [ - { disabledAt: { sort: "asc", nulls: "first" } }, - { title: "asc" }, - ], - }); -} - -function triggerProperties( - workflow: Pick, - externalSource?: Pick, - schedulerSource?: Pick, - internalSource?: Pick -): { - type: Workflow["type"]; - typeTitle: string; - title: string; - properties?: DisplayProperties["properties"]; -} { - switch (workflow.type) { - case "WEBHOOK": { - invariant(externalSource, "External source is required for webhook"); - - let displayProperties: DisplayProperties; - switch (externalSource.service) { - case "github": - if (github.internalIntegration.webhooks) { - displayProperties = - github.internalIntegration.webhooks?.displayProperties( - externalSource.source - ); - } else { - displayProperties = { - title: externalSource.service, - }; - } - break; - default: - displayProperties = { - title: externalSource.service, - }; - break; - } - - return { - type: workflow.type, - typeTitle: "Webhook", - title: displayProperties.title, - properties: displayProperties.properties, - }; - } - case "SCHEDULE": { - if (!schedulerSource) { - return { - type: workflow.type, - typeTitle: "Schedule", - title: "Not configured", - }; - } - - const source = ScheduleSourceSchema.parse(schedulerSource.schedule); - - if ("rateOf" in source) { - const unit = - "minutes" in source.rateOf - ? source.rateOf.minutes > 1 - ? "minutes" - : "minute" - : "hours" in source.rateOf - ? source.rateOf.hours > 1 - ? "hours" - : "hour" - : source.rateOf.days > 1 - ? "days" - : "day"; - - const value = - "minutes" in source.rateOf - ? source.rateOf.minutes - : "hours" in source.rateOf - ? source.rateOf.hours - : source.rateOf.days; - - return { - type: workflow.type, - typeTitle: "Schedule", - title: `Every ${value} ${unit}`, - }; - } else { - return { - type: workflow.type, - typeTitle: "Schedule", - title: cronstrue.toString(source.cron, { - throwExceptionOnParseError: false, - verbose: false, - use24HourTimeFormat: true, - }), - properties: [{ key: "Cron Expression", value: source.cron }], - }; - } - } - case "CUSTOM_EVENT": - return { - type: workflow.type, - typeTitle: "Custom event", - title: `on: ${workflow.eventNames.join(", ")}`, - }; - case "SLACK_INTERACTION": { - if (!internalSource) { - return { - type: workflow.type, - typeTitle: "Slack interaction", - title: "on: Slack interaction", - }; - } - - const slackSource = SlackInteractionSourceSchema.safeParse( - internalSource.source - ); - - if (!slackSource.success) { - return { - type: workflow.type, - typeTitle: "Slack interaction", - title: "on: Slack interaction", - }; - } - - const title = - slackSource.data.type === "block_action" - ? `block_id = ${slackSource.data.blockId}` - : `callback_id = ${slackSource.data.callbackIds.join(", ")}`; - - return { - type: workflow.type, - typeTitle: "Slack interaction", - title: title, - properties: - slackSource.data.type === "block_action" && - slackSource.data.actionIds.length > 0 - ? [ - { - key: "Action ID", - value: slackSource.data.actionIds.join(", "), - }, - ] - : undefined, - }; - } - default: { - return { - type: workflow.type, - typeTitle: triggerLabel(workflow.type), - title: workflow.type, - }; - } } } diff --git a/apps/webapp/app/presenters/organizationTemplatePresenter.server.ts b/apps/webapp/app/presenters/organizationTemplatePresenter.server.ts new file mode 100644 index 000000000..61dea80a0 --- /dev/null +++ b/apps/webapp/app/presenters/organizationTemplatePresenter.server.ts @@ -0,0 +1,55 @@ +import type { PrismaClient } from "~/db.server"; +import { prisma } from "~/db.server"; +import { getRuntimeEnvironment } from "~/models/runtimeEnvironment.server"; +import { WorkflowsPresenter } from "./workflowsPresenter.server"; + +export class OrganizationTemplatePresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + async data(templateId: string, environmentSlug: string) { + const organizationTemplate = + await this.#prismaClient.organizationTemplate.findUnique({ + where: { + id: templateId, + }, + include: { + template: true, + }, + }); + + if (!organizationTemplate) { + throw new Error("Organization template not found"); + } + + const runtimeEnvironment = await getRuntimeEnvironment({ + organizationId: organizationTemplate.organizationId, + slug: environmentSlug, + }); + + if (!runtimeEnvironment) { + throw new Error("Runtime environment not found"); + } + + const workflowsPresenter = new WorkflowsPresenter(this.#prismaClient); + + const workflows = await workflowsPresenter.data( + { + organizationId: organizationTemplate.organizationId, + slug: { + in: organizationTemplate.template.workflowIds, + }, + }, + runtimeEnvironment.id + ); + + return { + organizationTemplate, + apiKey: runtimeEnvironment.apiKey, + workflows, + }; + } +} diff --git a/apps/webapp/app/presenters/workflowsPresenter.server.ts b/apps/webapp/app/presenters/workflowsPresenter.server.ts new file mode 100644 index 000000000..6239fddda --- /dev/null +++ b/apps/webapp/app/presenters/workflowsPresenter.server.ts @@ -0,0 +1,277 @@ +import type { SchedulerSource, InternalSource } from ".prisma/client"; +import { + ScheduleSourceSchema, + SlackInteractionSourceSchema, +} from "@trigger.dev/common-schemas"; +import cronstrue from "cronstrue"; +import type { DisplayProperties } from "@trigger.dev/integration-sdk"; +import * as github from "@trigger.dev/github/internal"; +import invariant from "tiny-invariant"; +import { triggerLabel } from "~/components/triggers/triggerLabel"; +import type { PrismaClient } from "~/db.server"; +import { prisma, Prisma } from "~/db.server"; +import { + getIntegrationMetadata, + getIntegrations, +} from "../models/integrations.server"; +import type { ExternalSource, Workflow } from "../models/workflow.server"; + +export type WorkflowListItem = Awaited< + ReturnType +>[number]; + +export class WorkflowsPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + async data(whereInput: Prisma.WorkflowWhereInput, environmentId: string) { + const workflows = await getWorkflows( + this.#prismaClient, + whereInput, + environmentId + ); + const integrations = getIntegrations(true); + + return workflows.map((workflow) => { + const lastRun = + workflow.runs[0] === undefined + ? undefined + : { + finishedAt: workflow.runs[0].finishedAt, + status: workflow.runs[0].status, + }; + + return { + id: workflow.id, + title: workflow.title, + slug: workflow.slug, + status: workflow.status, + trigger: triggerProperties( + workflow, + workflow.externalSource ?? undefined, + workflow.schedulerSources[0] ?? undefined, + workflow.internalSources[0] ?? undefined + ), + integrations: { + source: workflow.service + ? getIntegrationMetadata(integrations, workflow.service) + : undefined, + services: workflow.externalServices.map((service) => + getIntegrationMetadata(integrations, service.service) + ), + }, + lastRun, + }; + }); + } +} + +function getWorkflows( + prismaClient: PrismaClient, + whereInput: Prisma.WorkflowWhereInput, + environmentId: string +) { + return prismaClient.workflow.findMany({ + where: whereInput, + include: { + externalServices: { + select: { + service: true, + }, + }, + externalSource: { + select: { + service: true, + source: true, + }, + }, + schedulerSources: { + select: { + schedule: true, + }, + where: { + environmentId, + }, + orderBy: { createdAt: "desc" }, + take: 1, + }, + internalSources: { + select: { + source: true, + type: true, + }, + where: { + environmentId, + }, + orderBy: { createdAt: "desc" }, + take: 1, + }, + runs: { + select: { + finishedAt: true, + status: true, + }, + take: 1, + orderBy: { finishedAt: { sort: "desc", nulls: "last" } }, + }, + }, + orderBy: [ + { disabledAt: { sort: "asc", nulls: "first" } }, + { title: "asc" }, + ], + }); +} + +function triggerProperties( + workflow: Pick, + externalSource?: Pick, + schedulerSource?: Pick, + internalSource?: Pick +): { + type: Workflow["type"]; + typeTitle: string; + title: string; + properties?: DisplayProperties["properties"]; +} { + switch (workflow.type) { + case "WEBHOOK": { + invariant(externalSource, "External source is required for webhook"); + + let displayProperties: DisplayProperties; + switch (externalSource.service) { + case "github": + if (github.internalIntegration.webhooks) { + displayProperties = + github.internalIntegration.webhooks?.displayProperties( + externalSource.source + ); + } else { + displayProperties = { + title: externalSource.service, + }; + } + break; + default: + displayProperties = { + title: externalSource.service, + }; + break; + } + + return { + type: workflow.type, + typeTitle: "Webhook", + title: displayProperties.title, + properties: displayProperties.properties, + }; + } + case "SCHEDULE": { + if (!schedulerSource) { + return { + type: workflow.type, + typeTitle: "Schedule", + title: "Not configured", + }; + } + + const source = ScheduleSourceSchema.parse(schedulerSource.schedule); + + if ("rateOf" in source) { + const unit = + "minutes" in source.rateOf + ? source.rateOf.minutes > 1 + ? "minutes" + : "minute" + : "hours" in source.rateOf + ? source.rateOf.hours > 1 + ? "hours" + : "hour" + : source.rateOf.days > 1 + ? "days" + : "day"; + + const value = + "minutes" in source.rateOf + ? source.rateOf.minutes + : "hours" in source.rateOf + ? source.rateOf.hours + : source.rateOf.days; + + return { + type: workflow.type, + typeTitle: "Schedule", + title: `Every ${value} ${unit}`, + }; + } else { + return { + type: workflow.type, + typeTitle: "Schedule", + title: cronstrue.toString(source.cron, { + throwExceptionOnParseError: false, + verbose: false, + use24HourTimeFormat: true, + }), + properties: [{ key: "Cron Expression", value: source.cron }], + }; + } + } + case "CUSTOM_EVENT": + return { + type: workflow.type, + typeTitle: "Custom event", + title: `on: ${workflow.eventNames.join(", ")}`, + }; + case "SLACK_INTERACTION": { + if (!internalSource) { + return { + type: workflow.type, + typeTitle: "Slack interaction", + title: "on: Slack interaction", + }; + } + + const slackSource = SlackInteractionSourceSchema.safeParse( + internalSource.source + ); + + if (!slackSource.success) { + return { + type: workflow.type, + typeTitle: "Slack interaction", + title: "on: Slack interaction", + }; + } + + const title = + slackSource.data.type === "block_action" + ? `block_id = ${slackSource.data.blockId}` + : `callback_id = ${slackSource.data.callbackIds.join(", ")}`; + + return { + type: workflow.type, + typeTitle: "Slack interaction", + title: title, + properties: + slackSource.data.type === "block_action" && + slackSource.data.actionIds.length > 0 + ? [ + { + key: "Action ID", + value: slackSource.data.actionIds.join(", "), + }, + ] + : undefined, + }; + } + default: { + return { + type: workflow.type, + typeTitle: triggerLabel(workflow.type), + title: workflow.type, + }; + } + } +} diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx index 99ecc6854..590eb6c26 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx @@ -1,44 +1,29 @@ -import { - ChevronRightIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/solid"; -import { Link } from "@remix-run/react"; import type { LoaderArgs } from "@remix-run/server-runtime"; -import classNames from "classnames"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import invariant from "tiny-invariant"; -import { ApiLogoIcon } from "~/components/code/ApiLogoIcon"; import { CreateNewWorkflow } from "~/components/CreateNewWorkflow"; import { Container } from "~/components/layout/Container"; -import { List } from "~/components/layout/List"; import { PanelInfo } from "~/components/layout/PanelInfo"; import { PrimaryLink } from "~/components/primitives/Buttons"; -import { Body } from "~/components/primitives/text/Body"; -import { Header2, Header3 } from "~/components/primitives/text/Headers"; import { SubTitle } from "~/components/primitives/text/SubTitle"; import { Title } from "~/components/primitives/text/Title"; -import { runStatusLabel } from "~/components/runs/runStatus"; -import { TriggerTypeIcon } from "~/components/triggers/TriggerIcons"; +import { WorkflowList } from "~/components/workflows/workflowList"; import { useCurrentOrganization } from "~/hooks/useOrganizations"; -import { getIntegrationMetadatas } from "~/models/integrations.server"; import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server"; -import type { WorkflowListItem } from "~/models/workflowListPresenter.server"; import { WorkflowListPresenter } from "~/models/workflowListPresenter.server"; import { requireUserId } from "~/services/session.server"; -import { formatDateTime } from "~/utils"; export const loader = async ({ request, params }: LoaderArgs) => { await requireUserId(request); invariant(params.organizationSlug, "Organization slug is required"); - const providers = getIntegrationMetadatas(false); const currentEnv = await getRuntimeEnvironmentFromRequest(request); const presenter = new WorkflowListPresenter(); try { const workflows = await presenter.data(params.organizationSlug, currentEnv); - return typedjson({ workflows, providers }); + return typedjson({ workflows }); } catch (error: any) { console.error(error); throw new Response("Error ", { status: 400 }); @@ -46,7 +31,7 @@ export const loader = async ({ request, params }: LoaderArgs) => { }; export default function Page() { - const { workflows, providers } = useTypedLoaderData(); + const { workflows } = useTypedLoaderData(); const currentOrganization = useCurrentOrganization(); if (currentOrganization === undefined) { return <>; @@ -83,163 +68,3 @@ export default function Page() { ); } - -function WorkflowList({ - workflows, - currentOrganizationSlug, -}: { - workflows: WorkflowListItem[]; - currentOrganizationSlug: string; -}) { - return ( - - {workflows.map((workflow) => { - return ( -
  • - - {workflow.lastRun === undefined && ( -
    - New -
    - )} - -
    -
    -
    - {workflow.status === "CREATED" && ( - - )} -
    - -
    -
    - - {workflow.title} - -
    - - - {workflow.trigger.title} - -
    -
    - {workflow.trigger.properties && - workflow.trigger.properties.map((property) => ( - - ))} -
    -
    -
    -
    -
    -
    -
    - - Last run: {lastRunDescription(workflow.lastRun)} - - - {workflow.slug} - -
    -
    - {workflow.integrations.source && ( - - )} - {workflow.integrations.services.map((service) => { - if (service === undefined) { - return null; - } - return ( - - ); - })} -
    -
    -
    -
    - -
  • - ); - })} -
    - ); -} - -function lastRunDescription(lastRun: WorkflowListItem["lastRun"]) { - if (lastRun === null || lastRun === undefined) { - return "Never"; - } - - if (lastRun.status === "SUCCESS") { - if (lastRun.finishedAt) { - return formatDateTime(lastRun.finishedAt); - } else { - return "Unknown"; - } - } - - return runStatusLabel(lastRun.status); -} - -function PillLabel({ label }: { label: string }) { - return ( - - {label} - - ); -} - -function WorkflowProperty({ - label, - content, -}: { - label: string; - content: string; -}) { - return ( -
    - - {label} - - - {content} - -
    - ); -} - -const workflowDisabled = "opacity-30"; diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/templates/$templateId.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/templates/$templateId.tsx index 38a74d94a..94d1913fb 100644 --- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/templates/$templateId.tsx +++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/templates/$templateId.tsx @@ -1,34 +1,38 @@ +import { ClipboardDocumentCheckIcon } from "@heroicons/react/24/outline"; +import { useRevalidator } from "@remix-run/react"; import { LoaderArgs } from "@remix-run/server-runtime"; -import { useTypedLoaderData } from "remix-typedjson"; -import invariant from "tiny-invariant"; +import { useEffect } from "react"; +import { + typedjson, + UseDataFunctionReturn, + useTypedLoaderData, +} from "remix-typedjson"; +import { useEventSource } from "remix-utils"; +import { CopyText } from "~/components/CopyText"; import { Container } from "~/components/layout/Container"; import { Header1 } from "~/components/primitives/text/Headers"; -import { prisma } from "~/db.server"; -import { useEventSource } from "remix-utils"; -import { useRevalidator } from "@remix-run/react"; -import { useEffect } from "react"; -import { OrganizationTemplate } from ".prisma/client"; +import { WorkflowList } from "~/components/workflows/workflowList"; +import { useCurrentOrganization } from "~/hooks/useOrganizations"; +import { getRuntimeEnvironmentFromRequest } from "~/models/runtimeEnvironment.server"; +import { OrganizationTemplatePresenter } from "~/presenters/organizationTemplatePresenter.server"; -export async function loader({ params }: LoaderArgs) { - const organizationTemplate = await prisma.organizationTemplate.findUnique({ - where: { - id: params.templateId, - }, - include: { - template: true, - }, - }); +export async function loader({ params, request }: LoaderArgs) { + const currentEnv = await getRuntimeEnvironmentFromRequest(request); - invariant(organizationTemplate, "Template not found"); + const presenter = new OrganizationTemplatePresenter(); - return { organizationTemplate }; + return typedjson( + await presenter.data(params.templateId as string, currentEnv) + ); } +type LoaderData = UseDataFunctionReturn; + export default function TemplatePage() { - const { organizationTemplate } = useTypedLoaderData(); + const loaderData = useTypedLoaderData(); const events = useEventSource( - `/resources/organizationTemplates/${organizationTemplate.id}` + `/resources/organizationTemplates/${loaderData.organizationTemplate.id}` ); const revalidator = useRevalidator(); @@ -38,45 +42,120 @@ export default function TemplatePage() { } }, [events]); + const organizationTemplateByStatus = ( + + ); + return ( - {organizationTemplate.template.title} + {loaderData.organizationTemplate.template.title}
    - {/* Output a loading spinner until the deploy happens */} - {organizationTemplate.status === "CREATED" ? ( -
    -
    -
    - ) : ( - - )} + {organizationTemplateByStatus}
    ); } -function OrganizationTemplateReadyToDeploy({ - organizationTemplate, -}: { - organizationTemplate: OrganizationTemplate; -}) { +function OrganizationTemplateByStatus(loaderData: LoaderData) { + if ( + loaderData.organizationTemplate.status === "PENDING" || + loaderData.organizationTemplate.status === "CREATED" + ) { + return ( +
    +
    +
    + ); + } + + return ; +} + +function OrganizationTemplateReady(loaderData: LoaderData) { return (

    Organization Template ready to deploy

    + -
    -
    Repo URL
    -
    - - {organizationTemplate.repositoryUrl} - -
    - -
    Is Private
    -
    {organizationTemplate.private ? "Yes" : "No"}
    -
    +
    ); } + +function TemplateHeader({ + organizationTemplate, +}: { + organizationTemplate: LoaderData["organizationTemplate"]; +}) { + return ( +
    +
    Repo URL
    +
    + + {organizationTemplate.repositoryUrl} + +
    + +
    Is Private
    +
    {organizationTemplate.private ? "Yes" : "No"}
    +
    + ); +} + +function DeploySection({ + organizationTemplate, + apiKey, + workflows, +}: { + organizationTemplate: LoaderData["organizationTemplate"]; + apiKey: string; + workflows: LoaderData["workflows"]; +}) { + const currentOrganization = useCurrentOrganization(); + + if (!currentOrganization) { + return null; + } + + if (organizationTemplate.status === "READY_TO_DEPLOY") { + return ( + <> + + Deploy to Render + +
    +
    +
    +
    + + + + + {apiKey} +
    + + ); + } else { + return ( + <> +
    + Deployed, view workflows here: +
    + + + + ); + } +} diff --git a/apps/webapp/app/services/github/githubApp.server.ts b/apps/webapp/app/services/github/githubApp.server.ts index cff98e4dc..dc8b1c65c 100644 --- a/apps/webapp/app/services/github/githubApp.server.ts +++ b/apps/webapp/app/services/github/githubApp.server.ts @@ -98,7 +98,8 @@ function createWebhooks() { { id: payload.repository.id, }, - {} + {}, + { deliverAfter: 1000 * 10 } ); } ); diff --git a/apps/webapp/app/services/messageBroker.server.ts b/apps/webapp/app/services/messageBroker.server.ts index 5ee23fe94..d9af1be19 100644 --- a/apps/webapp/app/services/messageBroker.server.ts +++ b/apps/webapp/app/services/messageBroker.server.ts @@ -483,6 +483,10 @@ const taskQueueCatalog = { data: z.object({ id: z.number() }), properties: z.object({}), }, + WORKFLOW_CREATED: { + data: z.object({ id: z.string() }), + properties: z.object({}), + }, }; function createTaskQueue() { @@ -837,6 +841,17 @@ function createTaskQueue() { await service.call(data.id); + return true; + }, + WORKFLOW_CREATED: async (id, data, properties, attributes) => { + if (attributes.redeliveryCount >= 4) { + return true; + } + + const service = new WorkflowCreated(); + + await service.call(data.id); + return true; }, }, @@ -859,6 +874,7 @@ export { taskQueue, requestTaskQueue, appEventPublisher }; import { ZodEventSubscriber } from "internal-platform"; import { EventEmitter } from "stream"; +import { WorkflowCreated } from "./workflows/events/workflowCreated.server"; export async function createEventEmitter({ id, diff --git a/apps/webapp/app/services/workflows/events/workflowCreated.server.ts b/apps/webapp/app/services/workflows/events/workflowCreated.server.ts new file mode 100644 index 000000000..219f69014 --- /dev/null +++ b/apps/webapp/app/services/workflows/events/workflowCreated.server.ts @@ -0,0 +1,76 @@ +import { env } from "process"; +import type { PrismaClient } from "~/db.server"; +import { prisma } from "~/db.server"; +import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server"; +import { appEventPublisher } from "~/services/messageBroker.server"; + +export class WorkflowCreated { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call(id: string) { + const workflow = await this.#prismaClient.workflow.findUnique({ + where: { id }, + }); + + if (!workflow) { + return; + } + + await this.#sendInternalEvent(workflow.id); + + const orgTemplates = await this.#prismaClient.organizationTemplate.findMany( + { + where: { + organizationId: workflow.organizationId, + template: { + workflowIds: { + has: workflow.slug, + }, + }, + status: "READY_TO_DEPLOY", + }, + include: { + template: true, + }, + } + ); + + for (const orgTemplate of orgTemplates) { + await this.#prismaClient.organizationTemplate.update({ + where: { id: orgTemplate.id }, + data: { + status: "DEPLOYED", + }, + }); + + await appEventPublisher.publish( + "organization-template.updated", + { + id: orgTemplate.id, + status: "DEPLOYED", + }, + { + "x-organization-template-id": orgTemplate.id, + } + ); + } + } + + async #sendInternalEvent(id: string) { + if (!env.INTERNAL_TRIGGER_API_KEY) { + return true; + } + + const ingestEventService = new IngestCustomEvent(); + + await ingestEventService.call({ + id, + event: { name: "workflow.created", payload: { id: id } }, + apiKey: env.INTERNAL_TRIGGER_API_KEY, + }); + } +} diff --git a/apps/webapp/app/services/workflows/registerWorkflow.server.ts b/apps/webapp/app/services/workflows/registerWorkflow.server.ts index bdd5089f2..f6e24a6eb 100644 --- a/apps/webapp/app/services/workflows/registerWorkflow.server.ts +++ b/apps/webapp/app/services/workflows/registerWorkflow.server.ts @@ -7,7 +7,7 @@ import { prisma } from "~/db.server"; import type { Organization } from "~/models/organization.server"; import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server"; import type { Workflow } from "~/models/workflow.server"; -import { taskQueue } from "../messageBroker.server"; +import { appEventPublisher, taskQueue } from "../messageBroker.server"; export class RegisterWorkflow { #prismaClient: PrismaClient; @@ -166,12 +166,8 @@ export class RegisterWorkflow { }); if (!existingWorkflow) { - await taskQueue.publish("SEND_INTERNAL_EVENT", { + await taskQueue.publish("WORKFLOW_CREATED", { id: workflow.id, - name: "workflow.created", - payload: { - id: workflow.id, - }, }); } diff --git a/apps/webapp/prisma/migrations/20230213154802_simplify_org_template_status/migration.sql b/apps/webapp/prisma/migrations/20230213154802_simplify_org_template_status/migration.sql new file mode 100644 index 000000000..ec215ac55 --- /dev/null +++ b/apps/webapp/prisma/migrations/20230213154802_simplify_org_template_status/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - The values [READY_TO_TEST,READY_TO_RUN] on the enum `OrganizationTemplateStatus` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "OrganizationTemplateStatus_new" AS ENUM ('PENDING', 'CREATED', 'READY_TO_DEPLOY', 'DEPLOYED'); +ALTER TABLE "OrganizationTemplate" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "OrganizationTemplate" ALTER COLUMN "status" TYPE "OrganizationTemplateStatus_new" USING ("status"::text::"OrganizationTemplateStatus_new"); +ALTER TYPE "OrganizationTemplateStatus" RENAME TO "OrganizationTemplateStatus_old"; +ALTER TYPE "OrganizationTemplateStatus_new" RENAME TO "OrganizationTemplateStatus"; +DROP TYPE "OrganizationTemplateStatus_old"; +ALTER TABLE "OrganizationTemplate" ALTER COLUMN "status" SET DEFAULT 'PENDING'; +COMMIT; diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma index 35fc43a96..042e44d39 100644 --- a/apps/webapp/prisma/schema.prisma +++ b/apps/webapp/prisma/schema.prisma @@ -641,6 +641,4 @@ enum OrganizationTemplateStatus { CREATED READY_TO_DEPLOY DEPLOYED - READY_TO_TEST - READY_TO_RUN } diff --git a/packages/trigger-sdk/src/client.ts b/packages/trigger-sdk/src/client.ts index f4dd9e0bc..19cf3aac4 100644 --- a/packages/trigger-sdk/src/client.ts +++ b/packages/trigger-sdk/src/client.ts @@ -84,7 +84,10 @@ export class TriggerClient { } this.#apiKey = apiKey; - this.#endpoint = this.#options.endpoint ?? "wss://wss.trigger.dev/ws"; + this.#endpoint = + this.#options.endpoint ?? + process.env.TRIGGER_WSS_URL ?? + "wss://wss.trigger.dev/ws"; this.#logger = new Logger( ["trigger.dev", this.#options.id], this.#options.logLevel