Compare commits

...

114 Commits

Author SHA1 Message Date
Eric Allam f275b47755 Fixed pnpm lock 2023-07-11 11:31:42 +01:00
Eric Allam cfb7481d7e Merge pull request #152 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-11 11:22:55 +01:00
github-actions[bot] 57fcce90d9 chore: Update version for release (next) 2023-07-11 10:22:13 +00:00
Eric Allam 2c0ea0c108 Published packages now work with 16.8 and above 2023-07-11 11:20:47 +01:00
Eric Allam e81317b72c Loosen the @trigger.dev/nextjs next version 2023-07-11 11:04:21 +01:00
James Ritchie 946e429906 Merge pull request #151 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-10 13:44:20 +01:00
github-actions[bot] 2e5a401ff7 chore: Update version for release (next) 2023-07-10 12:42:38 +00:00
James Ritchie b8b84d8d16 CLI: Improved the endpoint prompt 2023-07-10 13:41:04 +01:00
Matt Aitken 039777bf63 Merge pull request #150 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-10 10:59:15 +01:00
github-actions[bot] 3547ec13c3 chore: Update version for release (next) 2023-07-10 09:58:47 +00:00
Matt Aitken 3763577451 CLI dev: improved spinner and made port clear in logs 2023-07-10 10:57:10 +01:00
Matt Aitken c6618049f2 Merge pull request #149 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-10 10:35:07 +01:00
github-actions[bot] 28d6aa9885 chore: Update version for release (next) 2023-07-10 09:33:29 +00:00
Matt Aitken 568786e4f6 Fix when setting the url as an option 2023-07-10 10:32:07 +01:00
Eric Allam 38614ee526 Update pnpm lockfile 2023-07-09 20:43:27 +01:00
Eric Allam 15ad6bb5f8 Merge pull request #148 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-09 20:41:41 +01:00
github-actions[bot] 15f431ada6 chore: Update version for release (next) 2023-07-09 18:48:48 +00:00
Matt Aitken 5163acc6ef Plain changed to @latest
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-07-09 19:47:32 +01:00
Matt Aitken f9297d783a Added Plain to the integration catalog 2023-07-08 22:17:58 +01:00
Eric Allam c34a02c06f Improved OpenAI integration task errors
And also now allowing runTask.onError to also return an Error by itself if it wants to not retry the task
2023-07-08 22:14:27 +01:00
Eric Allam 61ed1fb202 Added support for output properties on tasks 2023-07-08 22:14:27 +01:00
Eric Allam 87638ba6c5 Merge pull request #147 from owen-duncan-snobel/main
Update README.md
2023-07-07 21:51:36 +01:00
owen-duncan-snobel 26b4100bb0 Update README.md
Use the permanent link to the discord
2023-07-07 15:26:34 -04:00
Matt Aitken 1157c2adf6 Merge pull request #144 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-07 19:05:26 +01:00
github-actions[bot] 8613d3288b chore: Update version for release (next) 2023-07-07 18:05:13 +00:00
Matt Aitken ab008fbdff Typeform example formId is optional 2023-07-07 19:03:17 +01:00
Matt Aitken 0b30197280 Set schema for passing in the formId 2023-07-07 19:03:17 +01:00
Matt Aitken 89a121f21e Typeform SDK getAllResponses fixed 2023-07-07 19:03:17 +01:00
Matt Aitken 045a8c56af Subtasks are now ordered correctly 2023-07-07 19:03:17 +01:00
Eric Allam d83c11db8c Add back in npm-run-all
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-07-07 16:39:29 +01:00
Eric Allam 7efe14b835 Logging in via github should work even after changing your email address 2023-07-07 16:34:41 +01:00
Matt Aitken 37116aecf9 Latest lockfile 2023-07-07 15:37:51 +01:00
Matt Aitken 9a606e8f2a Merge pull request #143 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-07 15:35:00 +01:00
github-actions[bot] b1e65ed500 chore: Update version for release (next) 2023-07-07 14:34:41 +00:00
Matt Aitken 26f3dbff06 Typeform: safeBodyParse webhook import fixed 2023-07-07 15:32:45 +01:00
Eric Allam 5ee0b188bd Improve the error display when a run cannot connect to an endpoint 2023-07-07 15:32:19 +01:00
Matt Aitken 4692cd6a55 Latest lockfile 2023-07-07 15:20:42 +01:00
Matt Aitken 7ad8c01853 Merge pull request #142 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-07 15:19:13 +01:00
github-actions[bot] bc27b45fd7 chore: Update version for release (next) 2023-07-07 14:18:55 +00:00
Matt Aitken 94cc28d377 Changeset: integration-kit uses webhook package 2023-07-07 15:17:32 +01:00
Matt Aitken 19e80cef3b Removed “internal-queue” from indexEndpoint 2023-07-07 15:16:18 +01:00
James Ritchie 5397cd3194 Added environment labels to the enironment and api keys help panel 2023-07-07 14:51:44 +01:00
Eric Allam f01af9c0ec Remove a bunch of unused webapp deps and upgrade packages to zod 3.21.4 2023-07-07 14:47:28 +01:00
James Ritchie a288010d02 Added a new help panel to the api and endpoints page 2023-07-07 14:20:35 +01:00
James Ritchie b9a7a1c32b Label component now uses the correct <label> html element 2023-07-07 14:20:35 +01:00
James Ritchie 65b9a0dce5 Improved the Header component to make it easier to add more props 2023-07-07 14:20:35 +01:00
Eric Allam 7464006e19 Associated trigger sources and dynamic triggers with their registration job 2023-07-07 13:47:19 +01:00
Matt Aitken 590d56b7e7 Merge pull request #141 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-07 12:18:57 +01:00
github-actions[bot] c75cb1e87e chore: Update version for release (next) 2023-07-07 11:16:51 +00:00
Matt Aitken 597b2f4a6f Plain example Job 2023-07-07 12:15:16 +01:00
Matt Aitken 2e84c6bbb2 Plain: Added upsertCustomTimelineEntry Task and exported the components from the plain package 2023-07-07 12:15:16 +01:00
Matt Aitken 06c0cd5297 Import plain jobs file so it gets run, also alphabetized them 2023-07-07 12:15:16 +01:00
Eric Allam 79eab3d62f Cleanup unused graphile tasks 2023-07-07 11:46:56 +01:00
Eric Allam 6527723a0e Refactor index endpoint to work sequentially so we can add associations between sources and jobs 2023-07-07 11:36:01 +01:00
Eric Allam 3d6652f132 WIP plain integration 2023-07-07 10:59:36 +01:00
Matt Aitken da5d9779d4 Check if the Highlight id is defined before attempting to send the error 2023-07-06 21:03:20 +01:00
Matt Aitken 865399e753 Enabled tracing so we can track down errors between frontend and backend 2023-07-06 20:56:50 +01:00
Matt Aitken 960ea6c8dd Removed the throw button from the NavBar… obvs should never have been committed ;) 2023-07-06 20:56:35 +01:00
Matt Aitken 39ab48f0cb Highlight server-side error tracking 2023-07-06 20:45:20 +01:00
Matt Aitken 7341b4cf61 Highlight identifies users 2023-07-06 20:45:00 +01:00
Matt Aitken 28c5acc6af Added Highlight to the project, with basic client-side tracking 2023-07-06 19:34:53 +01:00
Matt Aitken 4366eefc66 Removed unused import 2023-07-06 19:34:05 +01:00
Matt Aitken cb95e761c1 Added an error boundary to the _app route 2023-07-06 19:33:32 +01:00
Matt Aitken 7136748463 Added an error boundary to org 2023-07-06 19:29:42 +01:00
Matt Aitken b0cacfd75c Added an error boundary to project 2023-07-06 19:29:32 +01:00
Matt Aitken 81f03dac79 Better error when a job isn’t found 2023-07-06 19:29:17 +01:00
Matt Aitken e37d1eadcd Breadcrumb null checks in case error boundaries are hit deep in the app 2023-07-06 19:29:00 +01:00
Matt Aitken 23fb310374 Reusable ErrorDisplay 2023-07-06 19:28:36 +01:00
Matt Aitken 797a1732d2 Organize imports on ProjectSideMenu 2023-07-06 19:08:06 +01:00
Matt Aitken c405a417e9 If an error boundary is hit the meta function can return undefined… 2023-07-06 19:07:46 +01:00
James Ritchie 808deb1d77 Added data attributes to the side menu items 2023-07-06 17:50:44 +01:00
Eric Allam 6ac1473ec3 Merge pull request #139 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-06 15:31:53 +01:00
github-actions[bot] 9c183abf4b chore: Update version for release (next) 2023-07-06 14:27:36 +00:00
Eric Allam 33c33faae9 Adding the typeform integration to the catalog 2023-07-06 15:26:02 +01:00
Matt Aitken 4cccbc2039 PostHog updated to change hooks, also added “job” group 2023-07-06 14:58:29 +01:00
Matt Aitken cc061dedf8 Change hooks for User, Org, Project, Job 2023-07-06 14:58:29 +01:00
Matt Aitken 402ce2210f New hook that lets you trigger an action when an object changes 2023-07-06 14:58:29 +01:00
Matt Aitken 329c5c4070 Updated posthogjs 2023-07-06 14:58:29 +01:00
Matt Aitken 878aeb7a8c useOrganizationChanged 2023-07-06 14:58:29 +01:00
Eric Allam 3ee396d788 Adding the typeform integration with webhook support 2023-07-06 13:42:49 +01:00
Eric Allam 090037602c WIP create-integration CLI command 2023-07-06 13:42:49 +01:00
Matt Aitken bee77051e7 Added UI to guide you to fixing unconfigured integrations 2023-07-06 11:56:19 +01:00
Matt Aitken be6741a4a8 Example Job that has an unconfigured Integration (for testing) 2023-07-06 11:56:19 +01:00
Matt Aitken 3e9b440322 NamedIcon uses the new type 2023-07-06 11:56:19 +01:00
Matt Aitken dfea5bc77c The connection-alert trigger type has an amber icon so it doesn’t look like an error 2023-07-06 11:56:19 +01:00
Matt Aitken 1eef30e298 Callout uses to and supports internal and external links 2023-07-06 11:56:19 +01:00
James Ritchie e5b9a9de3f Noun’d up Job 2023-07-06 11:36:13 +01:00
Matt Aitken dfedd3a6e8 Consistently use “Log in” everywhere 2023-07-06 10:08:09 +01:00
Matt Aitken a19845b4ca Added a jsdoc markr to the source id 2023-07-06 09:32:15 +01:00
Matt Aitken 094f6f5a7a jsdocs for DynamicTrigger and DynamicSchedule changeset 2023-07-06 00:37:01 +01:00
Matt Aitken fa60edce2b jsdocs for DynamicTrigger and DynamicSchedule 2023-07-06 00:36:08 +01:00
Matt Aitken ba44652457 intervalTrigger() jsdocs 2023-07-05 20:17:09 +01:00
Matt Aitken 931be39912 cronTrigger jsdocs 2023-07-05 19:32:35 +01:00
James Ritchie 5048a27acb Capital J for Job 2023-07-05 18:27:04 +01:00
Matt Aitken 71fc615b31 Fix for the profile photo sizing being weird 2023-07-05 18:05:41 +01:00
Eric Allam 20ed4e22ef Implement the cloud private beta invite form
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-07-05 17:27:59 +01:00
Matt Aitken fc2d337a39 Optional trailing icon on a <TextLink> 2023-07-05 17:22:53 +01:00
Matt Aitken 31d96f3862 External icon uses the small version 2023-07-05 17:22:44 +01:00
Matt Aitken 7b43853ec6 Help content for the Job page 2023-07-05 17:11:45 +01:00
Matt Aitken 40fa913272 TextLink can have a to or an href 2023-07-05 17:10:34 +01:00
Matt Aitken cdd5482aad Capitalized Runs 2023-07-05 17:10:17 +01:00
Matt Aitken 4a3fa20ee7 Export the callout styles 2023-07-05 17:10:10 +01:00
Matt Aitken d49ed4cd47 Fix for div inside paragraph error 2023-07-05 17:09:53 +01:00
James Ritchie db3cf77f5b Updated the pagination buttons style 2023-07-05 17:08:35 +01:00
James Ritchie c4a08961d6 Added a Help Panel to the Jobs list page 2023-07-05 17:08:35 +01:00
James Ritchie 5ba1643d46 Added a new qr code named icon 2023-07-05 16:57:33 +01:00
Eric Allam d189f425e8 Limit access to the cloud product to people who have invite codes 2023-07-05 16:31:41 +01:00
Matt Aitken 0fe6fb83ac The test help panel is open when you load the page 2023-07-05 16:09:20 +01:00
Matt Aitken f15ae41f48 Help lightbulb is always yellow 2023-07-05 16:09:20 +01:00
Eric Allam 46a07fc72e Merge pull request #138 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-05 15:59:30 +01:00
github-actions[bot] 83af317739 chore: Update version for release (next) 2023-07-05 14:57:17 +00:00
Eric Allam f4db1b2735 @trigger.dev/cli init - Create a better file structure 2023-07-05 15:54:58 +01:00
Eric Allam 6d4922f419 api.trigger.dev is now the default cloud url 2023-07-05 14:47:41 +01:00
Eric Allam 6af098ee8a Default the apiUrl to cloud.trigger.dev 2023-07-05 12:29:48 +01:00
212 changed files with 7135 additions and 5572 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/openai": patch
---
Improved OpenAI task errors
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Don't return the apiKey when they don't match
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/typeform": patch
---
getAllResponses fixed
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/plain": patch
---
Added upsertCustomTimelineEntry Task and exported the components from the plain package
+10
View File
@@ -0,0 +1,10 @@
---
"@trigger.dev/typeform": patch
"@trigger.dev/sdk": patch
"@trigger.dev/github": patch
"@trigger.dev/openai": patch
"@trigger.dev/slack": patch
"@trigger.dev/cli": patch
---
Upgrade to zod 3.21.4
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/cli": patch
---
api.trigger.dev is now the default cloud url
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
cronTrigger jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
intervalTrigger() jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
jsdocs for DynamicTrigger and DynamicSchedule
+14
View File
@@ -0,0 +1,14 @@
---
"@trigger.dev/integration-kit": patch
"@trigger.dev/typeform": patch
"@trigger.dev/sdk": patch
"@trigger.dev/github": patch
"@trigger.dev/openai": patch
"@trigger.dev/resend": patch
"@trigger.dev/plain": patch
"@trigger.dev/slack": patch
"@trigger.dev/nextjs": patch
"@trigger.dev/cli": patch
---
Set Node version to 16.8 and above
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/nextjs": patch
---
Loosen the Next.js dependency version
+22 -1
View File
@@ -15,7 +15,9 @@
"@trigger.dev/init": "0.2.0",
"@trigger.dev/integration-kit": "2.0.0-next.1",
"@trigger.dev/resend": "1.0.0-next.1",
"@trigger.dev/cli": "0.2.1-next.8"
"@trigger.dev/cli": "0.2.1-next.8",
"@trigger.dev/typeform": "1.0.0-next.2",
"@trigger.dev/plain": "1.0.0-next.2"
},
"changesets": [
"afraid-hornets-cheer",
@@ -23,16 +25,22 @@
"big-tables-search",
"bright-buses-join",
"bright-horses-bathe",
"brown-bikes-bathe",
"chatty-cars-agree",
"chilly-meals-warn",
"clever-bats-learn",
"cuddly-pets-arrive",
"curly-hornets-sort",
"dirty-needles-kneel",
"dull-bulldogs-design",
"eight-buckets-agree",
"eighty-queens-prove",
"fast-ligers-wink",
"four-ligers-perform",
"friendly-parents-clean",
"funny-elephants-sing",
"gentle-dogs-end",
"gentle-guests-leave",
"great-meals-listen",
"healthy-rockets-cry",
"heavy-eyes-own",
@@ -41,33 +49,46 @@
"hot-seals-press",
"itchy-cars-count",
"itchy-rockets-provide",
"khaki-needles-wait",
"long-clocks-hide",
"lovely-hounds-accept",
"lovely-plants-brake",
"lucky-crews-matter",
"metal-donkeys-relate",
"modern-emus-suffer",
"modern-turtles-eat",
"moody-islands-melt",
"odd-colts-smash",
"pink-islands-pay",
"pink-numbers-jam",
"plenty-ties-raise",
"popular-cooks-boil",
"pretty-bats-obey",
"quick-stingrays-own",
"quiet-berries-tickle",
"quiet-dryers-collect",
"rich-clocks-attack",
"rude-bees-taste",
"shiny-mirrors-beam",
"shy-bobcats-hear",
"silent-mirrors-lay",
"silver-insects-sneeze",
"six-humans-happen",
"sixty-pandas-deliver",
"sixty-squids-wink",
"strange-jobs-shout",
"strong-cars-visit",
"stupid-files-cough",
"sweet-adults-applaud",
"sweet-cups-rescue",
"sweet-snails-refuse",
"tame-flowers-invite",
"tasty-bats-flow",
"ten-birds-confess",
"thirty-apes-happen",
"tidy-planes-compete",
"tough-walls-share",
"two-tools-hide",
"wild-rats-mix",
"young-birds-unite"
]
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/typeform": patch
---
safeBodyParse webhook import fixed
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Initializing a next.js project now creates a better file structure
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Fix when setting the url as an option
+7
View File
@@ -0,0 +1,7 @@
---
"@trigger.dev/integration-kit": patch
"@trigger.dev/sdk": patch
"@trigger.dev/typeform": patch
---
Creating the typeform integration package
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/openai": patch
---
Adding support for output properties on tasks
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/typeform": patch
"@trigger.dev/integration-kit": patch
---
integration-kit uses webhook package
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Improved the endpoint prompt
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Improved spinner and made port clear in logs
+1 -1
View File
@@ -7,7 +7,7 @@
### The background jobs framework for Next.js
[Discord](https://discord.gg/bpVDSjcG) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
[Discord](https://discord.gg/JtBAxBr2m3) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/triggerdotdev.svg?style=social&label=Follow%20%40trigger.dev)](https://twitter.com/triggerdotdev)
[![GitHub Repo stars](https://img.shields.io/github/stars/triggerdotdev/trigger.dev?style=social)](https://github.com/triggerdotdev/trigger.dev)
@@ -0,0 +1,59 @@
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
import { LinkButton } from "./primitives/Buttons";
import { Header1, Header3 } from "./primitives/Headers";
import { friendlyErrorDisplay } from "~/utils/httpErrors";
type ErrorDisplayOptions = {
button?: {
title: string;
to: string;
};
};
export function RouteErrorDisplay(options?: ErrorDisplayOptions) {
const error = useRouteError();
return (
<>
{isRouteErrorResponse(error) ? (
<ErrorDisplay
title={friendlyErrorDisplay(error.status, error.statusText).title}
message={
error.data.message ??
friendlyErrorDisplay(error.status, error.statusText).message
}
{...options}
/>
) : error instanceof Error ? (
<ErrorDisplay title={error.name} message={error.message} {...options} />
) : (
<ErrorDisplay
title="Oops"
message={JSON.stringify(error)}
{...options}
/>
)}
</>
);
}
type DisplayOptionsProps = {
title: string;
message?: string;
} & ErrorDisplayOptions;
export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
return (
<div className="p-4">
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
{message && <Header3>{message}</Header3>}
<LinkButton
to={button ? button.to : "/"}
variant="primary/medium"
className="mt-8"
>
{button ? button.title : "Home"}
</LinkButton>
</div>
);
}
@@ -0,0 +1,14 @@
import { H, HighlightOptions } from "highlight.run";
import { useEffect } from "react";
interface Props extends HighlightOptions {
projectId?: string;
}
export function HighlightInit({ projectId, ...highlightOptions }: Props) {
useEffect(() => {
projectId && H.init(projectId, highlightOptions);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return null;
}
@@ -23,9 +23,9 @@ export function UserAvatar({
className?: string;
}) {
return avatarUrl ? (
<div>
<div className={cn("grid aspect-square place-items-center", className)}>
<img
className={cn("aspect-square rounded-full p-[7%]", className)}
className={cn("aspect-square rounded-full p-[7%]")}
src={avatarUrl}
alt={name ?? "User"}
/>
@@ -1,10 +1,19 @@
import { Link } from "@remix-run/react";
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { jobTestPath } from "~/utils/pathBuilder";
import { InlineCode } from "../code/InlineCode";
import { HelpPanelProps } from "../integrations/ApiKeyHelp";
import { HelpInstall } from "../integrations/HelpInstall";
import { HelpSamples } from "../integrations/HelpSamples";
import { Button, LinkButton } from "../primitives/Buttons";
import { Callout, variantClasses } from "../primitives/Callout";
import {
ClientTabs,
ClientTabsContent,
@@ -15,11 +24,9 @@ import { ClipboardField } from "../primitives/ClipboardField";
import integrationButton from "./integration-button.png";
import selectEnvironment from "./select-environment.png";
import selectExample from "./select-example.png";
import { InlineCode } from "../code/InlineCode";
import { ApiKeyHelp, HelpPanelProps } from "../integrations/ApiKeyHelp";
import { Header2 } from "../primitives/Headers";
import { CodeBlock } from "../code/CodeBlock";
import { HelpInstall } from "../integrations/HelpInstall";
import { HelpSamples } from "../integrations/HelpSamples";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
export function HowToSetupYourProject() {
const devEnvironment = useDevEnvironment();
@@ -140,6 +147,65 @@ export function HowToSetupYourProject() {
);
}
export function HowToRunYourJob() {
const organization = useOrganization();
const project = useProject();
const job = useJob();
return (
<>
<Callout variant="info" className="mb-6">
<Paragraph variant={"small"} className={variantClasses.info.textColor}>
Scheduled Triggers <strong>do not</strong> trigger Jobs in the DEV
Environment. When developing locally you should use the{" "}
<Link
to={jobTestPath(organization, project, job)}
className="underline underline-offset-2 transition hover:text-blue-100"
>
Test feature
</Link>{" "}
to trigger any scheduled Jobs.
</Paragraph>
</Callout>
<Paragraph spacing>There are two ways to run your Job:</Paragraph>
<StepNumber stepNumber="1" title="Trigger a test Run" />
<StepContentContainer>
<Paragraph spacing>
You can perform a Run with any payload you want, or use one of our
examples, on the test page.
</Paragraph>
<LinkButton
to={jobTestPath(organization, project, job)}
variant={"primary/small"}
LeadingIcon={"beaker"}
leadingIconClassName="text-bright"
>
Test
</LinkButton>
</StepContentContainer>
<StepNumber stepNumber="2" title="Trigger your Job for real" />
<StepContentContainer>
<Paragraph spacing>
Performing a real run depends on the type of Trigger your Job is
using.
</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/running-jobs"
variant={"primary/small"}
LeadingIcon={"docs"}
leadingIconClassName="text-bright"
>
How to run a Job
</LinkButton>
</StepContentContainer>
</>
);
}
export function HowToRunATest() {
return (
<>
@@ -171,7 +237,7 @@ export function HowToRunATest() {
</StepContentContainer>
<Callout
variant="docs"
href="https://trigger.dev/docs/documentation/guides/testing-jobs"
to="https://trigger.dev/docs/documentation/guides/testing-jobs"
>
Learn more about running tests.
</Callout>
@@ -219,7 +285,7 @@ export function HowToConnectAnIntegration() {
</StepContentContainer>
<Callout
variant={"docs"}
href="https://trigger.dev/docs/integrations/introduction"
to="https://trigger.dev/docs/integrations/introduction"
>
View the Integration docs page for more information on connecting an API
using an Integration or other method.
@@ -255,6 +321,101 @@ export function HowToUseThisIntegration({
);
}
export function HowToUseApiKeysAndEndpoints() {
return (
<>
<Paragraph spacing>
Environments and Endpoints are used to connect your server to the
Trigger.dev platform.
</Paragraph>
<Header2 spacing>Environments</Header2>
<Paragraph spacing>
Each environment has an API Key associated with it. This API Key is used
to authenticate your Jobs with the Trigger.dev platform.
</Paragraph>
<Paragraph spacing>
The API Key you use for your{" "}
<TextLink to="https://trigger.dev/docs/documentation/concepts/client-adaptors">
Client
</TextLink>{" "}
is how we know which environment to run your code against:
</Paragraph>
<CodeBlock
showLineNumbers={false}
className="mb-4"
code={`export const client = new TriggerClient({
id: "nextjs-example",
//this environment variable should be set to your DEV API Key locally,
//and your PROD API Key in production
apiKey: process.env.TRIGGER_API_KEY!,
});`}
/>
<StepNumber
stepNumber="→"
title={
<span className="flex items-center gap-x-2">
<span>Development</span>
<EnvironmentLabel environment={{ type: "DEVELOPMENT" }} />
</span>
}
/>
<StepContentContainer>
<Paragraph>
The <InlineCode>DEV</InlineCode> environment should only be used for
local development. Its where you can test your Jobs before deploying
them to servers.
</Paragraph>
<Callout variant="warning" className="my-2">
Scheduled Triggers do not trigger Jobs in the DEV Environment. When
youre working locally you should use the Test feature to trigger any
scheduled Jobs.
</Callout>
</StepContentContainer>
<StepNumber
stepNumber="→"
title={
<span className="flex items-center gap-x-2">
<span>Production</span>
<EnvironmentLabel environment={{ type: "PRODUCTION" }} />
</span>
}
/>
<StepContentContainer>
<Paragraph spacing>
The <InlineCode>PROD</InlineCode> environment is where your Jobs will
run in production. Its where you can run your Jobs against real data.
</Paragraph>
</StepContentContainer>
<Header2 spacing>Endpoints</Header2>
<Paragraph spacing>
An Endpoint is a URL on your server that Trigger.dev can connect to.
This URL is used to register Jobs, start them and orchestrate runs and
retries.
</Paragraph>
<Paragraph spacing>
<InlineCode>DEV</InlineCode> has multiple endpoints associated with it
one for each team member. This allows each team member to run their own
Jobs, without interfering with each other.
</Paragraph>
<Paragraph spacing>
All other environments have just a single endpoint (with a single URL)
associated with them.
</Paragraph>
<Header2 spacing>Deployment</Header2>
<Paragraph spacing>
Deployment uses Environments and Endpoints to connect your Jobs to the
Trigger.dev platform.
</Paragraph>
<Callout
variant="docs"
to="https://trigger.dev/docs/documentation/guides/deployment"
>
Read the deployment guide to learn more.
</Callout>
</>
);
}
function StepContentContainer({ children }: { children: React.ReactNode }) {
return <div className="mb-6 ml-9 mt-1">{children}</div>;
}
@@ -31,9 +31,8 @@ export function ApiKeyHelp({
keys won't leave your server, we'll never see them.
</Paragraph>
<Paragraph spacing>
First install the{" "}
<InlineCode>{integration.packageName}@next</InlineCode> package using
your preferred package manager. For example:
First install the <InlineCode>{integration.packageName}</InlineCode>{" "}
package using your preferred package manager. For example:
</Paragraph>
<HelpInstall packageName={integration.packageName} />
{help && (
@@ -21,21 +21,21 @@ export function HelpInstall({ packageName }: { packageName: string }) {
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="secondary/medium"
value={`npm install ${packageName}@next`}
value={`npm install ${packageName}`}
className="mb-4"
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="secondary/medium"
value={`pnpm install ${packageName}@next`}
value={`pnpm install ${packageName}`}
className="mb-4"
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="secondary/medium"
value={`yarn add ${packageName}@next`}
value={`yarn add ${packageName}`}
className="mb-4"
/>
</ClientTabsContent>
+30 -6
View File
@@ -18,6 +18,7 @@ import { runStatusTitle } from "../runs/RunStatuses";
import { ProjectJob, useProject } from "~/hooks/useProject";
import { useOrganization } from "~/hooks/useOrganizations";
import { JobRunStatus } from "~/models/job.server";
import { cn } from "~/utils/cn";
export function JobsTable({
jobs,
@@ -46,7 +47,12 @@ export function JobsTable({
jobs.map((job) => {
const path = jobPath(organization, project, job);
return (
<TableRow key={job.id}>
<TableRow
key={job.id}
className={cn(
job.hasIntegrationsRequiringAction && "bg-rose-500/30"
)}
>
<TableCell to={path}>
<span className="flex items-center gap-2">
<NamedIcon name={job.event.icon} className="h-8 w-8" />
@@ -79,12 +85,30 @@ export function JobsTable({
<SimpleTooltip
key={integration.key}
button={
<NamedIcon
name={integration.icon}
className="h-6 w-6"
/>
<div className="relative">
<NamedIcon
name={integration.icon}
className="h-6 w-6"
/>
{integration.setupStatus === "MISSING_FIELDS" && (
<NamedIcon
name="error"
className="absolute -left-1 -top-1 h-4 w-4"
/>
)}
</div>
}
content={
<div>
<p className="mb-1 text-rose-400">
{integration.setupStatus === "MISSING_FIELDS" &&
"This integration requires configuration"}
</p>
<p>
{integration.title}: {integration.key}
</p>
</div>
}
content={`${integration.title}: ${integration.key}`}
/>
))}
</TableCell>
@@ -102,73 +102,77 @@ function BreadcrumbItem({
case "projects":
return <ProjectsMenu key={breadcrumb.slug} />;
case "jobs":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectPath(organization!, project!)}
to={projectPath(organization, project)}
title="Jobs"
/>
);
case "environments":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectEnvironmentsPath(organization!, project!)}
to={projectEnvironmentsPath(organization, project)}
title="Environments"
/>
);
case "integrations":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectIntegrationsPath(organization!, project!)}
to={projectIntegrationsPath(organization, project)}
title="Integrations"
/>
);
case "integration":
if (!organization || !project || !client) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={projectIntegrationsPath(organization!, project!)}
to={projectIntegrationsPath(organization, project)}
title="Integrations"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={integrationClientPath(organization!, project!, client!)}
title={client!.title}
to={integrationClientPath(organization, project, client)}
title={client.title}
/>
</Fragment>
);
case "integration-job":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientPath(organization!, project!, client!)}
to={integrationClientPath(organization, project, client)}
title={"Jobs"}
/>
);
case "integration-connections":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientConnectionsPath(
organization!,
project!,
client!
)}
to={integrationClientConnectionsPath(organization, project, client)}
title={"Connections"}
/>
);
case "integration-scopes":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientScopesPath(organization!, project!, client!)}
to={integrationClientScopesPath(organization, project, client)}
title={"Scopes"}
/>
);
case "job":
if (!organization || !project || !job) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={projectPath(organization!, project!)}
to={projectPath(organization, project)}
title="Jobs"
/>
<BreadcrumbIcon />
@@ -176,32 +180,35 @@ function BreadcrumbItem({
</Fragment>
);
case "test":
if (!organization || !project || !job) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={jobTestPath(organization!, project!, job!)}
to={jobTestPath(organization, project, job)}
title="Test"
/>
);
case "runs":
if (!organization || !project || !job) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={jobPath(organization!, project!, job!)}
to={jobPath(organization, project, job)}
title="Runs"
/>
);
case "run":
if (!organization || !project || !job || !run) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={jobPath(organization!, project!, job!)}
to={jobPath(organization, project, job)}
title="Runs"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={runDashboardPath(organization!, project!, job!, run!)}
title={`Run #${run!.number}`}
to={runDashboardPath(organization, project, job, run)}
title={`Run #${run.number}`}
/>
</Fragment>
);
@@ -1,7 +1,7 @@
import { useMatches } from "@remix-run/react";
import { motion } from "framer-motion";
import invariant from "tiny-invariant";
import { useJob, useOptionalJob } from "~/hooks/useJob";
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
import { useOptionalJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
@@ -13,11 +13,10 @@ import {
projectIntegrationsPath,
projectPath,
} from "~/utils/pathBuilder";
import { UserProfilePhoto } from "../UserProfilePhoto";
import { NavLinkButton } from "../primitives/Buttons";
import type { IconNames } from "../primitives/NamedIcon";
import { SimpleTooltip } from "../primitives/Tooltip";
import { UserProfilePhoto } from "../UserProfilePhoto";
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
return (
@@ -72,18 +71,21 @@ export function ProjectSideMenu() {
to={projectPath(organization, project)}
isCollapsed={isCollapsed}
forceActive={jobsActive}
data-action="jobs"
/>
<SideMenuItem
name="Integrations"
icon="integration"
to={projectIntegrationsPath(organization, project)}
isCollapsed={isCollapsed}
data-action="integrations"
/>
<SideMenuItem
name="Environments & API Keys"
icon="environment"
to={projectEnvironmentsPath(organization, project)}
isCollapsed={isCollapsed}
data-action="environments & api keys"
/>
</div>
<div className="flex flex-col">
@@ -92,18 +94,21 @@ export function ProjectSideMenu() {
icon="team"
to={organizationTeamPath(organization)}
isCollapsed={isCollapsed}
data-action="team"
/>
<SideMenuItem
name="Usage & Billing"
icon="billing"
to={organizationBillingPath(organization)}
isCollapsed={isCollapsed}
data-action="usage & billing"
/>
<SideMenuItem
name="Account"
icon={UserProfilePhoto}
to={accountPath()}
isCollapsed={isCollapsed}
data-action="account"
/>
</div>
</motion.div>
@@ -4,49 +4,56 @@ import {
InformationCircleIcon,
LightBulbIcon,
} from "@heroicons/react/20/solid";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import {
ArrowTopRightOnSquareIcon,
BookOpenIcon,
CheckCircleIcon,
ChevronRightIcon,
} from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
const variantClasses = {
export const variantClasses = {
info: {
className: "border-blue-400/20 bg-blue-400/30",
icon: <InformationCircleIcon className="h-5 w-5 shrink-0 text-blue-400" />,
text: "text-blue-200",
textColor: "text-blue-200",
linkClassName: "transition hover:bg-blue-400/40",
},
warning: {
className: "border-yellow-400/20 bg-yellow-400/30",
icon: (
<ExclamationCircleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
),
text: "text-yellow-200",
textColor: "text-yellow-200",
linkClassName: "transition hover:bg-yellow-400/40",
},
error: {
className: "border-[hsl(347,77%,50%)]/30 bg-[hsl(347,77%,50%)]/20",
className: "border-rose-500/20 bg-rose-500/30",
icon: (
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-rose-400" />
),
text: "text-rose-200",
textColor: "text-rose-200",
linkClassName: "transition hover:bg-rose-500/40",
},
idea: {
className: "border-green-400/20 bg-green-400/30",
icon: <LightBulbIcon className="h-5 w-5 shrink-0 text-green-400" />,
text: "text-green-200",
textColor: "text-green-200",
linkClassName: "transition hover:bg-green-400/40",
},
success: {
className: "border-green-400/20 bg-green-400/30",
icon: <CheckCircleIcon className="h-5 w-5 shrink-0 text-green-400" />,
text: "text-green-200",
textColor: "text-green-200",
linkClassName: "transition hover:bg-green-400/40",
},
docs: {
className:
"border-blue-400/20 bg-blue-400/30 transition hover:bg-blue-400/40",
className: "border-blue-400/20 bg-blue-400/30",
icon: <BookOpenIcon className="mt-0.5 h-5 w-5 shrink-0 text-blue-400" />,
text: "text-blue-200",
textColor: "text-blue-200",
linkClassName: "transition hover:bg-blue-400/40",
},
} as const;
@@ -55,41 +62,79 @@ export function Callout({
className,
icon,
variant,
href,
to,
}: {
children?: React.ReactNode;
className?: string;
icon?: React.ReactNode;
variant: keyof typeof variantClasses;
href?: string;
to?: string;
}) {
const variantDefinition = variantClasses[variant];
if (variant === "docs") {
return (
<a
href={href}
target="_blank"
className={cn(
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
variantDefinition.className,
className
)}
>
<div className="flex w-full items-start gap-x-2">
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.text}>
{children}
</Paragraph>
) : (
children
if (to !== undefined) {
if (to.startsWith("http")) {
return (
<a
href={to}
target="_blank"
className={cn(
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
variantDefinition.className,
variantDefinition.linkClassName,
className
)}
</div>
<ArrowTopRightOnSquareIcon className="h-5 w-5 text-blue-400" />
</a>
);
>
<div className={"flex w-full items-start gap-x-2"}>
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph
variant={"small"}
className={variantDefinition.textColor}
>
{children}
</Paragraph>
) : (
children
)}
</div>
<ArrowTopRightOnSquareIcon
className={cn("h-5 w-5", variantDefinition.textColor)}
/>
</a>
);
} else {
return (
<Link
to={to}
className={cn(
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
variantDefinition.className,
variantDefinition.linkClassName,
className
)}
>
<div className={"flex w-full items-start gap-x-2"}>
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph
variant={"small"}
className={variantDefinition.textColor}
>
{children}
</Paragraph>
) : (
children
)}
</div>
<ChevronRightIcon
className={cn("h-5 w-5", variantDefinition.textColor)}
/>
</Link>
);
}
}
return (
@@ -103,7 +148,7 @@ export function Callout({
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.text}>
<Paragraph variant={"small"} className={variantDefinition.textColor}>
{children}
</Paragraph>
) : (
@@ -1,40 +1,96 @@
import { cn } from "~/utils/cn";
const header1Styles =
"font-sans text-base md:text-lg lg:text-xl leading-5 md:leading-6 lg:leading-7 font-semibold";
const header2Styles = "font-sans text-base leading-6 font-medium text-bright";
const header3Styles = "font-sans text-sm leading-5 font-medium text-bright";
const headerVariants = {
header1: {
text: "font-sans text-base md:text-lg lg:text-xl leading-5 md:leading-6 lg:leading-7 font-semibold",
spacing: "mb-2",
},
header2: {
text: "font-sans text-base leading-6 font-medium",
spacing: "mb-2",
},
header3: {
text: "font-sans text-sm leading-5 font-medium",
spacing: "mb-2",
},
};
const header1Variants = {
default: "text-bright",
const textColorVariants = {
bright: "text-bright",
dimmed: "text-dimmed",
};
export type HeaderVariant = keyof typeof headerVariants;
type HeaderProps = {
className?: string;
children: React.ReactNode;
};
type Header1Props = HeaderProps & {
variant?: keyof typeof header1Variants;
};
spacing?: boolean;
textColor?: "bright" | "dimmed";
} & React.HTMLAttributes<HTMLHeadingElement>;
export function Header1({
variant = "default",
className,
children,
}: Header1Props) {
spacing = false,
textColor = "bright",
}: HeaderProps) {
return (
<h1 className={cn(header1Styles, header1Variants[variant], className)}>
<h1
className={cn(
headerVariants.header1.text,
spacing === true && headerVariants.header1.spacing,
textColor === "bright"
? textColorVariants.bright
: textColorVariants.dimmed,
className
)}
>
{children}
</h1>
);
}
export function Header2({ className, children }: HeaderProps) {
return <h2 className={cn(header2Styles, className)}>{children}</h2>;
export function Header2({
className,
children,
spacing = false,
textColor = "bright",
}: HeaderProps) {
return (
<h2
className={cn(
headerVariants.header2.text,
spacing === true && headerVariants.header2.spacing,
textColor === "bright"
? textColorVariants.bright
: textColorVariants.dimmed,
className
)}
>
{children}
</h2>
);
}
export function Header3({ className, children }: HeaderProps) {
return <h3 className={cn(header3Styles, className)}>{children}</h3>;
export function Header3({
className,
children,
spacing = false,
textColor = "bright",
}: HeaderProps) {
return (
<h3
className={cn(
headerVariants.header3.text,
spacing === true && headerVariants.header3.spacing,
textColor === "bright"
? textColorVariants.bright
: textColorVariants.dimmed,
className
)}
>
{children}
</h3>
);
}
@@ -52,7 +52,7 @@ export function HelpTrigger({ title }: { title: string }) {
<Button
variant="tertiary/small"
LeadingIcon="lightbulb"
leadingIconClassName="text-slate-400"
leadingIconClassName="text-amber-400"
onClick={() => setOpen(true)}
>
{title}
@@ -1,6 +1,6 @@
import * as React from "react";
import { cn } from "~/utils/cn";
import type { IconNames } from "./NamedIcon";
import type { IconNamesOrString } from "./NamedIcon";
import { NamedIcon } from "./NamedIcon";
const variants = {
@@ -44,7 +44,7 @@ const variants = {
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
variant?: keyof typeof variants;
icon?: IconNames;
icon?: IconNamesOrString;
shortcut?: string;
fullWidth?: boolean;
};
+17 -14
View File
@@ -1,12 +1,22 @@
import * as React from "react";
import { cn } from "~/utils/cn";
import { Header2, Header3 } from "./Headers";
import { Paragraph } from "./Paragraph";
const labelVariants = {
small: {
text: "font-sans text-sm font-normal",
},
medium: {
text: "font-sans text-sm leading-5 font-medium",
},
large: {
text: "font-sans text-base leading-6 font-medium",
},
};
type LabelProps = React.AllHTMLAttributes<HTMLLabelElement> & {
className?: string;
children: React.ReactNode;
variant?: "small" | "medium" | "large";
variant?: keyof typeof labelVariants;
};
export function Label({
@@ -15,17 +25,10 @@ export function Label({
variant = "medium",
...props
}: LabelProps) {
return variant === "medium" ? (
<Header3 className={cn(className)} {...props}>
const variation = labelVariants[variant];
return (
<label className={cn(variation.text, className)} {...props}>
{children}
</Header3>
) : variant === "large" ? (
<Header2 className={cn(className)} {...props}>
{children}
</Header2>
) : (
<Paragraph variant="small" className={cn(className)}>
{children}
</Paragraph>
</label>
);
}
@@ -1,4 +1,5 @@
import {
ArrowTopRightOnSquareIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
@@ -9,7 +10,6 @@ import {
ArrowLeftIcon,
ArrowPathIcon,
ArrowRightIcon,
ArrowTopRightOnSquareIcon,
BeakerIcon,
BellAlertIcon,
BoltIcon,
@@ -39,6 +39,7 @@ import {
MagnifyingGlassIcon,
PlusIcon,
PlusSmallIcon,
QrCodeIcon,
SquaresPlusIcon,
StarIcon,
UserCircleIcon,
@@ -103,7 +104,7 @@ const icons = {
<XMarkIcon className={cn("text-dimmed", className)} />
),
"connection-alert": (className: string) => (
<BellAlertIcon className={cn("text-rose-500", className)} />
<BellAlertIcon className={cn("text-amber-500", className)} />
),
docs: (className: string) => (
<BookOpenIcon className={cn("text-slate-400", className)} />
@@ -175,6 +176,9 @@ const icons = {
property: (className: string) => (
<Cog8ToothIcon className={cn("text-slate-600", className)} />
),
"qr-code": (className: string) => (
<QrCodeIcon className={cn("text-amber-400", className)} />
),
refresh: (className: string) => (
<ArrowPathIcon className={cn("text-bright", className)} />
),
@@ -224,6 +228,7 @@ const icons = {
};
export type IconNames = keyof typeof icons;
export type IconNamesOrString = IconNames | (string & {});
export const iconNames = Object.keys(icons) as IconNames[];
export function NamedIcon({
@@ -231,7 +236,7 @@ export function NamedIcon({
className,
fallback,
}: {
name: IconNames | (string & {});
name: IconNamesOrString;
className: string;
fallback?: JSX.Element;
}) {
@@ -241,7 +246,7 @@ export function NamedIcon({
if (hasIcon(name)) {
return (
<div
<span
className={cn(
"grid aspect-square min-h-fit place-items-center",
className
@@ -255,7 +260,7 @@ export function NamedIcon({
shapeRendering: "geometricPrecision",
}}
/>
</div>
</span>
);
}
@@ -50,7 +50,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
>
<ChevronLeftIcon className="h-6" />
<Header1
variant="dimmed"
textColor="dimmed"
className="transition group-hover:text-white"
>
{backButton.text}
@@ -1,4 +1,6 @@
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
const paragraphVariants = {
base: {
@@ -90,18 +92,47 @@ export function Paragraph({
}
type TextLinkProps = {
href: string;
href?: string;
to?: string;
className?: string;
trailingIcon?: IconNamesOrString;
trailingIconClassName?: string;
children: React.ReactNode;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
export function TextLink({ href, children, ...props }: TextLinkProps) {
return (
<a
href={href}
className="text-indigo-500 transition hover:text-indigo-400"
{...props}
>
{children}
const classes =
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group";
export function TextLink({
href,
to,
children,
className,
trailingIcon,
trailingIconClassName,
...props
}: TextLinkProps) {
return to ? (
<Link to={to} className={cn(classes, className)} {...props}>
{children}{" "}
{trailingIcon && (
<NamedIcon
name={trailingIcon}
className={cn("h-4 w-4", trailingIconClassName)}
/>
)}
</Link>
) : href ? (
<a href={href} className={cn(classes, className)} {...props}>
{children}{" "}
{trailingIcon && (
<NamedIcon
name={trailingIcon}
className={cn("h-4 w-4", trailingIconClassName)}
/>
)}
</a>
) : (
<span>Need to define a path or href</span>
);
}
@@ -11,7 +11,7 @@ export function StepNumber({
stepNumber?: string;
active?: boolean;
complete?: boolean;
title?: string;
title?: React.ReactNode;
className?: string;
}) {
return (
@@ -57,11 +57,11 @@ export function RunsTable({
<TableBody>
{total === 0 && !hasFilters ? (
<TableBlankRow colSpan={8}>
<NoRuns title="No runs found for this Job" />
<NoRuns title="No Runs found for this Job" />
</TableBlankRow>
) : runs.length === 0 ? (
<TableBlankRow colSpan={8}>
<NoRuns title="No runs match your filters" />
<NoRuns title="No Runs match your filters" />
</TableBlankRow>
) : (
runs.map((run) => {
@@ -20,17 +20,44 @@ export const Callouts: Story = {
function CalloutsSet() {
return (
<div className="flex flex-col items-start gap-y-4 p-4">
<Callout variant="info">This is an info callout</Callout>
<Callout variant="warning">This is a warning callout</Callout>
<Callout variant="error">This is an error callout</Callout>
<Callout variant="idea">This is an idea callout</Callout>
<Callout
variant="idea"
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
>
This has a custom icon
</Callout>
<div className="grid grid-cols-2">
<div className="flex flex-col items-start gap-y-4 p-4">
<Callout variant="info">This is an info callout</Callout>
<Callout variant="warning">This is a warning callout</Callout>
<Callout variant="error">This is an error callout</Callout>
<Callout variant="idea">This is an idea callout</Callout>
<Callout variant="docs">This is a docs callout</Callout>
<Callout
variant="idea"
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
>
This has a custom icon
</Callout>
</div>
<div className="flex flex-col items-start gap-y-4 p-4">
<Callout to="#" variant="info">
This is an info callout
</Callout>
<Callout to="#" variant="warning">
This is a warning callout
</Callout>
<Callout to="#" variant="error">
This is an error callout
</Callout>
<Callout to="#" variant="idea">
This is an idea callout
</Callout>
<Callout to="#" variant="docs">
This is a docs callout
</Callout>
<Callout
to="#"
variant="idea"
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
>
This has a custom icon
</Callout>
</div>
</div>
);
}
@@ -175,6 +175,33 @@ function SearchForm() {
fullWidth={false}
/>
</InputGroup>
<InputGroup>
<Label variant="small">This is a small label</Label>
<Input
placeholder="Search"
required={true}
icon="search"
shortcut="⌘K"
/>
</InputGroup>
<InputGroup>
<Label variant="medium">This is a medium label</Label>
<Input
placeholder="Search"
required={true}
icon="search"
shortcut="⌘K"
/>
</InputGroup>
<InputGroup>
<Label variant="large">This is a large label</Label>
<Input
placeholder="Search"
required={true}
icon="search"
shortcut="⌘K"
/>
</InputGroup>
</Fieldset>
</Form>
</div>
@@ -43,7 +43,7 @@ function Typography({ header1, header2, header3, paragraph }: TypographyProps) {
<div className="flex flex-col gap-8">
<div>
<Header1>{header1}</Header1>
<Header1 variant="dimmed">{header1}</Header1>
<Header1 textColor="dimmed">{header1}</Header1>
<Header2>{header2}</Header2>
<Header3>{header3}</Header3>
<Paragraph>{paragraph}</Paragraph>
@@ -70,10 +70,14 @@ function Typography({ header1, header2, header3, paragraph }: TypographyProps) {
TextLink. It takes an href and children.
</Paragraph>
<Paragraph>
Learn how to get started quickly using the included some example Jobs
which are great as a quick start project. You can check them out in
your project here in triggerdotdev/jobs/examples. You can also see the
examples in more detail in the docs.
Learn how to get{" "}
<TextLink href="#" trailingIcon="external-link">
started quickly
</TextLink>{" "}
using the included some example Jobs which are great as a quick start
project. You can check them out in your project here in
triggerdotdev/jobs/examples. You can also see the examples in more
detail in the docs.
</Paragraph>
</div>
<div>
+41 -4
View File
@@ -1,4 +1,5 @@
import type { EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
import { H } from "@highlight-run/node";
import type { DataFunctionArgs, EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
import { Response } from "@remix-run/node"; // or cloudflare/deno
import { RemixServer } from "@remix-run/react";
import { parseAcceptLanguage } from "intl-parse-accept-language";
@@ -11,6 +12,7 @@ import {
OperatingSystemContextProvider,
OperatingSystemPlatform,
} from "./components/primitives/OperatingSystemProvider";
import { env } from "./env.server";
const ABORT_DELAY = 30000;
@@ -107,6 +109,7 @@ function serveBrowsers(
) {
return new Promise((resolve, reject) => {
let didError = false;
let shellReady = false;
const { pipe, abort } = renderToPipeableStream(
<OperatingSystemContextProvider platform={platform}>
<LocaleContextProvider locales={locales}>
@@ -120,6 +123,7 @@ function serveBrowsers(
{
// use onShellReady to wait until a suspense boundary is triggered
onShellReady() {
shellReady = true;
responseHeaders.set("Content-Type", "text/html");
let body = new PassThrough();
pipe(body);
@@ -133,9 +137,11 @@ function serveBrowsers(
onShellError(err: unknown) {
reject(err);
},
onError(err: unknown) {
onError(error: unknown) {
didError = true;
console.error(err);
if (shellReady) {
logError(error, request);
}
},
}
);
@@ -143,4 +149,35 @@ function serveBrowsers(
});
}
Worker.init().catch(console.error);
if (env.HIGHLIGHT_PROJECT_ID) {
H.init({ projectID: env.HIGHLIGHT_PROJECT_ID });
}
export function handleError(
error: unknown,
{ request, params, context }: DataFunctionArgs
) {
logError(error, request);
}
Worker.init().catch((error) => {
logError(error);
});
function logError(error: unknown, request?: Request) {
if (env.HIGHLIGHT_PROJECT_ID) {
const parsed = request
? H.parseHeaders(Object.fromEntries(request.headers))
: undefined;
if (error instanceof Error) {
H.consumeError(error, parsed?.secureSessionId, parsed?.requestId);
} else {
H.consumeError(
new Error(`Unknown error: ${JSON.stringify(error)}`),
parsed?.secureSessionId,
parsed?.requestId
);
}
}
console.error(error);
}
+1
View File
@@ -27,6 +27,7 @@ const EnvironmentSchema = z.object({
.default(process.env.NODE_ENV),
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
POSTHOG_PROJECT_KEY: z.string().optional(),
HIGHLIGHT_PROJECT_ID: z.string().optional(),
MAGIC_LINK_SECRET: z.string(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
+1 -1
View File
@@ -12,7 +12,7 @@ export function featuresForRequest(request: Request): TriggerFeatures {
const isManagedCloud =
url.host === "cloud.trigger.dev" ||
url.host === "test-cloud.trigger.dev" ||
process.env.NODE_ENV === "development";
process.env.CLOUD_ENV === "development";
return {
isManagedCloud,
+26
View File
@@ -0,0 +1,26 @@
import { useEffect, useRef } from "react";
/** Call a function when the id of the item changes */
export function useChanged<T extends { id: string }>(
getItem: () => T | undefined,
action: (item: T | undefined) => void,
sendInitialUndefined = true
) {
const previousItemId = useRef<string | undefined>();
const item = getItem();
//when the value changes, call the action
useEffect(() => {
if (previousItemId.current !== item?.id) {
action(item);
}
previousItemId.current = item?.id;
}, [item]);
//if sendInitialUndefined is true, call the action when the component first renders
useEffect(() => {
if (item !== undefined || sendInitialUndefined === false) return;
action(item);
}, []);
}
+14
View File
@@ -0,0 +1,14 @@
import { H } from "highlight.run";
import { useUserChanged } from "./useUser";
export function useHighlight() {
useUserChanged((user) => {
if (!user) {
return;
}
H.identify(user.id, {
email: user.email,
});
});
}
+7
View File
@@ -5,6 +5,7 @@ import {
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
import { useOptionalProject } from "./useProject";
import { useChanged } from "./useChanged";
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
@@ -31,3 +32,9 @@ export function useJob() {
invariant(job, "Job must be defined");
return job;
}
export const useJobChanged = (
action: (org: MatchedJob | undefined) => void
) => {
useChanged(useOptionalJob, action);
};
@@ -6,6 +6,7 @@ import invariant from "tiny-invariant";
import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route";
import type { loader as appLoader } from "~/routes/_app/route";
import { hydrateObject, useMatchesData } from "~/utils";
import { useChanged } from "./useChanged";
export type MatchedOrganization = UseDataFunctionReturn<
typeof appLoader
@@ -59,3 +60,9 @@ function useOrganizationsFromMatchesData(paths: string[]) {
UseDataFunctionReturn<typeof appLoader>["organizations"]
>(routeMatch.data.organizations);
}
export const useOrganizationChanged = (
action: (org: MatchedOrganization | undefined) => void
) => {
useChanged(useOptionalOrganization, action);
};
+48 -27
View File
@@ -1,62 +1,83 @@
import { useLocation } from "@remix-run/react";
import posthog from "posthog-js";
import { useEffect, useRef } from "react";
import { useOptionalOrganization } from "./useOrganizations";
import { useOptionalUser } from "./useUser";
import { useOrganizationChanged } from "./useOrganizations";
import { useOptionalUser, useUserChanged } from "./useUser";
import { useProjectChanged } from "./useProject";
import { useJobChanged } from "./useJob";
export const usePostHog = (apiKey?: string, logging = false): void => {
export const usePostHog = (
apiKey?: string,
logging = false,
debug = false
): void => {
const postHogInitialized = useRef(false);
const location = useLocation();
const user = useOptionalUser();
const currentOrganization = useOptionalOrganization();
const currentOrganizationId = currentOrganization?.id;
//start PostHog once
useEffect(() => {
if (apiKey === undefined) return;
if (apiKey === "") return;
if (apiKey === undefined || apiKey === "") return;
if (postHogInitialized.current === true) return;
if (logging) console.log("posthog.init", apiKey);
postHogInitialized.current = true;
if (logging) console.log("Initializing PostHog");
posthog.init(apiKey, {
api_host: "https://app.posthog.com",
opt_in_site_apps: true,
debug: logging,
debug,
loaded: function (posthog) {
if (logging) console.log("posthog.loaded", apiKey);
if (logging) console.log("PostHog loaded");
if (user !== undefined) {
if (logging) console.log("posthog.identify", user.id, user.email);
if (logging) console.log("Loaded: Identifying user", user);
posthog.identify(user.id, { email: user.email });
}
},
});
postHogInitialized.current = true;
}, [apiKey, logging, user]);
//identify the user, and unidentify on log out
useEffect(() => {
useUserChanged((user) => {
if (postHogInitialized.current === false) return;
if (user === undefined) {
if (logging) console.log("posthog.reset");
posthog.reset();
} else {
if (logging) console.log("posthog.identify", user.id, user.email);
if (logging) console.log("User changed");
if (user) {
if (logging) console.log("Identifying user", user);
posthog.identify(user.id, { email: user.email });
} else {
if (logging) console.log("Resetting user");
posthog.reset();
}
}, [logging, user]);
});
//identify the organization
useEffect(() => {
useOrganizationChanged((org) => {
if (postHogInitialized.current === false) return;
if (currentOrganizationId !== undefined) {
if (logging) console.log("posthog.organization", currentOrganizationId);
posthog.group("organization", currentOrganizationId);
if (org) {
if (logging) console.log(`Grouping by organization`, org);
posthog.group("organization", org.id);
} else {
//reset the groups when you go to one of the top-level pages
if (logging) console.log("Resetting groups");
posthog.resetGroups();
}
}, [currentOrganizationId, logging]);
});
useProjectChanged((project) => {
if (postHogInitialized.current === false) return;
if (project) {
if (logging) console.log(`Grouping by project`, project);
posthog.group("project", project.id);
}
});
useJobChanged((job) => {
if (postHogInitialized.current === false) return;
if (job) {
if (logging) console.log(`Grouping by job`, job);
posthog.group("job", job.id);
}
});
//page view
useEffect(() => {
if (postHogInitialized.current === false) return;
if (logging) console.log("posthog.capture", "$pageview", location.pathname);
posthog.capture("$pageview");
}, [location, logging]);
};
+7
View File
@@ -4,6 +4,7 @@ import {
} from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
import { useChanged } from "./useChanged";
export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
@@ -22,3 +23,9 @@ export function useProject() {
invariant(project, "Project must be defined");
return project;
}
export const useProjectChanged = (
action: (org: MatchedProject | undefined) => void
) => {
useChanged(useOptionalProject, action);
};
+5
View File
@@ -1,5 +1,6 @@
import type { User } from "~/models/user.server";
import { useMatchesData } from "~/utils";
import { useChanged } from "./useChanged";
function isUser(user: any): user is User {
return user && typeof user === "object" && typeof user.email === "string";
@@ -22,3 +23,7 @@ export function useUser(): User {
}
return maybeUser;
}
export function useUserChanged(callback: (user: User | undefined) => void) {
useChanged(useOptionalUser, callback);
}
+5
View File
@@ -11,3 +11,8 @@ interface Array<T> {
thisArg?: any
): Exclude<S, Falsy>[];
}
// See this for more: https://twitter.com/mattpocockuk/status/1653403198885904387?s=20
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
+22
View File
@@ -0,0 +1,22 @@
import { prisma } from "~/db.server";
import { Prettify } from "~/lib.es5";
export type ExtendedEndpoint = Prettify<
Awaited<ReturnType<typeof findEndpoint>>
>;
export async function findEndpoint(id: string) {
return await prisma.endpoint.findUniqueOrThrow({
where: {
id,
},
include: {
environment: {
include: {
project: true,
organization: true,
},
},
},
});
}
@@ -116,10 +116,6 @@ export async function createOrganization(
userId,
});
await workerQueue.enqueue("organizationCreated", {
id: organization.id,
});
return { ...organization, projects: [project] };
}
+76 -12
View File
@@ -78,27 +78,72 @@ export async function findOrCreateGithubUser({
? (authenticationExtraParams as unknown as Prisma.JsonObject)
: undefined;
const fields = {
accessToken,
authenticationProfile: authProfile,
authenticationExtraParams: authExtraParams,
name,
avatarUrl,
displayName,
};
const authIdentifier = `github:${authenticationProfile.id}`;
const existingUser = await prisma.user.findFirst({
const existingUser = await prisma.user.findUnique({
where: {
authIdentifier,
},
});
const existingEmailUser = await prisma.user.findUnique({
where: {
email,
},
});
if (existingEmailUser && !existingUser) {
const user = await prisma.user.update({
where: {
email,
},
data: {
accessToken,
authenticationProfile: authProfile,
authenticationExtraParams: authExtraParams,
avatarUrl,
authIdentifier,
},
});
return {
user,
isNewUser: false,
};
}
if (existingEmailUser && existingUser) {
const user = await prisma.user.update({
where: {
id: existingUser.id,
},
data: {
accessToken,
},
});
return {
user,
isNewUser: false,
};
}
const user = await prisma.user.upsert({
where: {
email,
authIdentifier,
},
update: { accessToken },
create: {
accessToken,
authenticationProfile: authProfile,
authenticationExtraParams: authExtraParams,
name,
avatarUrl,
displayName,
authIdentifier,
email,
authenticationMethod: "GITHUB",
},
update: fields,
create: { ...fields, email, authenticationMethod: "GITHUB" },
});
return {
@@ -128,3 +173,22 @@ export function updateUser({
data: { name, email, marketingEmails, confirmedBasicDetails: true },
});
}
export async function grantUserCloudAccess({
id,
inviteCode,
}: {
id: string;
inviteCode: string;
}) {
return prisma.user.update({
where: { id },
data: {
invitationCode: {
connect: {
code: inviteCode,
},
},
},
});
}
+1 -1
View File
@@ -9,7 +9,7 @@ import type {
} from "graphile-worker";
import { run as graphileRun } from "graphile-worker";
import { omit } from "lodash";
import omit from "lodash.omit";
import { z } from "zod";
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
import { logger } from "~/services/logger.server";
@@ -57,6 +57,7 @@ export class ProjectPresenter {
select: {
slug: true,
definition: true,
setupStatus: true,
},
},
},
@@ -169,6 +170,7 @@ export class ProjectPresenter {
key: integration.key,
title: integration.integration.slug,
icon: integration.integration.definition.id,
setupStatus: integration.integration.setupStatus,
})
);
@@ -197,6 +199,9 @@ export class ProjectPresenter {
source: eventSpecification.source,
},
integrations,
hasIntegrationsRequiringAction: integrations.some(
(i) => i.setupStatus === "MISSING_FIELDS"
),
lastRun,
properties,
};
@@ -1,14 +1,11 @@
import {
DisplayPropertiesSchema,
DisplayProperty,
DisplayPropertySchema,
ErrorWithStack,
ErrorWithStackSchema,
EventSpecificationSchema,
StyleSchema,
} from "@trigger.dev/internal";
import { z } from "zod";
import { PrismaClient, prisma } from "~/db.server";
import { mergeProperties } from "~/utils/mergeProperties.server";
type RunOptions = {
id: string;
@@ -35,6 +32,7 @@ const taskSelect = {
delayUntil: true,
description: true,
properties: true,
outputProperties: true,
error: true,
startedAt: true,
completedAt: true,
@@ -67,30 +65,15 @@ export class RunPresenter {
return undefined;
}
//merge the properties from the version and the run, with the run properties taking precedence
const mergedProperties = new Map<string, DisplayProperty>();
if (run.version.properties) {
const properties = DisplayPropertiesSchema.parse(run.version.properties);
for (const property of properties) {
mergedProperties.set(property.label, property);
}
}
if (run.properties) {
const properties = DisplayPropertiesSchema.parse(run.properties);
for (const property of properties) {
mergedProperties.set(property.label, property);
}
}
if (run.version.eventSpecification) {
const eventSpecification = EventSpecificationSchema.parse(
run.version.eventSpecification
);
if (eventSpecification.properties) {
for (const properties of eventSpecification.properties) {
mergedProperties.set(properties.label, properties);
}
}
}
const eventSpecification = EventSpecificationSchema.parse(
run.version.eventSpecification
);
const runProperties = mergeProperties(
run.version.properties,
run.properties,
eventSpecification.properties
);
const enrichTask = (task: QueryTask) => {
const { children, ...t } = task;
@@ -98,10 +81,7 @@ export class RunPresenter {
...t,
error: t.error ? ErrorWithStackSchema.parse(t.error) : undefined,
connection: t.runConnection,
properties:
t.properties == null
? []
: z.array(DisplayPropertySchema).parse(t.properties),
properties: mergeProperties(t.properties, t.outputProperties),
style: t.style ? StyleSchema.parse(t.style) : undefined,
};
};
@@ -147,7 +127,7 @@ export class RunPresenter {
isTest: run.isTest,
version: run.version.version,
output: runOutput,
properties: Array.from(mergedProperties.values()),
properties: runProperties,
environment: {
type: run.environment.type,
slug: run.environment.slug,
@@ -212,14 +192,29 @@ export class RunPresenter {
select: {
...taskSelect,
},
orderBy: {
createdAt: "asc",
},
},
},
orderBy: {
createdAt: "asc",
},
},
},
orderBy: {
createdAt: "asc",
},
},
},
orderBy: {
createdAt: "asc",
},
},
},
orderBy: {
createdAt: "asc",
},
},
},
orderBy: {
@@ -1,5 +1,6 @@
import { DisplayPropertiesSchema, StyleSchema } from "@trigger.dev/internal";
import { PrismaClient, prisma } from "~/db.server";
import { mergeProperties } from "~/utils/mergeProperties.server";
type DetailsProps = {
id: string;
@@ -56,6 +57,7 @@ export class TaskDetailsPresenter {
noop: true,
description: true,
properties: true,
outputProperties: true,
params: true,
output: true,
error: true,
@@ -89,10 +91,7 @@ export class TaskDetailsPresenter {
...task,
connection: task.runConnection,
params: task.params as Record<string, any>,
properties:
task.properties == null
? []
: DisplayPropertiesSchema.parse(task.properties),
properties: mergeProperties(task.properties, task.outputProperties),
style: task.style ? StyleSchema.parse(task.style) : undefined,
};
}
+50 -62
View File
@@ -7,8 +7,6 @@ import {
Outlet,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
useRouteError,
} from "@remix-run/react";
import {
TypedMetaFunction,
@@ -18,26 +16,27 @@ import {
import type { ToastMessage } from "~/models/message.server";
import { commitSession, getSession } from "~/models/message.server";
import tailwindStylesheetUrl from "~/tailwind.css";
import { RouteErrorDisplay } from "./components/ErrorDisplay";
import { HighlightInit } from "./components/HighlightInit";
import {
AppContainer,
MainCenteredContainer,
} from "./components/layout/AppLayout";
import { LinkButton } from "./components/primitives/Buttons";
import { Header1, Header3 } from "./components/primitives/Headers";
import { Toast } from "./components/primitives/Toast";
import { env } from "./env.server";
import { featuresForRequest } from "./features.server";
import { usePostHog } from "./hooks/usePostHog";
import { getUser } from "./services/session.server";
import { appEnvTitleTag } from "./utils";
import { friendlyErrorDisplay } from "./utils/httpErrors";
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
import { useHighlight } from "./hooks/useHighlight";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
};
export const meta: TypedMetaFunction<typeof loader> = ({ data }) => ({
title: `Trigger.dev${appEnvTitleTag(data.appEnv)}`,
title: `Trigger.dev${appEnvTitleTag(data?.appEnv)}`,
charset: "utf-8",
viewport: "width=device-width,initial-scale=1",
});
@@ -46,6 +45,7 @@ export const loader = async ({ request }: LoaderArgs) => {
const session = await getSession(request.headers.get("cookie"));
const toastMessage = session.get("toastMessage") as ToastMessage;
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
const highlightProjectId = env.HIGHLIGHT_PROJECT_ID;
const features = featuresForRequest(request);
return typedjson(
@@ -53,6 +53,7 @@ export const loader = async ({ request }: LoaderArgs) => {
user: await getUser(request),
toastMessage,
posthogProjectKey,
highlightProjectId,
features,
appEnv: env.APP_ENV,
appOrigin: env.APP_ORIGIN,
@@ -72,70 +73,57 @@ export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
};
export function ErrorBoundary() {
const error = useRouteError();
return (
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<AppContainer showBackgroundGradient={true}>
<MainCenteredContainer>
{isRouteErrorResponse(error) ? (
<ErrorDisplay
title={
friendlyErrorDisplay(error.status, error.statusText).title
}
message={
error.data.message ??
friendlyErrorDisplay(error.status, error.statusText).message
}
/>
) : error instanceof Error ? (
<ErrorDisplay title={error.name} message={error.message} />
) : (
<ErrorDisplay title="Oops" message={JSON.stringify(error)} />
)}
</MainCenteredContainer>
</AppContainer>
<Scripts />
</body>
</html>
);
}
function ErrorDisplay({ title, message }: { title: string; message?: string }) {
return (
<div className="p-4">
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
{message && <Header3>{message}</Header3>}
<LinkButton to="/" variant="primary/medium" className="mt-8">
Home
</LinkButton>
</div>
<>
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<AppContainer showBackgroundGradient={true}>
<MainCenteredContainer>
<RouteErrorDisplay />
</MainCenteredContainer>
</AppContainer>
<Scripts />
</body>
</html>
</>
);
}
function App() {
const { posthogProjectKey } = useTypedLoaderData<typeof loader>();
const { posthogProjectKey, highlightProjectId } =
useTypedLoaderData<typeof loader>();
usePostHog(posthogProjectKey);
useHighlight();
return (
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<Outlet />
<Toast />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
<>
{highlightProjectId && (
<HighlightInit
projectId={highlightProjectId}
tracingOrigins={true}
networkRecording={{ enabled: true, recordHeadersAndBody: true }}
/>
)}
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<HighlightErrorBoundary>
<Outlet />
</HighlightErrorBoundary>
<Toast />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
</>
);
}
@@ -21,7 +21,7 @@ import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import useWindowSize from "react-use/lib/useWindowSize";
import { docsPath } from "~/utils/pathBuilder";
import { docsPath, projectIntegrationsPath } from "~/utils/pathBuilder";
export const handle: Handle = {
breadcrumb: {
@@ -83,6 +83,19 @@ export default function Page() {
)}
>
<div>
{project.jobs.length > 0 &&
project.jobs.some(
(j) => j.hasIntegrationsRequiringAction
) && (
<Callout
variant="error"
to={projectIntegrationsPath(organization, project)}
className="mb-2"
>
Some of your Jobs have Integrations that have not been
configured.
</Callout>
)}
<div className="mb-2 flex items-center justify-between gap-x-2">
{project.jobs.length === 0 ? (
<Header2>Jobs</Header2>
@@ -118,9 +131,7 @@ export default function Page() {
{project.jobs.length === 1 ? (
<Callout
variant="docs"
href={docsPath(
"documentation/quickstart#your-first-job"
)}
to={docsPath("documentation/quickstart#your-first-job")}
className="my-3"
>
Create your first Job in code
@@ -128,7 +139,7 @@ export default function Page() {
) : (
<Callout
variant="docs"
href={docsPath("documentation/guides/create-a-job")}
to={docsPath("documentation/guides/create-a-job")}
className="my-3"
>
Create another Job
@@ -9,7 +9,7 @@ import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { ButtonContent } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header1, Header2 } from "~/components/primitives/Headers";
import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
import {
PageDescription,
PageHeader,
@@ -36,6 +36,12 @@ import { ProjectParamSchema } from "~/utils/pathBuilder";
import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { cn } from "~/utils/cn";
import {
HowToSetupYourProject,
HowToUseApiKeysAndEndpoints,
} from "~/components/helpContent/HelpContentText";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -105,73 +111,94 @@ export default function Page() {
</PageDescription>
</PageHeader>
<PageBody>
<Header1>API Keys</Header1>
<div className="mb-8 mt-4 flex gap-4">
{environments.map((environment) => (
<ClipboardField
key={environment.id}
fullWidth={false}
secure
value={environment.apiKey}
variant={"primary/medium"}
icon={<EnvironmentLabel environment={environment} />}
/>
))}
</div>
<Help defaultOpen>
{(open) => (
<div
className={cn(
"grid h-full gap-4",
open ? "grid-cols-2" : "grid-cols-1"
)}
>
<div>
<div className="mb-2 flex items-center justify-between gap-x-2">
<Header2>API Keys</Header2>
<HelpTrigger title="How do I use API Keys and Endpoints?" />
</div>
<div className="mb-8 mt-4 flex gap-4">
{environments.map((environment) => (
<ClipboardField
key={environment.id}
fullWidth={false}
secure
value={environment.apiKey}
variant={"primary/medium"}
icon={<EnvironmentLabel environment={environment} />}
/>
))}
</div>
<Header1 className="mb-2">Endpoints</Header1>
<div className="flex flex-col gap-4">
{clients.length > 0 ? (
clients.map((client) => (
<div key={client.slug}>
<Header2 className="mb-2">{client.slug}</Header2>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Environment</TableHeaderCell>
<TableHeaderCell>Url</TableHeaderCell>
<TableHeaderCell>Last refreshed</TableHeaderCell>
<TableHeaderCell>Jobs</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
<EndpointRow
endpoint={client.endpoints.DEVELOPMENT}
type="DEVELOPMENT"
onClick={() =>
setSelected({
client: client.slug,
type: "DEVELOPMENT",
})
}
/>
<EndpointRow
endpoint={client.endpoints.PRODUCTION}
type="PRODUCTION"
onClick={() =>
setSelected({
client: client.slug,
type: "PRODUCTION",
})
}
/>
</TableBody>
</Table>
<Header2 className="mb-2">Endpoints</Header2>
<div className="flex flex-col gap-4">
{clients.length > 0 ? (
clients.map((client) => (
<div key={client.slug}>
<Header3 className="mb-2">{client.slug}</Header3>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Environment</TableHeaderCell>
<TableHeaderCell>Url</TableHeaderCell>
<TableHeaderCell>Last refreshed</TableHeaderCell>
<TableHeaderCell>Jobs</TableHeaderCell>
<TableHeaderCell hiddenLabel>
Go to page
</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
<EndpointRow
endpoint={client.endpoints.DEVELOPMENT}
type="DEVELOPMENT"
onClick={() =>
setSelected({
client: client.slug,
type: "DEVELOPMENT",
})
}
/>
<EndpointRow
endpoint={client.endpoints.PRODUCTION}
type="PRODUCTION"
onClick={() =>
setSelected({
client: client.slug,
type: "PRODUCTION",
})
}
/>
</TableBody>
</Table>
</div>
))
) : (
<Paragraph>You have no clients yet</Paragraph>
)}
</div>
{selectedEndpoint && (
<ConfigureEndpointSheet
slug={selectedEndpoint.clientSlug}
endpoint={selectedEndpoint.endpoint}
type={selectedEndpoint.type}
onClose={() => setSelected(undefined)}
/>
)}
</div>
))
) : (
<Paragraph>You have no clients yet</Paragraph>
<HelpContent title="How to use API Keys and Endpoints">
<HowToUseApiKeysAndEndpoints />
</HelpContent>
</div>
)}
</div>
{selectedEndpoint && (
<ConfigureEndpointSheet
slug={selectedEndpoint.clientSlug}
endpoint={selectedEndpoint.endpoint}
type={selectedEndpoint.type}
onClose={() => setSelected(undefined)}
/>
)}
</Help>
</PageBody>
</PageContainer>
);
@@ -390,7 +390,7 @@ function IntegrationsWithMissingFields({
<div className="mb-6">
<Header2 className="mb-2 flex items-center gap-1">
<NamedIcon name="error" className="h-5 w-5" />
Integrations with missing details
Integrations requiring configuration
</Header2>
<Table>
@@ -107,7 +107,7 @@ export default function Page() {
/>
<Callout
variant="docs"
href={docsIntegrationPath(client.integration.identifier)}
to={docsIntegrationPath(client.integration.identifier)}
>
View the docs to learn more about using the{" "}
{client.integration.name} Integration.
@@ -24,10 +24,11 @@ function NextButton({ cursor }: { cursor?: string }) {
return path ? (
<LinkButton
to={path}
variant={"secondary/small"}
variant={"tertiary/small"}
TrailingIcon="chevron-right"
className="flex items-center"
>
<span className="sr-only">Next</span>
Next
</LinkButton>
) : null;
}
@@ -38,10 +39,11 @@ function PreviousButton({ cursor }: { cursor?: string }) {
return path ? (
<LinkButton
to={path}
variant={"secondary/small"}
variant={"tertiary/small"}
LeadingIcon="chevron-left"
className="flex items-center"
>
<span className="sr-only">Previous</span>
Prev
</LinkButton>
) : null;
}
@@ -1,14 +1,21 @@
import { useNavigation } from "@remix-run/react";
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { z } from "zod";
import { HowToRunYourJob } from "~/components/helpContent/HelpContentText";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { RunsTable } from "~/components/runs/RunsTable";
import { RunListPresenter } from "~/presenters/RunListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { JobParamsSchema, projectIntegrationsPath } from "~/utils/pathBuilder";
import { ListPagination } from "./ListPagination";
import { useNavigation } from "@remix-run/react";
import { JobParamsSchema } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useJob } from "~/hooks/useJob";
import simplur from "simplur";
export const DirectionSchema = z.union([
z.literal("forward"),
@@ -54,17 +61,51 @@ export default function Page() {
const { list } = useTypedLoaderData<typeof loader>();
const navigation = useNavigation();
const isLoading = navigation.state !== "idle";
const organization = useOrganization();
const project = useProject();
const job = useJob();
return (
<div>
<ListPagination list={list} className="mb-1 justify-end" />
<RunsTable
total={list.runs.length}
hasFilters={false}
runs={list.runs}
isLoading={isLoading}
/>
<ListPagination list={list} className="mb-1 justify-end" />
</div>
<>
{job.hasIntegrationsRequiringAction && (
<Callout
variant="error"
to={projectIntegrationsPath(organization, project)}
className="mb-2"
>
{simplur`This Job has ${
job.integrations.filter((j) => j.setupStatus === "MISSING_FIELDS")
.length
} Integration[|s] that [has|have] not been configured.`}
</Callout>
)}
<Help defaultOpen={list.runs.length === 0}>
{(open) => (
<div
className={cn(
"grid h-fit gap-4",
open ? "grid-cols-2" : "grid-cols-1"
)}
>
<div>
<div className="mb-2 flex items-center justify-end gap-x-2">
<ListPagination list={list} />
<HelpTrigger title="How do I run my Job?" />
</div>
<RunsTable
total={list.runs.length}
hasFilters={false}
runs={list.runs}
isLoading={isLoading}
/>
<ListPagination list={list} className="mt-2 justify-end" />
</div>
<HelpContent title="How to run your Job">
<HowToRunYourJob />
</HelpContent>
</div>
)}
</Help>
</>
);
}
@@ -3,7 +3,6 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Header3 } from "~/components/primitives/Headers";
import { useLocales } from "~/components/primitives/LocaleProvider";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
@@ -58,7 +57,6 @@ export const loader = async ({ request, params }: LoaderArgs) => {
export default function Page() {
const { task } = useTypedLoaderData<typeof loader>();
const locales = useLocales();
const {
name,
@@ -139,7 +137,7 @@ export default function Page() {
{properties.length > 0 && (
<div className="mt-4 flex flex-col gap-2">
<Header3>Properties</Header3>
<RunPanelProperties properties={properties} layout="vertical" />
<RunPanelProperties properties={properties} layout="horizontal" />
</div>
)}
@@ -67,7 +67,7 @@ export default function Page() {
<Header3>Properties</Header3>
<RunPanelProperties
properties={run.properties}
layout="vertical"
layout="horizontal"
/>
</div>
)}
@@ -4,7 +4,7 @@ export default function Page() {
return (
<ComingSoon
title="Job settings"
description="Disable a job, archive it and more."
description="Disable a Job, archive it and more."
icon="settings"
/>
);
@@ -178,7 +178,7 @@ export default function Page() {
}
return (
<Help defaultOpen={!hasTestRuns}>
<Help defaultOpen={true}>
{(open) => (
<div
className={cn(
@@ -254,7 +254,7 @@ export default function Page() {
</Popover>
)}
</div>
<HelpTrigger title="How do I run a test" />
<HelpTrigger title="How do I run a test?" />
</div>
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<JSONEditor
@@ -4,7 +4,7 @@ export default function Page() {
return (
<ComingSoon
title="View all Triggers"
description="View all of the registered Triggers for this job."
description="View all of the registered Triggers for this Job."
icon="trigger"
/>
);
@@ -43,7 +43,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
if (job === null) {
throw new Response("Not Found", { status: 404 });
throw new Response("Not Found", {
status: 404,
statusText: `There is no Job ${jobParam} in this Project.`,
});
}
//todo identify job
@@ -111,7 +114,7 @@ export default function Job() {
<PageInfoProperty
label="Integrations"
value={
<div className="flex gap-0.5">
<span className="flex gap-0.5">
{job.integrations.map((integration, index) => (
<NamedIcon
key={index}
@@ -119,7 +122,7 @@ export default function Job() {
className={"h-4 w-4"}
/>
))}
</div>
</span>
}
/>
)}
@@ -2,14 +2,18 @@ import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import invariant from "tiny-invariant";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import {
ProjectSideMenu,
SideMenuContainer,
} from "~/components/navigation/ProjectSideMenu";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
import { analytics } from "~/services/analytics.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { projectPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -25,7 +29,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
if (!project) {
throw new Response("Not Found", { status: 404 });
throw new Response("Not Found", {
status: 404,
statusText: `Project ${projectParam} not found in your Organization.`,
});
}
analytics.project.identify({ project });
@@ -34,6 +41,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
project,
});
} catch (error) {
if (error instanceof Response) {
throw error;
}
console.error(error);
throw new Response(undefined, {
status: 400,
@@ -61,3 +72,13 @@ export default function Project() {
</>
);
}
export function ErrorBoundary() {
const org = useOrganization();
const project = useProject();
return (
<RouteErrorDisplay
button={{ title: project.name, to: projectPath(org, project) }}
/>
);
}
@@ -3,6 +3,8 @@ import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import invariant from "tiny-invariant";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { useOrganization } from "~/hooks/useOrganizations";
import { getOrganizationFromSlug } from "~/models/organization.server";
import { analytics } from "~/services/analytics.server";
import {
@@ -10,6 +12,7 @@ import {
setCurrentOrg,
} from "~/services/currentOrganization.server";
import { requireUserId } from "~/services/session.server";
import { organizationPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -49,6 +52,15 @@ export default function Organization() {
);
}
export function ErrorBoundary() {
const org = useOrganization();
return (
<RouteErrorDisplay
button={{ title: org.title, to: organizationPath(org) }}
/>
);
}
export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
if (options.formAction === "/resources/environment") {
return false;
+34 -2
View File
@@ -1,22 +1,41 @@
import { Outlet, ShouldRevalidateFunction } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { ImpersonationBanner } from "~/components/ImpersonationBanner";
import { NoMobileOverlay } from "~/components/NoMobileOverlay";
import { AppContainer } from "~/components/layout/AppLayout";
import {
AppContainer,
MainCenteredContainer,
} from "~/components/layout/AppLayout";
import { NavBar } from "~/components/navigation/NavBar";
import { featuresForRequest } from "~/features.server";
import { useIsProjectChildPage } from "~/hooks/useIsProjectChildPage";
import { getOrganizations } from "~/models/organization.server";
import { getImpersonationId } from "~/services/impersonation.server";
import { clearRedirectTo, commitSession } from "~/services/redirectTo.server";
import { requireUser } from "~/services/session.server";
import { confirmBasicDetailsPath } from "~/utils/pathBuilder";
import {
confirmBasicDetailsPath,
invitationCodePath,
} from "~/utils/pathBuilder";
export const loader = async ({ request }: LoaderArgs) => {
const user = await requireUser(request);
const organizations = await getOrganizations({ userId: user.id });
const impersonationId = await getImpersonationId(request);
const features = featuresForRequest(request);
const url = new URL(request.url);
if (
features.isManagedCloud &&
!user.invitationCodeId &&
url.pathname !== invitationCodePath()
) {
throw redirect(invitationCodePath());
}
//you have to confirm basic details before you can do anything
if (!user.confirmedBasicDetails) {
@@ -78,3 +97,16 @@ export default function App() {
</>
);
}
export function ErrorBoundary() {
return (
<>
<NoMobileOverlay />
<AppContainer showBackgroundGradient={true}>
<MainCenteredContainer>
<RouteErrorDisplay />
</MainCenteredContainer>
</AppContainer>
</>
);
}
@@ -146,6 +146,7 @@ export class CompleteRunTaskService {
status: "COMPLETED",
output: taskBody.output ?? undefined,
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
@@ -1,16 +1,25 @@
import type { LoaderFunction } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
import { redirectCookie } from "./auth.github";
import { logger } from "~/services/logger.server";
export let loader: LoaderFunction = async ({ request }) => {
const cookie = request.headers.get("Cookie");
const redirectValue = await redirectCookie.parse(cookie);
const redirectTo = redirectValue ?? "/";
logger.debug("auth.github.callback loader", {
redirectTo,
});
const authuser = await authenticator.authenticate("github", request, {
successRedirect: redirectTo,
failureRedirect: "/login",
});
logger.debug("auth.github.callback authuser", {
authuser,
});
return authuser;
};
+160
View File
@@ -0,0 +1,160 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { Form, useActionData } from "@remix-run/react";
import { ActionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import {
AppContainer,
MainCenteredContainer,
} from "~/components/layout/AppLayout";
import { NavBar } from "~/components/navigation/NavBar";
import { Button } from "~/components/primitives/Buttons";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { FormTitle } from "~/components/primitives/FormTitle";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { TextLink } from "~/components/primitives/Paragraph";
import { prisma } from "~/db.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { grantUserCloudAccess } from "~/models/user.server";
import { requireUserId } from "~/services/session.server";
import { organizationsPath } from "~/utils/pathBuilder";
function createSchema(
constraints: {
isValidCode?: (code: string) => Promise<boolean>;
} = {}
) {
return z.object({
code: z
.string()
.min(1, "Invite code missing")
.superRefine((code, ctx) => {
if (constraints.isValidCode === undefined) {
//client-side validation skips this
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: conform.VALIDATION_UNDEFINED,
});
} else {
// Tell zod this is an async validation by returning the promise
return constraints.isValidCode(code).then((isValid) => {
if (isValid) {
return;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid invitation code",
});
});
}
}),
});
}
export async function action({ request }: ActionArgs) {
const userId = await requireUserId(request);
const formData = await request.formData();
const formSchema = createSchema({
isValidCode: async (code) => {
const invitationCode = await prisma.invitationCode.findUnique({
where: {
code,
},
});
return invitationCode !== undefined && invitationCode !== null;
},
});
const submission = await parse(formData, { schema: formSchema, async: true });
if (!submission.value) {
return json(submission);
}
try {
await grantUserCloudAccess({
id: userId,
inviteCode: submission.value.code,
});
return redirectWithSuccessMessage(
organizationsPath(),
request,
"🚀 Welcome to the Trigger.dev Cloud private beta"
);
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
}
}
export default function Page() {
const lastSubmission = useActionData();
const [form, { code }] = useForm({
id: "invitation-code",
lastSubmission,
shouldRevalidate: "onSubmit",
onValidate({ formData }) {
return parse(formData, { schema: createSchema() });
},
});
return (
<AppContainer showBackgroundGradient={true}>
<NavBar />
<MainCenteredContainer>
<FormTitle
LeadingIcon="qr-code"
title="Trigger.dev Cloud Beta"
description={
<>
Enter your code to get access. If you don't have one you can
always{" "}
<TextLink
target="_blank"
href="https://trigger.dev/docs/documentation/guides/self-hosting"
trailingIcon="external-link"
trailingIconClassName="h-3 w-3 text-indigo-500 transition group-hover:text-indigo-400"
>
self-host
</TextLink>{" "}
Trigger.dev
</>
}
/>
<Form method="post" {...form.props}>
<Fieldset>
<InputGroup>
<Input
{...conform.input(code, { type: "text" })}
placeholder="Your super secret invite code"
icon="qr-code"
autoFocus={Boolean(code.initialError)}
spellCheck={false}
/>
<FormError id={code.errorId}>{code.error}</FormError>
</InputGroup>
<FormButtons
confirmButton={
<Button
type="submit"
variant={"primary/small"}
TrailingIcon={"arrow-right"}
>
Get access
</Button>
}
/>
</Fieldset>
</Form>
</MainCenteredContainer>
</AppContainer>
);
}
+1 -1
View File
@@ -35,7 +35,7 @@ export async function loader({ request }: LoaderArgs) {
return redirectWithSuccessMessage(
"/",
request,
"Please login to accept the invite.",
"Please log in to accept the invite.",
{ ephemeral: false }
);
}
@@ -26,7 +26,7 @@ import { requestUrl } from "~/utils/requestUrl.server";
export const meta: TypedMetaFunction<typeof loader, { root: RootLoader }> = ({
parentsData,
}) => ({
title: `Login to Trigger.dev${appEnvTitleTag(parentsData.root.appEnv)}`,
title: `Login to Trigger.dev${appEnvTitleTag(parentsData?.root.appEnv)}`,
});
export async function loader({ request }: LoaderArgs) {
@@ -71,7 +71,7 @@ export default function LoginPage() {
<a href="https://trigger.dev">
<LogoIcon className="mb-4 h-16 w-16" />
</a>
<FormTitle divide={false} title="Create your Trigger.dev account" />
<FormTitle divide={false} title="Log in to Trigger.dev" />
<Fieldset>
<div className="flex flex-col gap-y-2">
{data.showGithubAuth && (
+2 -5
View File
@@ -34,7 +34,7 @@ import { appEnvTitleTag } from "~/utils";
export const meta: TypedMetaFunction<typeof loader, { root: RootLoader }> = ({
parentsData,
}) => ({
title: `Login to Trigger.dev${appEnvTitleTag(parentsData.root.appEnv)}`,
title: `Login to Trigger.dev${appEnvTitleTag(parentsData?.root.appEnv)}`,
});
export async function loader({ request }: LoaderArgs) {
@@ -130,10 +130,7 @@ export default function LoginMagicLinkPage() {
</>
) : (
<>
<FormTitle
divide={false}
title="Create your Trigger.dev account"
/>
<FormTitle divide={false} title="Log in to Trigger.dev" />
<Fieldset className="flex w-full flex-col items-center gap-y-2">
<InputGroup>
<Label>Your email address</Label>
@@ -121,10 +121,6 @@ export class EndpointApi {
const anyBody = await response.json();
logger.debug("indexEndpoint() response from endpoint", {
body: anyBody,
});
const data = IndexEndpointResponseSchema.parse(anyBody);
return {
@@ -177,6 +173,7 @@ export class EndpointApi {
return {
response,
parser: RunJobResponseSchema,
errorParser: ErrorWithStackSchema,
};
}
@@ -1,11 +1,19 @@
import { $transaction, PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { EndpointApi } from "../endpointApi.server";
import { workerQueue } from "../worker.server";
import type { EndpointIndexSource } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
import { findEndpoint } from "~/models/endpoint.server";
import { EndpointApi } from "../endpointApi.server";
import { RegisterJobService } from "../jobs/registerJob.server";
import { logger } from "../logger.server";
import { RegisterSourceService } from "../sources/registerSource.server";
import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
export class IndexEndpointService {
#prismaClient: PrismaClient;
#registerJobService = new RegisterJobService();
#registerSourceService = new RegisterSourceService();
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
@@ -17,14 +25,7 @@ export class IndexEndpointService {
reason?: string,
sourceData?: any
) {
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
where: {
id,
},
include: {
environment: true,
},
});
const endpoint = await findEndpoint(id);
// Make a request to the endpoint to fetch a list of jobs
const client = new EndpointApi(
@@ -42,7 +43,19 @@ export class IndexEndpointService {
const { jobs, sources, dynamicTriggers, dynamicSchedules } =
indexResponse.data;
const queueName = `endpoint-${endpoint.id}`;
logger.debug("Indexing endpoint", {
endpointId: endpoint.id,
endpointUrl: endpoint.url,
endpointSlug: endpoint.slug,
source: source,
sourceData: sourceData,
stats: {
jobs: jobs.length,
sources: sources.length,
dynamicTriggers: dynamicTriggers.length,
dynamicSchedules: dynamicSchedules.length,
},
});
const indexStats = {
jobs: 0,
@@ -51,94 +64,98 @@ export class IndexEndpointService {
dynamicSchedules: 0,
};
return await $transaction(
this.#prismaClient,
async (tx) => {
for (const job of jobs) {
if (!job.enabled) {
continue;
}
for (const job of jobs) {
if (!job.enabled) {
continue;
}
indexStats.jobs++;
try {
await this.#registerJobService.call(endpoint, job);
await workerQueue.enqueue(
"registerJob",
{
job,
endpointId: endpoint.id,
},
{
queueName,
tx,
}
);
}
for (const source of sources) {
indexStats.sources++;
await workerQueue.enqueue(
"registerSource",
{
source,
endpointId: endpoint.id,
},
{
queueName,
tx,
}
);
}
for (const dynamicTrigger of dynamicTriggers) {
indexStats.dynamicTriggers++;
await workerQueue.enqueue(
"registerDynamicTrigger",
{
dynamicTrigger,
endpointId: endpoint.id,
},
{
queueName,
tx,
}
);
}
for (const dynamicSchedule of dynamicSchedules) {
indexStats.dynamicSchedules++;
await workerQueue.enqueue(
"registerDynamicSchedule",
{
dynamicSchedule,
endpointId: endpoint.id,
},
{
queueName,
tx,
}
);
}
return await tx.endpointIndex.create({
data: {
endpointId: endpoint.id,
stats: indexStats,
data: {
jobs,
sources,
dynamicTriggers,
dynamicSchedules,
},
source,
sourceData,
reason,
},
indexStats.jobs++;
} catch (error) {
logger.debug("Failed to register job", {
endpointId: endpoint.id,
job,
});
logger.error(error);
}
}
for (const source of sources) {
try {
await this.#registerSourceService.call(endpoint, source);
indexStats.sources++;
} catch (error) {
logger.debug("Failed to register source", {
endpointId: endpoint.id,
source,
});
logger.error(error);
}
}
for (const dynamicTrigger of dynamicTriggers) {
try {
await this.#registerDynamicTriggerService.call(
endpoint,
dynamicTrigger
);
indexStats.dynamicTriggers++;
} catch (error) {
logger.debug("Failed to register dynamic trigger", {
endpointId: endpoint.id,
dynamicTrigger,
});
logger.error(error);
}
}
for (const dynamicSchedule of dynamicSchedules) {
try {
await this.#registerDynamicScheduleService.call(
endpoint,
dynamicSchedule
);
indexStats.dynamicSchedules++;
} catch (error) {
logger.debug("Failed to register dynamic schedule", {
endpointId: endpoint.id,
dynamicSchedule,
});
logger.error(error);
}
}
logger.debug("Endpoint indexing complete", {
endpointId: endpoint.id,
indexStats,
source,
sourceData,
reason,
});
return await this.#prismaClient.endpointIndex.create({
data: {
endpointId: endpoint.id,
stats: indexStats,
data: {
jobs,
sources,
dynamicTriggers,
dynamicSchedules,
},
source,
sourceData,
reason,
},
{ timeout: 15000 }
);
});
}
}
@@ -180,6 +180,10 @@ export const apisList = [
identifier: "spotify",
name: "Spotify",
},
{
identifier: "stabilityai",
name: "Stability AI",
},
{
identifier: "stripe",
name: "Stripe",
@@ -204,6 +208,10 @@ export const apisList = [
identifier: "twitter",
name: "Twitter",
},
{
identifier: "typeform",
name: "Typeform",
},
{
identifier: "whatsapp",
name: "WhatsApp",
@@ -1,7 +1,9 @@
import { github } from "./integrations/github";
import { openai } from "./integrations/openai";
import { plain } from "./integrations/plain";
import { resend } from "./integrations/resend";
import { slack } from "./integrations/slack";
import { typeform } from "./integrations/typeform";
import type { Integration } from "./types";
export class IntegrationCatalog {
@@ -29,6 +31,8 @@ export const integrationCatalog = new IntegrationCatalog({
// airtable,
github,
openai,
plain,
resend,
slack,
typeform,
});
@@ -64,7 +64,7 @@ const usageSample: HelpSample = {
export const github: Integration = {
identifier: "github",
name: "GitHub",
packageName: "@trigger.dev/github",
packageName: "@trigger.dev/github@next",
authenticationMethods: {
oauth2: {
name: "OAuth",
@@ -5,7 +5,7 @@ export const openai: Integration = {
identifier: "openai",
name: "OpenAI",
description: "You can perform very long completions with the integration",
packageName: "@trigger.dev/openai",
packageName: "@trigger.dev/openai@latest",
authenticationMethods: {
apikey: {
type: "apikey",
@@ -0,0 +1,103 @@
import type { Integration } from "../types";
export const plain: Integration = {
identifier: "plain",
name: "Plain",
packageName: "@trigger.dev/plain@latest",
authenticationMethods: {
apikey: {
type: "apikey",
help: {
samples: [
{
title: "Creating the client",
code: `
import { Plain } from "@trigger.dev/plain";
export const plain = new Plain({
id: "__SLUG__",
apiKey: process.env.PLAIN_API_KEY!,
});
`,
},
{
title: "Using the client",
code: `
new Job(client, {
id: "plain-playground",
name: "Plain Playground",
version: "0.1.1",
integrations: {
plain,
},
trigger: eventTrigger({
name: "plain.playground",
}),
run: async (payload, io, ctx) => {
const { customer } = await io.plain.upsertCustomer("upsert-customer", {
identifier: {
emailAddress: "rick.astley@gmail.com",
},
onCreate: {
email: {
email: "rick.astley@gmail.com",
isVerified: true,
},
fullName: "Rick Astley",
externalId: "u_123",
},
onUpdate: {
fullName: {
value: "Rick Astley",
},
externalId: {
value: "u_123",
},
},
});
const foundCustomer = await io.plain.getCustomerById("get-customer", {
customerId: customer.id,
});
const timelineEntry = await io.plain.upsertCustomTimelineEntry(
"upsert-timeline-entry",
{
customerId: customer.id,
title: "My timeline entry",
components: [
{
componentText: {
text: \`This is a nice title\`,
},
},
{
componentDivider: {
dividerSpacingSize: ComponentDividerSpacingSize.M,
},
},
{
componentText: {
textSize: ComponentTextSize.S,
textColor: ComponentTextColor.Muted,
text: "External id",
},
},
{
componentText: {
text: foundCustomer?.externalId ?? "",
},
},
],
}
);
},
});
`,
highlight: [[12, 68]],
},
],
},
},
},
};
@@ -3,7 +3,7 @@ import type { Integration } from "../types";
export const resend: Integration = {
identifier: "resend",
name: "Resend",
packageName: "@trigger.dev/resend",
packageName: "@trigger.dev/resend@next",
authenticationMethods: {
apikey: {
type: "apikey",
@@ -45,7 +45,7 @@ new Job(client, {
export const slack: Integration = {
identifier: "slack",
name: "Slack",
packageName: "@trigger.dev/slack",
packageName: "@trigger.dev/slack@next",
authenticationMethods: {
oauth2Bot: {
name: "OAuth2 (Bot)",
@@ -0,0 +1,53 @@
import type { Integration } from "../types";
export const typeform: Integration = {
identifier: "typeform",
name: "Typeform",
description: "Use the Typeform API and trigger on new responses",
packageName: "@trigger.dev/typeform@latest",
authenticationMethods: {
apikey: {
type: "apikey",
help: {
samples: [
{
title: "Creating the client",
code: `
import { Typeform } from "@trigger.dev/typeform";
const typeform = new Typeform({
id: "__SLUG__",
apiKey: process.env.TYPEFORM_TOKEN!,
});
`,
},
{
title: "Using the client",
code: `
new Job(client, {
id: "typeform-response",
name: "Typeform Response",
version: "0.0.1",
trigger: typeform.onFormResponse({
uid: "<form uid>",
tag: "<webhook tag>",
}),
integrations: {
typeform,
},
run: async (payload, io, ctx) => {
await io.typeform.getForm(payload.form_response.form_id);
},
});
`,
highlight: [
[5, 8],
[9, 11],
[13, 13],
],
},
],
},
},
},
};
@@ -4,6 +4,7 @@ import { env } from "~/env.server";
import { findOrCreateUser } from "~/models/user.server";
import type { AuthUser } from "./authUser";
import { postAuthentication } from "./postAuth.server";
import { logger } from "./logger.server";
export function addGitHubStrategy(
authenticator: Authenticator<AuthUser>,
@@ -24,6 +25,12 @@ export function addGitHubStrategy(
}
try {
logger.debug("GitHub login", {
emails,
profile,
extraParams,
});
const { user, isNewUser } = await findOrCreateUser({
email: emails[0].value,
authenticationMethod: "GITHUB",
@@ -18,6 +18,7 @@ import type { AuthenticatedEnvironment } from "../apiAuth.server";
import { logger } from "../logger.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import { RegisterScheduleSourceService } from "../schedules/registerScheduleSource.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
export class RegisterJobService {
#prismaClient: PrismaClient;
@@ -26,22 +27,16 @@ export class RegisterJobService {
this.#prismaClient = prismaClient;
}
public async call(endpointId: string, metadata: JobMetadata) {
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
where: {
id: endpointId,
},
include: {
environment: {
include: {
project: true,
organization: true,
},
},
},
});
public async call(
endpointIdOrEndpoint: string | ExtendedEndpoint,
metadata: JobMetadata
) {
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
await this.#upsertJob(endpoint, endpoint.environment, metadata);
return this.#upsertJob(endpoint, endpoint.environment, metadata);
}
async #upsertJob(
@@ -49,12 +44,6 @@ export class RegisterJobService {
environment: AuthenticatedEnvironment,
metadata: JobMetadata
): Promise<JobVersion> {
logger.debug("Upserting job", {
endpoint,
organizationId: environment.organizationId,
metadata,
});
const integrations = new Map<string, Integration>();
for (const [, jobIntegration] of Object.entries(metadata.integrations)) {
+1 -1
View File
@@ -5,6 +5,6 @@ import { sensitiveDataReplacer } from "./sensitiveDataReplacer";
export const logger = new Logger(
"webapp",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
[],
["examples"],
sensitiveDataReplacer
);
@@ -247,7 +247,7 @@ export class PerformRunExecutionService {
run.event.sourceContext
);
const { response, parser } = await client.executeJobRequest({
const { response, parser, errorParser } = await client.executeJobRequest({
event,
job: {
id: run.version.job.slug,
@@ -288,13 +288,20 @@ export class PerformRunExecutionService {
});
}
const rawBody = await response.text();
if (!response.ok) {
const errorBody = safeJsonZodParse(errorParser, rawBody);
if (errorBody && errorBody.success) {
return await this.#failRunExecutionWithRetry(execution, errorBody.data);
}
return await this.#failRunExecutionWithRetry(execution, {
message: `Endpoint responded with ${response.status} status code`,
});
}
const rawBody = await response.text();
const safeBody = safeJsonZodParse(parser, rawBody);
if (!safeBody) {
@@ -3,9 +3,9 @@ import type { SourceMetadata } from "@trigger.dev/internal";
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
import { prisma } from "~/db.server";
import type { AuthenticatedEnvironment } from "../apiAuth.server";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { generateSecret } from "./utils.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
export class RegisterSourceService {
#prismaClient: PrismaClientOrTransaction;
@@ -15,25 +15,16 @@ export class RegisterSourceService {
}
public async call(
endpointId: string,
endpointIdOrEndpoint: string | ExtendedEndpoint,
metadata: SourceMetadata,
dynamicTriggerId?: string,
accountId?: string,
dynamicSource?: { id: string; metadata: any }
) {
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
where: {
id: endpointId,
},
include: {
environment: {
include: {
project: true,
organization: true,
},
},
},
});
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
return this.#upsertSource(
endpoint,
@@ -53,17 +44,21 @@ export class RegisterSourceService {
accountId?: string,
dynamicSource?: { id: string; metadata: any }
) {
logger.debug("Upserting source", {
endpoint,
organizationId: environment.organizationId,
metadata,
accountId,
});
const key = [dynamicTriggerId, dynamicSource?.id, metadata.key]
.filter(Boolean)
.join(":");
const registrationJob = metadata.registerSourceJob
? await this.#prismaClient.job.findUnique({
where: {
projectId_slug: {
projectId: endpoint.projectId,
slug: metadata.registerSourceJob.id,
},
},
})
: undefined;
const source = await $transaction(
this.#prismaClient,
async (tx) => {
@@ -148,6 +143,18 @@ export class RegisterSourceService {
},
dynamicSourceId: dynamicSource?.id,
dynamicSourceMetadata: dynamicSource?.metadata,
sourceRegistrationJob:
registrationJob && metadata.registerSourceJob
? {
connect: {
jobId_version_environmentId: {
jobId: registrationJob.id,
version: metadata.registerSourceJob.version,
environmentId: endpoint.environmentId,
},
},
}
: undefined,
},
update: {
endpoint: {
@@ -158,6 +165,18 @@ export class RegisterSourceService {
integration: { connect: { id: integration.id } },
dynamicSourceId: dynamicSource?.id,
dynamicSourceMetadata: dynamicSource?.metadata,
sourceRegistrationJob:
registrationJob && metadata.registerSourceJob
? {
connect: {
jobId_version_environmentId: {
jobId: registrationJob.id,
version: metadata.registerSourceJob.version,
environmentId: endpoint.environmentId,
},
},
}
: undefined,
},
include: {
events: true,
@@ -3,6 +3,7 @@ import {
SCHEDULED_EVENT,
} from "@trigger.dev/internal";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
export class RegisterDynamicScheduleService {
#prismaClient: PrismaClientOrTransaction;
@@ -12,13 +13,18 @@ export class RegisterDynamicScheduleService {
}
public async call(
endpointId: string,
endpointIdOrEndpoint: string | ExtendedEndpoint,
metadata: RegisterDynamicSchedulePayload
) {
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
const dynamicTrigger = await this.#prismaClient.dynamicTrigger.upsert({
where: {
endpointId_slug_type: {
endpointId: endpointId,
endpointId: endpoint.id,
slug: metadata.id,
type: "SCHEDULE",
},
@@ -28,7 +34,7 @@ export class RegisterDynamicScheduleService {
type: "SCHEDULE",
endpoint: {
connect: {
id: endpointId,
id: endpoint.id,
},
},
},
@@ -47,7 +53,7 @@ export class RegisterDynamicScheduleService {
},
versions: {
some: {
endpointId,
endpointId: endpoint.id,
},
},
},
@@ -1,6 +1,7 @@
import type { DynamicTriggerEndpointMetadata } from "@trigger.dev/internal";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
export class RegisterDynamicTriggerService {
#prismaClient: PrismaClient;
@@ -10,13 +11,29 @@ export class RegisterDynamicTriggerService {
}
public async call(
endpointId: string,
endpointIdOrEndpoint: string | ExtendedEndpoint,
metadata: DynamicTriggerEndpointMetadata
) {
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
const registrationJob = metadata.registerSourceJob
? await this.#prismaClient.job.findUnique({
where: {
projectId_slug: {
projectId: endpoint.projectId,
slug: metadata.registerSourceJob.id,
},
},
})
: undefined;
const dynamicTrigger = await this.#prismaClient.dynamicTrigger.upsert({
where: {
endpointId_slug_type: {
endpointId: endpointId,
endpointId: endpoint.id,
slug: metadata.id,
type: "EVENT",
},
@@ -26,11 +43,36 @@ export class RegisterDynamicTriggerService {
type: "EVENT",
endpoint: {
connect: {
id: endpointId,
id: endpoint.id,
},
},
sourceRegistrationJob:
registrationJob && metadata.registerSourceJob
? {
connect: {
jobId_version_environmentId: {
jobId: registrationJob.id,
version: metadata.registerSourceJob.version,
environmentId: endpoint.environmentId,
},
},
}
: undefined,
},
update: {
sourceRegistrationJob:
registrationJob && metadata.registerSourceJob
? {
connect: {
jobId_version_environmentId: {
jobId: registrationJob.id,
version: metadata.registerSourceJob.version,
environmentId: endpoint.environmentId,
},
},
}
: undefined,
},
update: {},
include: {
jobs: true,
},
@@ -44,7 +86,7 @@ export class RegisterDynamicTriggerService {
},
versions: {
some: {
endpointId,
endpointId: endpoint.id,
},
},
},

Some files were not shown because too many files have changed in this diff Show More