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 (
);
}
+
+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 (
+ <>
+
+
+
+
+
+
+
+
+
+ {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