More work on the org template page (added workflows list)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Automatically pickup on the TRIGGER_WSS_URL for the wss endpoint
|
||||
@@ -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 (
|
||||
<List>
|
||||
{workflows.map((workflow) => {
|
||||
return (
|
||||
<li key={workflow.id}>
|
||||
<Link
|
||||
to={`/orgs/${currentOrganizationSlug}/workflows/${workflow.slug}`}
|
||||
className={classNames(
|
||||
"relative block overflow-hidden transition hover:bg-slate-850/40",
|
||||
workflow.status === "DISABLED" ? workflowDisabled : ""
|
||||
)}
|
||||
>
|
||||
{workflow.lastRun === undefined && (
|
||||
<div className="absolute top-2 -right-8 rotate-45 bg-green-700 px-8 py-0.5 text-xs font-semibold uppercase tracking-wide text-green-200 shadow-md">
|
||||
New
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col flex-wrap justify-between py-4 pl-4 pr-4 lg:flex-row lg:flex-nowrap lg:items-center">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="relative flex items-center">
|
||||
{workflow.status === "CREATED" && (
|
||||
<ExclamationTriangleIcon className="absolute -top-1.5 -left-1.5 h-6 w-6 text-amber-400" />
|
||||
)}
|
||||
<div className="mr-4 h-20 w-20 flex-shrink-0 self-start rounded-md bg-slate-850 p-3">
|
||||
<TriggerTypeIcon
|
||||
type={workflow.trigger.type}
|
||||
provider={workflow.integrations.source}
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-1 flex flex-col gap-1 truncate">
|
||||
<Header2
|
||||
size="regular"
|
||||
className="truncate text-slate-200"
|
||||
>
|
||||
{workflow.title}
|
||||
</Header2>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<PillLabel label={workflow.trigger.typeTitle} />
|
||||
<Header3
|
||||
size="extra-small"
|
||||
className="truncate text-slate-400"
|
||||
>
|
||||
{workflow.trigger.title}
|
||||
</Header3>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-baseline gap-x-3">
|
||||
{workflow.trigger.properties &&
|
||||
workflow.trigger.properties.map((property) => (
|
||||
<WorkflowProperty
|
||||
key={property.key}
|
||||
label={property.key}
|
||||
content={`${property.value}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
className="ml-5 h-5 w-5 shrink-0 text-slate-400 lg:hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center lg:flex-grow-0">
|
||||
<div className="mt-2 flex w-full flex-wrap-reverse items-center justify-between gap-3 lg:mt-0 lg:justify-end">
|
||||
<div className="flex flex-col text-left lg:text-right">
|
||||
<Body size="extra-small" className="text-slate-500">
|
||||
Last run: {lastRunDescription(workflow.lastRun)}
|
||||
</Body>
|
||||
<Body size="extra-small" className="text-slate-500">
|
||||
{workflow.slug}
|
||||
</Body>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.integrations.source && (
|
||||
<ApiLogoIcon
|
||||
integration={workflow.integrations.source}
|
||||
size="regular"
|
||||
/>
|
||||
)}
|
||||
{workflow.integrations.services.map((service) => {
|
||||
if (service === undefined) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ApiLogoIcon
|
||||
size="regular"
|
||||
key={service.slug}
|
||||
integration={service}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
className="ml-5 hidden h-5 w-5 shrink-0 text-slate-400 lg:block"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<span className="rounded bg-slate-700 px-1.5 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowProperty({
|
||||
label,
|
||||
content,
|
||||
}: {
|
||||
label: string;
|
||||
content: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-baseline gap-x-1">
|
||||
<Body size="extra-small" className="uppercase text-slate-500">
|
||||
{label}
|
||||
</Body>
|
||||
<Body size="small" className="truncate text-slate-400">
|
||||
{content}
|
||||
</Body>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const workflowDisabled = "opacity-30";
|
||||
@@ -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<WorkflowListPresenter["data"]>
|
||||
@@ -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<Workflow, "type" | "eventNames">,
|
||||
externalSource?: Pick<ExternalSource, "service" | "source">,
|
||||
schedulerSource?: Pick<SchedulerSource, "schedule">,
|
||||
internalSource?: Pick<InternalSource, "type" | "source">
|
||||
): {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<WorkflowsPresenter["data"]>
|
||||
>[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<Workflow, "type" | "eventNames">,
|
||||
externalSource?: Pick<ExternalSource, "service" | "source">,
|
||||
schedulerSource?: Pick<SchedulerSource, "schedule">,
|
||||
internalSource?: Pick<InternalSource, "type" | "source">
|
||||
): {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<typeof loader>();
|
||||
const { workflows } = useTypedLoaderData<typeof loader>();
|
||||
const currentOrganization = useCurrentOrganization();
|
||||
if (currentOrganization === undefined) {
|
||||
return <></>;
|
||||
@@ -83,163 +68,3 @@ export default function Page() {
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowList({
|
||||
workflows,
|
||||
currentOrganizationSlug,
|
||||
}: {
|
||||
workflows: WorkflowListItem[];
|
||||
currentOrganizationSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<List>
|
||||
{workflows.map((workflow) => {
|
||||
return (
|
||||
<li key={workflow.id}>
|
||||
<Link
|
||||
to={`/orgs/${currentOrganizationSlug}/workflows/${workflow.slug}`}
|
||||
className={classNames(
|
||||
"relative block overflow-hidden transition hover:bg-slate-850/40",
|
||||
workflow.status === "DISABLED" ? workflowDisabled : ""
|
||||
)}
|
||||
>
|
||||
{workflow.lastRun === undefined && (
|
||||
<div className="absolute top-2 -right-8 rotate-45 bg-green-700 px-8 py-0.5 text-xs font-semibold uppercase tracking-wide text-green-200 shadow-md">
|
||||
New
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col flex-wrap justify-between py-4 pl-4 pr-4 lg:flex-row lg:flex-nowrap lg:items-center">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="relative flex items-center">
|
||||
{workflow.status === "CREATED" && (
|
||||
<ExclamationTriangleIcon className="absolute -top-1.5 -left-1.5 h-6 w-6 text-amber-400" />
|
||||
)}
|
||||
<div className="mr-4 h-20 w-20 flex-shrink-0 self-start rounded-md bg-slate-850 p-3">
|
||||
<TriggerTypeIcon
|
||||
type={workflow.trigger.type}
|
||||
provider={workflow.integrations.source}
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-1 flex flex-col gap-1 truncate">
|
||||
<Header2
|
||||
size="regular"
|
||||
className="truncate text-slate-200"
|
||||
>
|
||||
{workflow.title}
|
||||
</Header2>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<PillLabel label={workflow.trigger.typeTitle} />
|
||||
<Header3
|
||||
size="extra-small"
|
||||
className="truncate text-slate-400"
|
||||
>
|
||||
{workflow.trigger.title}
|
||||
</Header3>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-baseline gap-x-3">
|
||||
{workflow.trigger.properties &&
|
||||
workflow.trigger.properties.map((property) => (
|
||||
<WorkflowProperty
|
||||
key={property.key}
|
||||
label={property.key}
|
||||
content={`${property.value}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
className="ml-5 h-5 w-5 shrink-0 text-slate-400 lg:hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center lg:flex-grow-0">
|
||||
<div className="mt-2 flex w-full flex-wrap-reverse items-center justify-between gap-3 lg:mt-0 lg:justify-end">
|
||||
<div className="flex flex-col text-left lg:text-right">
|
||||
<Body size="extra-small" className="text-slate-500">
|
||||
Last run: {lastRunDescription(workflow.lastRun)}
|
||||
</Body>
|
||||
<Body size="extra-small" className="text-slate-500">
|
||||
{workflow.slug}
|
||||
</Body>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.integrations.source && (
|
||||
<ApiLogoIcon
|
||||
integration={workflow.integrations.source}
|
||||
size="regular"
|
||||
/>
|
||||
)}
|
||||
{workflow.integrations.services.map((service) => {
|
||||
if (service === undefined) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ApiLogoIcon
|
||||
size="regular"
|
||||
key={service.slug}
|
||||
integration={service}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
className="ml-5 hidden h-5 w-5 shrink-0 text-slate-400 lg:block"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<span className="rounded bg-slate-700 px-1.5 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowProperty({
|
||||
label,
|
||||
content,
|
||||
}: {
|
||||
label: string;
|
||||
content: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-baseline gap-x-1">
|
||||
<Body size="extra-small" className="uppercase text-slate-500">
|
||||
{label}
|
||||
</Body>
|
||||
<Body size="small" className="truncate text-slate-400">
|
||||
{content}
|
||||
</Body>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const workflowDisabled = "opacity-30";
|
||||
|
||||
+126
-47
@@ -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<typeof loader>;
|
||||
|
||||
export default function TemplatePage() {
|
||||
const { organizationTemplate } = useTypedLoaderData<typeof loader>();
|
||||
const loaderData = useTypedLoaderData<typeof loader>();
|
||||
|
||||
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 = (
|
||||
<OrganizationTemplateByStatus {...loaderData} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Header1>{organizationTemplate.template.title}</Header1>
|
||||
<Header1>{loaderData.organizationTemplate.template.title}</Header1>
|
||||
<br />
|
||||
|
||||
{/* Output a loading spinner until the deploy happens */}
|
||||
{organizationTemplate.status === "CREATED" ? (
|
||||
<div className="flex justify-center">
|
||||
<div className="h-32 w-32 animate-spin rounded-full border-b-2 border-slate-50"></div>
|
||||
</div>
|
||||
) : (
|
||||
<OrganizationTemplateReadyToDeploy
|
||||
organizationTemplate={organizationTemplate}
|
||||
/>
|
||||
)}
|
||||
{organizationTemplateByStatus}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function OrganizationTemplateReadyToDeploy({
|
||||
organizationTemplate,
|
||||
}: {
|
||||
organizationTemplate: OrganizationTemplate;
|
||||
}) {
|
||||
function OrganizationTemplateByStatus(loaderData: LoaderData) {
|
||||
if (
|
||||
loaderData.organizationTemplate.status === "PENDING" ||
|
||||
loaderData.organizationTemplate.status === "CREATED"
|
||||
) {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="h-32 w-32 animate-spin rounded-full border-b-2 border-slate-50"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <OrganizationTemplateReady {...loaderData} />;
|
||||
}
|
||||
|
||||
function OrganizationTemplateReady(loaderData: LoaderData) {
|
||||
return (
|
||||
<div>
|
||||
<p>Organization Template ready to deploy</p>
|
||||
<TemplateHeader organizationTemplate={loaderData.organizationTemplate} />
|
||||
|
||||
<dl className="space-y-2">
|
||||
<dt className="font-bold">Repo URL</dt>
|
||||
<dd>
|
||||
<a href={organizationTemplate.repositoryUrl} target="_blank">
|
||||
{organizationTemplate.repositoryUrl}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt className="font-bold">Is Private</dt>
|
||||
<dd>{organizationTemplate.private ? "Yes" : "No"}</dd>
|
||||
</dl>
|
||||
<DeploySection {...loaderData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TemplateHeader({
|
||||
organizationTemplate,
|
||||
}: {
|
||||
organizationTemplate: LoaderData["organizationTemplate"];
|
||||
}) {
|
||||
return (
|
||||
<dl className="space-y-2">
|
||||
<dt className="font-bold">Repo URL</dt>
|
||||
<dd>
|
||||
<a href={organizationTemplate.repositoryUrl} target="_blank">
|
||||
{organizationTemplate.repositoryUrl}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt className="font-bold">Is Private</dt>
|
||||
<dd>{organizationTemplate.private ? "Yes" : "No"}</dd>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<a
|
||||
href={`https://render.com/deploy?repo=${organizationTemplate.repositoryUrl}`}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://render.com/images/deploy-to-render-button.svg"
|
||||
alt="Deploy to Render"
|
||||
/>
|
||||
</a>
|
||||
<div className="flex justify-center">
|
||||
<div className="h-32 w-32 animate-spin rounded-full border-b-2 border-slate-50"></div>
|
||||
</div>
|
||||
<div className="relative select-all overflow-hidden rounded-sm border border-slate-800 p-1 pl-2 text-sm text-slate-400">
|
||||
<span className="pointer-events-none absolute right-7 top-0 block h-6 w-20 bg-gradient-to-r from-transparent to-slate-950"></span>
|
||||
<CopyText
|
||||
value={apiKey}
|
||||
className="group absolute right-0 top-0 flex h-full w-7 items-center justify-center rounded-sm border-l border-slate-800 bg-slate-950 transition hover:cursor-pointer hover:bg-slate-900 active:bg-green-900"
|
||||
>
|
||||
<ClipboardDocumentCheckIcon className="h-5 w-5 group-active:text-green-500" />
|
||||
</CopyText>
|
||||
{apiKey}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
Deployed, view workflows here:
|
||||
</div>
|
||||
|
||||
<WorkflowList
|
||||
workflows={workflows}
|
||||
currentOrganizationSlug={currentOrganization.slug}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,8 @@ function createWebhooks() {
|
||||
{
|
||||
id: payload.repository.id,
|
||||
},
|
||||
{}
|
||||
{},
|
||||
{ deliverAfter: 1000 * 10 }
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -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;
|
||||
@@ -641,6 +641,4 @@ enum OrganizationTemplateStatus {
|
||||
CREATED
|
||||
READY_TO_DEPLOY
|
||||
DEPLOYED
|
||||
READY_TO_TEST
|
||||
READY_TO_RUN
|
||||
}
|
||||
|
||||
@@ -84,7 +84,10 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user